From 00dde195d9773761a638a99c9c204ce2f89c8be5 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 21 Jun 2023 11:06:37 -0700 Subject: [PATCH 001/954] Bug Fixes for NARU * Change logic to show No Data Flags instead of hiding them * willimportFill was actually undefined despite being set to null, so check for that * Only save if form is dirty --- .../naru-details/naru-details.component.html | 28 ++++++++++--------- .../naru-proposal/naru-proposal.component.ts | 4 +-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html b/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html index e6105763da..73e17a217a 100644 --- a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html +++ b/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html @@ -14,7 +14,7 @@ What is the purpose of the proposal? -
+
{{ _applicationSubmission.naruPurpose }}
@@ -24,7 +24,7 @@ What is the total floor area (m2) of the proposed additional residence?
-
+
{{ _applicationSubmission.naruFloorArea }}
@@ -34,7 +34,7 @@ or long term.
-
+
{{ _applicationSubmission.naruResidenceNecessity }}
@@ -43,7 +43,7 @@ Describe the rationale for the proposed location of the additional residence.
-
+
{{ _applicationSubmission.naruLocationRationale }}
@@ -53,7 +53,7 @@ residence.
-
+
{{ _applicationSubmission.naruInfrastructure }}
@@ -64,7 +64,7 @@ What is the total floor area (m2) of the proposed principal residence?
-
+
{{ _applicationSubmission.naruFloorArea }}
@@ -74,7 +74,7 @@ short or long term.
-
+
{{ _applicationSubmission.naruResidenceNecessity }}
@@ -83,7 +83,7 @@ Describe the rationale for the proposed location of the principal residence.
-
+
{{ _applicationSubmission.naruLocationRationale }}
@@ -93,7 +93,7 @@ residence.
-
+
{{ _applicationSubmission.naruInfrastructure }}
@@ -104,7 +104,7 @@ located on the property.
-
+
{{ _applicationSubmission.naruExistingStructures }}
@@ -122,9 +122,11 @@ the property, including gravel for construction.
-
- {{ _applicationSubmission.naruWillImportFill ? 'Yes' : 'No' }} - +
+ {{ + _applicationSubmission.naruWillImportFill ? 'Yes' : 'No' + }} +
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts index 67d26716bc..b7685e0906 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts @@ -166,7 +166,7 @@ export class NaruProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId) { + if (this.fileId && this.form.dirty) { const { existingStructures, willImportFill, @@ -184,7 +184,7 @@ export class NaruProposalComponent extends FilesStepComponent implements OnInit, const updateDto: ApplicationSubmissionUpdateDto = { naruExistingStructures: existingStructures, - naruWillImportFill: willImportFill !== null ? willImportFill === 'true' : null, + naruWillImportFill: willImportFill !== undefined ? willImportFill === 'true' : null, naruFillType: fillType, naruFillOrigin: fillOrigin, naruToPlaceAverageDepth: this.fillTableData.averageDepth ?? null, From d88891a7235dbe142916306c2fa3d646849bda5e Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Wed, 21 Jun 2023 15:55:36 -0700 Subject: [PATCH 002/954] fix typo (#715) --- .../notice-of-intent.service.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts index e7e76967df..19d9533ec3 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts @@ -29,21 +29,14 @@ import { NoticeOfIntent } from './notice-of-intent.entity'; @Injectable() export class NoticeOfIntentService { private CARD_RELATIONS = { - card: { - board: true, - type: true, - status: true, - assignee: true, - }, + board: true, + type: true, + status: true, + assignee: true, }; private DEFAULT_RELATIONS: FindOptionsRelations = { - card: { - board: true, - type: true, - status: true, - assignee: true, - }, + card: this.CARD_RELATIONS, localGovernment: true, region: true, subtype: true, From 4c88c444abb87ba98da673391ffed43fa9fe9668 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 21 Jun 2023 16:32:24 -0700 Subject: [PATCH 003/954] Naru Tourism Step 8 + ALCS * Also bug fixes for existing versions --- .../application-details.component.spec.ts | 2 + .../naru-details/naru-details.component.html | 43 +++++ .../application-submission.service.spec.ts | 2 + .../services/application/application.dto.ts | 2 + .../naru-details/naru-details.component.html | 57 ++++++ .../naru-proposal.component.html | 181 ++++++++++++++---- .../naru-proposal/naru-proposal.component.ts | 12 ++ .../application-submission.dto.ts | 4 + .../src/alcs/application/application.dto.ts | 6 + .../application-submission.dto.ts | 14 ++ .../application-submission.entity.ts | 16 +- .../application-submission.service.ts | 12 ++ .../1687371977194-add_naru_tourism_fields.ts | 23 +++ 13 files changed, 339 insertions(+), 35 deletions(-) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1687371977194-add_naru_tourism_fields.ts diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts index 9164cc5dcb..63dd1d44c6 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts @@ -44,6 +44,8 @@ describe('ApplicationDetailsComponent', () => { naruToPlaceMaximumDepth: null, naruToPlaceVolume: null, naruWillImportFill: null, + naruAgriTourism: null, + naruSleepingUnits: null, nfuAgricultureSupport: null, nfuFillOriginDescription: null, nfuFillTypeDescription: null, diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html index 0ae8fde7dd..b7979c06cf 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html @@ -59,6 +59,39 @@ {{ _applicationSubmission.naruInfrastructure }}
+ + +
What is the total floor area (m2) of the proposed accommodation?
+
+ {{ _applicationSubmission.naruFloorArea }} +
+
How many "sleeping units" in total are proposed?
+
+ {{ _applicationSubmission.naruSleepingUnits }} +
+
+ Describe how the proposal for tourism accommodation will support agriculture in the short or long term. +
+
+ {{ _applicationSubmission.naruResidenceNecessity }} +
+
Describe the rationale for the proposed location of the tourism accommodation.
+
+ {{ _applicationSubmission.naruLocationRationale }} +
+
+ Provide the total area (m2) and a description of infrastructure necessary to support the tourism + accommodation. +
+
+ {{ _applicationSubmission.naruInfrastructure }} +
+
Describe any agri-tourism that is currently taking place on the property.
+
+ {{ _applicationSubmission.naruAgriTourism }} +
+
+
Describe the total floor area (m2), type, number, and occupancy of all residential structures currently located on the property. @@ -82,6 +115,16 @@ {{ _applicationSubmission.naruWillImportFill ? 'Yes' : 'No' }}
+
Describe the type and amount of fill proposed to be placed.
+
+ {{ _applicationSubmission.naruFillType }} +
+ +
Briefly describe the origin and quality of fill.
+
+ {{ _applicationSubmission.naruFillOrigin }} +
+
Fill to be Placed
Volume
diff --git a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts index 4b038fd3e4..96578b4656 100644 --- a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts @@ -26,6 +26,8 @@ describe('ApplicationSubmissionService', () => { naruSubtype: null, naruToPlaceArea: null, naruToPlaceAverageDepth: null, + naruSleepingUnits: null, + naruAgriTourism: null, naruToPlaceMaximumDepth: null, naruToPlaceVolume: null, naruWillImportFill: null, diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 7762e0c573..62966937a2 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -191,6 +191,8 @@ export interface ApplicationSubmissionDto { naruToPlaceArea: number | null; naruToPlaceMaximumDepth: number | null; naruToPlaceAverageDepth: number | null; + naruSleepingUnits: number | null; + naruAgriTourism: string | null; } export interface ApplicationDto { diff --git a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html b/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html index 73e17a217a..addd2ceb07 100644 --- a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html +++ b/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html @@ -99,6 +99,63 @@
+ +
+ What is the total floor area (m2) of the proposed accommodation? + +
+
+ {{ _applicationSubmission.naruFloorArea }} + +
+ +
+ How many "sleeping units" in total are proposed? + +
+
+ {{ _applicationSubmission.naruSleepingUnits }} + +
+ +
+ Describe how the proposal for tourism accommodation will support agriculture in the short or long term. + +
+
+ {{ _applicationSubmission.naruResidenceNecessity }} + +
+ +
+ Describe the rationale for the proposed location of the tourism accommodation. + +
+
+ {{ _applicationSubmission.naruLocationRationale }} + +
+ +
+ Provide the total area (m2) and a description of infrastructure necessary to support the tourism + accommodation. + +
+
+ {{ _applicationSubmission.naruInfrastructure }} + +
+ +
+ Describe any agri-tourism that is currently taking place on the property. + +
+
+ {{ _applicationSubmission.naruAgriTourism }} + +
+
+
Describe the total floor area (m2), type, number, and occupancy of all residential structures currently located on the property. diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html index 6e878e25bc..f0c9783f61 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html @@ -157,41 +157,107 @@

Proposal

This field is required
+ + + +
+ + + + m2 + +
+ warning +
This field is required
+
+
-
+
+ + Associated Parcel + + + #{{ parcel.index + 1 }} PID: + {{ parcel.pid | mask : '000-000-000' }} + No Data + + +
Visible To:
diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.scss b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.scss index 99825bd974..de0c2bac88 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.scss +++ b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.scss @@ -1,3 +1,5 @@ +@use '../../../../../styles/colors'; + .form { display: grid; grid-template-columns: 1fr 1fr; @@ -41,3 +43,9 @@ a { align-items: center; } } + +.superseded-warning { + background-color: colors.$secondary-color-dark; + color: #fff; + padding: 0 4px; +} diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts index ac1062aab4..e2c259bc1b 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ApplicationDocumentService } from '../../../../services/application/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../services/application/application-parcel/application-parcel.service'; import { DocumentUploadDialogComponent } from './document-upload-dialog.component'; @@ -11,9 +12,11 @@ describe('DocumentUploadDialogComponent', () => { let fixture: ComponentFixture; let mockAppDocService: DeepMocked; + let mockParcelService: DeepMocked; beforeEach(async () => { mockAppDocService = createMock(); + mockParcelService = createMock(); const mockDialogRef = { close: jest.fn(), @@ -29,6 +32,10 @@ describe('DocumentUploadDialogComponent', () => { provide: ApplicationDocumentService, useValue: mockAppDocService, }, + { + provide: ApplicationParcelService, + useValue: mockParcelService, + }, { provide: MatDialogRef, useValue: mockDialogRef }, { provide: MAT_DIALOG_DATA, useValue: {} }, ], diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts index 7619ce40d9..b0264511dc 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts @@ -12,6 +12,7 @@ import { DOCUMENT_SYSTEM, DOCUMENT_TYPE, } from '../../../../services/application/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../services/application/application-parcel/application-parcel.service'; @Component({ selector: 'app-document-upload-dialog', @@ -20,23 +21,26 @@ import { }) export class DocumentUploadDialogComponent implements OnInit, OnDestroy { $destroy = new Subject(); + DOCUMENT_TYPE = DOCUMENT_TYPE; title = 'Create'; isDirty = false; isSaving = false; allowsFileEdit = true; - sources = DOCUMENT_TYPE; documentTypeAhead: string | undefined = undefined; name = new FormControl('', [Validators.required]); type = new FormControl(undefined, [Validators.required]); source = new FormControl('', [Validators.required]); + parcelId = new FormControl(null); + visibleToInternal = new FormControl(false, [Validators.required]); visibleToPublic = new FormControl(false, [Validators.required]); documentTypes: ApplicationDocumentTypeDto[] = []; documentSources = Object.values(DOCUMENT_SOURCE); + selectableParcels: { uuid: string; index: number; pid?: string }[] = []; form = new FormGroup({ name: this.name, @@ -44,15 +48,18 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { source: this.source, visibleToInternal: this.visibleToInternal, visibleToPublic: this.visibleToPublic, + parcelId: this.parcelId, }); pendingFile: File | undefined; existingFile: { name: string; size: number } | undefined; + showCertificateOfTitleWarning = false; constructor( @Inject(MAT_DIALOG_DATA) public data: { fileId: string; existingDocument?: ApplicationDocumentDto }, protected dialog: MatDialogRef, - private applicationDocumentService: ApplicationDocumentService + private applicationDocumentService: ApplicationDocumentService, + private parcelService: ApplicationParcelService ) {} ngOnInit(): void { @@ -62,6 +69,9 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { const document = this.data.existingDocument; this.title = 'Edit'; this.allowsFileEdit = document.system === DOCUMENT_SYSTEM.ALCS; + if (document.type?.code === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { + this.prepareCertificateOfTitleUpload(document.uuid); + } this.form.patchValue({ name: document.fileName, type: document.type?.code, @@ -90,6 +100,8 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { visibilityFlags.push('P'); } + const parcelId = await this.parcelId.value; + const dto = { file: this.pendingFile, fileName: this.name.getRawValue()!, @@ -98,7 +110,10 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { visibilityFlags, }; this.isSaving = true; - if (this.data.existingDocument) { + if (parcelId && dto.file && this.type.value === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { + // @ts-ignore File is checked above to not be undefined, typescript still thinks it might be undefined + await this.applicationDocumentService.attachCertificateOfTitle(this.data.fileId, parcelId, dto); + } else if (this.data.existingDocument) { await this.applicationDocumentService.update(this.data.existingDocument.uuid, dto); } else if (dto.file !== undefined) { // @ts-ignore File is checked above to not be undefined, typescript still thinks it might be undefined @@ -127,12 +142,48 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { ); } - onDocTypeSelected($event?: ApplicationDocumentTypeDto) { + async prepareCertificateOfTitleUpload(uuid?: string) { + const parcels = await this.parcelService.fetchParcels(this.data.fileId); + debugger; + if (parcels.length > 0) { + this.parcelId.setValidators([Validators.required]); + this.parcelId.updateValueAndValidity(); + + this.visibleToInternal.setValue(true); + this.visibleToPublic.setValue(true); + this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + + const selectedParcel = parcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); + if (selectedParcel) { + this.parcelId.setValue(selectedParcel.uuid); + } else if (uuid) { + this.showCertificateOfTitleWarning = true; + } + + this.selectableParcels = parcels + .filter((parcel) => parcel.parcelType === 'application') + .map((parcel, index) => ({ + uuid: parcel.uuid, + pid: parcel.pid, + index: index, + })); + } + } + + async onDocTypeSelected($event?: ApplicationDocumentTypeDto) { if ($event) { this.type.setValue($event.code); } else { this.type.setValue(undefined); } + + if (this.type.value === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { + await this.prepareCertificateOfTitleUpload(); + } else { + this.parcelId.setValue(null); + this.parcelId.setValidators([]); + this.parcelId.updateValueAndValidity(); + } } uploadFile(event: Event) { diff --git a/alcs-frontend/src/app/services/application/application-document/application-document.service.ts b/alcs-frontend/src/app/services/application/application-document/application-document.service.ts index fc58585972..639fe02829 100644 --- a/alcs-frontend/src/app/services/application/application-document/application-document.service.ts +++ b/alcs-frontend/src/app/services/application/application-document/application-document.service.ts @@ -137,6 +137,25 @@ export class ApplicationDocumentService { return res; } + async attachCertificateOfTitle(fileNumber: string, parcelUuid: string, createDto: CreateDocumentDto) { + const file = createDto.file; + const isValidSize = verifyFileSize(file, this.toastService); + if (!isValidSize) { + return; + } + + let formData: FormData = new FormData(); + formData.append('documentType', createDto.typeCode); + formData.append('source', createDto.source); + formData.append('visibilityFlags', createDto.visibilityFlags.join(', ')); + formData.append('fileName', createDto.fileName); + formData.append('file', file, file.name); + formData.append('parcelUuid', parcelUuid); + const res = await firstValueFrom(this.http.post(`${this.url}/application/${fileNumber}/CERT`, formData)); + this.toastService.showSuccessToast('Review document uploaded'); + return res; + } + async updateSort(sortOrder: { uuid: string; order: number }[]) { try { await firstValueFrom(this.http.post(`${this.url}/sort`, sortOrder)); diff --git a/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.spec.ts b/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.spec.ts new file mode 100644 index 0000000000..7dbe4bebaf --- /dev/null +++ b/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.spec.ts @@ -0,0 +1,43 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of } from 'rxjs'; +import { ToastService } from '../../toast/toast.service'; +import { ApplicationSubmissionDto, SubmittedApplicationOwnerDto } from '../application.dto'; + +import { ApplicationParcelService } from './application-parcel.service'; + +describe('ApplicationParcelService', () => { + let service: ApplicationParcelService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { provide: ToastService, useValue: mockToastService }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(ApplicationParcelService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should successfully fetch parcels', async () => { + mockHttpClient.get.mockReturnValue(of([])); + + const result = await service.fetchParcels('1'); + + expect(result).toEqual([]); + expect(mockHttpClient.get).toBeCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.ts b/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.ts new file mode 100644 index 0000000000..d6f872b244 --- /dev/null +++ b/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.ts @@ -0,0 +1,24 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { ToastService } from '../../toast/toast.service'; +import { ApplicationParcelDto } from '../application.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationParcelService { + private baseUrl = `${environment.apiUrl}/application-parcel`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetchParcels(fileNumber: string): Promise { + try { + return firstValueFrom(this.http.get(`${this.baseUrl}/${fileNumber}`)); + } catch (e) { + this.toastService.showErrorToast('Failed to fetch Application Parcels'); + throw e; + } + } +} diff --git a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts index 96578b4656..e43c60d891 100644 --- a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts @@ -13,6 +13,26 @@ describe('ApplicationSubmissionService', () => { let mockHttpClient: DeepMocked; const mockSubmittedApplication: ApplicationSubmissionDto = { + applicant: '', + canEdit: false, + canReview: false, + canView: false, + createdAt: '', + fileNumber: '', + lastStatusUpdate: 0, + localGovernmentUuid: '', + owners: [], + soilAlreadyRemovedArea: null, + soilAlreadyRemovedAverageDepth: null, + soilAlreadyRemovedMaximumDepth: null, + soilAlreadyRemovedVolume: null, + soilToPlaceArea: null, + soilToPlaceVolume: null, + soilToRemoveAverageDepth: null, + soilToRemoveMaximumDepth: null, + type: '', + updatedAt: '', + uuid: '', naruExistingStructures: null, naruFillOrigin: null, naruFillType: null, @@ -31,9 +51,6 @@ describe('ApplicationSubmissionService', () => { naruToPlaceMaximumDepth: null, naruToPlaceVolume: null, naruWillImportFill: null, - parcels: [], - otherParcels: [], - documents: [], primaryContact: {} as SubmittedApplicationOwnerDto, parcelsAgricultureDescription: '', parcelsAgricultureImprovementDescription: '', diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 55f4880821..0ef7b869ba 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -2,7 +2,6 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; import { CardDto } from '../card/card.dto'; import { UserDto } from '../user/user.dto'; import { ApplicationRegionDto, ApplicationTypeDto } from './application-code.dto'; -import { ApplicationDocumentDto } from './application-document/application-document.dto'; import { ApplicationLocalGovernmentDto } from './application-local-government/application-local-government.dto'; export enum APPLICATION_SYSTEM_SOURCE_TYPES { @@ -81,7 +80,8 @@ export interface ApplicationParcelDocumentDto { documentUuid: string; } -export interface SubmittedApplicationParcelDto { +export interface ApplicationParcelDto { + uuid: string; pid?: string; pin?: string; legalDescription: string; @@ -91,50 +91,64 @@ export interface SubmittedApplicationParcelDto { ownershipType?: string; crownLandOwnerType?: string; parcelType?: string; - documentUuids: string[]; + certificateOfTitleUuid?: string; owners: SubmittedApplicationOwnerDto[]; - documents: ApplicationParcelDocumentDto[]; } export interface ApplicationSubmissionDto { - parcels: SubmittedApplicationParcelDto[]; - otherParcels: SubmittedApplicationParcelDto[]; - documents: ApplicationDocumentDto[]; + uuid: string; + fileNumber: string; + createdAt: string; + updatedAt: string; + lastStatusUpdate: number; + applicant: string; + type: string; + + typeCode: string; + localGovernmentUuid: string; + canEdit: boolean; + canReview: boolean; + canView: boolean; + owners: SubmittedApplicationOwnerDto[]; hasOtherParcelsInCommunity?: boolean | null; + returnedComment?: string; + + primaryContactOwnerUuid?: string; primaryContact?: SubmittedApplicationOwnerDto; - parcelsAgricultureDescription: string; - parcelsAgricultureImprovementDescription: string; - parcelsNonAgricultureUseDescription: string; - northLandUseType: string; - northLandUseTypeDescription: string; - eastLandUseType: string; - eastLandUseTypeDescription: string; - southLandUseType: string; - southLandUseTypeDescription: string; - westLandUseType: string; - westLandUseTypeDescription: string; - typeCode: string; - //NFU Data - nfuHectares: string | null; + parcelsAgricultureDescription?: string | null; + parcelsAgricultureImprovementDescription?: string | null; + parcelsNonAgricultureUseDescription?: string | null; + northLandUseType?: string | null; + northLandUseTypeDescription?: string | null; + eastLandUseType?: string | null; + eastLandUseTypeDescription?: string | null; + southLandUseType?: string | null; + southLandUseTypeDescription?: string | null; + westLandUseType?: string | null; + westLandUseTypeDescription?: string | null; + + //NFU Specific Fields + nfuHectares: number | null; nfuPurpose: string | null; nfuOutsideLands: string | null; nfuAgricultureSupport: string | null; nfuWillImportFill: boolean | null; - nfuTotalFillPlacement: string | null; - nfuMaxFillDepth: string | null; - nfuFillVolume: string | null; - nfuProjectDurationAmount: string | null; + nfuTotalFillPlacement: number | null; + nfuMaxFillDepth: number | null; + nfuFillVolume: number | null; + nfuProjectDurationAmount: number | null; nfuProjectDurationUnit: string | null; nfuFillTypeDescription: string | null; nfuFillOriginDescription: string | null; - //TUR Data + //TUR Fields turPurpose: string | null; - turOutsideLands: string | null; turAgriculturalActivities: string | null; turReduceNegativeImpacts: string | null; - turTotalCorridorArea: string | null; + turOutsideLands: string | null; + turTotalCorridorArea: number | null; + turAllOwnersNotified?: boolean | null; //Subdivision Fields subdPurpose: string | null; @@ -153,14 +167,14 @@ export interface ApplicationSubmissionDto { soilReduceNegativeImpacts: string | null; soilToRemoveVolume: number | null; soilToRemoveArea: number | null; - soilToRemoveMaximumDepth?: number | null; - soilToRemoveAverageDepth?: number | null; - soilAlreadyRemovedVolume?: number | null; - soilAlreadyRemovedArea?: number | null; - soilAlreadyRemovedMaximumDepth?: number | null; - soilAlreadyRemovedAverageDepth?: number | null; - soilToPlaceVolume?: number | null; - soilToPlaceArea?: number | null; + soilToRemoveMaximumDepth: number | null; + soilToRemoveAverageDepth: number | null; + soilAlreadyRemovedVolume: number | null; + soilAlreadyRemovedArea: number | null; + soilAlreadyRemovedMaximumDepth: number | null; + soilAlreadyRemovedAverageDepth: number | null; + soilToPlaceVolume: number | null; + soilToPlaceArea: number | null; soilToPlaceMaximumDepth: number | null; soilToPlaceAverageDepth: number | null; soilAlreadyPlacedVolume: number | null; diff --git a/docs/jobs.md b/docs/jobs.md index 64f7f08ada..8bce6c365c 100644 --- a/docs/jobs.md +++ b/docs/jobs.md @@ -8,7 +8,7 @@ Using the template below these jobs can be executed inside openshift. Once logge * Replace the JOB_NAME with the jobs name * Replace NAMESPACE with the project where you want to run the job * Replace VERSION_HERE with the version of the image to use -* Replace COMMAND_HERE with the command to execute such as tagDocuments +* Replace COMMAND_HERE with the command to execute such as import ## Template ``` diff --git a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.spec.ts b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.spec.ts index 48a91dec7f..27ac43c374 100644 --- a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.spec.ts @@ -7,6 +7,7 @@ import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; import { ApplicationProfile } from '../../../common/automapper/application.automapper.profile'; import { DOCUMENT_SOURCE } from '../../../document/document.dto'; +import { ApplicationParcelService } from '../../../portal/application-submission/application-parcel/application-parcel.service'; import { CodeService } from '../../code/code.service'; import { DOCUMENT_TYPE } from './application-document-code.entity'; import { ApplicationDocumentController } from './application-document.controller'; @@ -16,6 +17,7 @@ import { ApplicationDocumentService } from './application-document.service'; describe('ApplicationDocumentController', () => { let controller: ApplicationDocumentController; let appDocumentService: DeepMocked; + let mockParcelService: DeepMocked; const mockDocument = { document: { @@ -26,7 +28,8 @@ describe('ApplicationDocumentController', () => { } as ApplicationDocument; beforeEach(async () => { - appDocumentService = createMock(); + appDocumentService = createMock(); + mockParcelService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -45,6 +48,10 @@ describe('ApplicationDocumentController', () => { provide: ApplicationDocumentService, useValue: appDocumentService, }, + { + provide: ApplicationParcelService, + useValue: mockParcelService, + }, { provide: ClsService, useValue: {}, diff --git a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts index 482a68e8e2..24cb516c41 100644 --- a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts +++ b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts @@ -20,6 +20,7 @@ import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, } from '../../../document/document.dto'; +import { ApplicationParcelService } from '../../../portal/application-submission/application-parcel/application-parcel.service'; import { ApplicationDocumentCode, DOCUMENT_TYPE, @@ -40,6 +41,7 @@ import { ApplicationDocumentService } from './application-document.service'; export class ApplicationDocumentController { constructor( private applicationDocumentService: ApplicationDocumentService, + private parcelService: ApplicationParcelService, @InjectMapper() private mapper: Mapper, ) {} @@ -65,23 +67,51 @@ export class ApplicationDocumentController { if (!req.isMultipart()) { throw new BadRequestException('Request is not multipart'); } + const savedDocument = await this.saveUploadedFile(req, fileNumber); - const documentType = req.body.documentType.value as DOCUMENT_TYPE; - const file = req.body.file; - const fileName = req.body.fileName.value as string; - const documentSource = req.body.source.value as DOCUMENT_SOURCE; - const visibilityFlags = req.body.visibilityFlags.value.split(', '); + return this.mapper.map( + savedDocument, + ApplicationDocument, + ApplicationDocumentDto, + ); + } + + @Post('/application/:fileNumber/CERT') + @UserRoles(...ANY_AUTH_ROLE) + async attachCertificateOfTitle( + @Param('fileNumber') fileNumber: string, + @Req() req, + ): Promise { + if (!req.isMultipart()) { + throw new BadRequestException('Request is not multipart'); + } + + const parcelUuid = req.body.parcelUuid.value; + if (!parcelUuid) { + throw new BadRequestException('Request is missing parcel uuid'); + } + + const savedDocument = await this.saveUploadedFile(req, fileNumber); + + const parcel = await this.parcelService.getOneOrFail(parcelUuid); + if (parcel) { + if (parcel.certificateOfTitleUuid) { + const document = await this.applicationDocumentService.get( + parcel.certificateOfTitleUuid, + ); + await this.applicationDocumentService.update({ + uuid: document.uuid, + fileName: `${document.document.fileName}_superseded`, + source: document.document.source as DOCUMENT_SOURCE, + visibilityFlags: [], + documentType: document.type!.code as DOCUMENT_TYPE, + user: document.document.uploadedBy!, + }); + } + + await this.parcelService.setCertificateOfTitle(parcel, savedDocument); + } - const savedDocument = await this.applicationDocumentService.attachDocument({ - fileNumber, - fileName, - file, - user: req.user.entity, - documentType: documentType as DOCUMENT_TYPE, - source: documentSource, - visibilityFlags, - system: DOCUMENT_SYSTEM.ALCS, - }); return this.mapper.map( savedDocument, ApplicationDocument, @@ -218,4 +248,23 @@ export class ApplicationDocumentController { ): Promise { await this.applicationDocumentService.setSorting(data); } + + private async saveUploadedFile(req, fileNumber: string) { + const documentType = req.body.documentType.value as DOCUMENT_TYPE; + const file = req.body.file; + const fileName = req.body.fileName.value as string; + const documentSource = req.body.source.value as DOCUMENT_SOURCE; + const visibilityFlags = req.body.visibilityFlags.value.split(', '); + + return await this.applicationDocumentService.attachDocument({ + fileNumber, + fileName, + file, + user: req.user.entity, + documentType: documentType as DOCUMENT_TYPE, + source: documentSource, + visibilityFlags, + system: DOCUMENT_SYSTEM.ALCS, + }); + } } diff --git a/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.spec.ts b/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.spec.ts new file mode 100644 index 0000000000..4c3822c2cb --- /dev/null +++ b/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.spec.ts @@ -0,0 +1,45 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { ApplicationParcelService } from '../../../portal/application-submission/application-parcel/application-parcel.service'; +import { ApplicationParcelController } from './application-parcel.controller'; + +describe('ApplicationParcelController', () => { + let controller: ApplicationParcelController; + let mockParcelService: DeepMocked; + + beforeEach(async () => { + mockParcelService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [ApplicationParcelController], + providers: [ + { + provide: ApplicationParcelService, + useValue: mockParcelService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + ApplicationParcelController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.ts b/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.ts new file mode 100644 index 0000000000..0fd13d20ae --- /dev/null +++ b/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.ts @@ -0,0 +1,36 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { DocumentService } from '../../../document/document.service'; +import { ApplicationParcelDto } from '../../../portal/application-submission/application-parcel/application-parcel.dto'; +import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; +import { ApplicationParcelService } from '../../../portal/application-submission/application-parcel/application-parcel.service'; +import { ApplicationSubmissionService } from '../application-submission/application-submission.service'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(RolesGuard) +@Controller('application-parcel') +export class ApplicationParcelController { + constructor( + private applicationParcelService: ApplicationParcelService, + @InjectMapper() private mapper: Mapper, + ) {} + + @UserRoles(...ANY_AUTH_ROLE) + @Get('/:fileNumber') + async get(@Param('fileNumber') fileNumber: string) { + const parcels = + await this.applicationParcelService.fetchByApplicationFileId(fileNumber); + + return this.mapper.mapArray( + parcels, + ApplicationParcel, + ApplicationParcelDto, + ); + } +} diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.spec.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.spec.ts index bd4fcbb4ec..12170a45ff 100644 --- a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.spec.ts +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.spec.ts @@ -5,6 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ApplicationOwner } from '../../../portal/application-submission/application-owner/application-owner.entity'; +import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; import { APPLICATION_STATUS } from '../../../portal/application-submission/application-status/application-status.dto'; import { ApplicationStatus } from '../../../portal/application-submission/application-status/application-status.entity'; import { ApplicationSubmission } from '../../../portal/application-submission/application-submission.entity'; @@ -18,12 +19,14 @@ describe('ApplicationSubmissionService', () => { let mockApplicationStatusRepository: DeepMocked< Repository >; + let mockAppParcelRepo: DeepMocked>; let mapper: DeepMocked; beforeEach(async () => { mockApplicationSubmissionRepository = createMock(); mapper = createMock(); mockApplicationStatusRepository = createMock(); + mockAppParcelRepo = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -36,6 +39,10 @@ describe('ApplicationSubmissionService', () => { provide: getRepositoryToken(ApplicationStatus), useValue: mockApplicationStatusRepository, }, + { + provide: getRepositoryToken(ApplicationParcel), + useValue: mockAppParcelRepo, + }, { provide: getMapperToken(), useValue: mapper, @@ -74,15 +81,6 @@ describe('ApplicationSubmissionService', () => { document: true, }, }, - parcels: { - owners: { - type: true, - }, - certificateOfTitle: { - document: true, - }, - ownershipType: true, - }, owners: { type: true, }, diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts index a99409a8f1..737dcb9c65 100644 --- a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts @@ -5,6 +5,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ApplicationOwnerDto } from '../../../portal/application-submission/application-owner/application-owner.dto'; import { ApplicationOwner } from '../../../portal/application-submission/application-owner/application-owner.entity'; +import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; import { APPLICATION_STATUS } from '../../../portal/application-submission/application-status/application-status.dto'; import { ApplicationStatus } from '../../../portal/application-submission/application-status/application-status.entity'; import { ApplicationSubmission } from '../../../portal/application-submission/application-submission.entity'; @@ -17,6 +18,8 @@ export class ApplicationSubmissionService { private applicationSubmissionRepository: Repository, @InjectRepository(ApplicationStatus) private applicationStatusRepository: Repository, + @InjectRepository(ApplicationParcel) + private parcelRepository: Repository, @InjectMapper() private mapper: Mapper, ) {} @@ -30,15 +33,6 @@ export class ApplicationSubmissionService { document: true, }, }, - parcels: { - owners: { - type: true, - }, - certificateOfTitle: { - document: true, - }, - ownershipType: true, - }, owners: { type: true, }, diff --git a/services/apps/alcs/src/alcs/application/application.module.ts b/services/apps/alcs/src/alcs/application/application.module.ts index 3ac9e5b012..0884085b87 100644 --- a/services/apps/alcs/src/alcs/application/application.module.ts +++ b/services/apps/alcs/src/alcs/application/application.module.ts @@ -1,14 +1,17 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationOwnerProfile } from '../../common/automapper/application-owner.automapper.profile'; import { ApplicationParcelProfile } from '../../common/automapper/application-parcel.automapper.profile'; +import { ApplicationSubmissionProfile } from '../../common/automapper/application-submission.automapper.profile'; import { ApplicationSubtaskProfile } from '../../common/automapper/application-subtask.automapper.profile'; import { ApplicationProfile } from '../../common/automapper/application.automapper.profile'; import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; import { ApplicationSubmissionReview } from '../../portal/application-submission-review/application-submission-review.entity'; +import { ApplicationParcel } from '../../portal/application-submission/application-parcel/application-parcel.entity'; import { ApplicationStatus } from '../../portal/application-submission/application-status/application-status.entity'; import { ApplicationSubmission } from '../../portal/application-submission/application-submission.entity'; +import { ApplicationSubmissionModule } from '../../portal/application-submission/application-submission.module'; import { Board } from '../board/board.entity'; import { CardModule } from '../card/card.module'; import { ApplicationType } from '../code/application-code/application-type/application-type.entity'; @@ -24,6 +27,7 @@ import { ApplicationDocumentService } from './application-document/application-d import { ApplicationMeetingController } from './application-meeting/application-meeting.controller'; import { ApplicationMeeting } from './application-meeting/application-meeting.entity'; import { ApplicationMeetingService } from './application-meeting/application-meeting.service'; +import { ApplicationParcelController } from './application-parcel/application-parcel.controller'; import { ApplicationPaused } from './application-paused.entity'; import { ApplicationPausedService } from './application-paused/application-paused.service'; import { ApplicationSubmissionReviewController } from './application-submission-review/application-submission-review.controller'; @@ -45,6 +49,7 @@ import { ApplicationService } from './application.service'; ApplicationDocument, ApplicationDocumentCode, ApplicationLocalGovernment, + ApplicationParcel, Board, ApplicationSubmission, ApplicationSubmissionReview, @@ -55,6 +60,7 @@ import { ApplicationService } from './application.service'; CardModule, CodeModule, FileNumberModule, + forwardRef(() => ApplicationSubmissionModule), ], providers: [ ApplicationService, @@ -69,6 +75,7 @@ import { ApplicationService } from './application.service'; ApplicationLocalGovernmentService, ApplicationSubmissionService, ApplicationSubmissionReviewService, + ApplicationSubmissionProfile, ], controllers: [ ApplicationController, @@ -77,6 +84,7 @@ import { ApplicationService } from './application.service'; ApplicationLocalGovernmentController, ApplicationSubmissionController, ApplicationSubmissionReviewController, + ApplicationParcelController, ], exports: [ ApplicationService, diff --git a/services/apps/alcs/src/commands/tag.ts b/services/apps/alcs/src/commands/tag.ts deleted file mode 100644 index 6eaa8a6e1f..0000000000 --- a/services/apps/alcs/src/commands/tag.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NestFactory } from '@nestjs/core'; -import { NestFastifyApplication } from '@nestjs/platform-fastify'; -import { Logger } from 'nestjs-pino'; -import { MainModule } from '../main.module'; -import { DocumentService } from '../document/document.service'; - -export async function applyDefaultDocumentTags() { - const app = await NestFactory.create(MainModule, { - bufferLogs: false, - }); - app.useLogger(app.get(Logger)); - - await app.init(); - - const documentService = app.get(DocumentService); - await documentService.applyDefaultTags(); - process.exit(0); -} diff --git a/services/apps/alcs/src/common/automapper/application-submission.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-submission.automapper.profile.ts index 05b15def7d..82a094d1c4 100644 --- a/services/apps/alcs/src/common/automapper/application-submission.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-submission.automapper.profile.ts @@ -1,6 +1,7 @@ import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; +import { AlcsApplicationSubmissionDto } from '../../alcs/application/application.dto'; import { ApplicationOwnerDetailedDto, ApplicationOwnerDto, @@ -85,6 +86,36 @@ export class ApplicationSubmissionProfile extends AutomapperProfile { }), ), ); + + createMap( + mapper, + ApplicationSubmission, + AlcsApplicationSubmissionDto, + forMember( + (a) => a.lastStatusUpdate, + mapFrom((ad) => { + if (ad.statusHistory.length > 0) { + return ad.statusHistory[0].time; + } + //For older applications before status history was created + return Date.now(); + }), + ), + forMember( + (a) => a.owners, + mapFrom((ad) => { + if (ad.owners) { + return this.mapper.mapArray( + ad.owners, + ApplicationOwner, + ApplicationOwnerDetailedDto, + ); + } else { + return []; + } + }), + ), + ); }; } } diff --git a/services/apps/alcs/src/document/document.service.spec.ts b/services/apps/alcs/src/document/document.service.spec.ts index b9136b4a52..970d417347 100644 --- a/services/apps/alcs/src/document/document.service.spec.ts +++ b/services/apps/alcs/src/document/document.service.spec.ts @@ -95,4 +95,36 @@ describe('DocumentService', () => { expect(mockRepository.save).toHaveBeenCalledTimes(1); expect(res).toEqual(mockDoc); }); + + it('should call repository save on update Document', async () => { + const mockDoc = new Document({ + mimeType: 'mimeType', + fileKey: 'fileKey', + fileName: 'fileName', + uploadedBy: null, + source: DOCUMENT_SOURCE.APPLICANT, + }); + mockRepository.save.mockResolvedValue(mockDoc); + await service.update(mockDoc, { + source: DOCUMENT_SOURCE.APPLICANT, + fileName: 'file', + }); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + }); + + it('should call through to repo for get', async () => { + const mockDoc = new Document({ + mimeType: 'mimeType', + fileKey: 'fileKey', + fileName: 'fileName', + uploadedBy: null, + source: DOCUMENT_SOURCE.APPLICANT, + }); + mockRepository.findOneOrFail.mockResolvedValue(mockDoc); + + const res = await service.getDocument(''); + + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); + expect(res).toEqual(mockDoc); + }); }); diff --git a/services/apps/alcs/src/document/document.service.ts b/services/apps/alcs/src/document/document.service.ts index f841832c14..269c0139fd 100644 --- a/services/apps/alcs/src/document/document.service.ts +++ b/services/apps/alcs/src/document/document.service.ts @@ -147,32 +147,6 @@ export class DocumentService { }); } - //One time function used to apply default tags to existing documents - async applyDefaultTags() { - const documents = await this.documentRepository.find(); - console.warn( - `Applying default document tags to ${documents.length} Documents`, - ); - for (const document of documents) { - document.tags = DEFAULT_DB_TAGS; - const command = new PutObjectTaggingCommand({ - Bucket: this.config.get('STORAGE.BUCKET'), - Key: document.fileKey, - Tagging: { - TagSet: [ - { - Key: 'ORCS-Classification', - Value: '85100-20', - }, - ], - }, - }); - await this.dataStore.send(command); - await this.documentRepository.save(document); - } - console.warn(`${documents.length} Documents tagged successfully`); - } - async createDocumentRecord(data: CreateDocumentDto) { return this.documentRepository.save( new Document({ diff --git a/services/apps/alcs/src/main.ts b/services/apps/alcs/src/main.ts index ac36a0510d..5da3fc6e70 100644 --- a/services/apps/alcs/src/main.ts +++ b/services/apps/alcs/src/main.ts @@ -13,7 +13,6 @@ import { Logger } from 'nestjs-pino'; import { install } from 'source-map-support'; import { generateModuleGraph } from './commands/graph'; import { importNOIs } from './commands/import'; -import { applyDefaultDocumentTags } from './commands/tag'; import { MainModule } from './main.module'; const registerSwagger = (app: NestFastifyApplication) => { @@ -110,9 +109,6 @@ async function bootstrap() { if (extraArg == 'import') { await importNOIs(); } - if (extraArg == 'tagDocuments') { - await applyDefaultDocumentTags(); - } // config variables const port: number = config.get('ALCS.PORT'); diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.module.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.module.ts index 5b5c98cbed..41f1cb4a80 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.module.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.module.ts @@ -11,7 +11,7 @@ import { ApplicationSubmissionReviewService } from './application-submission-rev imports: [ TypeOrmModule.forFeature([ApplicationSubmissionReview]), forwardRef(() => ApplicationSubmissionModule), - ApplicationModule, + forwardRef(() => ApplicationModule), ], providers: [ ApplicationSubmissionReviewService, diff --git a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.dto.ts b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.dto.ts index 6b1f4876a2..e77990ae9d 100644 --- a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.dto.ts @@ -7,6 +7,7 @@ import { IsOptional, IsString, } from 'class-validator'; +import { Column } from 'typeorm'; import { ApplicationDocumentDto } from '../../../alcs/application/application-document/application-document.dto'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; import { ApplicationOwnerDetailedDto } from '../application-owner/application-owner.dto'; @@ -20,6 +21,9 @@ export class ApplicationParcelDto { @AutoMap() applicationSubmissionUuid: string; + @AutoMap(() => String) + certificateOfTitleUuid: string | null; + @AutoMap(() => String) pid?: string | null; diff --git a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts index d618e6872e..71e65c9661 100644 --- a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts @@ -139,4 +139,8 @@ export class ApplicationParcel extends Base { onDelete: 'SET NULL', }) certificateOfTitle?: ApplicationDocument; + + @AutoMap(() => String) + @Column({ nullable: true }) + certificateOfTitleUuid: string | null; } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts index 6dd1d4a569..6b204b2ab0 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationModule } from '../../alcs/application/application.module'; import { AuthorizationModule } from '../../common/authorization/authorization.module'; @@ -34,10 +34,10 @@ import { NaruSubtype } from './naru-subtype/naru-subtype.entity'; ApplicationOwnerType, NaruSubtype, ]), - ApplicationModule, + forwardRef(() => ApplicationModule), AuthorizationModule, - DocumentModule, - PdfGenerationModule, + forwardRef(() => DocumentModule), + forwardRef(() => PdfGenerationModule), ], providers: [ ApplicationSubmissionService, diff --git a/services/package.json b/services/package.json index 0e16529817..c186dc98db 100644 --- a/services/package.json +++ b/services/package.json @@ -21,7 +21,6 @@ "alcs:start:prod": "node dist/apps/alcs/main TZ=UTC", "alcs:import": "nest start alcs TZ=UTC -- import", "alcs:graph": "nest start TZ=UTC -- graph", - "alcs:tag": "nest start TZ=UTC -- tagDocuments", "alcs:typeorm": "typeorm-ts-node-esm -d apps/alcs/src/providers/typeorm/datasource.cli.orm.config.ts", "alcs:typeorm:cli": "node --require ts-node/register ./node_modules/typeorm/cli.js", "alcs:migration:generate": "npm run alcs:typeorm -- migration:generate apps/alcs/src/providers/typeorm/migrations/$npm_config_name", From 99322d17451f302fd0c2ac774966d8dc8e4b8ee9 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 27 Jun 2023 16:11:25 -0700 Subject: [PATCH 021/954] Code Review Feedback --- .../document-upload-dialog.component.ts | 21 +++++----- .../application-submission.service.spec.ts | 40 +++++++++---------- .../application-submission.service.ts | 3 -- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts index b0264511dc..5ac7312203 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts @@ -103,21 +103,26 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { const parcelId = await this.parcelId.value; const dto = { - file: this.pendingFile, fileName: this.name.getRawValue()!, source: this.source.getRawValue() as DOCUMENT_SOURCE, typeCode: this.type.getRawValue() as DOCUMENT_TYPE, visibilityFlags, }; + + const file = this.pendingFile; this.isSaving = true; - if (parcelId && dto.file && this.type.value === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { - // @ts-ignore File is checked above to not be undefined, typescript still thinks it might be undefined - await this.applicationDocumentService.attachCertificateOfTitle(this.data.fileId, parcelId, dto); + if (parcelId && file && this.type.value === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { + await this.applicationDocumentService.attachCertificateOfTitle(this.data.fileId, parcelId, { + ...dto, + file, + }); } else if (this.data.existingDocument) { await this.applicationDocumentService.update(this.data.existingDocument.uuid, dto); - } else if (dto.file !== undefined) { - // @ts-ignore File is checked above to not be undefined, typescript still thinks it might be undefined - await this.applicationDocumentService.upload(this.data.fileId, dto); + } else if (file !== undefined) { + await this.applicationDocumentService.upload(this.data.fileId, { + ...dto, + file, + }); } this.dialog.close(true); this.isSaving = false; @@ -144,13 +149,11 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { async prepareCertificateOfTitleUpload(uuid?: string) { const parcels = await this.parcelService.fetchParcels(this.data.fileId); - debugger; if (parcels.length > 0) { this.parcelId.setValidators([Validators.required]); this.parcelId.updateValueAndValidity(); this.visibleToInternal.setValue(true); - this.visibleToPublic.setValue(true); this.source.setValue(DOCUMENT_SOURCE.APPLICANT); const selectedParcel = parcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.spec.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.spec.ts index 12170a45ff..a7aeaaaca5 100644 --- a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.spec.ts +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.spec.ts @@ -1,11 +1,12 @@ -import { Mapper } from '@automapper/core'; -import { getMapperToken } from '@automapper/nestjs'; +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { ApplicationOwnerProfile } from '../../../common/automapper/application-owner.automapper.profile'; +import { ApplicationSubmissionProfile } from '../../../common/automapper/application-submission.automapper.profile'; import { ApplicationOwner } from '../../../portal/application-submission/application-owner/application-owner.entity'; -import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; import { APPLICATION_STATUS } from '../../../portal/application-submission/application-status/application-status.dto'; import { ApplicationStatus } from '../../../portal/application-submission/application-status/application-status.entity'; import { ApplicationSubmission } from '../../../portal/application-submission/application-submission.entity'; @@ -19,18 +20,21 @@ describe('ApplicationSubmissionService', () => { let mockApplicationStatusRepository: DeepMocked< Repository >; - let mockAppParcelRepo: DeepMocked>; - let mapper: DeepMocked; beforeEach(async () => { mockApplicationSubmissionRepository = createMock(); - mapper = createMock(); mockApplicationStatusRepository = createMock(); - mockAppParcelRepo = createMock(); const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], providers: [ ApplicationSubmissionService, + ApplicationSubmissionProfile, + ApplicationOwnerProfile, { provide: getRepositoryToken(ApplicationSubmission), useValue: mockApplicationSubmissionRepository, @@ -39,14 +43,6 @@ describe('ApplicationSubmissionService', () => { provide: getRepositoryToken(ApplicationStatus), useValue: mockApplicationStatusRepository, }, - { - provide: getRepositoryToken(ApplicationParcel), - useValue: mockAppParcelRepo, - }, - { - provide: getMapperToken(), - useValue: mapper, - }, ], }).compile(); @@ -89,15 +85,19 @@ describe('ApplicationSubmissionService', () => { }); it('should properly map to dto', async () => { - mapper.mapAsync.mockResolvedValue({} as any); - - const fakeSubmission = createMock(); - fakeSubmission.owners = [new ApplicationOwner()]; + const fakeSubmission = createMock({ + primaryContactOwnerUuid: 'uuid', + }); + fakeSubmission.owners = [ + new ApplicationOwner({ + uuid: 'uuid', + }), + ]; const result = await service.mapToDto(fakeSubmission); - expect(mapper.mapAsync).toBeCalledTimes(2); expect(result).toBeDefined(); + expect(result.primaryContact).toBeDefined(); }); it('should successfully retrieve status from repo', async () => { diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts index 737dcb9c65..eaa5dba45c 100644 --- a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts @@ -5,7 +5,6 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ApplicationOwnerDto } from '../../../portal/application-submission/application-owner/application-owner.dto'; import { ApplicationOwner } from '../../../portal/application-submission/application-owner/application-owner.entity'; -import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; import { APPLICATION_STATUS } from '../../../portal/application-submission/application-status/application-status.dto'; import { ApplicationStatus } from '../../../portal/application-submission/application-status/application-status.entity'; import { ApplicationSubmission } from '../../../portal/application-submission/application-submission.entity'; @@ -18,8 +17,6 @@ export class ApplicationSubmissionService { private applicationSubmissionRepository: Repository, @InjectRepository(ApplicationStatus) private applicationStatusRepository: Repository, - @InjectRepository(ApplicationParcel) - private parcelRepository: Repository, @InjectMapper() private mapper: Mapper, ) {} From f1c9d214135799a7bbc0766e8d0a756ad8101c00 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Tue, 27 Jun 2023 16:36:54 -0700 Subject: [PATCH 022/954] Feature/alcs 767 (#734) new reconsideration fields for applications originated in portal always display review outcome date in edit window for reconsideration of type 33 --- .../decision-input-v2.component.ts | 6 +- ...edit-reconsideration-dialog.component.html | 41 ++++++++++---- ...t-reconsideration-dialog.component.spec.ts | 5 +- .../edit-reconsideration-dialog.component.ts | 54 +++++++++++++----- .../post-decision.component.html | 52 +++++++++++++----- .../post-decision.component.scss | 6 ++ .../post-decision/post-decision.component.ts | 4 +- ...create-reconsideration-dialog.component.ts | 29 ++++++++-- .../create/create-reconsideration-dialog.html | 54 +++++++++++++----- .../reconsideration-dialog.component.spec.ts | 3 +- .../application-reconsideration.dto.ts | 13 +++++ .../application-decision.dto.ts | 1 + .../pipes/boolean-to-string.pipe.spec.ts | 8 +++ .../shared/pipes/boolean-to-string.pipe.ts | 17 ++++++ alcs-frontend/src/app/shared/shared.module.ts | 3 + .../application-decision.dto.ts | 3 + .../application-reconsideration.controller.ts | 2 +- .../application-reconsideration.dto.ts | 55 ++++++++++++++++--- .../application-reconsideration.entity.ts | 37 ++++++++++--- .../application-reconsideration.service.ts | 9 +++ .../application.automapper.profile.ts | 5 +- ...687808298996-new_reconsideration_fields.ts | 43 +++++++++++++++ 22 files changed, 370 insertions(+), 80 deletions(-) create mode 100644 alcs-frontend/src/app/shared/pipes/boolean-to-string.pipe.spec.ts create mode 100644 alcs-frontend/src/app/shared/pipes/boolean-to-string.pipe.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1687808298996-new_reconsideration_fields.ts diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index c8457c1fe1..7c9d951799 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -238,7 +238,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { .filter( (modification) => (existingDecision && existingDecision.modifies?.uuid === modification.uuid) || - (modification.reviewOutcome.code === 'PRC' && modification.resultingDecision === null) + (modification.reviewOutcome.code === 'PRC' && !modification.resultingDecision) ) .map((modification, index) => ({ label: `Modification Request #${modifications.length - index} - ${modification.modifiesDecisions @@ -247,12 +247,12 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { uuid: modification.uuid, type: PostDecisionType.Modification, })); - + const mappedRecons = reconsiderations .filter( (reconsideration) => (existingDecision && existingDecision.reconsiders?.uuid === reconsideration.uuid) || - (reconsideration.reviewOutcome?.code === 'PRC' && reconsideration.resultingDecision === null) + (reconsideration.reviewOutcome?.code === 'PRC' && !reconsideration.resultingDecision) ) .map((reconsideration, index) => ({ label: `Reconsideration Request #${reconsiderations.length - index} - ${reconsideration.reconsideredDecisions diff --git a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html index 7c3b80b56c..cd3c565515 100644 --- a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html +++ b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.html @@ -25,7 +25,7 @@

Edit Reconsideration

-
+
Reconsideration Type * Edit Reconsideration
+
+ New Evidence* + + Yes + No + +
+ +
+ Incorrect or False Info* + + Yes + No + +
+ +
+ New Proposal* + + Yes + No + +
+
Review Outcome * @@ -46,15 +70,7 @@

Edit Reconsideration

- + Outcome Notification Date @@ -71,6 +87,11 @@

Edit Reconsideration

+ + + Request Description + +
diff --git a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.spec.ts index 6ccc361c62..a4a9fbdc39 100644 --- a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.spec.ts @@ -27,7 +27,10 @@ describe('EditReconsiderationDialogComponent', () => { provide: ToastService, useValue: {}, }, - { provide: MAT_DIALOG_DATA, useValue: { existingDecision: { type: {}, reconsideredDecisions: [] } } }, + { + provide: MAT_DIALOG_DATA, + useValue: { existingRecon: { type: {}, reconsideredDecisions: [], application: { source: 'fake' } } }, + }, { provide: MatDialogRef, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], diff --git a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.ts b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.ts index 5f2af7f12b..6bba254b83 100644 --- a/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component.ts @@ -11,6 +11,7 @@ import { ApplicationDecisionService } from '../../../../services/application/dec import { ToastService } from '../../../../services/toast/toast.service'; import { BaseCodeDto } from '../../../../shared/dto/base.dto'; import { formatDateForApi } from '../../../../shared/utils/api-date-formatter'; +import { parseBooleanToString, parseStringToBoolean } from '../../../../shared/utils/boolean-helper'; @Component({ selector: 'app-edit-reconsideration-dialog', @@ -24,12 +25,16 @@ export class EditReconsiderationDialogComponent implements OnInit { typeControl = new FormControl(undefined, [Validators.required]); reviewOutcomeCodeControl = new FormControl(null); - form = new FormGroup({ + form: FormGroup = new FormGroup({ submittedDate: new FormControl(undefined, [Validators.required]), type: this.typeControl, reviewOutcomeCode: this.reviewOutcomeCodeControl, reviewDate: new FormControl(null), reconsidersDecisions: new FormControl([], [Validators.required]), + description: new FormControl('', [Validators.required]), + isNewProposal: new FormControl(undefined, [Validators.required]), + isIncorrectFalseInfo: new FormControl(undefined, [Validators.required]), + isNewEvidence: new FormControl(undefined, [Validators.required]), }); decisions: { uuid: string; resolution: string }[] = []; @@ -37,7 +42,7 @@ export class EditReconsiderationDialogComponent implements OnInit { @Inject(MAT_DIALOG_DATA) public data: { fileNumber: string; - existingDecision: ApplicationReconsiderationDetailedDto; + existingRecon: ApplicationReconsiderationDetailedDto; codes: BaseCodeDto[]; }, private dialogRef: MatDialogRef, @@ -46,15 +51,20 @@ export class EditReconsiderationDialogComponent implements OnInit { private toastService: ToastService ) { this.codes = data.codes; + this.form.patchValue({ - submittedDate: new Date(data.existingDecision.submittedDate), - type: data.existingDecision.type.code, + submittedDate: new Date(data.existingRecon.submittedDate), + type: data.existingRecon.type.code, reviewOutcomeCode: - data.existingDecision.type.code === RECONSIDERATION_TYPE.T_33 - ? data.existingDecision.reviewOutcome?.code || 'PEN' + data.existingRecon.type.code === RECONSIDERATION_TYPE.T_33 + ? data.existingRecon.reviewOutcome?.code || 'PEN' : null, - reviewDate: data.existingDecision.reviewDate ? new Date(data.existingDecision.reviewDate) : null, - reconsidersDecisions: data.existingDecision.reconsideredDecisions.map((dec) => dec.uuid), + reviewDate: data.existingRecon.reviewDate ? new Date(data.existingRecon.reviewDate) : null, + reconsidersDecisions: data.existingRecon.reconsideredDecisions.map((dec) => dec.uuid), + description: data.existingRecon.description ?? null, + isNewProposal: parseBooleanToString(data.existingRecon.isNewProposal), + isIncorrectFalseInfo: parseBooleanToString(data.existingRecon.isIncorrectFalseInfo), + isNewEvidence: parseBooleanToString(data.existingRecon.isNewEvidence), }); } @@ -65,17 +75,31 @@ export class EditReconsiderationDialogComponent implements OnInit { async onSubmit() { this.isLoading = true; - const { submittedDate, type, reviewOutcomeCode, reviewDate, reconsidersDecisions } = this.form.getRawValue(); + const { + submittedDate, + type, + reviewOutcomeCode, + reviewDate, + reconsidersDecisions, + description, + isNewProposal, + isIncorrectFalseInfo, + isNewEvidence, + } = this.form.getRawValue(); const data: UpdateApplicationReconsiderationDto = { submittedDate: formatDateForApi(submittedDate!), reviewOutcomeCode: reviewOutcomeCode, typeCode: type!, reviewDate: reviewDate ? formatDateForApi(reviewDate) : reviewDate, reconsideredDecisionUuids: reconsidersDecisions || [], + description: description, + isNewProposal: parseStringToBoolean(isNewProposal), + isIncorrectFalseInfo: parseStringToBoolean(isIncorrectFalseInfo), + isNewEvidence: parseStringToBoolean(isNewEvidence), }; try { - await this.reconsiderationService.update(this.data.existingDecision.uuid, { ...data }); + await this.reconsiderationService.update(this.data.existingRecon.uuid, { ...data }); this.toastService.showSuccessToast('Reconsideration updated'); } finally { this.isLoading = false; @@ -86,10 +110,12 @@ export class EditReconsiderationDialogComponent implements OnInit { async loadDecisions(fileNumber: string) { const decisions = await this.decisionService.fetchByApplication(fileNumber); if (decisions.length > 0) { - this.decisions = decisions.map((decision) => ({ - uuid: decision.uuid, - resolution: `#${decision.resolutionNumber}/${decision.resolutionYear}`, - })); + this.decisions = decisions + .filter((e) => !e.isDraft) + .map((decision) => ({ + uuid: decision.uuid, + resolution: `#${decision.resolutionNumber}/${decision.resolutionYear}`, + })); } } diff --git a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html index a9f395c2d2..2ad9a4abc4 100644 --- a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html +++ b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.html @@ -40,25 +40,27 @@
Reconsideration Type
{{ reconsideration.type.label }}
-
-
Review Outcome
- - - - {{ - reconsideration.reviewOutcome.label - }} -
+
Resolutions to be Reconsidered
{{ reconsideration.reconsidersDecisionsNumbers.join(', ') }}
-
-
Resulting Resolution
- #{{ reconsideration.resultingDecision.resolutionNumber }}/{{ reconsideration.resultingDecision.resolutionYear }} + +
+
New Evidence
+ {{ reconsideration.isNewEvidence | booleanToString }}
+ +
+
Incorrect or False Info
+ {{ reconsideration.isIncorrectFalseInfo | booleanToString }} +
+ +
+
New Proposal
+ {{ reconsideration.isNewProposal | booleanToString }} +
+
Outcome Notification Date @@ -73,6 +75,28 @@
>
+ +
+
Review Outcome
+ + + + {{ + reconsideration.reviewOutcome.label + }} +
+ +
+
Resulting Resolution
+ #{{ reconsideration.resultingDecision.resolutionNumber }}/{{ reconsideration.resultingDecision.resolutionYear }} +
+ +
+
Request Description
+ {{ reconsideration.description }} +
diff --git a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.scss b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.scss index 77afb94fab..81b0597964 100644 --- a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.scss +++ b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.scss @@ -94,6 +94,7 @@ section { display: grid; grid-template-columns: 1fr 1fr; grid-row-gap: 24px; + column-gap: 12px; .subheading2 { margin-bottom: 6px !important; @@ -101,5 +102,10 @@ section { & > div { font-size: 16px; + word-wrap: break-word; + } + + .full-grid-line { + grid-column: 1 / 3; } } diff --git a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.ts b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.ts index 37b9c4922f..66ba09cbb6 100644 --- a/alcs-frontend/src/app/features/application/post-decision/post-decision.component.ts +++ b/alcs-frontend/src/app/features/application/post-decision/post-decision.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { Subject, combineLatestWith, takeUntil, tap } from 'rxjs'; +import { combineLatestWith, Subject, takeUntil, tap } from 'rxjs'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; import { ApplicationModificationDto } from '../../../services/application/application-modification/application-modification.dto'; import { ApplicationModificationService } from '../../../services/application/application-modification/application-modification.service'; @@ -88,7 +88,7 @@ export class PostDecisionComponent implements OnInit, OnDestroy { autoFocus: false, data: { fileNumber: this.fileNumber, - existingDecision: reconsideration, + existingRecon: reconsideration, codes: this.reconCodes, }, }) diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts index b92a8044c4..02713c278c 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.component.ts @@ -16,6 +16,7 @@ import { ApplicationService } from '../../../../../services/application/applicat import { ApplicationDecisionService } from '../../../../../services/application/decision/application-decision-v1/application-decision.service'; import { CardService } from '../../../../../services/card/card.service'; import { ToastService } from '../../../../../services/toast/toast.service'; +import { parseStringToBoolean } from '../../../../../shared/utils/boolean-helper'; @Component({ selector: 'app-create', @@ -43,8 +44,12 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { reconTypeControl = new FormControl(null, [Validators.required]); localGovernmentControl = new FormControl(null, [Validators.required]); reconsidersDecisions = new FormControl([], [Validators.required]); + descriptionControl = new FormControl('', [Validators.required]); + isNewProposalControl = new FormControl(undefined, [Validators.required]); + isIncorrectFalseInfoControl = new FormControl(undefined, [Validators.required]); + isNewEvidenceControl = new FormControl(undefined, [Validators.required]); - createForm = new FormGroup({ + createForm: FormGroup = new FormGroup({ applicationType: this.applicationTypeControl, fileNumber: this.fileNumberControl, applicant: this.applicantControl, @@ -53,6 +58,10 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { submittedDate: this.submittedDateControl, reconType: this.reconTypeControl, reconsidersDecisions: this.reconsidersDecisions, + description: this.descriptionControl, + isNewProposal: this.isNewProposalControl, + isIncorrectFalseInfo: this.isIncorrectFalseInfoControl, + isNewEvidence: this.isNewEvidenceControl, }); constructor( @@ -150,6 +159,10 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { // card details boardCode: this.currentBoardCode, reconsideredDecisionUuids: formValues.reconsidersDecisions!, + description: formValues.description, + isNewProposal: parseStringToBoolean(formValues.isNewProposal), + isIncorrectFalseInfo: parseStringToBoolean(formValues.isIncorrectFalseInfo), + isNewEvidence: parseStringToBoolean(formValues.isNewEvidence), }; if (!recon.boardCode) { @@ -173,6 +186,10 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { this.submittedDateControl.reset(); this.reconTypeControl.reset(); this.reconsidersDecisions.reset(); + this.descriptionControl.reset(); + this.isIncorrectFalseInfoControl.reset(); + this.isNewEvidenceControl.reset(); + this.isNewProposalControl.reset(); this.fileNumberControl.enable(); this.applicantControl.enable(); @@ -194,10 +211,12 @@ export class CreateReconsiderationDialogComponent implements OnInit, OnDestroy { async loadDecisions(fileNumber: string) { const decisions = await this.decisionService.fetchByApplication(fileNumber); if (decisions.length > 0) { - this.decisions = decisions.map((decision) => ({ - uuid: decision.uuid, - resolution: `#${decision.resolutionNumber}/${decision.resolutionYear}`, - })); + this.decisions = decisions + .filter((e) => !e.isDraft) + .map((decision) => ({ + uuid: decision.uuid, + resolution: `#${decision.resolutionNumber}/${decision.resolutionYear}`, + })); this.reconsidersDecisions.enable(); } } diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html index ff3913d327..462d733b60 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/create/create-reconsideration-dialog.html @@ -130,8 +130,22 @@

Create Reconsideration

>
+
+ + Request Submission Date + + + + +
- Decisions to be Reconsidered + Resolutions to be Reconsidered {{decision.resolution}} @@ -150,21 +164,33 @@

Create Reconsideration

> -
- - Reconsideration Request Submitted to ALC - - - - +
+ New Evidence* + + Yes + No + +
+
+ Incorrect or False Info* + + Yes + No +
+
+ New Proposal* + + Yes + No + +
+ + Request Description + +
+
warning diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.spec.ts index ac1e3f696a..31b0b2b2e4 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.spec.ts @@ -1,7 +1,7 @@ import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MatMenuModule } from '@angular/material/menu'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { RouterTestingModule } from '@angular/router/testing'; @@ -52,6 +52,7 @@ describe('ReconsiderationDialogComponent', () => { } as ApplicationRegionDto, localGovernment: {} as ApplicationLocalGovernmentDto, decisionMeetings: [], + source: 'fake', }, card: { status: { diff --git a/alcs-frontend/src/app/services/application/application-reconsideration/application-reconsideration.dto.ts b/alcs-frontend/src/app/services/application/application-reconsideration/application-reconsideration.dto.ts index 37258495bc..8be327efb9 100644 --- a/alcs-frontend/src/app/services/application/application-reconsideration/application-reconsideration.dto.ts +++ b/alcs-frontend/src/app/services/application/application-reconsideration/application-reconsideration.dto.ts @@ -22,6 +22,7 @@ export interface ApplicationForReconsiderationDto { fileNumber: string; type: ApplicationTypeDto; applicant: string; + source: string; region: ApplicationRegionDto; localGovernment: ApplicationLocalGovernmentDto; decisionMeetings: ApplicationDecisionMeetingDto[]; @@ -44,6 +45,10 @@ export interface ApplicationReconsiderationDto { reviewOutcome?: ReconsiderationReviewOutcomeTypeDto | null; reconsideredDecisions: ApplicationDecisionDto[]; resultingDecision: ApplicationDecisionDto | null; + description?: string; + isNewProposal?: boolean | null; + isIncorrectFalseInfo?: boolean | null; + isNewEvidence?: boolean | null; } export interface ApplicationReconsiderationDetailedDto extends ApplicationReconsiderationDto {} @@ -58,6 +63,10 @@ export interface CreateApplicationReconsiderationDto { reconTypeCode: string; boardCode: string; reconsideredDecisionUuids: string[]; + description?: string; + isNewProposal?: boolean | null; + isIncorrectFalseInfo?: boolean | null; + isNewEvidence?: boolean | null; } export interface UpdateApplicationReconsiderationDto { @@ -66,4 +75,8 @@ export interface UpdateApplicationReconsiderationDto { reviewDate?: number | null; reviewOutcomeCode?: string | null; reconsideredDecisionUuids?: string[]; + description?: string; + isNewProposal?: boolean | null; + isIncorrectFalseInfo?: boolean | null; + isNewEvidence?: boolean | null; } diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v1/application-decision.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v1/application-decision.dto.ts index 1d980d8900..3f2184de92 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v1/application-decision.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v1/application-decision.dto.ts @@ -54,6 +54,7 @@ export interface ApplicationDecisionDto { reconsiders?: LinkedResolutionDto; reconsideredBy?: LinkedResolutionDto[]; modifiedBy?: LinkedResolutionDto[]; + isDraft: boolean; } export interface LinkedResolutionDto { diff --git a/alcs-frontend/src/app/shared/pipes/boolean-to-string.pipe.spec.ts b/alcs-frontend/src/app/shared/pipes/boolean-to-string.pipe.spec.ts new file mode 100644 index 0000000000..2ecc0baa98 --- /dev/null +++ b/alcs-frontend/src/app/shared/pipes/boolean-to-string.pipe.spec.ts @@ -0,0 +1,8 @@ +import { BooleanToStringPipe } from './boolean-to-string.pipe'; + +describe('BooleanToStringPipe', () => { + it('create an instance', () => { + const pipe = new BooleanToStringPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/pipes/boolean-to-string.pipe.ts b/alcs-frontend/src/app/shared/pipes/boolean-to-string.pipe.ts new file mode 100644 index 0000000000..a496835607 --- /dev/null +++ b/alcs-frontend/src/app/shared/pipes/boolean-to-string.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'booleanToString', +}) +export class BooleanToStringPipe implements PipeTransform { + transform(value: boolean | undefined | null): string { + switch (value) { + case true: + return 'Yes'; + case false: + return 'No'; + default: + return ''; + } + } +} diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 737d872de8..960399ce57 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -58,6 +58,7 @@ import { TimeTrackerComponent } from './time-tracker/time-tracker.component'; import { TimelineComponent } from './timeline/timeline.component'; import { DATE_FORMATS } from './utils/date-format'; import { ExtensionsDatepickerFormatter } from './utils/extensions-datepicker-formatter'; +import { BooleanToStringPipe } from './pipes/boolean-to-string.pipe'; @NgModule({ declarations: [ @@ -83,6 +84,7 @@ import { ExtensionsDatepickerFormatter } from './utils/extensions-datepicker-for StaffJournalNoteComponent, StaffJournalNoteInputComponent, InlineReviewOutcomeComponent, + BooleanToStringPipe, NoDataComponent, ], imports: [ @@ -164,6 +166,7 @@ import { ExtensionsDatepickerFormatter } from './utils/extensions-datepicker-for StaffJournalNoteComponent, StaffJournalNoteInputComponent, InlineReviewOutcomeComponent, + BooleanToStringPipe, NoDataComponent, ], }) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision/application-decision.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision/application-decision.dto.ts index c19a526e4a..2d61ed46eb 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision/application-decision.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision/application-decision.dto.ts @@ -149,6 +149,9 @@ export class ApplicationDecisionDto { @AutoMap(() => Boolean) isOther?: boolean | null; + @AutoMap(() => Boolean) + isDraft: boolean; + reconsiders?: LinkedResolutionDto; modifies?: LinkedResolutionDto; reconsideredBy?: LinkedResolutionDto[]; diff --git a/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.controller.ts index 5339c0596e..3c13906296 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.controller.ts @@ -10,10 +10,10 @@ import { } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; -import { BoardService } from '../../board/board.service'; import { ROLES_ALLOWED_APPLICATIONS } from '../../../common/authorization/roles'; import { RolesGuard } from '../../../common/authorization/roles-guard.service'; import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { BoardService } from '../../board/board.service'; import { ApplicationReconsiderationCreateDto, ApplicationReconsiderationUpdateDto, diff --git a/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.dto.ts index e2f182afa0..68895acff8 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.dto.ts @@ -2,6 +2,7 @@ import { AutoMap } from '@automapper/classes'; import { ArrayNotEmpty, IsArray, + IsBoolean, IsDefined, IsNotEmpty, IsNumber, @@ -57,6 +58,22 @@ export class ApplicationReconsiderationCreateDto { @IsArray() @ArrayNotEmpty() reconsideredDecisionUuids: string[]; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsBoolean() + isNewProposal?: boolean; + + @IsOptional() + @IsBoolean() + isIncorrectFalseInfo?: boolean; + + @IsOptional() + @IsBoolean() + isNewEvidence?: boolean; } export class ApplicationReconsiderationUpdateDto { @@ -80,6 +97,22 @@ export class ApplicationReconsiderationUpdateDto { @IsArray() @ArrayNotEmpty() reconsideredDecisionUuids?: string[]; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsBoolean() + isNewProposal?: boolean; + + @IsOptional() + @IsBoolean() + isIncorrectFalseInfo?: boolean; + + @IsOptional() + @IsBoolean() + isNewEvidence?: boolean; } export class ApplicationForReconsiderationDto { @@ -87,29 +120,37 @@ export class ApplicationForReconsiderationDto { type: ApplicationTypeDto; statusCode: string; applicant: string; + source: string; region: ApplicationRegionDto; localGovernment: string; decisionMeetings: ApplicationDecisionMeetingDto[]; } -export class ApplicationReconsiderationDto { +export class ApplicationReconsiderationWithoutApplicationDto { uuid: string; - application: ApplicationForReconsiderationDto; + applicationUuid: string; card: CardDto; type: ReconsiderationTypeDto; submittedDate: number; reviewDate: number; reviewOutcome: ApplicationReconsiderationOutcomeCodeDto | null; - reconsideredDecisions: ApplicationDecisionDto[]; - resultingDecision?: ApplicationDecisionDto; + @AutoMap(() => String) + description?: string | null; + @AutoMap(() => Boolean) + isNewProposal?: boolean | null; + @AutoMap(() => Boolean) + isIncorrectFalseInfo?: boolean | null; + @AutoMap(() => Boolean) + isNewEvidence?: boolean | null; } - -export class ApplicationReconsiderationWithoutApplicationDto { +export class ApplicationReconsiderationDto extends ApplicationReconsiderationWithoutApplicationDto { uuid: string; - applicationUuid: string; + application: ApplicationForReconsiderationDto; card: CardDto; type: ReconsiderationTypeDto; submittedDate: number; reviewDate: number; reviewOutcome: ApplicationReconsiderationOutcomeCodeDto | null; + reconsideredDecisions: ApplicationDecisionDto[]; + resultingDecision?: ApplicationDecisionDto; } diff --git a/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.entity.ts index 438c64eaa9..5ccea0691d 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.entity.ts @@ -30,6 +30,33 @@ export class ApplicationReconsideration extends Base { @Column({ type: 'timestamptz' }) submittedDate: Date; + @AutoMap() + @Column({ type: 'timestamptz', nullable: true }) + reviewDate: Date | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + nullable: true, + comment: 'Reconsideration description provided by ALCS staff', + }) + description?: string; + + @AutoMap(() => Boolean) + @Column({ + type: 'boolean', + nullable: true, + }) + isNewProposal?: boolean; + + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + isIncorrectFalseInfo?: boolean; + + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + isNewEvidence?: boolean; + @AutoMap() @ManyToOne(() => ApplicationReconsiderationType, { nullable: false, @@ -47,8 +74,8 @@ export class ApplicationReconsideration extends Base { reviewOutcome: ApplicationReconsiderationOutcomeType | null; @AutoMap() - @Column({ type: 'timestamptz', nullable: true }) - reviewDate: Date | null; + @Column({ type: 'uuid' }) + applicationUuid: string; @AutoMap() @ManyToOne(() => Application, { cascade: ['insert'] }) @@ -56,7 +83,7 @@ export class ApplicationReconsideration extends Base { @AutoMap() @Column({ type: 'uuid' }) - applicationUuid: string; + cardUuid: string; @AutoMap() @OneToOne(() => Card, { cascade: true }) @@ -64,10 +91,6 @@ export class ApplicationReconsideration extends Base { @Type(() => Card) card: Card | null; - @AutoMap() - @Column({ type: 'uuid' }) - cardUuid: string; - @ManyToMany(() => ApplicationDecision, (decision) => decision.reconsideredBy) @JoinTable({ name: 'application_reconsidered_decisions', diff --git a/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.service.ts b/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.service.ts index cb16345a2d..44b3e4d52e 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-reconsideration/application-reconsideration.service.ts @@ -130,6 +130,10 @@ export class ApplicationReconsiderationService { const reconsideration = new ApplicationReconsideration({ submittedDate: new Date(createDto.submittedDate), + description: createDto.description, + isIncorrectFalseInfo: createDto.isIncorrectFalseInfo, + isNewEvidence: createDto.isNewEvidence, + isNewProposal: createDto.isNewProposal, type, }); @@ -217,6 +221,11 @@ export class ApplicationReconsiderationService { ); } + reconsideration.description = updateDto.description; + reconsideration.isIncorrectFalseInfo = updateDto.isIncorrectFalseInfo; + reconsideration.isNewEvidence = updateDto.isNewEvidence; + reconsideration.isNewProposal = updateDto.isNewProposal; + const recon = await this.reconsiderationRepository.save(reconsideration); return this.getByUuid(recon.uuid); diff --git a/services/apps/alcs/src/common/automapper/application.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application.automapper.profile.ts index cbb72d3871..ce8ac72a64 100644 --- a/services/apps/alcs/src/common/automapper/application.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application.automapper.profile.ts @@ -18,7 +18,10 @@ import { } from '../../alcs/application/application-meeting/application-meeting.dto'; import { ApplicationMeeting } from '../../alcs/application/application-meeting/application-meeting.entity'; import { ApplicationPaused } from '../../alcs/application/application-paused.entity'; -import { ApplicationDto } from '../../alcs/application/application.dto'; +import { + AlcsApplicationSubmissionDto, + ApplicationDto, +} from '../../alcs/application/application.dto'; import { Application } from '../../alcs/application/application.entity'; import { CardDto } from '../../alcs/card/card.dto'; import { Card } from '../../alcs/card/card.entity'; diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687808298996-new_reconsideration_fields.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687808298996-new_reconsideration_fields.ts new file mode 100644 index 0000000000..abe88296c1 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687808298996-new_reconsideration_fields.ts @@ -0,0 +1,43 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class newReconsiderationFields1687808298996 + implements MigrationInterface +{ + name = 'newReconsiderationFields1687808298996'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_reconsideration" ADD "description" text`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_reconsideration"."description" IS 'Reconsideration description provided by ALCS staff'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_reconsideration" ADD "is_new_proposal" boolean`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_reconsideration" ADD "is_incorrect_false_info" boolean`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_reconsideration" ADD "is_new_evidence" boolean`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_reconsideration" DROP COLUMN "is_new_evidence"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_reconsideration" DROP COLUMN "is_incorrect_false_info"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_reconsideration" DROP COLUMN "is_new_proposal"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_reconsideration"."description" IS 'Reconsideration description provided by ALCS staff'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_reconsideration" DROP COLUMN "description"`, + ); + } +} From 5b8f1baa07b03b5181edba499af03218e4e6d574 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Tue, 27 Jun 2023 17:14:20 -0700 Subject: [PATCH 023/954] Subdivision portal proposal and ALCS app prep fix (#736) * Remove document upload UI error when file is not needed * Remove overflow scroll bar in proposed lot --- .../app/features/application/proposal/subd/sub.dcomponent.scss | 1 - .../proposal/subd-proposal/subd-proposal.component.html | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/application/proposal/subd/sub.dcomponent.scss b/alcs-frontend/src/app/features/application/proposal/subd/sub.dcomponent.scss index 727a1dd00e..deb66eaa6f 100644 --- a/alcs-frontend/src/app/features/application/proposal/subd/sub.dcomponent.scss +++ b/alcs-frontend/src/app/features/application/proposal/subd/sub.dcomponent.scss @@ -1,7 +1,6 @@ .parcel-table { display: grid; grid-template-columns: max-content max-content max-content; - overflow-x: auto; grid-column-gap: 36px; grid-row-gap: 12px; } diff --git a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html index f0d9c0073c..facd717e1e 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html @@ -232,7 +232,7 @@
Documents needed for this step:
(deleteFile)="onDeleteFile($event)" (openFile)="openFile($event)" [showErrors]="showErrors" - [isRequired]="true" + [isRequired]="isHomeSiteSeverance.getRawValue() !== 'false'" [allowMultiple]="true" [disabled]="isHomeSiteSeverance.getRawValue() === 'false'" > From f7fb0248f64abe5c0e1f665836c39f5716f84ad4 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 28 Jun 2023 11:16:09 -0700 Subject: [PATCH 024/954] Add Owner Dropdown for Corporate Summary * Clean up logic in controller * Fix warning banner preventing edit submission submit * Load naru subtype in edit submission to enable editing of NARU --- .../parcel/parcel.component.ts | 12 +- .../document-upload-dialog.component.html | 14 +- .../document-upload-dialog.component.spec.ts | 7 + .../document-upload-dialog.component.ts | 86 +++++++++--- .../application-document.dto.ts | 2 + .../application-document.service.ts | 61 +++------ .../services/application/application.dto.ts | 13 +- .../application-details.component.html | 2 +- .../application-document.controller.spec.ts | 121 ++++++++++++++++- .../application-document.controller.ts | 125 ++++++++++++------ .../application-document.service.ts | 2 +- .../application-submission-draft.service.ts | 2 + .../application-owner.dto.ts | 6 +- .../application-owner.service.spec.ts | 8 ++ .../application-owner.service.ts | 20 +-- ...nerate-submission-document.service.spec.ts | 4 +- 16 files changed, 336 insertions(+), 149 deletions(-) diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts index 74e0bb4f6a..d3df83e4ff 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts @@ -43,17 +43,7 @@ export class ParcelComponent implements OnInit, OnChanges { async loadParcels(fileNumber: string) { const parcels = await this.parcelService.fetchParcels(fileNumber); - this.parcels = parcels - .filter((e) => e.parcelType === this.parcelType) - .map((parcel) => { - return { - ...parcel, - owners: parcel.owners.map((owner) => ({ - ...owner, - corporateSummary: this.files.find((file) => file.documentUuid === owner.corporateSummaryDocumentUuid), - })), - }; - }); + this.parcels = parcels.filter((e) => e.parcelType === this.parcelType); } ngOnChanges(changes: SimpleChanges): void { diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.html b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.html index a10af437f6..2ba3ec7b06 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.html +++ b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.html @@ -1,6 +1,8 @@

{{ title }} Document

- Superseded - Not associated with Applicant Submission in Portal + Superseded - Not associated with Applicant Submission in Portal
@@ -81,6 +83,16 @@

{{ title }} Document

+
+ + Associated Organization + + + {{ owner.label }} + + + +
Visible To:
diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts index e2c259bc1b..ed346dab5c 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts @@ -4,6 +4,7 @@ import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/materia import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ApplicationDocumentService } from '../../../../services/application/application-document/application-document.service'; import { ApplicationParcelService } from '../../../../services/application/application-parcel/application-parcel.service'; +import { ApplicationSubmissionService } from '../../../../services/application/application-submission/application-submission.service'; import { DocumentUploadDialogComponent } from './document-upload-dialog.component'; @@ -13,10 +14,12 @@ describe('DocumentUploadDialogComponent', () => { let mockAppDocService: DeepMocked; let mockParcelService: DeepMocked; + let mockSubmissionService: DeepMocked; beforeEach(async () => { mockAppDocService = createMock(); mockParcelService = createMock(); + mockSubmissionService = createMock(); const mockDialogRef = { close: jest.fn(), @@ -36,6 +39,10 @@ describe('DocumentUploadDialogComponent', () => { provide: ApplicationParcelService, useValue: mockParcelService, }, + { + provide: ApplicationSubmissionService, + useValue: mockSubmissionService, + }, { provide: MatDialogRef, useValue: mockDialogRef }, { provide: MAT_DIALOG_DATA, useValue: {} }, ], diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts index 5ac7312203..0b6cc13f9f 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts @@ -5,6 +5,7 @@ import { Subject } from 'rxjs'; import { ApplicationDocumentDto, ApplicationDocumentTypeDto, + UpdateDocumentDto, } from '../../../../services/application/application-document/application-document.dto'; import { ApplicationDocumentService, @@ -13,6 +14,8 @@ import { DOCUMENT_TYPE, } from '../../../../services/application/application-document/application-document.service'; import { ApplicationParcelService } from '../../../../services/application/application-parcel/application-parcel.service'; +import { ApplicationSubmissionService } from '../../../../services/application/application-submission/application-submission.service'; +import { SubmittedApplicationOwnerDto } from '../../../../services/application/application.dto'; @Component({ selector: 'app-document-upload-dialog', @@ -34,6 +37,7 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { source = new FormControl('', [Validators.required]); parcelId = new FormControl(null); + ownerId = new FormControl(null); visibleToInternal = new FormControl(false, [Validators.required]); visibleToPublic = new FormControl(false, [Validators.required]); @@ -41,6 +45,7 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { documentTypes: ApplicationDocumentTypeDto[] = []; documentSources = Object.values(DOCUMENT_SOURCE); selectableParcels: { uuid: string; index: number; pid?: string }[] = []; + selectableOwners: { uuid: string; label: string }[] = []; form = new FormGroup({ name: this.name, @@ -49,17 +54,20 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { visibleToInternal: this.visibleToInternal, visibleToPublic: this.visibleToPublic, parcelId: this.parcelId, + ownerId: this.ownerId, }); pendingFile: File | undefined; existingFile: { name: string; size: number } | undefined; - showCertificateOfTitleWarning = false; + showSupersededWarning = false; constructor( - @Inject(MAT_DIALOG_DATA) public data: { fileId: string; existingDocument?: ApplicationDocumentDto }, + @Inject(MAT_DIALOG_DATA) + public data: { fileId: string; owners: SubmittedApplicationOwnerDto[]; existingDocument?: ApplicationDocumentDto }, protected dialog: MatDialogRef, private applicationDocumentService: ApplicationDocumentService, - private parcelService: ApplicationParcelService + private parcelService: ApplicationParcelService, + private submissionService: ApplicationSubmissionService ) {} ngOnInit(): void { @@ -71,6 +79,11 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { this.allowsFileEdit = document.system === DOCUMENT_SYSTEM.ALCS; if (document.type?.code === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { this.prepareCertificateOfTitleUpload(document.uuid); + this.allowsFileEdit = false; + } + if (document.type?.code === DOCUMENT_TYPE.CORPORATE_SUMMARY) { + this.allowsFileEdit = false; + this.prepareCorporateSummaryUpload(document.uuid); } this.form.patchValue({ name: document.fileName, @@ -100,23 +113,18 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { visibilityFlags.push('P'); } - const parcelId = await this.parcelId.value; - - const dto = { - fileName: this.name.getRawValue()!, - source: this.source.getRawValue() as DOCUMENT_SOURCE, - typeCode: this.type.getRawValue() as DOCUMENT_TYPE, + const dto: UpdateDocumentDto = { + fileName: this.name.value!, + source: this.source.value as DOCUMENT_SOURCE, + typeCode: this.type.value as DOCUMENT_TYPE, visibilityFlags, + parcelUuid: this.parcelId.value ?? undefined, + ownerUuid: this.ownerId.value ?? undefined, }; const file = this.pendingFile; this.isSaving = true; - if (parcelId && file && this.type.value === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { - await this.applicationDocumentService.attachCertificateOfTitle(this.data.fileId, parcelId, { - ...dto, - file, - }); - } else if (this.data.existingDocument) { + if (this.data.existingDocument) { await this.applicationDocumentService.update(this.data.existingDocument.uuid, dto); } else if (file !== undefined) { await this.applicationDocumentService.upload(this.data.fileId, { @@ -128,12 +136,6 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { this.isSaving = false; } - private async loadDocumentTypes() { - const docTypes = await this.applicationDocumentService.fetchTypes(); - docTypes.sort((a, b) => (a.label > b.label ? 1 : -1)); - this.documentTypes = docTypes.filter((type) => type.code !== DOCUMENT_TYPE.ORIGINAL_APPLICATION); - } - ngOnDestroy(): void { this.$destroy.next(); this.$destroy.complete(); @@ -160,7 +162,7 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { if (selectedParcel) { this.parcelId.setValue(selectedParcel.uuid); } else if (uuid) { - this.showCertificateOfTitleWarning = true; + this.showSupersededWarning = true; } this.selectableParcels = parcels @@ -173,6 +175,32 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { } } + async prepareCorporateSummaryUpload(uuid?: string) { + const submission = await this.submissionService.fetchSubmission(this.data.fileId); + if (submission.owners.length > 0) { + const owners = submission.owners; + this.ownerId.setValidators([Validators.required]); + this.ownerId.updateValueAndValidity(); + + this.visibleToInternal.setValue(true); + this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + + const selectedOwner = owners.find((owner) => owner.corporateSummaryUuid === uuid); + if (selectedOwner) { + this.ownerId.setValue(selectedOwner.uuid); + } else if (uuid) { + this.showSupersededWarning = true; + } + + this.selectableOwners = owners + .filter((owner) => owner.type.code === 'ORGZ') + .map((owner, index) => ({ + label: owner.organizationName ?? owner.displayName, + uuid: owner.uuid, + })); + } + } + async onDocTypeSelected($event?: ApplicationDocumentTypeDto) { if ($event) { this.type.setValue($event.code); @@ -187,6 +215,14 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { this.parcelId.setValidators([]); this.parcelId.updateValueAndValidity(); } + + if (this.type.value === DOCUMENT_TYPE.CORPORATE_SUMMARY) { + await this.prepareCorporateSummaryUpload(); + } else { + this.ownerId.setValue(null); + this.ownerId.setValidators([]); + this.ownerId.updateValueAndValidity(); + } } uploadFile(event: Event) { @@ -218,4 +254,10 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { ); } } + + private async loadDocumentTypes() { + const docTypes = await this.applicationDocumentService.fetchTypes(); + docTypes.sort((a, b) => (a.label > b.label ? 1 : -1)); + this.documentTypes = docTypes.filter((type) => type.code !== DOCUMENT_TYPE.ORIGINAL_APPLICATION); + } } diff --git a/alcs-frontend/src/app/services/application/application-document/application-document.dto.ts b/alcs-frontend/src/app/services/application/application-document/application-document.dto.ts index f9c8d24a19..a066c871c0 100644 --- a/alcs-frontend/src/app/services/application/application-document/application-document.dto.ts +++ b/alcs-frontend/src/app/services/application/application-document/application-document.dto.ts @@ -23,6 +23,8 @@ export interface ApplicationDocumentDto { export interface UpdateDocumentDto { file?: File; + parcelUuid?: string; + ownerUuid?: string; fileName: string; typeCode: DOCUMENT_TYPE; source: DOCUMENT_SOURCE; diff --git a/alcs-frontend/src/app/services/application/application-document/application-document.service.ts b/alcs-frontend/src/app/services/application/application-document/application-document.service.ts index 639fe02829..9b413c9e46 100644 --- a/alcs-frontend/src/app/services/application/application-document/application-document.service.ts +++ b/alcs-frontend/src/app/services/application/application-document/application-document.service.ts @@ -75,13 +75,8 @@ export class ApplicationDocumentService { if (!isValidSize) { return; } + let formData = this.convertDtoToFormData(createDto); - let formData: FormData = new FormData(); - formData.append('documentType', createDto.typeCode); - formData.append('source', createDto.source); - formData.append('visibilityFlags', createDto.visibilityFlags.join(', ')); - formData.append('fileName', createDto.fileName); - formData.append('file', file, file.name); const res = await firstValueFrom(this.http.post(`${this.url}/application/${fileNumber}`, formData)); this.toastService.showSuccessToast('Review document uploaded'); return res; @@ -117,45 +112,13 @@ export class ApplicationDocumentService { return firstValueFrom(this.http.get(`${this.url}/types`)); } - async update(uuid: string, createDto: UpdateDocumentDto) { - let formData: FormData = new FormData(); - - const file = createDto.file; - if (file) { - const isValidSize = verifyFileSize(file, this.toastService); - if (!isValidSize) { - return; - } - formData.append('file', file, file.name); - } - formData.append('documentType', createDto.typeCode); - formData.append('source', createDto.source); - formData.append('visibilityFlags', createDto.visibilityFlags.join(', ')); - formData.append('fileName', createDto.fileName); + async update(uuid: string, updateDto: UpdateDocumentDto) { + let formData = this.convertDtoToFormData(updateDto); const res = await firstValueFrom(this.http.post(`${this.url}/${uuid}`, formData)); this.toastService.showSuccessToast('Review document uploaded'); return res; } - async attachCertificateOfTitle(fileNumber: string, parcelUuid: string, createDto: CreateDocumentDto) { - const file = createDto.file; - const isValidSize = verifyFileSize(file, this.toastService); - if (!isValidSize) { - return; - } - - let formData: FormData = new FormData(); - formData.append('documentType', createDto.typeCode); - formData.append('source', createDto.source); - formData.append('visibilityFlags', createDto.visibilityFlags.join(', ')); - formData.append('fileName', createDto.fileName); - formData.append('file', file, file.name); - formData.append('parcelUuid', parcelUuid); - const res = await firstValueFrom(this.http.post(`${this.url}/application/${fileNumber}/CERT`, formData)); - this.toastService.showSuccessToast('Review document uploaded'); - return res; - } - async updateSort(sortOrder: { uuid: string; order: number }[]) { try { await firstValueFrom(this.http.post(`${this.url}/sort`, sortOrder)); @@ -163,4 +126,22 @@ export class ApplicationDocumentService { this.toastService.showErrorToast(`Failed to save document order`); } } + + private convertDtoToFormData(dto: UpdateDocumentDto) { + let formData: FormData = new FormData(); + formData.append('documentType', dto.typeCode); + formData.append('source', dto.source); + formData.append('visibilityFlags', dto.visibilityFlags.join(', ')); + formData.append('fileName', dto.fileName); + if (dto.file) { + formData.append('file', dto.file, dto.file.name); + } + if (dto.parcelUuid) { + formData.append('parcelUuid', dto.parcelUuid); + } + if (dto.ownerUuid) { + formData.append('ownerUuid', dto.ownerUuid); + } + return formData; + } } diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 0ef7b869ba..ae28139a65 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -60,6 +60,7 @@ export interface ApplicationReviewDto { } export interface SubmittedApplicationOwnerDto { + uuid: string; displayName: string; firstName: string; lastName: string; @@ -67,17 +68,7 @@ export interface SubmittedApplicationOwnerDto { phoneNumber: string; email: string; type: BaseCodeDto; - corporateSummaryDocumentUuid?: string; -} - -export interface ApplicationParcelDocumentDto { - type: string; - uuid: string; - fileName: string; - fileSize: number; - uploadedBy?: string; - uploadedAt: number; - documentUuid: string; + corporateSummaryUuid?: string; } export interface ApplicationParcelDto { diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/application-details/application-details.component.html index f779cfbfd2..67d73bab7c 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/application-details/application-details.component.html @@ -19,7 +19,7 @@

3. Primary Contact

- + Changes made to this section will not be flagged as - ensure accuracy before saving. diff --git a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.spec.ts b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.spec.ts index 27ac43c374..6a12aa5a95 100644 --- a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.spec.ts @@ -7,7 +7,12 @@ import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; import { ApplicationProfile } from '../../../common/automapper/application.automapper.profile'; import { DOCUMENT_SOURCE } from '../../../document/document.dto'; +import { Document } from '../../../document/document.entity'; +import { ApplicationOwner } from '../../../portal/application-submission/application-owner/application-owner.entity'; +import { ApplicationOwnerService } from '../../../portal/application-submission/application-owner/application-owner.service'; +import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; import { ApplicationParcelService } from '../../../portal/application-submission/application-parcel/application-parcel.service'; +import { User } from '../../../user/user.entity'; import { CodeService } from '../../code/code.service'; import { DOCUMENT_TYPE } from './application-document-code.entity'; import { ApplicationDocumentController } from './application-document.controller'; @@ -18,18 +23,20 @@ describe('ApplicationDocumentController', () => { let controller: ApplicationDocumentController; let appDocumentService: DeepMocked; let mockParcelService: DeepMocked; + let mockOwnerService: DeepMocked; - const mockDocument = { - document: { + const mockDocument = new ApplicationDocument({ + document: new Document({ mimeType: 'mimeType', - uploadedBy: {}, + uploadedBy: new User(), uploadedAt: new Date(), - }, - } as ApplicationDocument; + }), + }); beforeEach(async () => { appDocumentService = createMock(); mockParcelService = createMock(); + mockOwnerService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -52,6 +59,10 @@ describe('ApplicationDocumentController', () => { provide: ApplicationParcelService, useValue: mockParcelService, }, + { + provide: ApplicationOwnerService, + useValue: mockOwnerService, + }, { provide: ClsService, useValue: {}, @@ -196,4 +207,104 @@ describe('ApplicationDocumentController', () => { expect(appDocumentService.setSorting).toHaveBeenCalledTimes(1); }); + + it('should set the certificate of title on the supplied parcel', async () => { + const mockFile = {}; + const mockUser = {}; + + appDocumentService.attachDocument.mockResolvedValue( + new ApplicationDocument({ + ...mockDocument, + typeCode: DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + }), + ); + mockParcelService.getOneOrFail.mockResolvedValue(new ApplicationParcel()); + mockParcelService.setCertificateOfTitle.mockResolvedValue( + new ApplicationParcel(), + ); + + const res = await controller.attachDocument('fileNumber', { + isMultipart: () => true, + body: { + documentType: { + value: DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + }, + fileName: { + value: 'file', + }, + source: { + value: DOCUMENT_SOURCE.APPLICANT, + }, + visibilityFlags: { + value: '', + }, + parcelUuid: { + value: 'parcel-uuid', + }, + file: mockFile, + }, + user: { + entity: mockUser, + }, + }); + + expect(res.mimeType).toEqual(mockDocument.document.mimeType); + + expect(appDocumentService.attachDocument).toHaveBeenCalledTimes(1); + const callData = appDocumentService.attachDocument.mock.calls[0][0]; + expect(callData.fileName).toEqual('file'); + expect(callData.file).toEqual(mockFile); + expect(callData.user).toEqual(mockUser); + expect(mockParcelService.getOneOrFail).toHaveBeenCalledTimes(1); + expect(mockParcelService.setCertificateOfTitle).toHaveBeenCalledTimes(1); + }); + + it('should set the corporate summary on the supplied owner', async () => { + const mockFile = {}; + const mockUser = {}; + + appDocumentService.attachDocument.mockResolvedValue( + new ApplicationDocument({ + ...mockDocument, + typeCode: DOCUMENT_TYPE.CORPORATE_SUMMARY, + }), + ); + mockOwnerService.getOwner.mockResolvedValue(new ApplicationOwner()); + mockOwnerService.save.mockResolvedValue(); + + const res = await controller.attachDocument('fileNumber', { + isMultipart: () => true, + body: { + documentType: { + value: DOCUMENT_TYPE.CORPORATE_SUMMARY, + }, + fileName: { + value: 'file', + }, + source: { + value: DOCUMENT_SOURCE.APPLICANT, + }, + visibilityFlags: { + value: '', + }, + ownerUuid: { + value: 'parcel-uuid', + }, + file: mockFile, + }, + user: { + entity: mockUser, + }, + }); + + expect(res.mimeType).toEqual(mockDocument.document.mimeType); + + expect(appDocumentService.attachDocument).toHaveBeenCalledTimes(1); + const callData = appDocumentService.attachDocument.mock.calls[0][0]; + expect(callData.fileName).toEqual('file'); + expect(callData.file).toEqual(mockFile); + expect(callData.user).toEqual(mockUser); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + expect(mockOwnerService.save).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts index 24cb516c41..ebd49f02d3 100644 --- a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts +++ b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts @@ -20,6 +20,7 @@ import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, } from '../../../document/document.dto'; +import { ApplicationOwnerService } from '../../../portal/application-submission/application-owner/application-owner.service'; import { ApplicationParcelService } from '../../../portal/application-submission/application-parcel/application-parcel.service'; import { ApplicationDocumentCode, @@ -42,6 +43,7 @@ export class ApplicationDocumentController { constructor( private applicationDocumentService: ApplicationDocumentService, private parcelService: ApplicationParcelService, + private ownerService: ApplicationOwnerService, @InjectMapper() private mapper: Mapper, ) {} @@ -67,49 +69,23 @@ export class ApplicationDocumentController { if (!req.isMultipart()) { throw new BadRequestException('Request is not multipart'); } - const savedDocument = await this.saveUploadedFile(req, fileNumber); - - return this.mapper.map( - savedDocument, - ApplicationDocument, - ApplicationDocumentDto, - ); - } - - @Post('/application/:fileNumber/CERT') - @UserRoles(...ANY_AUTH_ROLE) - async attachCertificateOfTitle( - @Param('fileNumber') fileNumber: string, - @Req() req, - ): Promise { - if (!req.isMultipart()) { - throw new BadRequestException('Request is not multipart'); - } - - const parcelUuid = req.body.parcelUuid.value; - if (!parcelUuid) { - throw new BadRequestException('Request is missing parcel uuid'); - } const savedDocument = await this.saveUploadedFile(req, fileNumber); - const parcel = await this.parcelService.getOneOrFail(parcelUuid); - if (parcel) { - if (parcel.certificateOfTitleUuid) { - const document = await this.applicationDocumentService.get( - parcel.certificateOfTitleUuid, - ); - await this.applicationDocumentService.update({ - uuid: document.uuid, - fileName: `${document.document.fileName}_superseded`, - source: document.document.source as DOCUMENT_SOURCE, - visibilityFlags: [], - documentType: document.type!.code as DOCUMENT_TYPE, - user: document.document.uploadedBy!, - }); - } + const parcelUuid = req.body.parcelUuid?.value as string | undefined; + if ( + parcelUuid && + savedDocument.typeCode === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE + ) { + await this.handleCertificateOfTitleUpdates(parcelUuid, savedDocument); + } - await this.parcelService.setCertificateOfTitle(parcel, savedDocument); + const ownerUuid = req.body.ownerUuid?.value as string | undefined; + if ( + ownerUuid && + savedDocument.typeCode === DOCUMENT_TYPE.CORPORATE_SUMMARY + ) { + await this.handleCorporateSummaryUpdates(ownerUuid, savedDocument); } return this.mapper.map( @@ -145,6 +121,22 @@ export class ApplicationDocumentController { user: req.user.entity, }); + const parcelUuid = req.body.parcelUuid?.value as string | undefined; + if ( + parcelUuid && + savedDocument.typeCode === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE + ) { + await this.handleCertificateOfTitleUpdates(parcelUuid, savedDocument); + } + + const ownerUuid = req.body.ownerUuid?.value as string | undefined; + if ( + ownerUuid && + savedDocument.typeCode === DOCUMENT_TYPE.CORPORATE_SUMMARY + ) { + await this.handleCorporateSummaryUpdates(ownerUuid, savedDocument); + } + return this.mapper.map( savedDocument, ApplicationDocument, @@ -249,6 +241,61 @@ export class ApplicationDocumentController { await this.applicationDocumentService.setSorting(data); } + private async handleCertificateOfTitleUpdates( + parcelUuid: string, + savedDocument: ApplicationDocument, + ) { + const parcel = await this.parcelService.getOneOrFail(parcelUuid); + if (parcel) { + if ( + parcel.certificateOfTitleUuid && + parcel.certificateOfTitleUuid !== savedDocument.uuid + ) { + const document = await this.applicationDocumentService.get( + parcel.certificateOfTitleUuid, + ); + await this.applicationDocumentService.update({ + uuid: document.uuid, + fileName: `${document.document.fileName}_superseded`, + source: document.document.source as DOCUMENT_SOURCE, + visibilityFlags: [], + documentType: document.type!.code as DOCUMENT_TYPE, + user: document.document.uploadedBy!, + }); + } + + await this.parcelService.setCertificateOfTitle(parcel, savedDocument); + } + } + + private async handleCorporateSummaryUpdates( + ownerUuid: string, + savedDocument: ApplicationDocument, + ) { + const owner = await this.ownerService.getOwner(ownerUuid); + if (owner) { + if ( + owner.corporateSummaryUuid && + owner.corporateSummaryUuid !== savedDocument.uuid + ) { + const document = await this.applicationDocumentService.get( + owner.corporateSummaryUuid, + ); + await this.applicationDocumentService.update({ + uuid: document.uuid, + fileName: `${document.document.fileName}_superseded`, + source: document.document.source as DOCUMENT_SOURCE, + visibilityFlags: [], + documentType: document.type!.code as DOCUMENT_TYPE, + user: document.document.uploadedBy!, + }); + } + + owner.corporateSummary = savedDocument; + await this.ownerService.save(owner); + } + } + private async saveUploadedFile(req, fileNumber: string) { const documentType = req.body.documentType.value as DOCUMENT_TYPE; const file = req.body.file; diff --git a/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts b/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts index a271a6df08..fce35a5e9e 100644 --- a/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts +++ b/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts @@ -297,7 +297,7 @@ export class ApplicationDocumentService { appDocument.type = undefined; appDocument.typeCode = documentType; appDocument.visibilityFlags = visibilityFlags; - await this.applicationDocumentRepository.save(appDocument); + return await this.applicationDocumentRepository.save(appDocument); } async setSorting(data: { uuid: string; order: number }[]) { diff --git a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.ts b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.ts index 668aa10959..84830f932e 100644 --- a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.ts +++ b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.ts @@ -40,6 +40,7 @@ export class ApplicationSubmissionDraftService { isDraft: true, }, relations: { + naruSubtype: true, owners: { type: true, corporateSummary: { @@ -61,6 +62,7 @@ export class ApplicationSubmissionDraftService { isDraft: false, }, relations: { + naruSubtype: true, status: true, owners: { type: true, diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts index 35440043f4..09d3377c24 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts @@ -28,6 +28,9 @@ export class ApplicationOwnerDto { @AutoMap() applicationSubmissionUuid: string; + @AutoMap(() => String) + corporateSummaryUuid?: string; + displayName: string; @AutoMap(() => String) @@ -78,7 +81,8 @@ export class ApplicationOwnerUpdateDto { email?: string; @IsString() - typeCode: string; + @IsOptional() + typeCode?: string; @IsUUID() @IsOptional() diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts index b2d89351c9..48ef867b65 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts @@ -202,6 +202,14 @@ describe('ApplicationOwnerService', () => { expect(mockRepo.find).toHaveBeenCalledTimes(1); }); + it('should call through for save', async () => { + mockRepo.save.mockResolvedValue(new ApplicationOwner()); + + await service.save(new ApplicationOwner()); + + expect(mockRepo.save).toHaveBeenCalledTimes(1); + }); + it('should call update for the application with the first parcels last name', async () => { mockRepo.find.mockResolvedValue([new ApplicationOwner()]); const owners = [ diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts index c0a7741af0..b46252a209 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts @@ -28,22 +28,6 @@ export class ApplicationOwnerService { private applicationDocumentService: ApplicationDocumentService, ) {} - async fetchBySubmissionUuid(uuid: string) { - return this.repository.find({ - where: { - applicationSubmission: { - uuid, - }, - }, - relations: { - type: true, - corporateSummary: { - document: true, - }, - }, - }); - } - async fetchByApplicationFileId(fileId: string) { return this.repository.find({ where: { @@ -104,6 +88,10 @@ export class ApplicationOwnerService { await this.repository.save(existingOwner); } + async save(owner: ApplicationOwner) { + await this.repository.save(owner); + } + async removeFromParcel(uuid: string, parcelUuid: string) { const existingOwner = await this.repository.findOneOrFail({ where: { diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts index 727de053fc..f3c5c0dcc9 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts @@ -194,7 +194,9 @@ describe('GenerateSubmissionDocumentService', () => { const userEntity = new User({ name: user.user.entity, }); - mockApplicationDocumentService.update.mockResolvedValue(); + mockApplicationDocumentService.update.mockResolvedValue( + new ApplicationDocument(), + ); mockApplicationDocumentService.attachDocumentAsBuffer.mockResolvedValue( new ApplicationDocument(), ); From 5b8b94adec40b8e5d6339a54503a07a2d409a1d3 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 28 Jun 2023 11:42:52 -0700 Subject: [PATCH 025/954] Only show Active Decision Makers for V2 Decision --- .../application-decision/application-decision-v2.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts index 037f544887..f6080377f8 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts @@ -672,6 +672,9 @@ export class ApplicationDecisionV2Service { order: { label: 'ASC', }, + where: { + isActive: true, + }, }), this.ceoCriterionRepository.find({ order: { From 61d32bd754b3bd86c4396338531d4cd73792bada Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 28 Jun 2023 13:55:19 -0700 Subject: [PATCH 026/954] bugfix to use uuid instead of name go brrr --- .../typeorm/migrations/1687557623816-local_gov_name_edits.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts index b25bf01a1f..f56b282fa8 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts @@ -5,7 +5,7 @@ export class localGovNameEdits1687557623816 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` UPDATE "alcs"."application_local_government" SET "name" = 'Village of Masset' WHERE "name" = 'Village of Massett'; - DELETE FROM alcs.application_local_government WHERE name = 'Northern Rockies Regional Municipality'; + DELETE FROM alcs.application_local_government WHERE uuid = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789'; `); } From 7deeb998f281e77c035ce1e34a2d34fb5d63def9 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 28 Jun 2023 14:47:06 -0700 Subject: [PATCH 027/954] Maintain extension when adding superseded * DRY Up code more --- .../application-document.controller.ts | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts index ebd49f02d3..da36e57475 100644 --- a/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts +++ b/services/apps/alcs/src/alcs/application/application-document/application-document.controller.ts @@ -13,6 +13,7 @@ import { } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; +import * as path from 'path'; import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; import { RolesGuard } from '../../../common/authorization/roles-guard.service'; import { UserRoles } from '../../../common/authorization/roles.decorator'; @@ -251,23 +252,26 @@ export class ApplicationDocumentController { parcel.certificateOfTitleUuid && parcel.certificateOfTitleUuid !== savedDocument.uuid ) { - const document = await this.applicationDocumentService.get( - parcel.certificateOfTitleUuid, - ); - await this.applicationDocumentService.update({ - uuid: document.uuid, - fileName: `${document.document.fileName}_superseded`, - source: document.document.source as DOCUMENT_SOURCE, - visibilityFlags: [], - documentType: document.type!.code as DOCUMENT_TYPE, - user: document.document.uploadedBy!, - }); + await this.supersedeDocument(parcel.certificateOfTitleUuid); } await this.parcelService.setCertificateOfTitle(parcel, savedDocument); } } + private async supersedeDocument(documentUuid: string) { + const document = await this.applicationDocumentService.get(documentUuid); + const parsedFileName = path.parse(document.document.fileName); + await this.applicationDocumentService.update({ + uuid: document.uuid, + fileName: `${parsedFileName.name}_superseded${parsedFileName.ext}`, + source: document.document.source as DOCUMENT_SOURCE, + visibilityFlags: [], + documentType: document.type!.code as DOCUMENT_TYPE, + user: document.document.uploadedBy!, + }); + } + private async handleCorporateSummaryUpdates( ownerUuid: string, savedDocument: ApplicationDocument, @@ -278,17 +282,7 @@ export class ApplicationDocumentController { owner.corporateSummaryUuid && owner.corporateSummaryUuid !== savedDocument.uuid ) { - const document = await this.applicationDocumentService.get( - owner.corporateSummaryUuid, - ); - await this.applicationDocumentService.update({ - uuid: document.uuid, - fileName: `${document.document.fileName}_superseded`, - source: document.document.source as DOCUMENT_SOURCE, - visibilityFlags: [], - documentType: document.type!.code as DOCUMENT_TYPE, - user: document.document.uploadedBy!, - }); + await this.supersedeDocument(owner.corporateSummaryUuid); } owner.corporateSummary = savedDocument; From 162dfc9e06a54efa664ea966624b4b6fbf4367a4 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 28 Jun 2023 15:22:44 -0700 Subject: [PATCH 028/954] updating uuid from to be deleted local gov uuid --- .../typeorm/migrations/1687557623816-local_gov_name_edits.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts index f56b282fa8..fb4d194d28 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts @@ -5,9 +5,10 @@ export class localGovNameEdits1687557623816 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` UPDATE "alcs"."application_local_government" SET "name" = 'Village of Masset' WHERE "name" = 'Village of Massett'; + UPDATE "alcs"."application" SET "local_government_uuid" = '92961db6-b74c-460b-bbad-e285398fa491' WHERE "local_government_uuid" = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789'; + UPDATE "alcs"."notice_of_intent" SET "local_government_uuid" = '92961db6-b74c-460b-bbad-e285398fa491' WHERE "local_government_uuid" = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789'; DELETE FROM alcs.application_local_government WHERE uuid = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789'; `); - } public async down(queryRunner: QueryRunner): Promise { From 9f78019678d9b3ed9efed201f4788e811831d778 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 28 Jun 2023 15:39:16 -0700 Subject: [PATCH 029/954] uuids were reversed, added support for covenants and planning review --- .../migrations/1687557623816-local_gov_name_edits.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts index fb4d194d28..a2c3ddc0b5 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687557623816-local_gov_name_edits.ts @@ -5,9 +5,11 @@ export class localGovNameEdits1687557623816 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` UPDATE "alcs"."application_local_government" SET "name" = 'Village of Masset' WHERE "name" = 'Village of Massett'; - UPDATE "alcs"."application" SET "local_government_uuid" = '92961db6-b74c-460b-bbad-e285398fa491' WHERE "local_government_uuid" = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789'; - UPDATE "alcs"."notice_of_intent" SET "local_government_uuid" = '92961db6-b74c-460b-bbad-e285398fa491' WHERE "local_government_uuid" = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789'; - DELETE FROM alcs.application_local_government WHERE uuid = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789'; + UPDATE "alcs"."application" SET "local_government_uuid" = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789' WHERE "local_government_uuid" = '92961db6-b74c-460b-bbad-e285398fa491'; + UPDATE "alcs"."notice_of_intent" SET "local_government_uuid" = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789' WHERE "local_government_uuid" = '92961db6-b74c-460b-bbad-e285398fa491'; + UPDATE "alcs"."planning_review" SET "local_government_uuid" = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789' WHERE "local_government_uuid" = '92961db6-b74c-460b-bbad-e285398fa491'; + UPDATE "alcs"."covenant" SET "local_government_uuid" = '33aa1f7d-3b65-4ed5-badf-11dafb0b2789' WHERE "local_government_uuid" = '92961db6-b74c-460b-bbad-e285398fa491'; + DELETE FROM alcs.application_local_government WHERE uuid = '92961db6-b74c-460b-bbad-e285398fa491'; `); } From a076d150816a2de0197047d6136fea4621f2b394 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 28 Jun 2023 16:00:52 -0700 Subject: [PATCH 030/954] Add Decision Condition Types to Admin --- .../app/features/admin/admin.component.html | 4 +- .../src/app/features/admin/admin.component.ts | 7 + .../src/app/features/admin/admin.module.ts | 4 + ...sion-condition-types-dialog.component.html | 52 ++++++ ...sion-condition-types-dialog.component.scss | 19 +++ ...n-condition-types-dialog.component.spec.ts | 37 +++++ ...cision-condition-types-dialog.component.ts | 49 ++++++ .../decision-condition-types.component.html | 35 ++++ .../decision-condition-types.component.scss | 9 + ...decision-condition-types.component.spec.ts | 53 ++++++ .../decision-condition-types.component.ts | 73 +++++++++ .../decision-condition-types.service.spec.ts | 154 ++++++++++++++++++ .../decision-condition-types.service.ts | 57 +++++++ .../apps/alcs/src/alcs/admin/admin.module.ts | 6 + ...ecision-condition-types.controller.spec.ts | 77 +++++++++ ...ion-decision-condition-types.controller.ts | 51 ++++++ ...cation-decision-condition-types.service.ts | 53 ++++++ ...application-decision-maker.service.spec.ts | 84 ++++++++++ 18 files changed, 822 insertions(+), 2 deletions(-) create mode 100644 alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.html create mode 100644 alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.scss create mode 100644 alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.spec.ts create mode 100644 alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.ts create mode 100644 alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.html create mode 100644 alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.scss create mode 100644 alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts create mode 100644 alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.ts create mode 100644 alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.spec.ts create mode 100644 alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.ts create mode 100644 services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.ts create mode 100644 services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.service.ts create mode 100644 services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-maker.service.spec.ts diff --git a/alcs-frontend/src/app/features/admin/admin.component.html b/alcs-frontend/src/app/features/admin/admin.component.html index d7a3ed3394..741584d64b 100644 --- a/alcs-frontend/src/app/features/admin/admin.component.html +++ b/alcs-frontend/src/app/features/admin/admin.component.html @@ -9,8 +9,8 @@ [routerLinkActiveOptions]="{ exact: true }" class="nav-item" > - {{ route.icon }} - {{ route.menuTitle }} + {{ route.icon }}{{ route.menuTitle }}
diff --git a/alcs-frontend/src/app/features/admin/admin.component.ts b/alcs-frontend/src/app/features/admin/admin.component.ts index 6ab5aaa76b..66886e92c0 100644 --- a/alcs-frontend/src/app/features/admin/admin.component.ts +++ b/alcs-frontend/src/app/features/admin/admin.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CeoCriterionComponent } from './ceo-criterion/ceo-criterion.component'; +import { DecisionConditionTypesComponent } from './decision-condition-types/decision-condition-types.component'; import { DecisionMakerComponent } from './decision-maker/decision-maker.component'; import { HolidayComponent } from './holiday/holiday.component'; import { LocalGovernmentComponent } from './local-government/local-government.component'; @@ -32,6 +33,12 @@ export const childRoutes = [ icon: 'coffee_maker', component: DecisionMakerComponent, }, + { + path: 'dct', + menuTitle: 'Decision Condition Types', + icon: 'hvac', + component: DecisionConditionTypesComponent, + }, { path: 'noi', menuTitle: 'NOI Subtypes', diff --git a/alcs-frontend/src/app/features/admin/admin.module.ts b/alcs-frontend/src/app/features/admin/admin.module.ts index 4ff51a3e61..127a80f3d0 100644 --- a/alcs-frontend/src/app/features/admin/admin.module.ts +++ b/alcs-frontend/src/app/features/admin/admin.module.ts @@ -6,6 +6,8 @@ import { SharedModule } from '../../shared/shared.module'; import { AdminComponent, childRoutes } from './admin.component'; import { CeoCriterionDialogComponent } from './ceo-criterion/ceo-criterion-dialog/ceo-criterion-dialog.component'; import { CeoCriterionComponent } from './ceo-criterion/ceo-criterion.component'; +import { DecisionConditionTypesDialogComponent } from './decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component'; +import { DecisionConditionTypesComponent } from './decision-condition-types/decision-condition-types.component'; import { DecisionMakerDialogComponent } from './decision-maker/decision-maker-dialog/decision-maker-dialog.component'; import { DecisionMakerComponent } from './decision-maker/decision-maker.component'; import { HolidayDialogComponent } from './holiday/holiday-dialog/holiday-dialog.component'; @@ -37,6 +39,8 @@ const routes: Routes = [ UnarchiveComponent, DecisionMakerComponent, DecisionMakerDialogComponent, + DecisionConditionTypesComponent, + DecisionConditionTypesDialogComponent, ], imports: [CommonModule, SharedModule.forRoot(), RouterModule.forChild(routes), MatPaginatorModule], }) diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.html b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.html new file mode 100644 index 0000000000..29d89237fb --- /dev/null +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.html @@ -0,0 +1,52 @@ +
+

{{ isEdit ? 'Edit' : 'Create' }} Decision Condition Type

+
+
+ +
+ + Code + + +
+ +
+ + Label + + +
+ +
+ + Description + + +
+ + + +
+ + +
+
+
diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.scss b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.scss new file mode 100644 index 0000000000..d42a3e2513 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.scss @@ -0,0 +1,19 @@ +.dialog { + padding: 24px; + + form { + display: grid; + grid-template-columns: 1fr 1fr; + row-gap: 24px; + column-gap: 24px; + margin-bottom: 12px; + + .full-width { + grid-column: 1/3; + } + + .mat-mdc-form-field { + width: 100%; + } + } +} diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.spec.ts b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.spec.ts new file mode 100644 index 0000000000..f27ef9c36f --- /dev/null +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.spec.ts @@ -0,0 +1,37 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { CeoCriterionService } from '../../../../services/ceo-criterion/ceo-criterion.service'; +import { DecisionConditionTypesService } from '../../../../services/decision-condition-types/decision-condition-types.service'; + +import { DecisionConditionTypesDialogComponent } from './decision-condition-types-dialog.component'; + +describe('DecisionConditionTypesDialogComponent', () => { + let component: DecisionConditionTypesDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, FormsModule], + declarations: [DecisionConditionTypesDialogComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: undefined }, + { provide: MatDialogRef, useValue: {} }, + { + provide: DecisionConditionTypesService, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionConditionTypesDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.ts b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.ts new file mode 100644 index 0000000000..8cb9bdd4fd --- /dev/null +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.ts @@ -0,0 +1,49 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { ApplicationDecisionConditionTypeDto } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { DecisionConditionTypesService } from '../../../../services/decision-condition-types/decision-condition-types.service'; + +@Component({ + selector: 'app-decision-condition-types-dialog', + templateUrl: './decision-condition-types-dialog.component.html', + styleUrls: ['./decision-condition-types-dialog.component.scss'], +}) +export class DecisionConditionTypesDialogComponent { + description = ''; + label = ''; + code = ''; + + isLoading = false; + isEdit = false; + + constructor( + @Inject(MAT_DIALOG_DATA) public data: ApplicationDecisionConditionTypeDto | undefined, + private dialogRef: MatDialogRef, + private decisionConditionTypesService: DecisionConditionTypesService + ) { + if (data) { + this.description = data.description; + this.label = data.label; + this.code = data.code; + } + this.isEdit = !!data; + } + + async onSubmit() { + this.isLoading = true; + + const dto = { + code: this.code, + label: this.label, + description: this.description, + }; + + if (this.isEdit) { + await this.decisionConditionTypesService.update(this.code, dto); + } else { + await this.decisionConditionTypesService.create(dto); + } + this.isLoading = false; + this.dialogRef.close(true); + } +} diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.html b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.html new file mode 100644 index 0000000000..914e76f0e5 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.html @@ -0,0 +1,35 @@ +
+
+

Decision Condition Types

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Label{{ row.label }}Description{{ row.description }}Code{{ row.code }}Actions + +
+
diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.scss b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.scss new file mode 100644 index 0000000000..4320920313 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.scss @@ -0,0 +1,9 @@ +.container { + .edit-btn { + color: green; + } + + .delete-btn { + color: red; + } +} diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts new file mode 100644 index 0000000000..732defedb3 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts @@ -0,0 +1,53 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { CeoCriterionService } from '../../../services/ceo-criterion/ceo-criterion.service'; +import { DecisionConditionTypesService } from '../../../services/decision-condition-types/decision-condition-types.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { DecisionConditionTypesComponent } from './decision-condition-types.component'; + +describe('DecisionConditionTypesComponent', () => { + let component: DecisionConditionTypesComponent; + let fixture: ComponentFixture; + let mockDecTypesService: DeepMocked; + let mockDialog: DeepMocked; + let mockConfirmationDialogService: DeepMocked; + + beforeEach(async () => { + mockDecTypesService = createMock(); + mockDialog = createMock(); + mockConfirmationDialogService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [DecisionConditionTypesComponent], + providers: [ + { + provide: DecisionConditionTypesService, + useValue: mockDecTypesService, + }, + { + provide: MatDialog, + useValue: mockDialog, + }, + { + provide: ConfirmationDialogService, + useValue: mockConfirmationDialogService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + imports: [HttpClientTestingModule], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionConditionTypesComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.ts b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.ts new file mode 100644 index 0000000000..d730203f41 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Subject } from 'rxjs'; +import { ApplicationDecisionConditionTypeDto } from '../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { DecisionConditionTypesService } from '../../../services/decision-condition-types/decision-condition-types.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { DecisionConditionTypesDialogComponent } from './decision-condition-types-dialog/decision-condition-types-dialog.component'; + +@Component({ + selector: 'app-decision-condition-types', + templateUrl: './decision-condition-types.component.html', + styleUrls: ['./decision-condition-types.component.scss'], +}) +export class DecisionConditionTypesComponent implements OnInit { + destroy = new Subject(); + + decisionConditionTypeDtos: ApplicationDecisionConditionTypeDto[] = []; + displayedColumns: string[] = ['label', 'description', 'code', 'actions']; + + constructor( + private decisionConditionTypesService: DecisionConditionTypesService, + public dialog: MatDialog, + private confirmationDialogService: ConfirmationDialogService + ) {} + + ngOnInit(): void { + this.fetch(); + } + + async fetch() { + this.decisionConditionTypeDtos = await this.decisionConditionTypesService.fetch(); + } + + async onCreate() { + const dialog = this.dialog.open(DecisionConditionTypesDialogComponent, { + minWidth: '600px', + maxWidth: '800px', + width: '70%', + }); + dialog.beforeClosed().subscribe(async (result) => { + if (result) { + await this.fetch(); + } + }); + } + + async onEdit(decisionConditionTypeDto: ApplicationDecisionConditionTypeDto) { + const dialog = this.dialog.open(DecisionConditionTypesDialogComponent, { + minWidth: '600px', + maxWidth: '800px', + width: '70%', + data: decisionConditionTypeDto, + }); + dialog.beforeClosed().subscribe(async (result) => { + if (result) { + await this.fetch(); + } + }); + } + + async onDelete(decisionConditionTypeDto: ApplicationDecisionConditionTypeDto) { + this.confirmationDialogService + .openDialog({ + body: `Are you sure you want to delete ${decisionConditionTypeDto.label}?`, + }) + .subscribe(async (answer) => { + if (answer) { + await this.decisionConditionTypesService.delete(decisionConditionTypeDto.code); + await this.fetch(); + } + }); + } +} diff --git a/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.spec.ts b/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.spec.ts new file mode 100644 index 0000000000..0d6ff26077 --- /dev/null +++ b/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.spec.ts @@ -0,0 +1,154 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../toast/toast.service'; +import { DecisionConditionTypesService } from './decision-condition-types.service'; + +describe('DecisionConditionTypesService', () => { + let service: DecisionConditionTypesService; + let mockHttpClient: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(() => { + mockHttpClient = createMock(); + mockToastService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + ], + }); + service = TestBed.inject(DecisionConditionTypesService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call post on create', async () => { + mockHttpClient.post.mockReturnValue( + of({ + code: 'fake', + }) + ); + + const res = await service.create({ + code: '', + label: '', + description: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res!.code).toEqual('fake'); + }); + + it('should show toast if create fails', async () => { + mockHttpClient.post.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.create({ + code: '', + label: '', + description: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeUndefined(); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should call patch on update', async () => { + mockHttpClient.patch.mockReturnValue( + of({ + code: 'fake', + }) + ); + + const res = await service.update('fake', { + code: '', + label: '', + description: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res!.code).toEqual('fake'); + }); + + it('should show toast if update fails', async () => { + mockHttpClient.patch.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.update('mock', { + code: '', + label: '', + description: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should call get on fetch', async () => { + mockHttpClient.get.mockReturnValue(of([])); + + await service.fetch(); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + }); + + it('should show toast if get fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.fetch(); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should call delete on delete', async () => { + mockHttpClient.delete.mockReturnValue( + of({ + code: 'fake', + }) + ); + + const res = await service.delete('fake'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res!.code).toEqual('fake'); + }); + + it('should show toast if delete fails', async () => { + mockHttpClient.delete.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.delete('mock'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.ts b/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.ts new file mode 100644 index 0000000000..094e6edc15 --- /dev/null +++ b/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.ts @@ -0,0 +1,57 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { ApplicationDecisionConditionTypeDto } from '../application/decision/application-decision-v2/application-decision-v2.dto'; +import { ToastService } from '../toast/toast.service'; + +@Injectable({ + providedIn: 'root', +}) +export class DecisionConditionTypesService { + private url = `${environment.apiUrl}/decision-condition-types`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetch() { + try { + return await firstValueFrom(this.http.get(`${this.url}`)); + } catch (err) { + console.error(err); + this.toastService.showErrorToast('Failed to fetch decision condition types'); + } + return []; + } + + async create(createDto: ApplicationDecisionConditionTypeDto) { + try { + return await firstValueFrom(this.http.post(`${this.url}`, createDto)); + } catch (e) { + this.toastService.showErrorToast('Failed to create decision condition type'); + console.log(e); + } + return; + } + + async update(code: string, updateDto: ApplicationDecisionConditionTypeDto) { + try { + return await firstValueFrom( + this.http.patch(`${this.url}/${code}`, updateDto) + ); + } catch (e) { + this.toastService.showErrorToast('Failed to update decision condition type'); + console.log(e); + } + return; + } + + async delete(code: string) { + try { + return await firstValueFrom(this.http.delete(`${this.url}/${code}`)); + } catch (e) { + this.toastService.showErrorToast('Failed to delete decision condition type'); + console.log(e); + } + return; + } +} diff --git a/services/apps/alcs/src/alcs/admin/admin.module.ts b/services/apps/alcs/src/alcs/admin/admin.module.ts index 09182186a2..23e3cf81f2 100644 --- a/services/apps/alcs/src/alcs/admin/admin.module.ts +++ b/services/apps/alcs/src/alcs/admin/admin.module.ts @@ -1,5 +1,6 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationDecisionConditionType } from '../application-decision/application-decision-condition/application-decision-condition-code.entity'; import { ApplicationDecisionMakerCode } from '../application-decision/application-decision-maker/application-decision-maker.entity'; import { ApplicationModule } from '../application/application.module'; import { CovenantModule } from '../covenant/covenant.module'; @@ -11,6 +12,8 @@ import { NoticeOfIntentModule } from '../notice-of-intent/notice-of-intent.modul import { PlanningReviewModule } from '../planning-review/planning-review.module'; import { ApplicationCeoCriterionController } from './application-ceo-criterion/application-ceo-criterion.controller'; import { ApplicationCeoCriterionService } from './application-ceo-criterion/application-ceo-criterion.service'; +import { ApplicationDecisionConditionTypesController } from './application-decision-condition-types/application-decision-condition-types.controller'; +import { ApplicationDecisionConditionTypesService } from './application-decision-condition-types/application-decision-condition-types.service'; import { ApplicationDecisionMakerController } from './application-decision-maker/application-decision-maker.controller'; import { ApplicationDecisionMakerService } from './application-decision-maker/application-decision-maker.service'; import { HolidayController } from './holiday/holiday.controller'; @@ -29,6 +32,7 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service'; ApplicationCeoCriterionCode, ApplicationDecisionMakerCode, NoticeOfIntentSubtype, + ApplicationDecisionConditionType, ]), ApplicationModule, forwardRef(() => ApplicationDecisionModule), @@ -44,6 +48,7 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service'; UnarchiveCardController, NoiSubtypeController, ApplicationDecisionMakerController, + ApplicationDecisionConditionTypesController, ], providers: [ HolidayService, @@ -51,6 +56,7 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service'; ApplicationDecisionMakerService, UnarchiveCardService, NoiSubtypeService, + ApplicationDecisionConditionTypesService, ], }) export class AdminModule {} diff --git a/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.spec.ts b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.spec.ts new file mode 100644 index 0000000000..78977b4d27 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.spec.ts @@ -0,0 +1,77 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { ConfigModule } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { ApplicationDecisionConditionType } from '../../application-decision/application-decision-condition/application-decision-condition-code.entity'; +import { ApplicationDecisionConditionTypesController } from './application-decision-condition-types.controller'; +import { ApplicationDecisionConditionTypesService } from './application-decision-condition-types.service'; + +describe('ApplicationDecisionConditionTypesController', () => { + let controller: ApplicationDecisionConditionTypesController; + let mockDecTypesService: DeepMocked; + + beforeEach(async () => { + mockDecTypesService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [ApplicationDecisionConditionTypesController], + providers: [ + { + provide: ApplicationDecisionConditionTypesService, + useValue: mockDecTypesService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + imports: [ConfigModule], + }).compile(); + + controller = module.get( + ApplicationDecisionConditionTypesController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call out to service when fetching ceo criterion', async () => { + mockDecTypesService.fetch.mockResolvedValue([]); + + const applicationDecisionConditionTypes = await controller.fetch(); + + expect(applicationDecisionConditionTypes).toBeDefined(); + expect(mockDecTypesService.fetch).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when updating ceo criterion', async () => { + mockDecTypesService.update.mockResolvedValue( + new ApplicationDecisionConditionType(), + ); + + const applicationDecisionConditionType = await controller.update( + 'fake', + new ApplicationDecisionConditionType(), + ); + + expect(applicationDecisionConditionType).toBeDefined(); + expect(mockDecTypesService.update).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when creating ceo criterion', async () => { + mockDecTypesService.create.mockResolvedValue( + new ApplicationDecisionConditionType(), + ); + + const applicationDecisionConditionType = await controller.create( + new ApplicationDecisionConditionType(), + ); + + expect(applicationDecisionConditionType).toBeDefined(); + expect(mockDecTypesService.create).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.ts b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.ts new file mode 100644 index 0000000000..8441783428 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.ts @@ -0,0 +1,51 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { ApplicationDecisionConditionTypeDto } from '../../application-decision/application-decision-condition/application-decision-condition.dto'; +import { ApplicationDecisionConditionTypesService } from './application-decision-condition-types.service'; + +@Controller('decision-condition-types') +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(RolesGuard) +export class ApplicationDecisionConditionTypesController { + constructor( + private applicationDecisionConditionTypesService: ApplicationDecisionConditionTypesService, + ) {} + + @Get() + @UserRoles(AUTH_ROLE.ADMIN) + async fetch() { + return await this.applicationDecisionConditionTypesService.fetch(); + } + + @Patch('/:code') + @UserRoles(AUTH_ROLE.ADMIN) + async update( + @Param('code') code: string, + @Body() updateDto: ApplicationDecisionConditionTypeDto, + ) { + return await this.applicationDecisionConditionTypesService.update( + code, + updateDto, + ); + } + + @Post('') + @UserRoles(AUTH_ROLE.ADMIN) + async create(@Body() createDto: ApplicationDecisionConditionTypeDto) { + return await this.applicationDecisionConditionTypesService.create( + createDto, + ); + } +} diff --git a/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.service.ts b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.service.ts new file mode 100644 index 0000000000..3011dad527 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ApplicationDecisionConditionType } from '../../application-decision/application-decision-condition/application-decision-condition-code.entity'; +import { ApplicationDecisionConditionTypeDto } from '../../application-decision/application-decision-condition/application-decision-condition.dto'; + +@Injectable() +export class ApplicationDecisionConditionTypesService { + constructor( + @InjectRepository(ApplicationDecisionConditionType) + private applicationDecisionMakerCodeRepository: Repository, + ) {} + + async fetch() { + return await this.applicationDecisionMakerCodeRepository.find({ + order: { label: 'ASC' }, + select: { + code: true, + label: true, + description: true, + }, + }); + } + + async getOneOrFail(code: string) { + return await this.applicationDecisionMakerCodeRepository.findOneOrFail({ + where: { code }, + }); + } + + async update(code: string, updateDto: ApplicationDecisionConditionTypeDto) { + const decisionMakerCode = await this.getOneOrFail(code); + + decisionMakerCode.description = updateDto.description; + decisionMakerCode.label = updateDto.label; + + return await this.applicationDecisionMakerCodeRepository.save( + decisionMakerCode, + ); + } + + async create(createDto: ApplicationDecisionConditionTypeDto) { + const decisionMakerCode = new ApplicationDecisionConditionType(); + + decisionMakerCode.code = createDto.code; + decisionMakerCode.description = createDto.description; + decisionMakerCode.label = createDto.label; + + return await this.applicationDecisionMakerCodeRepository.save( + decisionMakerCode, + ); + } +} diff --git a/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-maker.service.spec.ts b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-maker.service.spec.ts new file mode 100644 index 0000000000..ac68baa379 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-maker.service.spec.ts @@ -0,0 +1,84 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ApplicationDecisionConditionType } from '../../application-decision/application-decision-condition/application-decision-condition-code.entity'; +import { ApplicationDecisionConditionTypesService } from './application-decision-condition-types.service'; + +describe('ApplicationDecisionConditionTypesService', () => { + let service: ApplicationDecisionConditionTypesService; + let mockRepository: DeepMocked>; + + const decisionMakerCode = new ApplicationDecisionConditionType(); + + beforeEach(async () => { + mockRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + providers: [ + ApplicationDecisionConditionTypesService, + { + provide: getRepositoryToken(ApplicationDecisionConditionType), + useValue: mockRepository, + }, + ], + }).compile(); + + service = module.get( + ApplicationDecisionConditionTypesService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should successfully create decision maker entry', async () => { + mockRepository.save.mockResolvedValue( + new ApplicationDecisionConditionType(), + ); + + const result = await service.create({ + code: '', + description: '', + label: '', + }); + + expect(mockRepository.save).toBeCalledTimes(1); + expect(result).toBeDefined(); + }); + + it('should successfully update decision maker entry if it exists', async () => { + mockRepository.save.mockResolvedValue(decisionMakerCode); + mockRepository.findOneOrFail.mockResolvedValue(decisionMakerCode); + + const result = await service.update(decisionMakerCode.code, { + code: '', + description: '', + label: '', + }); + + expect(mockRepository.save).toBeCalledTimes(1); + expect(mockRepository.findOneOrFail).toBeCalledTimes(1); + expect(mockRepository.findOneOrFail).toBeCalledWith({ + where: { uuid: decisionMakerCode.code }, + }); + expect(result).toBeDefined(); + }); + + it('should successfully fetch decision maker', async () => { + mockRepository.find.mockResolvedValue([decisionMakerCode]); + + const result = await service.fetch(); + + expect(mockRepository.find).toBeCalledTimes(1); + expect(result).toBeDefined(); + }); +}); From 64e61adbbfd4a75ab69e0845e6b0b2e37b1b59d9 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Thu, 29 Jun 2023 09:51:13 -0700 Subject: [PATCH 031/954] Show primary contact field validations in review if no contact is selected (#743) --- .../application-details.component.html | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/application-details/application-details.component.html index 67d73bab7c..06061e4614 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/application-details/application-details.component.html @@ -25,38 +25,38 @@

3. Primary Contact

- -
First Name
-
- {{ primaryContact.firstName }} - -
-
Last Name
-
- {{ primaryContact.lastName }} - -
-
- Organization (optional) - Ministry/Department Responsible -
-
- {{ primaryContact.organizationName }} - -
-
Phone
-
- {{ primaryContact.phoneNumber }} - - Invalid Format -
-
Email
-
- {{ primaryContact.email }} - - Invalid Format -
-
+
First Name
+
+ {{ primaryContact?.firstName }} + +
+
Last Name
+
+ {{ primaryContact?.lastName }} + +
+
+ Organization (optional) + Ministry/Department Responsible +
+
+ {{ primaryContact?.organizationName }} + +
+
Phone
+
+ {{ primaryContact?.phoneNumber }} + + Invalid Format +
+
Email
+
+ {{ primaryContact?.email }} + + Invalid Format +
Authorization Letter(s)
From 9c8b3ace06c7cdd4e7acaedda1dea7bbb7a2df59 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 29 Jun 2023 11:07:30 -0700 Subject: [PATCH 032/954] Code Review Feedback --- .../decision-condition-types.component.spec.ts | 1 - .../application-decision-condition-types.controller.spec.ts | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts index 732defedb3..68d15c3272 100644 --- a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts @@ -3,7 +3,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { CeoCriterionService } from '../../../services/ceo-criterion/ceo-criterion.service'; import { DecisionConditionTypesService } from '../../../services/decision-condition-types/decision-condition-types.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; diff --git a/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.spec.ts b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.spec.ts index 78977b4d27..c405165089 100644 --- a/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.spec.ts +++ b/services/apps/alcs/src/alcs/admin/application-decision-condition-types/application-decision-condition-types.controller.spec.ts @@ -39,7 +39,7 @@ describe('ApplicationDecisionConditionTypesController', () => { expect(controller).toBeDefined(); }); - it('should call out to service when fetching ceo criterion', async () => { + it('should call out to service when fetching decision condition type', async () => { mockDecTypesService.fetch.mockResolvedValue([]); const applicationDecisionConditionTypes = await controller.fetch(); @@ -48,7 +48,7 @@ describe('ApplicationDecisionConditionTypesController', () => { expect(mockDecTypesService.fetch).toHaveBeenCalledTimes(1); }); - it('should call out to service when updating ceo criterion', async () => { + it('should call out to service when updating decision condition type', async () => { mockDecTypesService.update.mockResolvedValue( new ApplicationDecisionConditionType(), ); @@ -62,7 +62,7 @@ describe('ApplicationDecisionConditionTypesController', () => { expect(mockDecTypesService.update).toHaveBeenCalledTimes(1); }); - it('should call out to service when creating ceo criterion', async () => { + it('should call out to service when creating decision condition type', async () => { mockDecTypesService.create.mockResolvedValue( new ApplicationDecisionConditionType(), ); From ef06244a5c1576e15bd67259bc6eb8398a0b5c34 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 29 Jun 2023 11:31:34 -0700 Subject: [PATCH 033/954] Copy local government from Submission to Application when updating --- .../apps/alcs/src/alcs/application/application.dto.ts | 1 + .../application-submission.service.spec.ts | 3 +++ .../application-submission.service.ts | 9 +++++++++ 3 files changed, 13 insertions(+) diff --git a/services/apps/alcs/src/alcs/application/application.dto.ts b/services/apps/alcs/src/alcs/application/application.dto.ts index 9ca8c020b9..86b9adc71d 100644 --- a/services/apps/alcs/src/alcs/application/application.dto.ts +++ b/services/apps/alcs/src/alcs/application/application.dto.ts @@ -278,6 +278,7 @@ export class ApplicationUpdateServiceDto { proposalEndDate?: Date | null; proposalExpiryDate?: Date | null; staffObservations?: string | null; + localGovernmentUuid?: string; } export class CreateApplicationServiceDto { diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts index 04b041f46b..7f81d6e29e 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts @@ -400,6 +400,9 @@ describe('ApplicationSubmissionService', () => { mockRepository.findOne.mockResolvedValue(mockApplication); mockRepository.save.mockResolvedValue(mockApplication); + mockApplicationService.updateByFileNumber.mockResolvedValue( + new Application(), + ); const result = await service.update(fileNumber, { applicant, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index 7efd55cc6c..14e425a730 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -160,6 +160,15 @@ export class ApplicationSubmissionService { await this.applicationSubmissionRepository.save(applicationSubmission); + if (!applicationSubmission.isDraft && updateDto.localGovernmentUuid) { + await this.applicationService.updateByFileNumber( + applicationSubmission.fileNumber, + { + localGovernmentUuid: updateDto.localGovernmentUuid, + }, + ); + } + return this.getOrFailByFileNumber(applicationSubmission.fileNumber); } From dd8b7e6c8cfc81f217e4a7aad35947a2be196f55 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 14:55:22 -0700 Subject: [PATCH 034/954] local gov bug fixed --- .../sql/insert-batch-application.sql | 153 +++++++++--------- .../1687546406729-seed_add_adl_loc_gov.ts | 1 - .../1687810457499-seed_inactive_local_gov.ts | 1 + 3 files changed, 78 insertions(+), 77 deletions(-) diff --git a/bin/migrate-oats-data/sql/insert-batch-application.sql b/bin/migrate-oats-data/sql/insert-batch-application.sql index bdb992cbf5..2fe3d63bc4 100644 --- a/bin/migrate-oats-data/sql/insert-batch-application.sql +++ b/bin/migrate-oats-data/sql/insert-batch-application.sql @@ -1,4 +1,3 @@ - -- Step 1: Perform a lookup to retrieve the applicant's name or organization for each application ID WITH applicant_lookup AS ( @@ -26,82 +25,84 @@ WITH GROUP BY oaap.alr_application_id ), - --- Step 2: get local gov application name & match to uuid -oats_gov AS ( - SELECT - oaap.alr_application_id AS application_id, - oo.organization_name AS oats_gov_name - FROM - oats.oats_alr_application_parties oaap - JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - WHERE - oo.organization_type_cd = 'MUNI' - OR oo.organization_type_cd = 'FN' -), - -alcs_gov AS ( - SELECT - oats_gov.application_id AS application_id, - alg.uuid AS gov_uuid - FROM - oats_gov - JOIN alcs.application_local_government alg on ( + -- Step 2: get local gov application name & match to uuid + oats_gov AS ( + SELECT + oaap.alr_application_id AS application_id, + oo.organization_name AS oats_gov_name + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oo.organization_type_cd = 'MUNI' + OR oo.organization_type_cd = 'FN' + OR oo.organization_type_cd = 'RD' + ), + alcs_gov AS ( + SELECT + oats_gov.application_id AS application_id, + alg.uuid AS gov_uuid + FROM + oats_gov + JOIN alcs.application_local_government alg on ( + CASE + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gabriola Island' THEN 'Islands Trust Gabriola Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Galiano Island' THEN 'Islands Trust Galiano Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gambier Island' THEN 'Islands Trust Gambier Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Hornby Island' THEN 'Islands Trust Hornby Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Lasqueti Island' THEN 'Islands Trust Lasqueti Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Mayne Island' THEN 'Islands Trust Mayne Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Pender Island' THEN 'Islands Trust Pender Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Quadra Island' THEN 'Islands Trust Quadra Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Salt Spring Island' THEN 'Islands Trust Salt Spring Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Sunshine Coast' THEN 'Islands Trust Sunshine Coast (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Bowen Island' THEN 'Bowen Island (Island Municipality)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Denman Island' THEN 'Islands Trust Denman Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' + ELSE oats_gov.oats_gov_name + END + ) = alg."name" + ), + -- Step 3: Perform a lookup to retrieve the region code for each application ID + panel_lookup AS ( + SELECT DISTINCT + oaap.alr_application_id AS application_id, CASE - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gabriola Island' THEN 'Islands Trust Gabriola Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Galiano Island' THEN 'Islands Trust Galiano Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gambier Island' THEN 'Islands Trust Gambier Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Hornby Island' THEN 'Islands Trust Hornby Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Lasqueti Island' THEN 'Islands Trust Lasqueti Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Mayne Island' THEN 'Islands Trust Mayne Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Pender Island' THEN 'Islands Trust Pender Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Quadra Island' THEN 'Islands Trust Quadra Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Salt Spring Island' THEN 'Islands Trust Salt Spring Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Sunshine Coast' THEN 'Islands Trust Sunshine Coast (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Bowen Island' THEN 'Bowen Island (Island Municipality)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Denman Island' THEN 'Islands Trust Denman Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' - ELSE oats_gov.oats_gov_name - END - ) = alg."name" -), --- Step 3: Perform a lookup to retrieve the region code for each application ID -panel_lookup AS ( - SELECT DISTINCT - oaap.alr_application_id AS application_id, - oo2.organization_name AS panel_region - FROM - oats.oats_alr_application_parties oaap - JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - JOIN oats.oats_organizations oo2 ON oo.parent_organization_id = oo2.organization_id - WHERE - oo2.organization_type_cd = 'PANEL' -), - --- Step 4: Perform lookup to retrieve type code -application_type_lookup AS ( - SELECT - oaac.alr_application_id AS application_id, - oacc."description" AS "description", - oaac.alr_change_code AS code - FROM - oats.oats_alr_appl_components AS oaac - JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code - LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id - WHERE - aee.component_id IS NULL -) - + WHEN oo2.parent_organization_id isnull then oo2.organization_name + WHEN oo3.parent_organization_id isnull then oo3.organization_name + ELSE 'NONE' + END AS panel_region + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + LEFT JOIN oats.oats_organizations oo2 ON oo.parent_organization_id = oo2.organization_id + LEFT JOIN oats.oats_organizations oo3 ON oo2.parent_organization_id = oo3.organization_id + WHERE + oo2.organization_type_cd = 'PANEL' OR oo3.organization_type_cd = 'PANEL' + ), + -- Step 4: Perform lookup to retrieve type code + application_type_lookup AS ( + SELECT + oaac.alr_application_id AS application_id, + oacc."description" AS "description", + oaac.alr_change_code AS code + FROM + oats.oats_alr_appl_components AS oaac + JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code + LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id + WHERE + aee.component_id IS NULL + ) -- Step 5: Insert new records into the alcs_applications table SELECT oa.alr_application_id :: text AS file_number, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts index ec15f01b62..aeba419435 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts @@ -8,7 +8,6 @@ export class seedAddAdlLocGov1687546406729 implements MigrationInterface { ('b8a622d7-66da-4598-9687-2e8727fb2561',NULL,NOW(),NULL,'migration_seed',NULL,'City of Port Alberni','ISLR',NULL,false,true,'{}'), ('18bd941a-7c0f-44fe-9d12-a084d2a5f372',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Port Clements','NORR',NULL,false,true,'{}'), ('0235ecdb-95fc-47f9-b7f6-e05e1e388d5b',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Slocan','KOOR',NULL,false,true,'{}'), - ('0136a261-6237-4332-ae6a-1c7d7f49f0e8',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Chase','INTR',NULL,false,true,'{}'), ('5b5b05d3-df2e-403c-9cf5-3c52c68ab559',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Cumberland','ISLR',NULL,false,true,'{}'), ('e0684ddf-fa61-47c2-87b3-1c72bed5c365',NULL,NOW(),NULL,'migration_seed',NULL,'District of Fort St. James','NORR',NULL,false,true,'{}'), ('5dc1a21a-a740-402e-a56f-8f6c85991739',NULL,NOW(),NULL,'migration_seed',NULL,'District of Wells','INTR',NULL,false,true,'{}'), diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts index 0a7dd674a3..9afe363fbf 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts @@ -24,6 +24,7 @@ export class seedInactiveLocalGov1687810457499 implements MigrationInterface { ('476febec-ca88-4615-810c-fcae936867ef',NULL,NOW(),NULL,'migration_seed',NULL,'Multiple Jurisdictions for Sunshine','SOUR',NULL,false,false,'{}'), ('f0bd3817-5825-4656-b685-d306ddd1fa70',NULL,NOW(),NULL,'migration_seed',NULL,'Multiple Jurisdictions for Thompson-Nicola','INTR',NULL,false,false,'{}'), ('927ece9d-b4e3-4862-aa85-67539d9abb0d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Gabriola Island (Historical)','ISLR',NULL,false,false,'{}'), + ('e2540566-52a8-4a3c-b373-58e1744703af',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Galiano Island (Historical)','ISLR',NULL,false,false,'{}'), ('9ff02b37-9046-48c8-aa96-01977fb3d1b9',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Gambier Island (Historical)','SOUR',NULL,false,false,'{}'), ('d325a7a4-fe68-49b0-9df2-599beb06766e',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Hornby Island (Historical)','ISLR',NULL,false,false,'{}'), ('9f0bdb45-c212-4aee-8413-3cf3c79e988c',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Lasqueti Island (Historical)','ISLR',NULL,false,false,'{}'), From e89b36a9a48aabac2d770436e48cede2d483f691 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 15:01:23 -0700 Subject: [PATCH 035/954] Regions and local govs are working togethergit add sql/insert-batch-application.sql --- .../sql/insert-batch-application.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bin/migrate-oats-data/sql/insert-batch-application.sql b/bin/migrate-oats-data/sql/insert-batch-application.sql index 2fe3d63bc4..9f06f4b581 100644 --- a/bin/migrate-oats-data/sql/insert-batch-application.sql +++ b/bin/migrate-oats-data/sql/insert-batch-application.sql @@ -68,6 +68,33 @@ WITH WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' + WHEN oats_gov.oats_gov_name LIKE 'Thompson Nicola%' THEN 'Thompson Nicola Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Cariboo%' THEN 'Cariboo Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Fraser Valley%' THEN 'Fraser Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Columbia Shuswap%' THEN 'Columbia Shuswap Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Okanagan%' THEN 'Central Okanagan Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Squamish Lillooet%' THEN 'Squamish Lillooet Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Alberni-Clayoquot%' THEN 'Alberni-Clayoquot Regional District' + WHEN oats_gov.oats_gov_name LIKE 'qathet%' THEN 'qathet Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Peace River%' THEN 'Peace River Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Okanagan Similkameen%' THEN 'Okanagan Similkameen Regional District' + WHEN oats_gov.oats_gov_name LIKE 'East Kootenay%' THEN 'East Kootenay Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Bulkley-Nechako%' THEN 'Bulkley-Nechako Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Sunshine Coast%' THEN 'Sunshine Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Nanaimo%' THEN 'Nanaimo Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Kitimat Stikine%' THEN 'Kitimat Stikine Regional District' + WHEN oats_gov.oats_gov_name LIKE 'North Okanagan%' THEN 'North Okanagan Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Fraser Fort George%' THEN 'Fraser Fort George Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Cowichan Valley%' THEN 'Cowichan Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Kootenay Boundary%' THEN 'Kootenay Boundary Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Comox Valley%' THEN 'Comox Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Kootenay%' THEN 'Central Kootenay Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Capital%' THEN 'Capital Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Metro Vancouver%' THEN 'Metro Vancouver Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Coast%' THEN 'Central Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'North Coast%' THEN 'North Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Strathcona%' THEN 'Strathcona Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Mount Waddington%' THEN 'Mount Waddington Regional District' ELSE oats_gov.oats_gov_name END ) = alg."name" From acd4be28d3074e69c5c52532f3109c7cc6dfba16 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 16:21:34 -0700 Subject: [PATCH 036/954] MR feedback updates --- bin/migrate-oats-data/sql/insert-batch-application.sql | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bin/migrate-oats-data/sql/insert-batch-application.sql b/bin/migrate-oats-data/sql/insert-batch-application.sql index 9f06f4b581..499a817fd5 100644 --- a/bin/migrate-oats-data/sql/insert-batch-application.sql +++ b/bin/migrate-oats-data/sql/insert-batch-application.sql @@ -35,9 +35,7 @@ WITH JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id WHERE - oo.organization_type_cd = 'MUNI' - OR oo.organization_type_cd = 'FN' - OR oo.organization_type_cd = 'RD' + oo.organization_type_cd IN ('MUNI','FN','RD') ), alcs_gov AS ( SELECT @@ -104,8 +102,8 @@ WITH SELECT DISTINCT oaap.alr_application_id AS application_id, CASE - WHEN oo2.parent_organization_id isnull then oo2.organization_name - WHEN oo3.parent_organization_id isnull then oo3.organization_name + WHEN oo2.parent_organization_id IS NULL THEN oo2.organization_name + WHEN oo3.parent_organization_id IS NULL THEN oo3.organization_name ELSE 'NONE' END AS panel_region FROM From 55cc2024fcb6054d9de611af6db387c87a97893e Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 16:39:53 -0700 Subject: [PATCH 037/954] revert file to prev Migration --- .../1687810457499-seed_inactive_local_gov.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts index 9afe363fbf..d57b15c352 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts @@ -24,7 +24,6 @@ export class seedInactiveLocalGov1687810457499 implements MigrationInterface { ('476febec-ca88-4615-810c-fcae936867ef',NULL,NOW(),NULL,'migration_seed',NULL,'Multiple Jurisdictions for Sunshine','SOUR',NULL,false,false,'{}'), ('f0bd3817-5825-4656-b685-d306ddd1fa70',NULL,NOW(),NULL,'migration_seed',NULL,'Multiple Jurisdictions for Thompson-Nicola','INTR',NULL,false,false,'{}'), ('927ece9d-b4e3-4862-aa85-67539d9abb0d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Gabriola Island (Historical)','ISLR',NULL,false,false,'{}'), - ('e2540566-52a8-4a3c-b373-58e1744703af',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Galiano Island (Historical)','ISLR',NULL,false,false,'{}'), ('9ff02b37-9046-48c8-aa96-01977fb3d1b9',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Gambier Island (Historical)','SOUR',NULL,false,false,'{}'), ('d325a7a4-fe68-49b0-9df2-599beb06766e',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Hornby Island (Historical)','ISLR',NULL,false,false,'{}'), ('9f0bdb45-c212-4aee-8413-3cf3c79e988c',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Lasqueti Island (Historical)','ISLR',NULL,false,false,'{}'), @@ -34,13 +33,13 @@ export class seedInactiveLocalGov1687810457499 implements MigrationInterface { ('dc0357b1-aacd-45ba-8fa9-0e62cbe88e9f',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Salt Spring Island (Historical)','ISLR',NULL,false,false,'{}'), ('3890ccf8-94e3-4b9a-9a1c-4d33518e1b5d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Saturna Island (Historical)','ISLR',NULL,false,false,'{}'), ('538b895f-f0e1-4388-93cb-9eeb7cfa987e',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Sidney Island (Historical)','ISLR',NULL,false,false,'{}'), - ('88532d18-3059-4720-986a-363d9bce6be0',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Comox Strathcona (Historical)','ISLR',NULL,false,false,'{}'), - ('4e721d39-09b3-4f9b-ad1f-601a69315808',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Nanaimo (Historical)','ISLR',NULL,false,false,'{}'), - ('9355bf78-280a-4695-adad-fafa06969c99',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Capital (Historical)','ISLR',NULL,false,false,'{}'), - ('8017df89-8e71-41c3-bee9-1a48f042e157',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Powell River (Historical)','ISLR',NULL,false,false,'{}'), - ('26f395a1-44ab-4812-9e0d-f2dbc815859d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Sunshine Coast (Historical)','SOUR',NULL,false,false,'{}'), - ('b9056ee1-5ba3-4d7c-9029-2cec57ba75d5',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Cowichan Valley (Historical)','ISLR',NULL,false,false,'{}'), - ('55f665bc-c91b-4bbb-85c2-39691088b297',NULL,NOW(),NULL,'migration_seed',NULL,'Northern Rockies (Historical)','NORR',NULL,false,false,'{}'), + ('88532d18-3059-4720-986a-363d9bce6be0',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust- Comox Strathcona (Historical)','ISLR',NULL,false,false,'{}'), + ('4e721d39-09b3-4f9b-ad1f-601a69315808',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust- Nanaimo (Historical)','ISLR',NULL,false,false,'{}'), + ('9355bf78-280a-4695-adad-fafa06969c99',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust-Capital (Historical)','ISLR',NULL,false,false,'{}'), + ('8017df89-8e71-41c3-bee9-1a48f042e157',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust-Powell River (Historical)','ISLR',NULL,false,false,'{}'), + ('26f395a1-44ab-4812-9e0d-f2dbc815859d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust-Sunshine Coast (Historical)','SOUR',NULL,false,false,'{}'), + ('b9056ee1-5ba3-4d7c-9029-2cec57ba75d5',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust - Cowichan Valley (Historical)','ISLR',NULL,false,false,'{}'), + ('55f665bc-c91b-4bbb-85c2-39691088b297',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Bowen Island (Historical)','SOUR',NULL,false,false,'{}'), ('7577c7e9-d65b-4051-a0b1-bc95c462bdc3',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Denman Island (Historical)','ISLR',NULL,false,false,'{}'); `); } From ed9bdf83be8d8512c167f856f354ab0c87cedd55 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 16:47:06 -0700 Subject: [PATCH 038/954] removed other modified migration file --- .../typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts index aeba419435..ec15f01b62 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts @@ -8,6 +8,7 @@ export class seedAddAdlLocGov1687546406729 implements MigrationInterface { ('b8a622d7-66da-4598-9687-2e8727fb2561',NULL,NOW(),NULL,'migration_seed',NULL,'City of Port Alberni','ISLR',NULL,false,true,'{}'), ('18bd941a-7c0f-44fe-9d12-a084d2a5f372',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Port Clements','NORR',NULL,false,true,'{}'), ('0235ecdb-95fc-47f9-b7f6-e05e1e388d5b',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Slocan','KOOR',NULL,false,true,'{}'), + ('0136a261-6237-4332-ae6a-1c7d7f49f0e8',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Chase','INTR',NULL,false,true,'{}'), ('5b5b05d3-df2e-403c-9cf5-3c52c68ab559',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Cumberland','ISLR',NULL,false,true,'{}'), ('e0684ddf-fa61-47c2-87b3-1c72bed5c365',NULL,NOW(),NULL,'migration_seed',NULL,'District of Fort St. James','NORR',NULL,false,true,'{}'), ('5dc1a21a-a740-402e-a56f-8f6c85991739',NULL,NOW(),NULL,'migration_seed',NULL,'District of Wells','INTR',NULL,false,true,'{}'), From 5a582d881c24d2eb7b265b4450e421d0c9072332 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 30 Jun 2023 10:54:34 -0700 Subject: [PATCH 039/954] Add Parcel Prep Table to App Prep * Add editable ALR Area * Add new column for it * Add new component with table for display --- .../applicant-info.component.ts | 1 + .../parcel/parcel.component.html | 2 +- .../parcel/parcel.component.spec.ts | 10 ++- .../parcel/parcel.component.ts | 31 +++++++++- .../application/application.module.ts | 2 + .../parcel-prep/parcel-prep.component.html | 62 +++++++++++++++++++ .../parcel-prep/parcel-prep.component.scss | 17 +++++ .../parcel-prep/parcel-prep.component.spec.ts | 33 ++++++++++ .../parcel-prep/parcel-prep.component.ts | 50 +++++++++++++++ .../proposal/proposal.component.html | 6 +- .../application-parcel.service.ts | 16 +++++ .../services/application/application.dto.ts | 1 + .../application-parcel.controller.spec.ts | 22 +++++++ .../application-parcel.controller.ts | 21 ++++++- .../application-parcel.dto.ts | 13 +++- .../application-parcel.entity.ts | 11 ++++ .../application-parcel.service.ts | 7 ++- .../1688078238188-add_parcel_alr_area.ts | 17 +++++ 18 files changed, 310 insertions(+), 12 deletions(-) create mode 100644 alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.html create mode 100644 alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.scss create mode 100644 alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1688078238188-add_parcel_alr_area.ts diff --git a/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts b/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts index f302553db1..8fd982baba 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts @@ -1,4 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; import { DOCUMENT_TYPE } from '../../../services/application/application-document/application-document.service'; diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.html index a561191781..2847f9dcd6 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.html @@ -13,7 +13,7 @@

-

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

+

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

Parcel Information
Ownership Type
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.spec.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.spec.ts index b2df3aa9a4..c9b6ee0c52 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.spec.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.spec.ts @@ -1,7 +1,8 @@ -import { HttpClient } from '@angular/common/http'; 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 { Observable } from 'rxjs'; import { ApplicationDocumentService } from '../../../../../services/application/application-document/application-document.service'; import { SharedModule } from '../../../../../shared/shared.module'; import { ParcelComponent } from './parcel.component'; @@ -10,10 +11,13 @@ describe('ParcelComponent', () => { let component: ParcelComponent; let fixture: ComponentFixture; + let mockRoute: DeepMocked; let mockAppDocumentService: DeepMocked; beforeEach(async () => { mockAppDocumentService = createMock(); + mockRoute = createMock(); + mockRoute.fragment = new Observable(); await TestBed.configureTestingModule({ declarations: [ParcelComponent], @@ -22,6 +26,10 @@ describe('ParcelComponent', () => { provides: ApplicationDocumentService, useValue: mockAppDocumentService, }, + { + provide: ActivatedRoute, + useValue: mockRoute, + }, ], schemas: [NO_ERRORS_SCHEMA], imports: [SharedModule], diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts index d3df83e4ff..77d5dada58 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts @@ -1,4 +1,6 @@ -import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; import { ApplicationDocumentDto } from '../../../../../services/application/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application/application-document/application-document.service'; import { ApplicationParcelService } from '../../../../../services/application/application-parcel/application-parcel.service'; @@ -9,7 +11,9 @@ import { ApplicationSubmissionDto, PARCEL_OWNERSHIP_TYPE } from '../../../../../ templateUrl: './parcel.component.html', styleUrls: ['./parcel.component.scss'], }) -export class ParcelComponent implements OnInit, OnChanges { +export class ParcelComponent implements OnInit, OnChanges, OnDestroy { + $destroy = new Subject(); + @Input() application!: ApplicationSubmissionDto; @Input() files: ApplicationDocumentDto[] = []; @Input() parcelType!: string; @@ -24,7 +28,8 @@ export class ParcelComponent implements OnInit, OnChanges { constructor( private applicationDocumentService: ApplicationDocumentService, - private parcelService: ApplicationParcelService + private parcelService: ApplicationParcelService, + private route: ActivatedRoute ) {} ngOnInit(): void { @@ -32,6 +37,21 @@ export class ParcelComponent implements OnInit, OnChanges { this.pageTitle = 'Other Parcels in the Community'; this.showCertificateOfTitle = false; } + + this.route.fragment.pipe(takeUntil(this.$destroy)).subscribe((fragment) => { + if (fragment) { + setTimeout(() => { + const el = document.getElementById(fragment); + if (el) { + el.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'start', + }); + } + }, 200); + } + }); } async onOpenFile(uuid: string) { @@ -49,4 +69,9 @@ export class ParcelComponent implements OnInit, OnChanges { ngOnChanges(changes: SimpleChanges): void { this.loadParcels(this.application.fileNumber); } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } } diff --git a/alcs-frontend/src/app/features/application/application.module.ts b/alcs-frontend/src/app/features/application/application.module.ts index e6bb5d3e84..f9e4f39e9e 100644 --- a/alcs-frontend/src/app/features/application/application.module.ts +++ b/alcs-frontend/src/app/features/application/application.module.ts @@ -23,6 +23,7 @@ import { EditReconsiderationDialogComponent } from './post-decision/edit-reconsi import { PostDecisionComponent } from './post-decision/post-decision.component'; import { NaruProposalComponent } from './proposal/naru/naru.component'; import { NfuProposalComponent } from './proposal/nfu/nfu.component'; +import { ParcelPrepComponent } from './proposal/parcel-prep/parcel-prep.component'; import { ProposalComponent } from './proposal/proposal.component'; import { SoilProposalComponent } from './proposal/soil/soil.component'; import { SubdProposalComponent } from './proposal/subd/subd.component'; @@ -72,6 +73,7 @@ const routes: Routes = [ SoilProposalComponent, TurProposalComponent, NaruProposalComponent, + ParcelPrepComponent, ], imports: [SharedModule, RouterModule.forChild(routes), ApplicationDetailsModule, DecisionModule], }) diff --git a/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.html b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.html new file mode 100644 index 0000000000..b53344d89b --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.html @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#1PID + {{ row.pid }} + + PIN + {{ row.pin }} + + Civic Address + {{ row.civicAddress }} + Area (ha){{ row.mapAreaHectares }}ALR Area (ha) + + Owner(s) + {{ row.owners }} + Actions + +
diff --git a/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.scss b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.scss new file mode 100644 index 0000000000..c68304cfac --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.scss @@ -0,0 +1,17 @@ +@use '../../../../../styles/colors'; + +.link-button { + color: colors.$link-color; +} + +:host::ng-deep { + th { + font-weight: 700; + } +} + +.civic-address { + white-space: nowrap; + overflow: hidden; + text-overflow: clip; +} diff --git a/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.spec.ts b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.spec.ts new file mode 100644 index 0000000000..784da28e39 --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ApplicationParcelService } from '../../../../services/application/application-parcel/application-parcel.service'; + +import { ParcelPrepComponent } from './parcel-prep.component'; + +describe('ParcelPrepComponent', () => { + let component: ParcelPrepComponent; + let fixture: ComponentFixture; + let mockAppParcelService: DeepMocked; + + beforeEach(async () => { + mockAppParcelService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ParcelPrepComponent], + providers: [ + { + provide: ApplicationParcelService, + useValue: mockAppParcelService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelPrepComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.ts b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.ts new file mode 100644 index 0000000000..5bf3bce19a --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.ts @@ -0,0 +1,50 @@ +import { Component, Input, OnChanges } from '@angular/core'; +import { Router } from '@angular/router'; +import { ApplicationParcelService } from '../../../../services/application/application-parcel/application-parcel.service'; + +@Component({ + selector: 'app-parcel-prep[fileNumber]', + templateUrl: './parcel-prep.component.html', + styleUrls: ['./parcel-prep.component.scss'], +}) +export class ParcelPrepComponent implements OnChanges { + @Input() fileNumber = ''; + + displayedColumns = ['number', 'pid', 'pin', 'civicAddress', 'area', 'alrArea', 'owners', 'actions']; + parcels: { + pin?: string; + pid?: string; + mapAreaHectares: string; + alrArea: number; + owners: string; + fullOwners: string; + hasManyOwners: boolean; + uuid: string; + }[] = []; + + constructor(private parcelService: ApplicationParcelService, private router: Router) {} + + async loadParcels(fileNumber: string) { + const parcels = await this.parcelService.fetchParcels(fileNumber); + this.parcels = parcels.map((parcel) => ({ + ...parcel, + owners: `${parcel.owners[0].displayName} ${parcel.owners.length > 1 ? ' et al.' : ''}`, + fullOwners: parcel.owners.map((owner) => owner.displayName).join(', '), + hasManyOwners: parcel.owners.length > 1, + })); + } + + async saveParcel(uuid: string, alrArea: string | null) { + await this.parcelService.setParcelArea(uuid, alrArea ? parseFloat(alrArea) : null); + } + + ngOnChanges(): void { + this.loadParcels(this.fileNumber); + } + + async navigateToParcelDetails(uuid: any) { + await this.router.navigate(['application', this.fileNumber, 'applicant-info'], { + fragment: uuid, + }); + } +} diff --git a/alcs-frontend/src/app/features/application/proposal/proposal.component.html b/alcs-frontend/src/app/features/application/proposal/proposal.component.html index 14c4105fb8..65e2e600ae 100644 --- a/alcs-frontend/src/app/features/application/proposal/proposal.component.html +++ b/alcs-frontend/src/app/features/application/proposal/proposal.component.html @@ -3,7 +3,7 @@

Application Prep

Proposal Components - {{ application?.type?.label }}
-
ALR Area (ha)
+
Proposal ALR Area (ha)
@@ -45,6 +45,10 @@
Proposal Components - {{ application?.type?.label }}
> +
+
Application Parcels
+ +
Staff Comments and Observations
{ + try { + const res = await firstValueFrom( + this.http.post(`${this.baseUrl}/${uuid}`, { + alrArea, + }) + ); + this.toastService.showSuccessToast('Application updated'); + + return res; + } catch (e) { + this.toastService.showErrorToast('Failed to update Application Parcel'); + throw e; + } + } } diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index ae28139a65..e98b27f16d 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -84,6 +84,7 @@ export interface ApplicationParcelDto { parcelType?: string; certificateOfTitleUuid?: string; owners: SubmittedApplicationOwnerDto[]; + alrArea: number; } export interface ApplicationSubmissionDto { diff --git a/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.spec.ts b/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.spec.ts index 4c3822c2cb..f196b0c069 100644 --- a/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.spec.ts @@ -42,4 +42,26 @@ describe('ApplicationParcelController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + it('should call through to service for get', async () => { + mockParcelService.fetchByApplicationFileId.mockResolvedValue([]); + + await controller.get(''); + expect(mockParcelService.fetchByApplicationFileId).toHaveBeenCalledTimes(1); + }); + + it('should call through to service for update', async () => { + mockParcelService.update.mockResolvedValue([]); + + await controller.update('12', { + alrArea: 5, + }); + expect(mockParcelService.update).toHaveBeenCalledTimes(1); + expect(mockParcelService.update).toHaveBeenCalledWith([ + { + uuid: '12', + alrArea: 5, + }, + ]); + }); }); diff --git a/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.ts b/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.ts index 0fd13d20ae..632dc4c510 100644 --- a/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.ts +++ b/services/apps/alcs/src/alcs/application/application-parcel/application-parcel.controller.ts @@ -1,16 +1,14 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; import { RolesGuard } from '../../../common/authorization/roles-guard.service'; import { UserRoles } from '../../../common/authorization/roles.decorator'; -import { DocumentService } from '../../../document/document.service'; import { ApplicationParcelDto } from '../../../portal/application-submission/application-parcel/application-parcel.dto'; import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; import { ApplicationParcelService } from '../../../portal/application-submission/application-parcel/application-parcel.service'; -import { ApplicationSubmissionService } from '../application-submission/application-submission.service'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @UseGuards(RolesGuard) @@ -33,4 +31,21 @@ export class ApplicationParcelController { ApplicationParcelDto, ); } + + @UserRoles(...ANY_AUTH_ROLE) + @Post('/:uuid') + async update(@Param('uuid') uuid: string, @Body() body: { alrArea: number }) { + const parcels = await this.applicationParcelService.update([ + { + uuid, + alrArea: body.alrArea, + }, + ]); + + return this.mapper.mapArray( + parcels, + ApplicationParcel, + ApplicationParcelDto, + )[0]; + } } diff --git a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.dto.ts b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.dto.ts index e77990ae9d..b00a1195fb 100644 --- a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.dto.ts @@ -59,6 +59,9 @@ export class ApplicationParcelDto { @AutoMap(() => String) parcelType: string; + @AutoMap(() => Number) + alrArea: number | null; + certificateOfTitle?: ApplicationDocumentDto; owners: ApplicationOwnerDetailedDto[]; } @@ -110,7 +113,8 @@ export class ApplicationParcelUpdateDto { isFarm?: boolean | null; @IsBoolean() - isConfirmedByApplicant: boolean; + @IsOptional() + isConfirmedByApplicant?: boolean; @IsString() @IsOptional() @@ -121,7 +125,12 @@ export class ApplicationParcelUpdateDto { crownLandOwnerType?: string | null; @IsArray() - ownerUuids: string[] | null; + @IsOptional() + ownerUuids?: string[] | null; + + @IsNumber() + @IsOptional() + alrArea?: number | null; } export enum PARCEL_TYPE { diff --git a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts index 71e65c9661..3fd81342d6 100644 --- a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts @@ -10,6 +10,7 @@ import { import { ApplicationDocumentDto } from '../../../alcs/application/application-document/application-document.dto'; import { ApplicationDocument } from '../../../alcs/application/application-document/application-document.entity'; import { Base } from '../../../common/entities/base.entity'; +import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; import { ApplicationOwner } from '../application-owner/application-owner.entity'; import { ApplicationSubmission } from '../application-submission.entity'; import { ApplicationParcelOwnershipType } from './application-parcel-ownership-type/application-parcel-ownership-type.entity'; @@ -143,4 +144,14 @@ export class ApplicationParcel extends Base { @AutoMap(() => String) @Column({ nullable: true }) certificateOfTitleUuid: string | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + alrArea?: number | null; } diff --git a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.service.ts b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.service.ts index 64de5f87d2..1ecad9f688 100644 --- a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.service.ts @@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { ApplicationDocument } from '../../../alcs/application/application-document/application-document.entity'; import { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { filterUndefined } from '../../../utils/undefined'; import { ApplicationOwnerService } from '../application-owner/application-owner.service'; import { ApplicationParcelUpdateDto } from './application-parcel.dto'; import { ApplicationParcel } from './application-parcel.entity'; @@ -89,8 +90,12 @@ export class ApplicationParcelService { parcel.isFarm = updateDto.isFarm; parcel.purchasedDate = formatIncomingDate(updateDto.purchasedDate); parcel.ownershipTypeCode = updateDto.ownershipTypeCode; - parcel.isConfirmedByApplicant = updateDto.isConfirmedByApplicant; + parcel.isConfirmedByApplicant = filterUndefined( + updateDto.isConfirmedByApplicant, + parcel.isConfirmedByApplicant, + ); parcel.crownLandOwnerType = updateDto.crownLandOwnerType; + parcel.alrArea = updateDto.alrArea; if (updateDto.ownerUuids) { hasOwnerUpdate = true; diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1688078238188-add_parcel_alr_area.ts b/services/apps/alcs/src/providers/typeorm/migrations/1688078238188-add_parcel_alr_area.ts new file mode 100644 index 0000000000..2be91975e2 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1688078238188-add_parcel_alr_area.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addParcelAlrArea1688078238188 implements MigrationInterface { + name = 'addParcelAlrArea1688078238188'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_parcel" ADD "alr_area" numeric(12,2)`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_parcel" DROP COLUMN "alr_area"`, + ); + } +} From 9d5b429ef594867a7f1a479e2ad60400b2bb98c5 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 30 Jun 2023 11:08:39 -0700 Subject: [PATCH 040/954] fixing errors in migrations --- .../1688144882418-local_gov_corrections.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts b/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts new file mode 100644 index 0000000000..12c9abfc32 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class localGovCorrections1688144882418 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Comox Strathcona (Historical)' WHERE "name" = 'Islands Trust- Comox Strathcona (Historical)'; + UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Nanaimo (Historical)' WHERE "name" = 'Islands Trust- Nanaimo (Historical)'; + UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Capital (Historical)' WHERE "name" = 'Islands Trust-Capital (Historical)'; + UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Powell River (Historical)' WHERE "name" = 'Islands Trust-Powell River (Historical)'; + UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Sunshine Coast (Historical)' WHERE "name" = 'Islands Trust-Sunshine Coast (Historical)'; + UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Cowichan Valley (Historical)' WHERE "name" = 'Islands Trust - Cowichan Valley (Historical)'; + UPDATE "alcs"."application_local_government" SET "name" = 'Northern Rockies (Historical)' WHERE "uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; + UPDATE "alcs"."application_local_government" SET "preferred_region_code" = 'NORR' WHERE "uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; + DELETE FROM alcs.application_local_government WHERE uuid = '0136a261-6237-4332-ae6a-1c7d7f49f0e8'; + INSERT INTO alcs.application_local_government (uuid,audit_deleted_date_at,audit_created_at,audit_updated_at,audit_created_by,audit_updated_by,"name",preferred_region_code,bceid_business_guid,is_first_nation,is_active,emails) VALUES + ('e2540566-52a8-4a3c-b373-58e1744703af',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Galiano Island (Historical)','ISLR',NULL,false,false,'{}'); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + //N/A + } + +} From 659fd95e140585496382bc580f5b67a77b799f08 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 27 Jun 2023 15:53:37 -0700 Subject: [PATCH 041/954] updated migration & implemented case-when for historical IslTrst --- .../sql/insert-batch-application.sql | 23 +++++++++++++++++-- .../1687810457499-seed_inactive_local_gov.ts | 14 +++++------ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/bin/migrate-oats-data/sql/insert-batch-application.sql b/bin/migrate-oats-data/sql/insert-batch-application.sql index 2358d2c521..bdb992cbf5 100644 --- a/bin/migrate-oats-data/sql/insert-batch-application.sql +++ b/bin/migrate-oats-data/sql/insert-batch-application.sql @@ -49,12 +49,31 @@ alcs_gov AS ( oats_gov JOIN alcs.application_local_government alg on ( CASE - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust%' THEN 'Islands Trust' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gabriola Island' THEN 'Islands Trust Gabriola Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Galiano Island' THEN 'Islands Trust Galiano Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gambier Island' THEN 'Islands Trust Gambier Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Hornby Island' THEN 'Islands Trust Hornby Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Lasqueti Island' THEN 'Islands Trust Lasqueti Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Mayne Island' THEN 'Islands Trust Mayne Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Pender Island' THEN 'Islands Trust Pender Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Quadra Island' THEN 'Islands Trust Quadra Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Salt Spring Island' THEN 'Islands Trust Salt Spring Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Sunshine Coast' THEN 'Islands Trust Sunshine Coast (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Bowen Island' THEN 'Bowen Island (Island Municipality)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Denman Island' THEN 'Islands Trust Denman Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' ELSE oats_gov.oats_gov_name END ) = alg."name" -), +), -- Step 3: Perform a lookup to retrieve the region code for each application ID panel_lookup AS ( SELECT DISTINCT diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts index d57b15c352..0a7dd674a3 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts @@ -33,13 +33,13 @@ export class seedInactiveLocalGov1687810457499 implements MigrationInterface { ('dc0357b1-aacd-45ba-8fa9-0e62cbe88e9f',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Salt Spring Island (Historical)','ISLR',NULL,false,false,'{}'), ('3890ccf8-94e3-4b9a-9a1c-4d33518e1b5d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Saturna Island (Historical)','ISLR',NULL,false,false,'{}'), ('538b895f-f0e1-4388-93cb-9eeb7cfa987e',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Sidney Island (Historical)','ISLR',NULL,false,false,'{}'), - ('88532d18-3059-4720-986a-363d9bce6be0',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust- Comox Strathcona (Historical)','ISLR',NULL,false,false,'{}'), - ('4e721d39-09b3-4f9b-ad1f-601a69315808',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust- Nanaimo (Historical)','ISLR',NULL,false,false,'{}'), - ('9355bf78-280a-4695-adad-fafa06969c99',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust-Capital (Historical)','ISLR',NULL,false,false,'{}'), - ('8017df89-8e71-41c3-bee9-1a48f042e157',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust-Powell River (Historical)','ISLR',NULL,false,false,'{}'), - ('26f395a1-44ab-4812-9e0d-f2dbc815859d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust-Sunshine Coast (Historical)','SOUR',NULL,false,false,'{}'), - ('b9056ee1-5ba3-4d7c-9029-2cec57ba75d5',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust - Cowichan Valley (Historical)','ISLR',NULL,false,false,'{}'), - ('55f665bc-c91b-4bbb-85c2-39691088b297',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Bowen Island (Historical)','SOUR',NULL,false,false,'{}'), + ('88532d18-3059-4720-986a-363d9bce6be0',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Comox Strathcona (Historical)','ISLR',NULL,false,false,'{}'), + ('4e721d39-09b3-4f9b-ad1f-601a69315808',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Nanaimo (Historical)','ISLR',NULL,false,false,'{}'), + ('9355bf78-280a-4695-adad-fafa06969c99',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Capital (Historical)','ISLR',NULL,false,false,'{}'), + ('8017df89-8e71-41c3-bee9-1a48f042e157',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Powell River (Historical)','ISLR',NULL,false,false,'{}'), + ('26f395a1-44ab-4812-9e0d-f2dbc815859d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Sunshine Coast (Historical)','SOUR',NULL,false,false,'{}'), + ('b9056ee1-5ba3-4d7c-9029-2cec57ba75d5',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Cowichan Valley (Historical)','ISLR',NULL,false,false,'{}'), + ('55f665bc-c91b-4bbb-85c2-39691088b297',NULL,NOW(),NULL,'migration_seed',NULL,'Northern Rockies (Historical)','NORR',NULL,false,false,'{}'), ('7577c7e9-d65b-4051-a0b1-bc95c462bdc3',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Denman Island (Historical)','ISLR',NULL,false,false,'{}'); `); } From b61f06774a958010ab8e7d210ac3e288b777d442 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 14:55:22 -0700 Subject: [PATCH 042/954] local gov bug fixed --- .../sql/insert-batch-application.sql | 153 +++++++++--------- .../1687546406729-seed_add_adl_loc_gov.ts | 1 - .../1687810457499-seed_inactive_local_gov.ts | 1 + 3 files changed, 78 insertions(+), 77 deletions(-) diff --git a/bin/migrate-oats-data/sql/insert-batch-application.sql b/bin/migrate-oats-data/sql/insert-batch-application.sql index bdb992cbf5..2fe3d63bc4 100644 --- a/bin/migrate-oats-data/sql/insert-batch-application.sql +++ b/bin/migrate-oats-data/sql/insert-batch-application.sql @@ -1,4 +1,3 @@ - -- Step 1: Perform a lookup to retrieve the applicant's name or organization for each application ID WITH applicant_lookup AS ( @@ -26,82 +25,84 @@ WITH GROUP BY oaap.alr_application_id ), - --- Step 2: get local gov application name & match to uuid -oats_gov AS ( - SELECT - oaap.alr_application_id AS application_id, - oo.organization_name AS oats_gov_name - FROM - oats.oats_alr_application_parties oaap - JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - WHERE - oo.organization_type_cd = 'MUNI' - OR oo.organization_type_cd = 'FN' -), - -alcs_gov AS ( - SELECT - oats_gov.application_id AS application_id, - alg.uuid AS gov_uuid - FROM - oats_gov - JOIN alcs.application_local_government alg on ( + -- Step 2: get local gov application name & match to uuid + oats_gov AS ( + SELECT + oaap.alr_application_id AS application_id, + oo.organization_name AS oats_gov_name + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oo.organization_type_cd = 'MUNI' + OR oo.organization_type_cd = 'FN' + OR oo.organization_type_cd = 'RD' + ), + alcs_gov AS ( + SELECT + oats_gov.application_id AS application_id, + alg.uuid AS gov_uuid + FROM + oats_gov + JOIN alcs.application_local_government alg on ( + CASE + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gabriola Island' THEN 'Islands Trust Gabriola Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Galiano Island' THEN 'Islands Trust Galiano Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gambier Island' THEN 'Islands Trust Gambier Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Hornby Island' THEN 'Islands Trust Hornby Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Lasqueti Island' THEN 'Islands Trust Lasqueti Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Mayne Island' THEN 'Islands Trust Mayne Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Pender Island' THEN 'Islands Trust Pender Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Quadra Island' THEN 'Islands Trust Quadra Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Salt Spring Island' THEN 'Islands Trust Salt Spring Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Sunshine Coast' THEN 'Islands Trust Sunshine Coast (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Bowen Island' THEN 'Bowen Island (Island Municipality)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Denman Island' THEN 'Islands Trust Denman Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' + ELSE oats_gov.oats_gov_name + END + ) = alg."name" + ), + -- Step 3: Perform a lookup to retrieve the region code for each application ID + panel_lookup AS ( + SELECT DISTINCT + oaap.alr_application_id AS application_id, CASE - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gabriola Island' THEN 'Islands Trust Gabriola Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Galiano Island' THEN 'Islands Trust Galiano Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gambier Island' THEN 'Islands Trust Gambier Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Hornby Island' THEN 'Islands Trust Hornby Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Lasqueti Island' THEN 'Islands Trust Lasqueti Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Mayne Island' THEN 'Islands Trust Mayne Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Pender Island' THEN 'Islands Trust Pender Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Quadra Island' THEN 'Islands Trust Quadra Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Salt Spring Island' THEN 'Islands Trust Salt Spring Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Sunshine Coast' THEN 'Islands Trust Sunshine Coast (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Bowen Island' THEN 'Bowen Island (Island Municipality)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Denman Island' THEN 'Islands Trust Denman Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' - ELSE oats_gov.oats_gov_name - END - ) = alg."name" -), --- Step 3: Perform a lookup to retrieve the region code for each application ID -panel_lookup AS ( - SELECT DISTINCT - oaap.alr_application_id AS application_id, - oo2.organization_name AS panel_region - FROM - oats.oats_alr_application_parties oaap - JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - JOIN oats.oats_organizations oo2 ON oo.parent_organization_id = oo2.organization_id - WHERE - oo2.organization_type_cd = 'PANEL' -), - --- Step 4: Perform lookup to retrieve type code -application_type_lookup AS ( - SELECT - oaac.alr_application_id AS application_id, - oacc."description" AS "description", - oaac.alr_change_code AS code - FROM - oats.oats_alr_appl_components AS oaac - JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code - LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id - WHERE - aee.component_id IS NULL -) - + WHEN oo2.parent_organization_id isnull then oo2.organization_name + WHEN oo3.parent_organization_id isnull then oo3.organization_name + ELSE 'NONE' + END AS panel_region + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + LEFT JOIN oats.oats_organizations oo2 ON oo.parent_organization_id = oo2.organization_id + LEFT JOIN oats.oats_organizations oo3 ON oo2.parent_organization_id = oo3.organization_id + WHERE + oo2.organization_type_cd = 'PANEL' OR oo3.organization_type_cd = 'PANEL' + ), + -- Step 4: Perform lookup to retrieve type code + application_type_lookup AS ( + SELECT + oaac.alr_application_id AS application_id, + oacc."description" AS "description", + oaac.alr_change_code AS code + FROM + oats.oats_alr_appl_components AS oaac + JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code + LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id + WHERE + aee.component_id IS NULL + ) -- Step 5: Insert new records into the alcs_applications table SELECT oa.alr_application_id :: text AS file_number, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts index ec15f01b62..aeba419435 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts @@ -8,7 +8,6 @@ export class seedAddAdlLocGov1687546406729 implements MigrationInterface { ('b8a622d7-66da-4598-9687-2e8727fb2561',NULL,NOW(),NULL,'migration_seed',NULL,'City of Port Alberni','ISLR',NULL,false,true,'{}'), ('18bd941a-7c0f-44fe-9d12-a084d2a5f372',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Port Clements','NORR',NULL,false,true,'{}'), ('0235ecdb-95fc-47f9-b7f6-e05e1e388d5b',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Slocan','KOOR',NULL,false,true,'{}'), - ('0136a261-6237-4332-ae6a-1c7d7f49f0e8',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Chase','INTR',NULL,false,true,'{}'), ('5b5b05d3-df2e-403c-9cf5-3c52c68ab559',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Cumberland','ISLR',NULL,false,true,'{}'), ('e0684ddf-fa61-47c2-87b3-1c72bed5c365',NULL,NOW(),NULL,'migration_seed',NULL,'District of Fort St. James','NORR',NULL,false,true,'{}'), ('5dc1a21a-a740-402e-a56f-8f6c85991739',NULL,NOW(),NULL,'migration_seed',NULL,'District of Wells','INTR',NULL,false,true,'{}'), diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts index 0a7dd674a3..9afe363fbf 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts @@ -24,6 +24,7 @@ export class seedInactiveLocalGov1687810457499 implements MigrationInterface { ('476febec-ca88-4615-810c-fcae936867ef',NULL,NOW(),NULL,'migration_seed',NULL,'Multiple Jurisdictions for Sunshine','SOUR',NULL,false,false,'{}'), ('f0bd3817-5825-4656-b685-d306ddd1fa70',NULL,NOW(),NULL,'migration_seed',NULL,'Multiple Jurisdictions for Thompson-Nicola','INTR',NULL,false,false,'{}'), ('927ece9d-b4e3-4862-aa85-67539d9abb0d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Gabriola Island (Historical)','ISLR',NULL,false,false,'{}'), + ('e2540566-52a8-4a3c-b373-58e1744703af',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Galiano Island (Historical)','ISLR',NULL,false,false,'{}'), ('9ff02b37-9046-48c8-aa96-01977fb3d1b9',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Gambier Island (Historical)','SOUR',NULL,false,false,'{}'), ('d325a7a4-fe68-49b0-9df2-599beb06766e',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Hornby Island (Historical)','ISLR',NULL,false,false,'{}'), ('9f0bdb45-c212-4aee-8413-3cf3c79e988c',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Lasqueti Island (Historical)','ISLR',NULL,false,false,'{}'), From d8ef1c73207cd7d68a90794bc2fc077ac35c3c85 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 15:01:23 -0700 Subject: [PATCH 043/954] Regions and local govs are working togethergit add sql/insert-batch-application.sql --- .../sql/insert-batch-application.sql | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bin/migrate-oats-data/sql/insert-batch-application.sql b/bin/migrate-oats-data/sql/insert-batch-application.sql index 2fe3d63bc4..9f06f4b581 100644 --- a/bin/migrate-oats-data/sql/insert-batch-application.sql +++ b/bin/migrate-oats-data/sql/insert-batch-application.sql @@ -68,6 +68,33 @@ WITH WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' + WHEN oats_gov.oats_gov_name LIKE 'Thompson Nicola%' THEN 'Thompson Nicola Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Cariboo%' THEN 'Cariboo Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Fraser Valley%' THEN 'Fraser Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Columbia Shuswap%' THEN 'Columbia Shuswap Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Okanagan%' THEN 'Central Okanagan Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Squamish Lillooet%' THEN 'Squamish Lillooet Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Alberni-Clayoquot%' THEN 'Alberni-Clayoquot Regional District' + WHEN oats_gov.oats_gov_name LIKE 'qathet%' THEN 'qathet Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Peace River%' THEN 'Peace River Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Okanagan Similkameen%' THEN 'Okanagan Similkameen Regional District' + WHEN oats_gov.oats_gov_name LIKE 'East Kootenay%' THEN 'East Kootenay Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Bulkley-Nechako%' THEN 'Bulkley-Nechako Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Sunshine Coast%' THEN 'Sunshine Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Nanaimo%' THEN 'Nanaimo Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Kitimat Stikine%' THEN 'Kitimat Stikine Regional District' + WHEN oats_gov.oats_gov_name LIKE 'North Okanagan%' THEN 'North Okanagan Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Fraser Fort George%' THEN 'Fraser Fort George Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Cowichan Valley%' THEN 'Cowichan Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Kootenay Boundary%' THEN 'Kootenay Boundary Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Comox Valley%' THEN 'Comox Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Kootenay%' THEN 'Central Kootenay Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Capital%' THEN 'Capital Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Metro Vancouver%' THEN 'Metro Vancouver Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Coast%' THEN 'Central Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'North Coast%' THEN 'North Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Strathcona%' THEN 'Strathcona Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Mount Waddington%' THEN 'Mount Waddington Regional District' ELSE oats_gov.oats_gov_name END ) = alg."name" From 6d3c34850247ef2de6ebdfc557b16a4578109737 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 16:21:34 -0700 Subject: [PATCH 044/954] MR feedback updates --- bin/migrate-oats-data/sql/insert-batch-application.sql | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/bin/migrate-oats-data/sql/insert-batch-application.sql b/bin/migrate-oats-data/sql/insert-batch-application.sql index 9f06f4b581..499a817fd5 100644 --- a/bin/migrate-oats-data/sql/insert-batch-application.sql +++ b/bin/migrate-oats-data/sql/insert-batch-application.sql @@ -35,9 +35,7 @@ WITH JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id WHERE - oo.organization_type_cd = 'MUNI' - OR oo.organization_type_cd = 'FN' - OR oo.organization_type_cd = 'RD' + oo.organization_type_cd IN ('MUNI','FN','RD') ), alcs_gov AS ( SELECT @@ -104,8 +102,8 @@ WITH SELECT DISTINCT oaap.alr_application_id AS application_id, CASE - WHEN oo2.parent_organization_id isnull then oo2.organization_name - WHEN oo3.parent_organization_id isnull then oo3.organization_name + WHEN oo2.parent_organization_id IS NULL THEN oo2.organization_name + WHEN oo3.parent_organization_id IS NULL THEN oo3.organization_name ELSE 'NONE' END AS panel_region FROM From d223c86dcbbe49adb3b017d243cd91d22ea89f3f Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 16:39:53 -0700 Subject: [PATCH 045/954] revert file to prev Migration --- .../1687810457499-seed_inactive_local_gov.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts index 9afe363fbf..d57b15c352 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687810457499-seed_inactive_local_gov.ts @@ -24,7 +24,6 @@ export class seedInactiveLocalGov1687810457499 implements MigrationInterface { ('476febec-ca88-4615-810c-fcae936867ef',NULL,NOW(),NULL,'migration_seed',NULL,'Multiple Jurisdictions for Sunshine','SOUR',NULL,false,false,'{}'), ('f0bd3817-5825-4656-b685-d306ddd1fa70',NULL,NOW(),NULL,'migration_seed',NULL,'Multiple Jurisdictions for Thompson-Nicola','INTR',NULL,false,false,'{}'), ('927ece9d-b4e3-4862-aa85-67539d9abb0d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Gabriola Island (Historical)','ISLR',NULL,false,false,'{}'), - ('e2540566-52a8-4a3c-b373-58e1744703af',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Galiano Island (Historical)','ISLR',NULL,false,false,'{}'), ('9ff02b37-9046-48c8-aa96-01977fb3d1b9',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Gambier Island (Historical)','SOUR',NULL,false,false,'{}'), ('d325a7a4-fe68-49b0-9df2-599beb06766e',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Hornby Island (Historical)','ISLR',NULL,false,false,'{}'), ('9f0bdb45-c212-4aee-8413-3cf3c79e988c',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Lasqueti Island (Historical)','ISLR',NULL,false,false,'{}'), @@ -34,13 +33,13 @@ export class seedInactiveLocalGov1687810457499 implements MigrationInterface { ('dc0357b1-aacd-45ba-8fa9-0e62cbe88e9f',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Salt Spring Island (Historical)','ISLR',NULL,false,false,'{}'), ('3890ccf8-94e3-4b9a-9a1c-4d33518e1b5d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Saturna Island (Historical)','ISLR',NULL,false,false,'{}'), ('538b895f-f0e1-4388-93cb-9eeb7cfa987e',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Sidney Island (Historical)','ISLR',NULL,false,false,'{}'), - ('88532d18-3059-4720-986a-363d9bce6be0',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Comox Strathcona (Historical)','ISLR',NULL,false,false,'{}'), - ('4e721d39-09b3-4f9b-ad1f-601a69315808',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Nanaimo (Historical)','ISLR',NULL,false,false,'{}'), - ('9355bf78-280a-4695-adad-fafa06969c99',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Capital (Historical)','ISLR',NULL,false,false,'{}'), - ('8017df89-8e71-41c3-bee9-1a48f042e157',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Powell River (Historical)','ISLR',NULL,false,false,'{}'), - ('26f395a1-44ab-4812-9e0d-f2dbc815859d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Sunshine Coast (Historical)','SOUR',NULL,false,false,'{}'), - ('b9056ee1-5ba3-4d7c-9029-2cec57ba75d5',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Cowichan Valley (Historical)','ISLR',NULL,false,false,'{}'), - ('55f665bc-c91b-4bbb-85c2-39691088b297',NULL,NOW(),NULL,'migration_seed',NULL,'Northern Rockies (Historical)','NORR',NULL,false,false,'{}'), + ('88532d18-3059-4720-986a-363d9bce6be0',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust- Comox Strathcona (Historical)','ISLR',NULL,false,false,'{}'), + ('4e721d39-09b3-4f9b-ad1f-601a69315808',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust- Nanaimo (Historical)','ISLR',NULL,false,false,'{}'), + ('9355bf78-280a-4695-adad-fafa06969c99',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust-Capital (Historical)','ISLR',NULL,false,false,'{}'), + ('8017df89-8e71-41c3-bee9-1a48f042e157',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust-Powell River (Historical)','ISLR',NULL,false,false,'{}'), + ('26f395a1-44ab-4812-9e0d-f2dbc815859d',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust-Sunshine Coast (Historical)','SOUR',NULL,false,false,'{}'), + ('b9056ee1-5ba3-4d7c-9029-2cec57ba75d5',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust - Cowichan Valley (Historical)','ISLR',NULL,false,false,'{}'), + ('55f665bc-c91b-4bbb-85c2-39691088b297',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Bowen Island (Historical)','SOUR',NULL,false,false,'{}'), ('7577c7e9-d65b-4051-a0b1-bc95c462bdc3',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Denman Island (Historical)','ISLR',NULL,false,false,'{}'); `); } From 62e3138d51d6efeeb9387e7fdb844c7f0015ee16 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 29 Jun 2023 16:47:06 -0700 Subject: [PATCH 046/954] removed other modified migration file --- .../typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts b/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts index aeba419435..ec15f01b62 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1687546406729-seed_add_adl_loc_gov.ts @@ -8,6 +8,7 @@ export class seedAddAdlLocGov1687546406729 implements MigrationInterface { ('b8a622d7-66da-4598-9687-2e8727fb2561',NULL,NOW(),NULL,'migration_seed',NULL,'City of Port Alberni','ISLR',NULL,false,true,'{}'), ('18bd941a-7c0f-44fe-9d12-a084d2a5f372',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Port Clements','NORR',NULL,false,true,'{}'), ('0235ecdb-95fc-47f9-b7f6-e05e1e388d5b',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Slocan','KOOR',NULL,false,true,'{}'), + ('0136a261-6237-4332-ae6a-1c7d7f49f0e8',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Chase','INTR',NULL,false,true,'{}'), ('5b5b05d3-df2e-403c-9cf5-3c52c68ab559',NULL,NOW(),NULL,'migration_seed',NULL,'Village of Cumberland','ISLR',NULL,false,true,'{}'), ('e0684ddf-fa61-47c2-87b3-1c72bed5c365',NULL,NOW(),NULL,'migration_seed',NULL,'District of Fort St. James','NORR',NULL,false,true,'{}'), ('5dc1a21a-a740-402e-a56f-8f6c85991739',NULL,NOW(),NULL,'migration_seed',NULL,'District of Wells','INTR',NULL,false,true,'{}'), From bfddf48d213d2b1ee5c89313bb5b6e89fb9e76b8 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 30 Jun 2023 11:20:02 -0700 Subject: [PATCH 047/954] ensuring no migration errors --- .../typeorm/migrations/1688144882418-local_gov_corrections.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts b/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts index 12c9abfc32..76180a4bd7 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts @@ -12,6 +12,10 @@ export class localGovCorrections1688144882418 implements MigrationInterface { UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Cowichan Valley (Historical)' WHERE "name" = 'Islands Trust - Cowichan Valley (Historical)'; UPDATE "alcs"."application_local_government" SET "name" = 'Northern Rockies (Historical)' WHERE "uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; UPDATE "alcs"."application_local_government" SET "preferred_region_code" = 'NORR' WHERE "uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; + UPDATE "alcs"."application" SET "local_government_uuid" = '360b05b3-55d4-4f57-a5ea-e0ffd6125db4' WHERE "local_government_uuid" = '0136a261-6237-4332-ae6a-1c7d7f49f0e8'; + UPDATE "alcs"."notice_of_intent" SET "local_government_uuid" = '360b05b3-55d4-4f57-a5ea-e0ffd6125db4' WHERE "local_government_uuid" = '0136a261-6237-4332-ae6a-1c7d7f49f0e8'; + UPDATE "alcs"."planning_review" SET "local_government_uuid" = '360b05b3-55d4-4f57-a5ea-e0ffd6125db4' WHERE "local_government_uuid" = '0136a261-6237-4332-ae6a-1c7d7f49f0e8'; + UPDATE "alcs"."covenant" SET "local_government_uuid" = '360b05b3-55d4-4f57-a5ea-e0ffd6125db4' WHERE "local_government_uuid" = '0136a261-6237-4332-ae6a-1c7d7f49f0e8'; DELETE FROM alcs.application_local_government WHERE uuid = '0136a261-6237-4332-ae6a-1c7d7f49f0e8'; INSERT INTO alcs.application_local_government (uuid,audit_deleted_date_at,audit_created_at,audit_updated_at,audit_created_by,audit_updated_by,"name",preferred_region_code,bceid_business_guid,is_first_nation,is_active,emails) VALUES ('e2540566-52a8-4a3c-b373-58e1744703af',NULL,NOW(),NULL,'migration_seed',NULL,'Islands Trust Galiano Island (Historical)','ISLR',NULL,false,false,'{}'); From df79b6bd2a53e125381781fd64653c568909f393 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 30 Jun 2023 11:31:54 -0700 Subject: [PATCH 048/954] ensure any used uuids are not deleted/ changed improperly --- .../typeorm/migrations/1688144882418-local_gov_corrections.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts b/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts index 76180a4bd7..b33cac8790 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1688144882418-local_gov_corrections.ts @@ -10,6 +10,10 @@ export class localGovCorrections1688144882418 implements MigrationInterface { UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Powell River (Historical)' WHERE "name" = 'Islands Trust-Powell River (Historical)'; UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Sunshine Coast (Historical)' WHERE "name" = 'Islands Trust-Sunshine Coast (Historical)'; UPDATE "alcs"."application_local_government" SET "name" = 'Islands Trust Cowichan Valley (Historical)' WHERE "name" = 'Islands Trust - Cowichan Valley (Historical)'; + UPDATE "alcs"."application" SET "local_government_uuid" = '6e5da7c6-9a72-44c5-8b8d-690439fb7cd1' WHERE "local_government_uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; + UPDATE "alcs"."notice_of_intent" SET "local_government_uuid" = '6e5da7c6-9a72-44c5-8b8d-690439fb7cd1' WHERE "local_government_uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; + UPDATE "alcs"."planning_review" SET "local_government_uuid" = '6e5da7c6-9a72-44c5-8b8d-690439fb7cd1' WHERE "local_government_uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; + UPDATE "alcs"."covenant" SET "local_government_uuid" = '6e5da7c6-9a72-44c5-8b8d-690439fb7cd1' WHERE "local_government_uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; UPDATE "alcs"."application_local_government" SET "name" = 'Northern Rockies (Historical)' WHERE "uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; UPDATE "alcs"."application_local_government" SET "preferred_region_code" = 'NORR' WHERE "uuid" = '55f665bc-c91b-4bbb-85c2-39691088b297'; UPDATE "alcs"."application" SET "local_government_uuid" = '360b05b3-55d4-4f57-a5ea-e0ffd6125db4' WHERE "local_government_uuid" = '0136a261-6237-4332-ae6a-1c7d7f49f0e8'; From 0585816fe0db2ae4ecb8eab909268b574de8f72c Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 30 Jun 2023 11:34:35 -0700 Subject: [PATCH 049/954] fixing git thinking file was changed --- .../sql/insert-batch-application.sql | 159 +++++++----------- 1 file changed, 57 insertions(+), 102 deletions(-) diff --git a/bin/migrate-oats-data/sql/insert-batch-application.sql b/bin/migrate-oats-data/sql/insert-batch-application.sql index 499a817fd5..2358d2c521 100644 --- a/bin/migrate-oats-data/sql/insert-batch-application.sql +++ b/bin/migrate-oats-data/sql/insert-batch-application.sql @@ -1,3 +1,4 @@ + -- Step 1: Perform a lookup to retrieve the applicant's name or organization for each application ID WITH applicant_lookup AS ( @@ -25,109 +26,63 @@ WITH GROUP BY oaap.alr_application_id ), - -- Step 2: get local gov application name & match to uuid - oats_gov AS ( - SELECT - oaap.alr_application_id AS application_id, - oo.organization_name AS oats_gov_name - FROM - oats.oats_alr_application_parties oaap - JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - WHERE - oo.organization_type_cd IN ('MUNI','FN','RD') - ), - alcs_gov AS ( - SELECT - oats_gov.application_id AS application_id, - alg.uuid AS gov_uuid - FROM - oats_gov - JOIN alcs.application_local_government alg on ( - CASE - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gabriola Island' THEN 'Islands Trust Gabriola Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Galiano Island' THEN 'Islands Trust Galiano Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gambier Island' THEN 'Islands Trust Gambier Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Hornby Island' THEN 'Islands Trust Hornby Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Lasqueti Island' THEN 'Islands Trust Lasqueti Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Mayne Island' THEN 'Islands Trust Mayne Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Pender Island' THEN 'Islands Trust Pender Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Quadra Island' THEN 'Islands Trust Quadra Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Salt Spring Island' THEN 'Islands Trust Salt Spring Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Sunshine Coast' THEN 'Islands Trust Sunshine Coast (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Bowen Island' THEN 'Bowen Island (Island Municipality)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Denman Island' THEN 'Islands Trust Denman Island (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' - WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' - WHEN oats_gov.oats_gov_name LIKE 'Thompson Nicola%' THEN 'Thompson Nicola Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Cariboo%' THEN 'Cariboo Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Fraser Valley%' THEN 'Fraser Valley Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Columbia Shuswap%' THEN 'Columbia Shuswap Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Central Okanagan%' THEN 'Central Okanagan Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Squamish Lillooet%' THEN 'Squamish Lillooet Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Alberni-Clayoquot%' THEN 'Alberni-Clayoquot Regional District' - WHEN oats_gov.oats_gov_name LIKE 'qathet%' THEN 'qathet Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Peace River%' THEN 'Peace River Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Okanagan Similkameen%' THEN 'Okanagan Similkameen Regional District' - WHEN oats_gov.oats_gov_name LIKE 'East Kootenay%' THEN 'East Kootenay Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Bulkley-Nechako%' THEN 'Bulkley-Nechako Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Sunshine Coast%' THEN 'Sunshine Coast Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Nanaimo%' THEN 'Nanaimo Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Kitimat Stikine%' THEN 'Kitimat Stikine Regional District' - WHEN oats_gov.oats_gov_name LIKE 'North Okanagan%' THEN 'North Okanagan Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Fraser Fort George%' THEN 'Fraser Fort George Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Cowichan Valley%' THEN 'Cowichan Valley Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Kootenay Boundary%' THEN 'Kootenay Boundary Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Comox Valley%' THEN 'Comox Valley Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Central Kootenay%' THEN 'Central Kootenay Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Capital%' THEN 'Capital Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Metro Vancouver%' THEN 'Metro Vancouver Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Central Coast%' THEN 'Central Coast Regional District' - WHEN oats_gov.oats_gov_name LIKE 'North Coast%' THEN 'North Coast Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Strathcona%' THEN 'Strathcona Regional District' - WHEN oats_gov.oats_gov_name LIKE 'Mount Waddington%' THEN 'Mount Waddington Regional District' - ELSE oats_gov.oats_gov_name - END - ) = alg."name" - ), - -- Step 3: Perform a lookup to retrieve the region code for each application ID - panel_lookup AS ( - SELECT DISTINCT - oaap.alr_application_id AS application_id, + +-- Step 2: get local gov application name & match to uuid +oats_gov AS ( + SELECT + oaap.alr_application_id AS application_id, + oo.organization_name AS oats_gov_name + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oo.organization_type_cd = 'MUNI' + OR oo.organization_type_cd = 'FN' +), + +alcs_gov AS ( + SELECT + oats_gov.application_id AS application_id, + alg.uuid AS gov_uuid + FROM + oats_gov + JOIN alcs.application_local_government alg on ( CASE - WHEN oo2.parent_organization_id IS NULL THEN oo2.organization_name - WHEN oo3.parent_organization_id IS NULL THEN oo3.organization_name - ELSE 'NONE' - END AS panel_region - FROM - oats.oats_alr_application_parties oaap - JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - LEFT JOIN oats.oats_organizations oo2 ON oo.parent_organization_id = oo2.organization_id - LEFT JOIN oats.oats_organizations oo3 ON oo2.parent_organization_id = oo3.organization_id - WHERE - oo2.organization_type_cd = 'PANEL' OR oo3.organization_type_cd = 'PANEL' - ), - -- Step 4: Perform lookup to retrieve type code - application_type_lookup AS ( - SELECT - oaac.alr_application_id AS application_id, - oacc."description" AS "description", - oaac.alr_change_code AS code - FROM - oats.oats_alr_appl_components AS oaac - JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code - LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id - WHERE - aee.component_id IS NULL - ) + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust%' THEN 'Islands Trust' + WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' + ELSE oats_gov.oats_gov_name + END + ) = alg."name" +), +-- Step 3: Perform a lookup to retrieve the region code for each application ID +panel_lookup AS ( + SELECT DISTINCT + oaap.alr_application_id AS application_id, + oo2.organization_name AS panel_region + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + JOIN oats.oats_organizations oo2 ON oo.parent_organization_id = oo2.organization_id + WHERE + oo2.organization_type_cd = 'PANEL' +), + +-- Step 4: Perform lookup to retrieve type code +application_type_lookup AS ( + SELECT + oaac.alr_application_id AS application_id, + oacc."description" AS "description", + oaac.alr_change_code AS code + FROM + oats.oats_alr_appl_components AS oaac + JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code + LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id + WHERE + aee.component_id IS NULL +) + -- Step 5: Insert new records into the alcs_applications table SELECT oa.alr_application_id :: text AS file_number, From 38805b15adda760862d4c9c70b9002b4afd72455 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 30 Jun 2023 12:44:09 -0700 Subject: [PATCH 050/954] Code Review Feedback --- .../parcel/parcel.component.ts | 30 +++++++++++-------- .../parcel-prep/parcel-prep.component.html | 2 +- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts index 77d5dada58..e7b7bb695d 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/parcel/parcel.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { AfterContentChecked, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; import { ApplicationDocumentDto } from '../../../../../services/application/application-document/application-document.dto'; @@ -11,7 +11,7 @@ import { ApplicationSubmissionDto, PARCEL_OWNERSHIP_TYPE } from '../../../../../ templateUrl: './parcel.component.html', styleUrls: ['./parcel.component.scss'], }) -export class ParcelComponent implements OnInit, OnChanges, OnDestroy { +export class ParcelComponent implements OnInit, OnChanges, OnDestroy, AfterContentChecked { $destroy = new Subject(); @Input() application!: ApplicationSubmissionDto; @@ -25,6 +25,7 @@ export class ParcelComponent implements OnInit, OnChanges, OnDestroy { parcels: any[] = []; PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; + private anchorededParcelUuid: string | undefined; constructor( private applicationDocumentService: ApplicationDocumentService, @@ -40,16 +41,7 @@ export class ParcelComponent implements OnInit, OnChanges, OnDestroy { this.route.fragment.pipe(takeUntil(this.$destroy)).subscribe((fragment) => { if (fragment) { - setTimeout(() => { - const el = document.getElementById(fragment); - if (el) { - el.scrollIntoView({ - behavior: 'smooth', - block: 'start', - inline: 'start', - }); - } - }, 200); + this.anchorededParcelUuid = fragment; } }); } @@ -74,4 +66,18 @@ export class ParcelComponent implements OnInit, OnChanges, OnDestroy { this.$destroy.next(); this.$destroy.complete(); } + + ngAfterContentChecked(): void { + if (this.anchorededParcelUuid) { + const el = document.getElementById(this.anchorededParcelUuid); + if (el) { + this.anchorededParcelUuid = undefined; + el.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'start', + }); + } + } + } } diff --git a/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.html b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.html index b53344d89b..91cfbd81db 100644 --- a/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.html +++ b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.html @@ -4,7 +4,7 @@ # - 1 + {{ i + 1 }} From 0cca1f3e8416ebe03541d488df6fee0d175721d6 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Tue, 4 Jul 2023 09:36:42 -0700 Subject: [PATCH 051/954] Fix empty agents creation/deletion and auth letter validation (#748) * Delete empty agents before creating new one and fix auth letter validation * Fix authorization letter validation if user changes selection from agent --- .../primary-contact/primary-contact.component.ts | 5 +++-- .../application-owner/application-owner.controller.spec.ts | 2 ++ .../application-owner/application-owner.controller.ts | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index b3f2f49ece..4e8871f591 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -135,9 +135,10 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.isCrownOwner = selectedOwner.type.code === APPLICATION_OWNER.CROWN; } } - this.needsAuthorizationLetter = !( - this.owners.length === 1 && this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL + this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL && + (this.owners.length === 1 || + (this.owners.length === 2 && this.owners[1].type.code === APPLICATION_OWNER.AGENT && !hasSelectedAgent)) ); this.files = this.files.map((file) => ({ ...file, diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts index d8e6bd913e..fdd19fd8df 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts @@ -255,6 +255,7 @@ describe('ApplicationOwnerController', () => { }); it('should create a new owner when setting primary contact to third party agent that doesnt exist', async () => { + mockAppOwnerService.deleteAgents.mockResolvedValue([]); mockAppOwnerService.create.mockResolvedValue(new ApplicationOwner()); mockAppOwnerService.setPrimaryContact.mockResolvedValue(); mockApplicationSubmissionService.verifyAccessByUuid.mockResolvedValue( @@ -270,6 +271,7 @@ describe('ApplicationOwnerController', () => { }, ); + expect(mockAppOwnerService.deleteAgents).toHaveBeenCalledTimes(1); expect(mockAppOwnerService.create).toHaveBeenCalledTimes(1); expect(mockAppOwnerService.setPrimaryContact).toHaveBeenCalledTimes(1); expect( diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts index 4620c22463..1349a1d89f 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts @@ -161,6 +161,7 @@ export class ApplicationOwnerController { //Create Owner if (!data.ownerUuid) { + await this.ownerService.deleteAgents(applicationSubmission); const agentOwner = await this.ownerService.create( { email: data.agentEmail, From f9fde1eaf63b21e044b83229877aad5934552c9a Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Tue, 4 Jul 2023 12:10:13 -0700 Subject: [PATCH 052/954] Allow portal submission by non-creator with access (#752) --- .../application-submission.controller.spec.ts | 6 +++--- .../application-submission.controller.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts index dcac2d03f6..314f631fce 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts @@ -302,7 +302,7 @@ describe('ApplicationSubmissionController', () => { }, }); - expect(mockAppService.getIfCreatorByUuid).toHaveBeenCalledTimes(1); + expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledTimes(1); expect(mockAppService.submitToAlcs).toHaveBeenCalledTimes(1); expect(mockAppService.updateStatus).toHaveBeenCalledTimes(1); }); @@ -328,13 +328,13 @@ describe('ApplicationSubmissionController', () => { }, }); - expect(mockAppService.getIfCreatorByUuid).toHaveBeenCalledTimes(1); + expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledTimes(1); expect(mockAppService.submitToLg).toHaveBeenCalledTimes(1); }); it('should throw an exception if application fails validation', async () => { const mockFileId = 'file-id'; - mockAppService.getIfCreatorByUuid.mockResolvedValue( + mockAppService.verifyAccessByUuid.mockResolvedValue( new ApplicationSubmission({ typeCode: 'NOT-TURP', }), diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts index fb69a3c5ba..07b0167178 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts @@ -185,7 +185,7 @@ export class ApplicationSubmissionController { @Post('/alcs/submit/:uuid') async submitAsApplicant(@Param('uuid') uuid: string, @Req() req) { const applicationSubmission = - await this.applicationSubmissionService.getIfCreatorByUuid( + await this.applicationSubmissionService.verifyAccessByUuid( uuid, req.user.entity, ); From 6ff013dfd99d96e9039141d6e4041b83aee33421 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Wed, 5 Jul 2023 13:56:20 -0700 Subject: [PATCH 053/954] Match ngif directive to other btns to fix click action (#754) --- .../application-details/application-details.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/application-details/application-details.component.html index 06061e4614..6dee41c5ba 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/application-details/application-details.component.html @@ -304,8 +304,8 @@

7. Optional Documents

-
- +
+
From c987057826cbf9b691a709d1d57fc69d8dc49bf0 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Thu, 6 Jul 2023 12:53:49 -0700 Subject: [PATCH 054/954] Feature/alcs 502 (#753) conditions view pate transform single component link to condition into multi-select --- alcs-frontend/src/app/app.module.ts | 2 +- .../condition/condition.component.html | 68 ++++++++ .../condition/condition.component.scss | 0 .../condition/condition.component.spec.ts | 48 ++++++ .../condition/condition.component.ts | 96 +++++++++++ .../conditions/conditions.component.html | 78 +++++++++ .../conditions/conditions.component.scss | 90 +++++++++++ .../conditions/conditions.component.spec.ts | 55 +++++++ .../conditions/conditions.component.ts | 151 ++++++++++++++++++ .../decision-condition.component.html | 19 +-- .../decision-condition.component.ts | 49 +++--- .../decision-conditions.component.spec.ts | 7 +- .../decision-conditions.component.ts | 17 +- .../decision-v2/decision-v2.component.html | 15 +- .../decision-v2/decision-v2.component.spec.ts | 12 +- .../decision-v2/decision-v2.component.ts | 48 ++++-- .../application/decision/decision.module.ts | 24 ++- ...ication-decision-condition.service.spec.ts | 66 ++++++++ .../application-decision-condition.service.ts | 30 ++++ .../application-decision-v2.dto.ts | 20 ++- .../application-decision-v2.service.ts | 16 +- .../application-type-pill.constants.ts | 24 +++ alcs-frontend/src/app/shared/shared.module.ts | 2 +- alcs-frontend/src/styles.scss | 16 ++ ...tion-decision-condition.controller.spec.ts | 109 +++++++++++++ ...plication-decision-condition.controller.ts | 48 ++++++ .../application-decision-condition.dto.ts | 46 +++++- .../application-decision-condition.entity.ts | 38 ++++- ...ication-decision-condition.service.spec.ts | 2 + .../application-decision-condition.service.ts | 56 ++++--- .../application-decision-v2.module.ts | 2 + .../application-decision-v2.service.spec.ts | 2 + .../application-decision-v2.service.ts | 6 + .../application-decision-component.entity.ts | 9 +- ...lication-decision-v2.automapper.profile.ts | 22 ++- ...ion_and_superseded_dates_for_conditions.ts | 37 +++++ ...15958-many_to_many_component_conditions.ts | 55 +++++++ ...03477-many_to_many_component_conditions.ts | 25 +++ services/apps/alcs/test/mocks/mockEntities.ts | 17 +- 39 files changed, 1308 insertions(+), 119 deletions(-) create mode 100644 alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html create mode 100644 alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss create mode 100644 alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts create mode 100644 alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html create mode 100644 alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss create mode 100644 alcs-frontend/src/app/features/application/decision/conditions/conditions.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts create mode 100644 alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.spec.ts create mode 100644 alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.ts create mode 100644 services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1688064562389-completion_and_superseded_dates_for_conditions.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1688150315958-many_to_many_component_conditions.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1688150503477-many_to_many_component_conditions.ts diff --git a/alcs-frontend/src/app/app.module.ts b/alcs-frontend/src/app/app.module.ts index 0e624a156d..03a4d798a7 100644 --- a/alcs-frontend/src/app/app.module.ts +++ b/alcs-frontend/src/app/app.module.ts @@ -5,7 +5,7 @@ import { MAT_DIALOG_DEFAULT_OPTIONS } from '@angular/material/dialog'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { NgSelectConfig } from '@ng-select/ng-select'; -import { provideEnvironmentNgxMask, provideNgxMask } from 'ngx-mask'; +import { provideEnvironmentNgxMask } from 'ngx-mask'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { AuthorizationComponent } from './features/authorization/authorization.component'; diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html new file mode 100644 index 0000000000..c1fac6d8bd --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html @@ -0,0 +1,68 @@ +
+

{{ condition.type.label }}

+ + + + + + + + + +
+
+
+
Component to Condition
+ {{ condition.componentLabels }} +
+ +
+
Approval Dependent
+ {{ condition.approvalDependant | booleanToString }} +
+ +
+
Security Amount
+ {{ condition.securityAmount }} + +
+ +
+
Admin Fee
+ {{ condition.administrativeFee }} + +
+ +
+
Completion Date
+ +
+ +
+
Superseded Date
+ +
+ +
+
Description
+ {{ + condition.description + }} + + + +
+
diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.spec.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.spec.ts new file mode 100644 index 0000000000..4d75e7fec6 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.spec.ts @@ -0,0 +1,48 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ApplicationDecisionConditionService } from '../../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service'; +import { ApplicationDecisionConditionDto } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { SharedModule } from '../../../../../shared/shared.module'; + +import { ConditionComponent } from './condition.component'; + +describe('ConditionComponent', () => { + let component: ConditionComponent; + let fixture: ComponentFixture; + let mockApplicationDecisionConditionService: DeepMocked; + + beforeEach(async () => { + mockApplicationDecisionConditionService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ConditionComponent], + providers: [ + { + provide: ApplicationDecisionConditionService, + useValue: mockApplicationDecisionConditionService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + imports: [SharedModule, BrowserAnimationsModule], + }).compileComponents(); + + fixture = TestBed.createComponent(ConditionComponent); + component = fixture.componentInstance; + + component.condition = createMock< + ApplicationDecisionConditionDto & { + conditionComponentsLabels?: string[]; + status: string; + } + >(); + component.condition.conditionComponentsLabels = []; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts new file mode 100644 index 0000000000..d090247a4e --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts @@ -0,0 +1,96 @@ +import { AfterViewInit, Component, Input, OnInit } from '@angular/core'; +import moment from 'moment'; +import { ApplicationDecisionConditionService } from '../../../../../services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service'; +import { UpdateApplicationDecisionConditionDto } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { + DECISION_CONDITION_COMPLETE_LABEL, + DECISION_CONDITION_INCOMPLETE_LABEL, + DECISION_CONDITION_SUPERSEDED_LABEL, +} from '../../../../../shared/application-type-pill/application-type-pill.constants'; +import { ApplicationDecisionConditionWithStatus, CONDITION_STATUS } from '../conditions.component'; + +type Condition = ApplicationDecisionConditionWithStatus & { + componentLabels?: string; +}; + +@Component({ + selector: 'app-condition', + templateUrl: './condition.component.html', + styleUrls: ['./condition.component.scss'], +}) +export class ConditionComponent implements OnInit, AfterViewInit { + @Input() condition!: Condition; + @Input() isDraftDecision!: boolean; + + incompleteLabel = DECISION_CONDITION_INCOMPLETE_LABEL; + completeLabel = DECISION_CONDITION_COMPLETE_LABEL; + supersededLabel = DECISION_CONDITION_SUPERSEDED_LABEL; + + CONDITION_STATUS = CONDITION_STATUS; + + isReadMoreClicked = false; + isReadMoreVisible = false; + conditionStatus: string = ''; + + constructor(private conditionService: ApplicationDecisionConditionService) {} + + ngOnInit() { + this.updateStatus(); + if (this.condition) { + this.condition = { + ...this.condition, + componentLabels: this.condition.conditionComponentsLabels?.join(', '), + }; + } + } + + ngAfterViewInit(): void { + setTimeout(() => (this.isReadMoreVisible = this.checkIfReadMoreVisible())); + } + + async onUpdateCondition( + field: keyof UpdateApplicationDecisionConditionDto, + value: string[] | string | number | null + ) { + const condition = this.condition; + + if (condition) { + const update = await this.conditionService.update(condition.uuid, { + [field]: value, + }); + + const labels = this.condition.componentLabels; + this.condition = { ...update, componentLabels: labels } as Condition; + + this.updateStatus(); + } + } + + onToggleReadMore() { + this.isReadMoreClicked = !this.isReadMoreClicked; + } + + isEllipsisActive(e: string): boolean { + const el = document.getElementById(e); + // + 2 required as adjustment to height + return el ? el.clientHeight + 2 < el.scrollHeight : false; + } + + checkIfReadMoreVisible(): boolean { + return this.isReadMoreClicked || this.isEllipsisActive(this.condition.uuid + 'Description'); + } + + updateStatus() { + const today = moment().startOf('day').toDate().getTime(); + + if (this.condition.supersededDate && this.condition.supersededDate <= today) { + this.conditionStatus = CONDITION_STATUS.SUPERSEDED; + } else if (this.condition.completionDate && this.condition.completionDate <= today) { + this.conditionStatus = CONDITION_STATUS.COMPLETE; + } else if (this.isDraftDecision === false) { + this.conditionStatus = CONDITION_STATUS.INCOMPLETE; + } else { + this.conditionStatus = ''; + } + } +} diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html new file mode 100644 index 0000000000..fff731fabf --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.html @@ -0,0 +1,78 @@ +
+
+

View Conditions

+ +
+
+
+
+
+ Modified By:  + {{ decision.modifiedByResolutions?.join(', ') }} + N/A +
+
+ Reconsidered By:  + {{ decision.reconsideredByResolutions?.join(', ') }} + N/A +
+
+
+
+
Decision #{{ decision.index }}
+
+ + calendar_month + {{ application.activeDays }} + + + pause + {{ application.pausedDays }} + +
+ + + + + + + Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }} + + + + +
+
+
+ +
+
+
+ +
diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss new file mode 100644 index 0000000000..cf53302c55 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss @@ -0,0 +1,90 @@ +@use '../../../../../styles/colors.scss'; + +h3, +div, +span, +p { + color: colors.$black; +} + +.header, +.footer { + display: flex; + padding-left: 16px; +} + +.header { + justify-content: space-between; +} + +.footer { + justify-content: flex-end; +} + +:host ::ng-deep { + .display-none { + display: none !important; + } + + .read-more { + display: flex; + justify-content: flex-end; + } + + .decision-container { + padding: 28px 0; + + .post-decisions { + padding: 8px 16px; + background-color: colors.$grey-light; + grid-template-columns: 50% 50%; + display: grid; + min-height: 36px; + text-transform: uppercase; + border-radius: 4px 4px 0 0; + } + + .header { + margin: 0; + display: flex; + justify-content: space-between; + padding: 0 16px; + + .title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + } + } + + .even-condition { + background-color: colors.$grey-light; + } + + .condition-container { + display: block; + padding: 24px 16px; + + .header { + display: flex; + padding: 0px; + gap: 16px; + padding-bottom: 16px; + + color: colors.$black; + justify-content: flex-start; + } + + .grid-3 { + display: grid; + grid-template-columns: 33% 33% 33%; + grid-row-gap: 24px; + + .full-width { + grid-column: 1/4; + } + } + } + } +} diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.spec.ts b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.spec.ts new file mode 100644 index 0000000000..f3116b4875 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.spec.ts @@ -0,0 +1,55 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, convertToParamMap } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; +import { ApplicationDto } from '../../../../services/application/application.dto'; +import { ApplicationDecisionV2Service } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; + +import { ConditionsComponent } from './conditions.component'; + +describe('ConditionsComponent', () => { + let component: ConditionsComponent; + let fixture: ComponentFixture; + let mockApplicationDetailService: DeepMocked; + let mockApplicationDecisionV2Service: DeepMocked; + + beforeEach(async () => { + mockApplicationDetailService = createMock(); + mockApplicationDetailService.$application = new BehaviorSubject(undefined); + + mockApplicationDecisionV2Service = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ConditionsComponent], + providers: [ + { + provide: ApplicationDetailService, + useValue: mockApplicationDetailService, + }, + { + provide: ApplicationDecisionV2Service, + useValue: mockApplicationDecisionV2Service, + }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({ uuid: 'fake' }), + }, + }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ConditionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts new file mode 100644 index 0000000000..977dc47ae1 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts @@ -0,0 +1,151 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import moment from 'moment'; +import { Subject, takeUntil } from 'rxjs'; +import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; +import { ApplicationDto } from '../../../../services/application/application.dto'; +import { + ApplicationDecisionConditionDto, + ApplicationDecisionDto, + ApplicationDecisionWithLinkedResolutionDto, + DecisionCodesDto, +} from '../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionV2Service } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; +import { + DRAFT_DECISION_TYPE_LABEL, + MODIFICATION_TYPE_LABEL, + RECON_TYPE_LABEL, + RELEASED_DECISION_TYPE_LABEL, +} from '../../../../shared/application-type-pill/application-type-pill.constants'; + +export type ApplicationDecisionConditionWithStatus = ApplicationDecisionConditionDto & { + conditionComponentsLabels?: string[]; + status: string; +}; + +export type ApplicationDecisionWithConditionComponentLabels = ApplicationDecisionWithLinkedResolutionDto & { + conditions: ApplicationDecisionConditionWithStatus[]; +}; + +export const CONDITION_STATUS = { + INCOMPLETE: 'incomplete', + COMPLETE: 'complete', + SUPERSEDED: 'superseded', +}; + +@Component({ + selector: 'app-conditions', + templateUrl: './conditions.component.html', + styleUrls: ['./conditions.component.scss'], +}) +export class ConditionsComponent implements OnInit { + $destroy = new Subject(); + + decisionUuid: string = ''; + fileNumber: string = ''; + decisions: ApplicationDecisionWithConditionComponentLabels[] = []; + decision!: ApplicationDecisionWithConditionComponentLabels; + conditionDecision!: ApplicationDecisionDto; + application: ApplicationDto | undefined; + codes!: DecisionCodesDto; + today!: number; + + dratDecisionLabel = DRAFT_DECISION_TYPE_LABEL; + releasedDecisionLabel = RELEASED_DECISION_TYPE_LABEL; + reconLabel = RECON_TYPE_LABEL; + modificationLabel = MODIFICATION_TYPE_LABEL; + + constructor( + private applicationDetailService: ApplicationDetailService, + private decisionService: ApplicationDecisionV2Service, + private activatedRouter: ActivatedRoute + ) { + this.today = moment().startOf('day').toDate().getTime(); + } + + ngOnInit(): void { + this.fileNumber = this.activatedRouter.parent?.parent?.snapshot.paramMap.get('fileNumber')!; + this.decisionUuid = this.activatedRouter.snapshot.paramMap.get('uuid')!; + + this.applicationDetailService.$application.pipe(takeUntil(this.$destroy)).subscribe((application) => { + if (application) { + this.application = application; + this.loadDecisions(application.fileNumber); + } + }); + } + + async loadDecisions(fileNumber: string) { + this.codes = await this.decisionService.fetchCodes(); + this.decisionService.$decisions.pipe(takeUntil(this.$destroy)).subscribe((decisions) => { + this.decisions = decisions.map((decision) => { + if (decision.uuid === this.decisionUuid) { + const conditions = this.mapConditions(decision); + + this.sortConditions(decision, conditions); + + this.decision = decision as ApplicationDecisionWithConditionComponentLabels; + } + + return decision as ApplicationDecisionWithConditionComponentLabels; + }); + }); + + this.decisionService.loadDecisions(fileNumber); + } + + private sortConditions( + decision: ApplicationDecisionWithLinkedResolutionDto, + conditions: ApplicationDecisionConditionWithStatus[] + ) { + decision.conditions = conditions.sort((a, b) => { + const order = [CONDITION_STATUS.INCOMPLETE, CONDITION_STATUS.COMPLETE, CONDITION_STATUS.SUPERSEDED]; + if (a.status === b.status) { + if (a.type && b.type) { + return a.type?.label.localeCompare(b.type.label); + } else { + return -1; + } + } else { + return order.indexOf(a.status) - order.indexOf(b.status); + } + }); + } + + private mapConditions(decision: ApplicationDecisionWithLinkedResolutionDto) { + return decision.conditions.map((condition) => { + const status = this.getStatus(condition, decision); + + return { + ...condition, + status, + conditionComponentsLabels: condition.components?.map((c) => { + const matchingType = this.codes.decisionComponentTypes.find( + (type) => type.code === c.applicationDecisionComponentTypeCode + ); + + const label = + decision.resolutionNumber && decision.resolutionYear + ? `#${decision.resolutionNumber}/${decision.resolutionYear} ${matchingType?.label}` + : `Draft ${matchingType?.label}`; + + return label; + }), + } as ApplicationDecisionConditionWithStatus; + }); + } + + private getStatus(condition: ApplicationDecisionConditionDto, decision: ApplicationDecisionWithLinkedResolutionDto) { + let status = ''; + if (condition.supersededDate && condition.supersededDate <= this.today) { + status = CONDITION_STATUS.SUPERSEDED; + } else if (condition.completionDate && condition.completionDate <= this.today) { + status = CONDITION_STATUS.COMPLETE; + } else if (decision.isDraft === false) { + status = CONDITION_STATUS.INCOMPLETE; + } else { + status = ''; + } + return status; + } +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html index 281c030708..1d1ec85e2b 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html @@ -13,17 +13,14 @@ [clearable]="false" > - + + Component to Condition + + {{ + component.label + }} + +
Approval Dependent* diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts index c1cfaba7d7..3e280f37b6 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts @@ -17,7 +17,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { @Input() selectableComponents: SelectableComponent[] = []; type = new FormControl(null, [Validators.required]); - componentToCondition = new FormControl(null, [Validators.required]); + componentToConditions = new FormControl(null, [Validators.required]); approvalDependant = new FormControl(null, [Validators.required]); securityAmount = new FormControl(null); @@ -30,7 +30,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { securityAmount: this.securityAmount, administrativeFee: this.administrativeFee, description: this.description, - componentToCondition: this.componentToCondition, + componentToCondition: this.componentToConditions, }); ngOnInit(): void { @@ -40,15 +40,15 @@ export class DecisionConditionComponent implements OnInit, OnChanges { approvalDependant = this.data.approvalDependant ? 'true' : 'false'; } - const selectedOption = this.selectableComponents.find( - (component) => - this.data.componentToConditionType && - this.data.componentToConditionType === component.code && - this.data.componentDecisionUuid && - this.data.componentDecisionUuid === component.decisionUuid - ); + const selectedOptions = this.selectableComponents + .filter((component) => this.data.componentToConditions?.map((e) => e.tempId)?.includes(component.tempId)) + .map((e) => ({ + componentDecisionUuid: e.decisionUuid, + componentToConditionType: e.code, + tempId: e.tempId, + })); - this.componentToCondition.setValue(selectedOption?.tempId ?? null); + this.componentToConditions.setValue(selectedOptions.map((e) => e.tempId) ?? null); this.form.patchValue({ type: this.data.type?.code ?? null, @@ -61,9 +61,14 @@ export class DecisionConditionComponent implements OnInit, OnChanges { this.form.valueChanges.subscribe((changes) => { const matchingType = this.codes.find((code) => code.code === this.type.value); - const selectedOption = this.selectableComponents.find( - (component) => this.componentToCondition.value && component.tempId === this.componentToCondition.value - ); + + const selectedOptions = this.selectableComponents + .filter((component) => this.componentToConditions.value?.includes(component.tempId)) + .map((e) => ({ + componentDecisionUuid: e.decisionUuid, + componentToConditionType: e.code, + })); + this.dataChange.emit({ tempUuid: this.data.tempUuid, uuid: this.data.uuid, @@ -72,20 +77,22 @@ export class DecisionConditionComponent implements OnInit, OnChanges { securityAmount: this.securityAmount.value !== null ? parseFloat(this.securityAmount.value) : undefined, administrativeFee: this.administrativeFee.value !== null ? parseFloat(this.administrativeFee.value) : undefined, description: this.description.value ?? undefined, - componentToConditionType: selectedOption?.code, - componentDecisionUuid: selectedOption?.decisionUuid, + componentToConditions: selectedOptions, }); }); } ngOnChanges(changes: SimpleChanges): void { if (changes['selectableComponents']) { - const selectedValue = this.componentToCondition.value; - const selectedOption = this.selectableComponents.find( - (component) => selectedValue && component.tempId === selectedValue - ); - if (!selectedOption) { - this.componentToCondition.setValue(null); + const selectedOptions = this.selectableComponents + .filter((component) => this.componentToConditions.value?.includes(component.tempId)) + .map((e) => ({ + componentDecisionUuid: e.decisionUuid, + componentToConditionType: e.code, + })); + + if (selectedOptions && selectedOptions.length < 1) { + this.componentToConditions.setValue(null); } } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts index 8f1ef52428..7d5a8dabd0 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts @@ -2,7 +2,10 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDecisionDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { + ApplicationDecisionDto, + ApplicationDecisionWithLinkedResolutionDto, +} from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { DecisionConditionsComponent } from './decision-conditions.component'; @@ -15,7 +18,7 @@ describe('DecisionConditionComponent', () => { beforeEach(async () => { mockDecisionService = createMock(); mockDecisionService.$decision = new BehaviorSubject(undefined); - mockDecisionService.$decisions = new BehaviorSubject([]); + mockDecisionService.$decisions = new BehaviorSubject([]); await TestBed.configureTestingModule({ providers: [ diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 43dee69a9f..fbceb95c1a 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -76,17 +76,24 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy this.selectableComponents = [...this.allComponents, ...updatedComponents]; this.decision = selectedDecision; + this.mappedConditions = selectedDecision.conditions.map((condition) => { - const selectedComponent = this.selectableComponents.find( - (component) => component.uuid === condition.componentUuid - ); + const selectedComponents = this.selectableComponents + .filter((component) => + condition.components?.map((conditionComponent) => conditionComponent.uuid).includes(component.uuid) + ) + .map((e) => ({ + componentDecisionUuid: e.decisionUuid, + componentToConditionType: e.code, + tempId: e.tempId, + })); return { ...condition, - componentToConditionType: selectedComponent?.code, - componentDecisionUuid: selectedComponent?.decisionUuid, + componentToConditions: selectedComponents, }; }); + this.onChanges(); } }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html index 5c76145e3a..1312d17e2d 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html @@ -5,12 +5,13 @@

Decision

- + + @@ -30,16 +31,16 @@

Condition Compliance

-
+
Modified By:  - {{ decision.modifiedByResolutions.join(', ') }} + {{ decision.modifiedByResolutions?.join(', ') }} N/A
Reconsidered By:  - {{ decision.reconsideredByResolutions.join(', ') }} + {{ decision.reconsideredByResolutions?.join(', ') }} N/A
@@ -47,7 +48,7 @@

Condition Compliance

- Decision #{{ decisions.length - i }} + Decision #{{ decision.index }}
calendar_month @@ -250,7 +251,7 @@
Decision Components and Conditions
>

Conditions

diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.spec.ts index 379dcbeac2..25930efded 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.spec.ts @@ -2,12 +2,16 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { ActivatedRoute } from '@angular/router'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; import { ApplicationDto } from '../../../../services/application/application.dto'; import { ApplicationDecisionComponentService } from '../../../../services/application/decision/application-decision-v2/application-decision-component/application-decision-component.service'; -import { ApplicationDecisionDto } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { + ApplicationDecisionDto, + ApplicationDecisionWithLinkedResolutionDto, +} from '../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { ToastService } from '../../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; @@ -24,7 +28,7 @@ describe('DecisionV2Component', () => { beforeEach(async () => { mockApplicationDecisionService = createMock(); mockApplicationDecisionService.$decision = new BehaviorSubject(undefined); - mockApplicationDecisionService.$decisions = new BehaviorSubject([]); + mockApplicationDecisionService.$decisions = new BehaviorSubject([]); mockAppDetailService = createMock(); mockAppDetailService.$application = new BehaviorSubject(undefined); @@ -63,6 +67,10 @@ describe('DecisionV2Component', () => { provide: ApplicationDecisionComponentService, useValue: mockApplicationDecisionComponentService, }, + { + provide: ActivatedRoute, + useValue: {}, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts index 69308bb1bd..3c3a81ec91 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts @@ -1,12 +1,12 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; import { ApplicationDto } from '../../../../services/application/application.dto'; import { ApplicationDecisionComponentService } from '../../../../services/application/decision/application-decision-v2/application-decision-component/application-decision-component.service'; import { - ApplicationDecisionDto, + ApplicationDecisionWithLinkedResolutionDto, APPLICATION_DECISION_COMPONENT_TYPE, CeoCriterionDto, DecisionMakerDto, @@ -25,9 +25,7 @@ import { formatDateForApi } from '../../../../shared/utils/api-date-formatter'; import { decisionChildRoutes } from '../decision.module'; import { RevertToDraftDialogComponent } from './revert-to-draft-dialog/revert-to-draft-dialog.component'; -type LoadingDecision = ApplicationDecisionDto & { - reconsideredByResolutions: string[]; - modifiedByResolutions: string[]; +type LoadingDecision = ApplicationDecisionWithLinkedResolutionDto & { loading: boolean; }; @@ -65,7 +63,9 @@ export class DecisionV2Component implements OnInit, OnDestroy { private toastService: ToastService, private confirmationDialogService: ConfirmationDialogService, private applicationDecisionComponentService: ApplicationDecisionComponentService, - private router: Router + private router: Router, + private activatedRouter: ActivatedRoute, + private elementRef: ElementRef ) {} ngOnInit(): void { @@ -90,11 +90,11 @@ export class DecisionV2Component implements OnInit, OnDestroy { this.decisionService.$decisions.pipe(takeUntil(this.$destroy)).subscribe((decisions) => { this.decisions = decisions.map((decision) => ({ ...decision, - reconsideredByResolutions: decision.reconsideredBy?.flatMap((r) => r.linkedResolutions) || [], - modifiedByResolutions: decision.modifiedBy?.flatMap((r) => r.linkedResolutions) || [], loading: false, })); + this.scrollToDecision(); + this.isDraftExists = this.decisions.some((d) => d.isDraft); this.disabledCreateBtnTooltip = this.isPaused ? 'This application is currently paused. Only active applications can have decisions.' @@ -102,6 +102,16 @@ export class DecisionV2Component implements OnInit, OnDestroy { }); } + scrollToDecision() { + const decisionUuid = this.activatedRouter.snapshot.queryParamMap.get('uuid'); + + setTimeout(() => { + if (this.decisions.length > 0 && decisionUuid) { + this.scrollToElement(decisionUuid); + } + }); + } + async onCreate() { const newDecision = await this.decisionService.create({ resolutionYear: new Date().getFullYear(), @@ -116,7 +126,7 @@ export class DecisionV2Component implements OnInit, OnDestroy { await this.router.navigate([`/application/${this.fileNumber}/decision/draft/${newDecision.uuid}/edit`]); } - async onEdit(decision: LoadingDecision) { + async onEdit(decision: ApplicationDecisionWithLinkedResolutionDto) { await this.router.navigate([`/application/${this.fileNumber}/decision/draft/${decision.uuid}/edit`]); } @@ -215,14 +225,22 @@ export class DecisionV2Component implements OnInit, OnDestroy { await this.loadDecisions(this.fileNumber); } - onNavigateToConditions() { - // some other ticket - return false; - } - ngOnDestroy(): void { this.decisionService.clearDecisions(); this.$destroy.next(); this.$destroy.complete(); } + + scrollToElement(elementId: string) { + const id = `#${CSS.escape(elementId)}`; + const element = this.elementRef.nativeElement.querySelector(id); + + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'start', + }); + } + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision.module.ts b/alcs-frontend/src/app/features/application/decision/decision.module.ts index 4edb3d5bdd..8f823aa052 100644 --- a/alcs-frontend/src/app/features/application/decision/decision.module.ts +++ b/alcs-frontend/src/app/features/application/decision/decision.module.ts @@ -2,15 +2,21 @@ import { NgModule } from '@angular/core'; import { MatTabsModule } from '@angular/material/tabs'; import { RouterModule } from '@angular/router'; import { SharedModule } from '../../../shared/shared.module'; +import { ConditionComponent } from './conditions/condition/condition.component'; +import { ConditionsComponent } from './conditions/conditions.component'; import { DecisionV1DialogComponent } from './decision-v1/decision-v1-dialog/decision-v1-dialog.component'; import { DecisionV1Component } from './decision-v1/decision-v1.component'; +import { NaruComponent } from './decision-v2/decision-component/naru/naru.component'; import { NfupComponent } from './decision-v2/decision-component/nfup/nfup.component'; +import { PfrsComponent } from './decision-v2/decision-component/pfrs/pfrs.component'; import { PofoComponent } from './decision-v2/decision-component/pofo/pofo.component'; import { RosoComponent } from './decision-v2/decision-component/roso/roso.component'; import { TurpComponent } from './decision-v2/decision-component/turp/turp.component'; import { DecisionDocumentsComponent } from './decision-v2/decision-documents/decision-documents.component'; import { DecisionComponentComponent } from './decision-v2/decision-input/decision-components/decision-component/decision-component.component'; +import { NaruInputComponent } from './decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component'; import { NfuInputComponent } from './decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component'; +import { PfrsInputComponent } from './decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component'; import { PofoInputComponent } from './decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component'; import { RosoInputComponent } from './decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component'; import { TurpInputComponent } from './decision-v2/decision-input/decision-components/decision-component/turp-input/turp-input.component'; @@ -23,29 +29,31 @@ import { DecisionV2Component } from './decision-v2/decision-v2.component'; import { ReleaseDialogComponent } from './decision-v2/release-dialog/release-dialog.component'; import { RevertToDraftDialogComponent } from './decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component'; import { DecisionComponent } from './decision.component'; -import { PfrsInputComponent } from './decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component'; -import { PfrsComponent } from './decision-v2/decision-component/pfrs/pfrs.component'; -import { NaruComponent } from './decision-v2/decision-component/naru/naru.component'; -import { NaruInputComponent } from './decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component'; export const decisionChildRoutes = [ { path: '', menuTitle: 'Decision', component: DecisionComponent, - portalOnly: true, + portalOnly: false, }, { path: 'create', menuTitle: 'Decision', component: DecisionInputV2Component, - portalOnly: true, + portalOnly: false, }, { path: 'draft/:uuid/edit', menuTitle: 'Decision', component: DecisionInputV2Component, - portalOnly: true, + portalOnly: false, + }, + { + path: 'conditions/:uuid', + menuTitle: 'Conditions', + component: ConditionsComponent, + portalOnly: false, }, ]; @@ -76,6 +84,8 @@ export const decisionChildRoutes = [ PfrsComponent, NaruComponent, NaruInputComponent, + ConditionsComponent, + ConditionComponent, ], imports: [SharedModule.forRoot(), RouterModule.forChild(decisionChildRoutes), MatTabsModule], }) diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.spec.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.spec.ts new file mode 100644 index 0000000000..f778a957d6 --- /dev/null +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.spec.ts @@ -0,0 +1,66 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ToastService } from '../../../../toast/toast.service'; +import { of, throwError } from 'rxjs'; + +import { ApplicationDecisionConditionService } from './application-decision-condition.service'; + +describe('ApplicationDecisionConditionService', () => { + let service: ApplicationDecisionConditionService; + let mockHttpClient: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(() => { + mockHttpClient = createMock() + mockToastService = createMock() + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + ], + }); + service = TestBed.inject(ApplicationDecisionConditionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make an http patch and show a success toast when updating', async () => { + mockHttpClient.patch.mockReturnValue( + of({ + applicationFileNumber: '1', + }) + ); + + await service.update('1', {}); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showSuccessToast).toHaveBeenCalledTimes(1); + }); + + it('should show a toast message if update fails', async () => { + mockHttpClient.patch.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + try { + await service.update('1', {}); + } catch (e) { + //OM NOM NOM + } + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.ts new file mode 100644 index 0000000000..ccc6c838b5 --- /dev/null +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-condition/application-decision-condition.service.ts @@ -0,0 +1,30 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../../../environments/environment'; +import { ToastService } from '../../../../toast/toast.service'; +import { ApplicationDecisionConditionDto, UpdateApplicationDecisionConditionDto } from '../application-decision-v2.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationDecisionConditionService { + private url = `${environment.apiUrl}/v2/application-decision-condition`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async update(uuid: string, data: UpdateApplicationDecisionConditionDto) { + try { + const res = await firstValueFrom(this.http.patch(`${this.url}/${uuid}`, data)); + this.toastService.showSuccessToast('Condition updated'); + return res; + } catch (e) { + if (e instanceof HttpErrorResponse && e.status === 400 && e.error?.message) { + this.toastService.showErrorToast(e.error.message); + } else { + this.toastService.showErrorToast('Failed to update condition'); + } + throw e; + } + } +} diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts index bbab2f3216..3f3b7c2f24 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts @@ -84,6 +84,12 @@ export interface LinkedResolutionDto { linkedResolutions: string[]; } +export interface ApplicationDecisionWithLinkedResolutionDto extends ApplicationDecisionDto { + reconsideredByResolutions?: string[]; + modifiedByResolutions?: string[]; + index: number; +} + export interface DecisionDocumentDto { uuid: string; fileName: string; @@ -156,6 +162,7 @@ export interface DecisionComponentDto applicationDecisionComponentTypeCode: string; applicationDecisionComponentType?: DecisionComponentTypeDto; applicationDecisionUuid?: string; + conditionComponentsLabels?: string; } export interface DecisionCodesDto { @@ -188,16 +195,25 @@ export interface ApplicationDecisionConditionDto { securityAmount?: number | null; administrativeFee?: number | null; description?: string | null; + completionDate?: number | null; + supersededDate?: number | null; type?: ApplicationDecisionConditionTypeDto | null; + components?: DecisionComponentDto[] | null; } -export interface UpdateApplicationDecisionConditionDto { - uuid?: string; +export interface ComponentToCondition { componentDecisionUuid?: string; componentToConditionType?: string; + tempId?: string; +} +export interface UpdateApplicationDecisionConditionDto { + uuid?: string; + componentToConditions?: ComponentToCondition[] | null; approvalDependant?: boolean | null; securityAmount?: number | null; administrativeFee?: number | null; description?: string | null; + completionDate?: number | null; + supersededDate?: number | null; type?: ApplicationDecisionConditionTypeDto | null; } diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts index fbf8c8d146..9f8c895f3d 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts @@ -7,6 +7,7 @@ import { verifyFileSize } from '../../../../shared/utils/file-size-checker'; import { ToastService } from '../../../toast/toast.service'; import { ApplicationDecisionDto, + ApplicationDecisionWithLinkedResolutionDto, CreateApplicationDecisionDto, DecisionCodesDto, UpdateApplicationDecisionDto, @@ -18,9 +19,9 @@ import { export class ApplicationDecisionV2Service { private url = `${environment.apiUrl}/v2/application-decision`; private decision: ApplicationDecisionDto | undefined; - private decisions: ApplicationDecisionDto[] = []; + private decisions: ApplicationDecisionWithLinkedResolutionDto[] = []; $decision = new BehaviorSubject(undefined); - $decisions = new BehaviorSubject([]); + $decisions = new BehaviorSubject([]); constructor(private http: HttpClient, private toastService: ToastService) {} @@ -139,7 +140,16 @@ export class ApplicationDecisionV2Service { async loadDecisions(fileNumber: string) { this.clearDecisions(); - this.decisions = await this.fetchByApplication(fileNumber); + const decisions = await this.fetchByApplication(fileNumber); + const decisionsLength = decisions.length; + + this.decisions = decisions.map((decision, ind) => ({ + ...decision, + reconsideredByResolutions: decision.reconsideredBy?.flatMap((r) => r.linkedResolutions) || [], + modifiedByResolutions: decision.modifiedBy?.flatMap((r) => r.linkedResolutions) || [], + index: decisionsLength - ind, + })); + this.$decisions.next(this.decisions); } diff --git a/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.constants.ts b/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.constants.ts index 432820b9c4..998afea8b4 100644 --- a/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.constants.ts +++ b/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.constants.ts @@ -65,3 +65,27 @@ export const RELEASED_DECISION_TYPE_LABEL = { description: 'Draft', textColor: '#000', }; + +export const DECISION_CONDITION_INCOMPLETE_LABEL = { + label: 'Incomplete', + shortLabel: 'INCD', + backgroundColor: '#fff', + borderColor: '#fcba19', + textColor: '#000', +}; + +export const DECISION_CONDITION_COMPLETE_LABEL = { + label: 'Complete', + shortLabel: 'COMD', + backgroundColor: '#fff', + borderColor: '#c08106', + textColor: '#000', +}; + +export const DECISION_CONDITION_SUPERSEDED_LABEL = { + label: 'Superseded', + shortLabel: 'SUPD', + backgroundColor: '#fff', + borderColor: '#C08106', + textColor: '#000', +}; diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 960399ce57..70c5e60efd 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -47,6 +47,7 @@ import { InlineTextComponent } from './inline-text/inline-text.component'; import { InlineTextareaComponent } from './inline-textarea/inline-textarea.component'; import { MeetingOverviewComponent } from './meeting-overview/meeting-overview.component'; import { NoDataComponent } from './no-data/no-data.component'; +import { BooleanToStringPipe } from './pipes/boolean-to-string.pipe'; import { FileSizePipe } from './pipes/fileSize.pipe'; import { MomentPipe } from './pipes/moment.pipe'; import { SafePipe } from './pipes/safe.pipe'; @@ -58,7 +59,6 @@ import { TimeTrackerComponent } from './time-tracker/time-tracker.component'; import { TimelineComponent } from './timeline/timeline.component'; import { DATE_FORMATS } from './utils/date-format'; import { ExtensionsDatepickerFormatter } from './utils/extensions-datepicker-formatter'; -import { BooleanToStringPipe } from './pipes/boolean-to-string.pipe'; @NgModule({ declarations: [ diff --git a/alcs-frontend/src/styles.scss b/alcs-frontend/src/styles.scss index fef0f91808..a9fadb0cd9 100644 --- a/alcs-frontend/src/styles.scss +++ b/alcs-frontend/src/styles.scss @@ -300,3 +300,19 @@ mat-button-toggle-group { .mat-mdc-form-field-subscript-wrapper { display: none !important; } + +@mixin text-ellipsis($lines: 1) { + text-overflow: ellipsis; + overflow: hidden; + @if ($lines > 1) { + display: -webkit-box; + -webkit-line-clamp: $lines; + -webkit-box-orient: vertical; + } @else { + white-space: nowrap; + } +} + +.ellipsis-3 { + @include text-ellipsis($lines: 3); +} \ No newline at end of file diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts new file mode 100644 index 0000000000..2f56b6f470 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.spec.ts @@ -0,0 +1,109 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { ApplicationDecisionProfile } from '../../../common/automapper/application-decision-v2.automapper.profile'; +import { ApplicationDecisionConditionController } from './application-decision-condition.controller'; +import { UpdateApplicationDecisionConditionDto } from './application-decision-condition.dto'; +import { ApplicationDecisionCondition } from './application-decision-condition.entity'; +import { ApplicationDecisionConditionService } from './application-decision-condition.service'; + +describe('ApplicationDecisionConditionController', () => { + let controller: ApplicationDecisionConditionController; + let mockApplicationDecisionConditionService: DeepMocked; + + beforeEach(async () => { + mockApplicationDecisionConditionService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [ApplicationDecisionConditionController], + providers: [ + ApplicationDecisionProfile, + { + provide: ApplicationDecisionConditionService, + useValue: mockApplicationDecisionConditionService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + ApplicationDecisionConditionController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('update', () => { + it('should update the condition and return updated condition', async () => { + // Arrange + const uuid = 'example-uuid'; + const date = new Date(); + const updates: UpdateApplicationDecisionConditionDto = { + approvalDependant: true, + securityAmount: 1000, + administrativeFee: 50, + description: 'example description', + completionDate: date.getTime(), + supersededDate: date.getTime(), + }; + + const condition = new ApplicationDecisionCondition({ + uuid, + approvalDependant: false, + securityAmount: 500, + administrativeFee: 25, + description: 'existing description', + completionDate: new Date(), + supersededDate: new Date(), + }); + + const updated = new ApplicationDecisionCondition({ + uuid, + approvalDependant: updates.approvalDependant, + securityAmount: updates.securityAmount, + administrativeFee: updates.administrativeFee, + description: updates.description, + completionDate: date, + supersededDate: date, + }); + + mockApplicationDecisionConditionService.getOneOrFail.mockResolvedValue( + condition, + ); + mockApplicationDecisionConditionService.update.mockResolvedValue(updated); + + const result = await controller.update(uuid, updates); + + expect( + mockApplicationDecisionConditionService.getOneOrFail, + ).toHaveBeenCalledWith(uuid); + expect( + mockApplicationDecisionConditionService.update, + ).toHaveBeenCalledWith(condition, { + ...updates, + completionDate: date, + supersededDate: date, + }); + expect(new Date(result.completionDate!)).toEqual(updated.completionDate); + expect(new Date(result.supersededDate!)).toEqual(updated.supersededDate); + expect(result.description).toEqual(updated.description); + expect(result.administrativeFee).toEqual(updated.administrativeFee); + expect(result.securityAmount).toEqual(updated.securityAmount); + expect(result.approvalDependant).toEqual(updated.approvalDependant); + }); + }); +}); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts new file mode 100644 index 0000000000..c38a6b7e34 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts @@ -0,0 +1,48 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { + ApplicationDecisionConditionDto, + UpdateApplicationDecisionConditionDto, +} from './application-decision-condition.dto'; +import { ApplicationDecisionCondition } from './application-decision-condition.entity'; +import { ApplicationDecisionConditionService } from './application-decision-condition.service'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@Controller('application-decision-condition') +@UseGuards(RolesGuard) +export class ApplicationDecisionConditionController { + constructor( + private conditionService: ApplicationDecisionConditionService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Patch('/:uuid') + @UserRoles(...ANY_AUTH_ROLE) + async update( + @Param('uuid') uuid: string, + @Body() updates: UpdateApplicationDecisionConditionDto, + ) { + const condition = await this.conditionService.getOneOrFail(uuid); + + const updatedCondition = await this.conditionService.update(condition, { + approvalDependant: updates.approvalDependant, + securityAmount: updates.securityAmount, + administrativeFee: updates.administrativeFee, + description: updates.description, + completionDate: formatIncomingDate(updates.completionDate), + supersededDate: formatIncomingDate(updates.supersededDate), + }); + return await this.mapper.mapAsync( + updatedCondition, + ApplicationDecisionCondition, + ApplicationDecisionConditionDto, + ); + } +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts index 9ee0a58ec6..c05683ab2e 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts @@ -1,5 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { + IsArray, IsBoolean, IsNumber, IsOptional, @@ -7,6 +8,7 @@ import { IsUUID, } from 'class-validator'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; +import { ApplicationDecisionComponentDto } from '../application-decision-v2/application-decision/component/application-decision-component.dto'; export class ApplicationDecisionConditionTypeDto extends BaseCodeDto {} export class ApplicationDecisionConditionDto { @@ -30,13 +32,18 @@ export class ApplicationDecisionConditionDto { @AutoMap(() => String) componentUuid: string | null; -} -export class UpdateApplicationDecisionConditionDto { - @IsOptional() - @IsString() - uuid?: string; + @AutoMap() + completionDate?: number; + + @AutoMap() + supersededDate?: number; + @AutoMap() + components?: ApplicationDecisionComponentDto[]; +} + +export class ComponentToConditionDto { @IsOptional() @IsUUID() componentDecisionUuid?: string; @@ -44,6 +51,16 @@ export class UpdateApplicationDecisionConditionDto { @IsOptional() @IsString() componentToConditionType?: string; +} + +export class UpdateApplicationDecisionConditionDto { + @IsOptional() + @IsString() + uuid?: string; + + @IsOptional() + @IsArray() + componentToConditions?: ComponentToConditionDto[]; @IsOptional() @IsBoolean() @@ -64,4 +81,23 @@ export class UpdateApplicationDecisionConditionDto { @IsOptional() @IsString() type?: ApplicationDecisionConditionTypeDto; + + @IsOptional() + @IsNumber() + completionDate?: number; + + @IsOptional() + @IsNumber() + supersededDate?: number; +} + +export class UpdateApplicationDecisionConditionServiceDto { + componentDecisionUuid?: string; + componentToConditionType?: string; + approvalDependant?: boolean; + securityAmount?: number; + administrativeFee?: number; + description?: string; + completionDate?: Date | null; + supersededDate?: Date | null; } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts index 8a876ed209..b3cc1f7031 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts @@ -1,9 +1,9 @@ import { AutoMap } from '@automapper/classes'; -import { Column, Entity, ManyToOne } from 'typeorm'; +import { Column, Entity, JoinTable, ManyToMany, ManyToOne } from 'typeorm'; import { Base } from '../../../common/entities/base.entity'; import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; -import { ApplicationDecision } from '../application-decision.entity'; import { ApplicationDecisionComponent } from '../application-decision-v2/application-decision/component/application-decision-component.entity'; +import { ApplicationDecision } from '../application-decision.entity'; import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; @Entity() @@ -15,6 +15,7 @@ export class ApplicationDecisionCondition extends Base { } } + @AutoMap(() => Boolean) @Column({ type: 'boolean', nullable: true }) approvalDependant: boolean | null; @@ -38,24 +39,47 @@ export class ApplicationDecisionCondition extends Base { }) administrativeFee: number | null; + @AutoMap(() => String) @Column({ type: 'text', nullable: true }) description: string | null; + @AutoMap(() => String) + @Column({ + type: 'timestamptz', + comment: 'Condition Completion date', + nullable: true, + }) + completionDate?: Date | null; + + @AutoMap() + @Column({ + type: 'timestamptz', + comment: 'Condition Superseded date', + nullable: true, + }) + supersededDate?: Date | null; + @ManyToOne(() => ApplicationDecisionConditionType) type: ApplicationDecisionConditionType; + @AutoMap(() => String) @Column({ type: 'text', nullable: true }) typeCode: string | null; @ManyToOne(() => ApplicationDecision, { nullable: false }) decision: ApplicationDecision; + @AutoMap(() => String) @Column() decisionUuid: string; - @ManyToOne(() => ApplicationDecisionComponent) - component: ApplicationDecisionComponent | null; - - @Column({ type: 'uuid', nullable: true }) - componentUuid: string | null; + @ManyToMany( + () => ApplicationDecisionComponent, + (component) => component.conditions, + { nullable: true }, + ) + @JoinTable({ + name: 'application_decision_condition_component', + }) + components: ApplicationDecisionComponent[] | null; } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts index 6fd22039ba..63b309e1d4 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts @@ -48,6 +48,7 @@ describe('ApplicationDecisionConditionService', () => { mockApplicationDecisionConditionRepository.findOneOrFail, ).toBeCalledWith({ where: { uuid: 'fake' }, + relations: { type: true }, }); expect(result).toBeDefined(); }); @@ -107,6 +108,7 @@ describe('ApplicationDecisionConditionService', () => { mockApplicationDecisionConditionRepository.findOneOrFail, ).toBeCalledWith({ where: { uuid: 'uuid' }, + relations: { type: true }, }); expect(result[0].uuid).toEqual(mockDto.uuid); }); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts index 6a0979fc9f..c6b3bfa1a5 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts @@ -1,10 +1,12 @@ -import { ServiceValidationException } from '@app/common/exceptions/base.exception'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { CreateApplicationDecisionComponentDto } from '../application-decision-v2/application-decision/component/application-decision-component.dto'; +import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; import { ApplicationDecisionComponent } from '../application-decision-v2/application-decision/component/application-decision-component.entity'; -import { UpdateApplicationDecisionConditionDto } from './application-decision-condition.dto'; +import { + UpdateApplicationDecisionConditionDto, + UpdateApplicationDecisionConditionServiceDto, +} from './application-decision-condition.dto'; import { ApplicationDecisionCondition } from './application-decision-condition.entity'; @Injectable() @@ -17,6 +19,9 @@ export class ApplicationDecisionConditionService { async getOneOrFail(uuid: string) { return this.repository.findOneOrFail({ where: { uuid }, + relations: { + type: true, + }, }); } @@ -43,38 +48,41 @@ export class ApplicationDecisionConditionService { condition.approvalDependant = updateDto.approvalDependant ?? null; if ( - updateDto.componentDecisionUuid && - updateDto.componentToConditionType + updateDto.componentToConditions !== undefined && + updateDto.componentToConditions.length > 0 ) { - const matchingComponent = allComponents.find( + const matchingComponent = allComponents.filter( (component) => - component.applicationDecisionUuid === - updateDto.componentDecisionUuid && - component.applicationDecisionComponentType.code === - updateDto.componentToConditionType, + updateDto.componentToConditions + ?.flatMap((e) => e.componentDecisionUuid) + .includes(component.applicationDecisionUuid) && + updateDto.componentToConditions + ?.flatMap((e) => e.componentToConditionType) + .includes(component.applicationDecisionComponentTypeCode), ); - if (matchingComponent) { - condition.componentUuid = matchingComponent.uuid; + + if (matchingComponent && matchingComponent.length > 0) { + condition.components = matchingComponent; updatedConditions.push(condition); continue; } - const matchingComponent2 = newComponents.find( - (component) => - component.applicationDecisionComponentTypeCode === - updateDto.componentToConditionType, + const matchingComponent2 = newComponents.filter((component) => + updateDto.componentToConditions + ?.flatMap((e) => e.componentToConditionType) + .includes(component.applicationDecisionComponentTypeCode), ); - if (matchingComponent2) { - condition.component = matchingComponent2; + + if (matchingComponent2 && matchingComponent2.length > 0) { + condition.components = matchingComponent2; updatedConditions.push(condition); continue; } - throw new ServiceValidationException( 'Failed to find matching component', ); } else { - condition.componentUuid = null; + condition.components = null; updatedConditions.push(condition); } } @@ -89,4 +97,12 @@ export class ApplicationDecisionConditionService { async remove(components: ApplicationDecisionCondition[]) { await this.repository.remove(components); } + + async update( + existingCondition: ApplicationDecisionCondition, + updates: UpdateApplicationDecisionConditionServiceDto, + ) { + await this.repository.update(existingCondition.uuid, updates); + return await this.getOneOrFail(existingCondition.uuid); + } } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts index 2a5891e1e4..124775e2a6 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts @@ -32,6 +32,7 @@ import { ApplicationDecisionComponentService } from './application-decision/comp import { LinkedResolutionOutcomeType } from './application-decision/linked-resolution-outcome-type.entity'; import { ApplicationDecisionComponentController } from './application-decision/component/application-decision-component.controller'; import { NaruSubtype } from '../../../portal/application-submission/naru-subtype/naru-subtype.entity'; +import { ApplicationDecisionConditionController } from '../application-decision-condition/application-decision-condition.controller'; @Module({ imports: [ @@ -76,6 +77,7 @@ import { NaruSubtype } from '../../../portal/application-submission/naru-subtype controllers: [ ApplicationDecisionV2Controller, ApplicationDecisionComponentController, + ApplicationDecisionConditionController, ], exports: [ApplicationDecisionV2Service], }) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts index 2a602d60ae..40856e89cc 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts @@ -171,6 +171,8 @@ describe('ApplicationDecisionV2Service', () => { mockNaruSubtypeRepository.find.mockResolvedValue([]); mockDecisionComponentService.createOrUpdate.mockResolvedValue([]); + + mockDecisionConditionService.remove.mockResolvedValue({} as any); }); describe('ApplicationDecisionService Core Tests', () => { diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts index f6080377f8..af671e4126 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts @@ -113,6 +113,10 @@ export class ApplicationDecisionV2Service { applicationDecisionComponentType: true, naruSubtype: true, }, + conditions: { + type: true, + components: true, + }, }, }); @@ -203,6 +207,7 @@ export class ApplicationDecisionV2Service { }, conditions: { type: true, + components: true, }, chairReviewOutcome: true, }, @@ -217,6 +222,7 @@ export class ApplicationDecisionV2Service { decision.documents = decision.documents.filter( (document) => !!document.document, ); + return decision; } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts index 9532ad09ba..2a4ec38d7f 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts @@ -1,8 +1,9 @@ import { AutoMap } from '@automapper/classes'; -import { Column, Entity, Index, ManyToOne } from 'typeorm'; +import { Column, Entity, Index, ManyToMany, ManyToOne } from 'typeorm'; import { Base } from '../../../../../common/entities/base.entity'; import { NaruSubtype } from '../../../../../portal/application-submission/naru-subtype/naru-subtype.entity'; import { ColumnNumericTransformer } from '../../../../../utils/column-numeric-transform'; +import { ApplicationDecisionCondition } from '../../../application-decision-condition/application-decision-condition.entity'; import { ApplicationDecision } from '../../../application-decision.entity'; import { ApplicationDecisionComponentType } from './application-decision-component-type.entity'; @@ -202,4 +203,10 @@ export class ApplicationDecisionComponent extends Base { @AutoMap() @ManyToOne(() => ApplicationDecision, { nullable: false }) applicationDecision: ApplicationDecision; + + @ManyToMany( + () => ApplicationDecisionCondition, + (condition) => condition.components, + ) + conditions: ApplicationDecisionCondition[]; } diff --git a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts index c91472184c..35153cd374 100644 --- a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts @@ -31,8 +31,8 @@ import { ApplicationDecisionComponent } from '../../alcs/application-decision/ap import { LinkedResolutionOutcomeType } from '../../alcs/application-decision/application-decision-v2/application-decision/linked-resolution-outcome-type.entity'; import { ApplicationDecision } from '../../alcs/application-decision/application-decision.entity'; import { PortalDecisionDto } from '../../portal/application-decision/application-decision.dto'; -import { NaruSubtype } from '../../portal/application-submission/naru-subtype/naru-subtype.entity'; import { NaruSubtypeDto } from '../../portal/application-submission/application-submission.dto'; +import { NaruSubtype } from '../../portal/application-submission/naru-subtype/naru-subtype.entity'; @Injectable() export class ApplicationDecisionProfile extends AutomapperProfile { @@ -214,6 +214,26 @@ export class ApplicationDecisionProfile extends AutomapperProfile { mapper, ApplicationDecisionCondition, ApplicationDecisionConditionDto, + forMember( + (ad) => ad.completionDate, + mapFrom((a) => a.completionDate?.getTime()), + ), + forMember( + (ad) => ad.supersededDate, + mapFrom((a) => a.supersededDate?.getTime()), + ), + forMember( + (ad) => ad.components, + mapFrom((a) => + a.components && a.components.length > 0 + ? this.mapper.mapArray( + a.components, + ApplicationDecisionComponent, + ApplicationDecisionComponentDto, + ) + : [], + ), + ), ); createMap( diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1688064562389-completion_and_superseded_dates_for_conditions.ts b/services/apps/alcs/src/providers/typeorm/migrations/1688064562389-completion_and_superseded_dates_for_conditions.ts new file mode 100644 index 0000000000..7e76188b2a --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1688064562389-completion_and_superseded_dates_for_conditions.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class completionAndSupersededDatesForConditions1688064562389 + implements MigrationInterface +{ + name = 'completionAndSupersededDatesForConditions1688064562389'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition" ADD "completion_date" TIMESTAMP WITH TIME ZONE`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_decision_condition"."completion_date" IS 'Condition Completion date'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition" ADD "superseded_date" TIMESTAMP WITH TIME ZONE`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_decision_condition"."superseded_date" IS 'Condition Superseded date'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_decision_condition"."superseded_date" IS 'Condition Superseded date'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition" DROP COLUMN "superseded_date"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_decision_condition"."completion_date" IS 'Condition Completion date'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition" DROP COLUMN "completion_date"`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1688150315958-many_to_many_component_conditions.ts b/services/apps/alcs/src/providers/typeorm/migrations/1688150315958-many_to_many_component_conditions.ts new file mode 100644 index 0000000000..3d04e90f3a --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1688150315958-many_to_many_component_conditions.ts @@ -0,0 +1,55 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class manyToManyComponentConditions1688150315958 + implements MigrationInterface +{ + name = 'manyToManyComponentConditions1688150315958'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition" DROP CONSTRAINT "FK_9745fcca6616819f804a129fef4"`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."application_decision_condition_component" ("application_decision_condition_uuid" uuid NOT NULL, "application_decision_component_uuid" uuid NOT NULL, CONSTRAINT "PK_5794bea9870c0f65417a3de5e78" PRIMARY KEY ("application_decision_condition_uuid", "application_decision_component_uuid"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_9471c8341a43328f1705e5cc2b" ON "alcs"."application_decision_condition_component" ("application_decision_condition_uuid") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_a26a88960a996c58891c744beb" ON "alcs"."application_decision_condition_component" ("application_decision_component_uuid") `, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition" DROP COLUMN "component_uuid"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component" ADD CONSTRAINT "FK_9471c8341a43328f1705e5cc2bf" FOREIGN KEY ("application_decision_condition_uuid") REFERENCES "alcs"."application_decision_condition"("uuid") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component" ADD CONSTRAINT "FK_a26a88960a996c58891c744beba" FOREIGN KEY ("application_decision_component_uuid") REFERENCES "alcs"."application_decision_component"("uuid") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component" DROP CONSTRAINT "FK_a26a88960a996c58891c744beba"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component" DROP CONSTRAINT "FK_9471c8341a43328f1705e5cc2bf"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition" ADD "component_uuid" uuid`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_a26a88960a996c58891c744beb"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_9471c8341a43328f1705e5cc2b"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."application_decision_condition_component"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition" ADD CONSTRAINT "FK_9745fcca6616819f804a129fef4" FOREIGN KEY ("component_uuid") REFERENCES "alcs"."application_decision_component"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1688150503477-many_to_many_component_conditions.ts b/services/apps/alcs/src/providers/typeorm/migrations/1688150503477-many_to_many_component_conditions.ts new file mode 100644 index 0000000000..46e3e39147 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1688150503477-many_to_many_component_conditions.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class manyToManyComponentConditions1688150503477 + implements MigrationInterface +{ + name = 'manyToManyComponentConditions1688150503477'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component" DROP CONSTRAINT "FK_a26a88960a996c58891c744beba"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component" ADD CONSTRAINT "FK_a26a88960a996c58891c744beba" FOREIGN KEY ("application_decision_component_uuid") REFERENCES "alcs"."application_decision_component"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component" DROP CONSTRAINT "FK_a26a88960a996c58891c744beba"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component" ADD CONSTRAINT "FK_a26a88960a996c58891c744beba" FOREIGN KEY ("application_decision_component_uuid") REFERENCES "alcs"."application_decision_component"("uuid") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + } +} diff --git a/services/apps/alcs/test/mocks/mockEntities.ts b/services/apps/alcs/test/mocks/mockEntities.ts index 3be83e4dc9..543d73a0f2 100644 --- a/services/apps/alcs/test/mocks/mockEntities.ts +++ b/services/apps/alcs/test/mocks/mockEntities.ts @@ -1,3 +1,11 @@ +import { ApplicationDecisionOutcomeCode } from '../../src/alcs/application-decision/application-decision-outcome.entity'; +import { ApplicationDecisionMeeting } from '../../src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.entity'; +import { ApplicationDecision } from '../../src/alcs/application-decision/application-decision.entity'; +import { ApplicationModificationOutcomeType } from '../../src/alcs/application-decision/application-modification/application-modification-outcome-type/application-modification-outcome-type.entity'; +import { ApplicationModification } from '../../src/alcs/application-decision/application-modification/application-modification.entity'; +import { ApplicationReconsideration } from '../../src/alcs/application-decision/application-reconsideration/application-reconsideration.entity'; +import { ApplicationReconsiderationOutcomeType } from '../../src/alcs/application-decision/application-reconsideration/reconsideration-outcome-type/application-reconsideration-outcome-type.entity'; +import { ApplicationReconsiderationType } from '../../src/alcs/application-decision/application-reconsideration/reconsideration-type/application-reconsideration-type.entity'; import { ApplicationMeeting } from '../../src/alcs/application/application-meeting/application-meeting.entity'; import { ApplicationPaused } from '../../src/alcs/application/application-paused.entity'; import { Application } from '../../src/alcs/application/application.entity'; @@ -12,14 +20,6 @@ import { ApplicationRegion } from '../../src/alcs/code/application-code/applicat import { ApplicationType } from '../../src/alcs/code/application-code/application-type/application-type.entity'; import { Comment } from '../../src/alcs/comment/comment.entity'; import { CommentMention } from '../../src/alcs/comment/mention/comment-mention.entity'; -import { ApplicationDecisionOutcomeCode } from '../../src/alcs/application-decision/application-decision-outcome.entity'; -import { ApplicationDecision } from '../../src/alcs/application-decision/application-decision.entity'; -import { ApplicationModification } from '../../src/alcs/application-decision/application-modification/application-modification.entity'; -import { ApplicationModificationOutcomeType } from '../../src/alcs/application-decision/application-modification/application-modification-outcome-type/application-modification-outcome-type.entity'; -import { ApplicationReconsideration } from '../../src/alcs/application-decision/application-reconsideration/application-reconsideration.entity'; -import { ApplicationReconsiderationOutcomeType } from '../../src/alcs/application-decision/application-reconsideration/reconsideration-outcome-type/application-reconsideration-outcome-type.entity'; -import { ApplicationReconsiderationType } from '../../src/alcs/application-decision/application-reconsideration/reconsideration-type/application-reconsideration-type.entity'; -import { ApplicationDecisionMeeting } from '../../src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.entity'; import { AssigneeDto, UserDto } from '../../src/user/user.dto'; import { User } from '../../src/user/user.entity'; @@ -291,6 +291,7 @@ const initApplicationDecisionMock = (application?: Application) => { applicationUuid: application ? application.uuid : 'fake-application-uuid', application, documents: [], + conditions: [], }); }; From 04adc6b4dd4f37db7441d567a4b19f531520d2d1 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Thu, 6 Jul 2023 16:11:59 -0700 Subject: [PATCH 055/954] Show toggle button validation on parcel search (#756) * Show toggle button validation on parcel search * Fix farm validation on form click and search --- .../parcel-details/parcel-entry/parcel-entry.component.html | 4 ++-- .../parcel-details/parcel-entry/parcel-entry.component.ts | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html index 533bcab509..cbfa6a50b5 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html @@ -154,7 +154,7 @@
Parcel Lookup
>No -
+
warning
This field is required
@@ -243,7 +243,7 @@
Owner Information
Date: Mon, 10 Jul 2023 09:14:30 -0700 Subject: [PATCH 056/954] Only set visibility when changing document types * Do not set on load/edit --- .../document-upload-dialog.component.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts index 0b6cc13f9f..ed36bc8f65 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts @@ -154,8 +154,6 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { if (parcels.length > 0) { this.parcelId.setValidators([Validators.required]); this.parcelId.updateValueAndValidity(); - - this.visibleToInternal.setValue(true); this.source.setValue(DOCUMENT_SOURCE.APPLICANT); const selectedParcel = parcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); @@ -181,8 +179,6 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { const owners = submission.owners; this.ownerId.setValidators([Validators.required]); this.ownerId.updateValueAndValidity(); - - this.visibleToInternal.setValue(true); this.source.setValue(DOCUMENT_SOURCE.APPLICANT); const selectedOwner = owners.find((owner) => owner.corporateSummaryUuid === uuid); @@ -210,6 +206,7 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { if (this.type.value === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { await this.prepareCertificateOfTitleUpload(); + this.visibleToInternal.setValue(true); } else { this.parcelId.setValue(null); this.parcelId.setValidators([]); @@ -218,6 +215,7 @@ export class DocumentUploadDialogComponent implements OnInit, OnDestroy { if (this.type.value === DOCUMENT_TYPE.CORPORATE_SUMMARY) { await this.prepareCorporateSummaryUpload(); + this.visibleToInternal.setValue(true); } else { this.ownerId.setValue(null); this.ownerId.setValidators([]); From 2dce88b22340b3ddeecb7ca2f072efdcca480799 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 10 Jul 2023 12:58:02 -0700 Subject: [PATCH 057/954] Add card statuses to Admin * Allow creation/edit/deletion of card statuses * Add special logic for deletion --- .../src/app/features/admin/admin.component.ts | 7 + .../src/app/features/admin/admin.module.ts | 4 + .../card-status-dialog.component.html | 56 +++++++ .../card-status-dialog.component.scss | 19 +++ .../card-status-dialog.component.spec.ts | 37 +++++ .../card-status-dialog.component.ts | 63 +++++++ .../card-status/card-status.component.html | 35 ++++ .../card-status/card-status.component.scss | 9 + .../card-status/card-status.component.spec.ts | 53 ++++++ .../card-status/card-status.component.ts | 73 +++++++++ .../card-status/card-status.service.spec.ts | 154 ++++++++++++++++++ .../card/card-status/card-status.service.ts | 65 ++++++++ .../apps/alcs/src/alcs/admin/admin.module.ts | 6 + .../card-status.controller.spec.ts | 125 ++++++++++++++ .../card-status/card-status.controller.ts | 89 ++++++++++ .../apps/alcs/src/alcs/board/board.service.ts | 18 +- .../card-status.controller.spec.ts | 49 ------ .../card-status/card-status.controller.ts | 26 --- .../card/card-status/card-status.entity.ts | 1 + .../card-status/card-status.service.spec.ts | 83 +++++++--- .../card/card-status/card-status.service.ts | 52 ++++-- .../apps/alcs/src/alcs/card/card.module.ts | 3 +- .../apps/alcs/src/alcs/card/card.service.ts | 7 + 23 files changed, 922 insertions(+), 112 deletions(-) create mode 100644 alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.html create mode 100644 alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.scss create mode 100644 alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.spec.ts create mode 100644 alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.ts create mode 100644 alcs-frontend/src/app/features/admin/card-status/card-status.component.html create mode 100644 alcs-frontend/src/app/features/admin/card-status/card-status.component.scss create mode 100644 alcs-frontend/src/app/features/admin/card-status/card-status.component.spec.ts create mode 100644 alcs-frontend/src/app/features/admin/card-status/card-status.component.ts create mode 100644 alcs-frontend/src/app/services/card/card-status/card-status.service.spec.ts create mode 100644 alcs-frontend/src/app/services/card/card-status/card-status.service.ts create mode 100644 services/apps/alcs/src/alcs/admin/card-status/card-status.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/admin/card-status/card-status.controller.ts delete mode 100644 services/apps/alcs/src/alcs/card/card-status/card-status.controller.spec.ts delete mode 100644 services/apps/alcs/src/alcs/card/card-status/card-status.controller.ts diff --git a/alcs-frontend/src/app/features/admin/admin.component.ts b/alcs-frontend/src/app/features/admin/admin.component.ts index 66886e92c0..87c6eeb1e1 100644 --- a/alcs-frontend/src/app/features/admin/admin.component.ts +++ b/alcs-frontend/src/app/features/admin/admin.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { CardStatusComponent } from './card-status/card-status.component'; import { CeoCriterionComponent } from './ceo-criterion/ceo-criterion.component'; import { DecisionConditionTypesComponent } from './decision-condition-types/decision-condition-types.component'; import { DecisionMakerComponent } from './decision-maker/decision-maker.component'; @@ -51,6 +52,12 @@ export const childRoutes = [ icon: 'unarchive', component: UnarchiveComponent, }, + { + path: 'card-status', + menuTitle: 'Columns', + icon: 'view_week', + component: CardStatusComponent, + }, ]; @Component({ diff --git a/alcs-frontend/src/app/features/admin/admin.module.ts b/alcs-frontend/src/app/features/admin/admin.module.ts index 127a80f3d0..61defb2f23 100644 --- a/alcs-frontend/src/app/features/admin/admin.module.ts +++ b/alcs-frontend/src/app/features/admin/admin.module.ts @@ -4,6 +4,8 @@ import { MatPaginatorModule } from '@angular/material/paginator'; import { RouterModule, Routes } from '@angular/router'; import { SharedModule } from '../../shared/shared.module'; import { AdminComponent, childRoutes } from './admin.component'; +import { CardStatusDialogComponent } from './card-status/card-status-dialog/card-status-dialog.component'; +import { CardStatusComponent } from './card-status/card-status.component'; import { CeoCriterionDialogComponent } from './ceo-criterion/ceo-criterion-dialog/ceo-criterion-dialog.component'; import { CeoCriterionComponent } from './ceo-criterion/ceo-criterion.component'; import { DecisionConditionTypesDialogComponent } from './decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component'; @@ -41,6 +43,8 @@ const routes: Routes = [ DecisionMakerDialogComponent, DecisionConditionTypesComponent, DecisionConditionTypesDialogComponent, + CardStatusComponent, + CardStatusDialogComponent, ], imports: [CommonModule, SharedModule.forRoot(), RouterModule.forChild(routes), MatPaginatorModule], }) diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.html b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.html new file mode 100644 index 0000000000..221f66fe25 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.html @@ -0,0 +1,56 @@ +
+

{{ isEdit ? 'Edit' : 'Create' }} Column

+
+
+
+
+ + Code + + +
+ +
+ + Label + + + Note: The change will be reflected on all boards +
+ +
+ + Description + + +
+
+ +
+
+ +
+
+ + +
+
+
diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.scss b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.scss new file mode 100644 index 0000000000..60e066882d --- /dev/null +++ b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.scss @@ -0,0 +1,19 @@ +.dialog { + padding: 24px; + + form { + display: grid; + grid-template-columns: 1fr; + row-gap: 24px; + column-gap: 24px; + margin-bottom: 12px; + + .mat-mdc-form-field { + width: 100%; + } + } +} + +.split { + margin-top: 36px; +} diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.spec.ts b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.spec.ts new file mode 100644 index 0000000000..ef7e3af3d4 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.spec.ts @@ -0,0 +1,37 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { CardStatusService } from '../../../../services/card/card-status/card-status.service'; +import { DecisionConditionTypesService } from '../../../../services/decision-condition-types/decision-condition-types.service'; + +import { CardStatusDialogComponent } from './card-status-dialog.component'; + +describe('CardStatusDialogComponent', () => { + let component: CardStatusDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, FormsModule], + declarations: [CardStatusDialogComponent], + providers: [ + { provide: MAT_DIALOG_DATA, useValue: undefined }, + { provide: MatDialogRef, useValue: {} }, + { + provide: CardStatusService, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(CardStatusDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.ts b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.ts new file mode 100644 index 0000000000..c05c5a1d04 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.ts @@ -0,0 +1,63 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { CardStatusDto } from '../../../../services/application/application-code.dto'; +import { CardStatusService } from '../../../../services/card/card-status/card-status.service'; + +@Component({ + selector: 'app-decision-condition-types-dialog', + templateUrl: './card-status-dialog.component.html', + styleUrls: ['./card-status-dialog.component.scss'], +}) +export class CardStatusDialogComponent implements OnInit { + description = ''; + label = ''; + code = ''; + + isLoading = false; + isEdit = false; + canDelete = false; + canDeleteReason = ''; + + constructor( + @Inject(MAT_DIALOG_DATA) public data: CardStatusDto | undefined, + private dialogRef: MatDialogRef, + private cardStatusService: CardStatusService + ) { + if (data) { + this.description = data.description; + this.label = data.label; + this.code = data.code; + } + this.isEdit = !!data; + } + + async onSubmit() { + this.isLoading = true; + + const dto = { + code: this.code, + label: this.label, + description: this.description, + }; + + if (this.isEdit) { + await this.cardStatusService.update(this.code, dto); + } else { + await this.cardStatusService.create(dto); + } + this.isLoading = false; + this.dialogRef.close(true); + } + + ngOnInit(): void { + this.loadCanDelete(); + } + + private async loadCanDelete() { + const res = await this.cardStatusService.canDelete(this.code); + if (res) { + this.canDelete = res.canDelete; + this.canDeleteReason = res.reason; + } + } +} diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status.component.html b/alcs-frontend/src/app/features/admin/card-status/card-status.component.html new file mode 100644 index 0000000000..0126c89c37 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/card-status/card-status.component.html @@ -0,0 +1,35 @@ +
+
+

Columns

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
Label{{ row.label }}Description{{ row.description }}Code{{ row.code }}Actions + +
+
diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status.component.scss b/alcs-frontend/src/app/features/admin/card-status/card-status.component.scss new file mode 100644 index 0000000000..4320920313 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/card-status/card-status.component.scss @@ -0,0 +1,9 @@ +.container { + .edit-btn { + color: green; + } + + .delete-btn { + color: red; + } +} diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status.component.spec.ts b/alcs-frontend/src/app/features/admin/card-status/card-status.component.spec.ts new file mode 100644 index 0000000000..d038f664e1 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/card-status/card-status.component.spec.ts @@ -0,0 +1,53 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { CardStatusService } from '../../../services/card/card-status/card-status.service'; +import { DecisionConditionTypesService } from '../../../services/decision-condition-types/decision-condition-types.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { CardStatusComponent } from './card-status.component'; + +describe('CardStatusComponent', () => { + let component: CardStatusComponent; + let fixture: ComponentFixture; + let mockCardStatusService: DeepMocked; + let mockDialog: DeepMocked; + let mockConfirmationDialogService: DeepMocked; + + beforeEach(async () => { + mockCardStatusService = createMock(); + mockDialog = createMock(); + mockConfirmationDialogService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [CardStatusComponent], + providers: [ + { + provide: CardStatusService, + useValue: mockCardStatusService, + }, + { + provide: MatDialog, + useValue: mockDialog, + }, + { + provide: ConfirmationDialogService, + useValue: mockConfirmationDialogService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + imports: [HttpClientTestingModule], + }).compileComponents(); + + fixture = TestBed.createComponent(CardStatusComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status.component.ts b/alcs-frontend/src/app/features/admin/card-status/card-status.component.ts new file mode 100644 index 0000000000..a0bc8ce223 --- /dev/null +++ b/alcs-frontend/src/app/features/admin/card-status/card-status.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Subject } from 'rxjs'; +import { CardStatusDto } from '../../../services/application/application-code.dto'; +import { CardStatusService } from '../../../services/card/card-status/card-status.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { CardStatusDialogComponent } from './card-status-dialog/card-status-dialog.component'; + +@Component({ + selector: 'app-card-status', + templateUrl: './card-status.component.html', + styleUrls: ['./card-status.component.scss'], +}) +export class CardStatusComponent implements OnInit { + destroy = new Subject(); + + cardStatusDtos: CardStatusDto[] = []; + displayedColumns: string[] = ['label', 'description', 'code', 'actions']; + + constructor( + private cardStatusService: CardStatusService, + public dialog: MatDialog, + private confirmationDialogService: ConfirmationDialogService + ) {} + + ngOnInit(): void { + this.fetch(); + } + + async fetch() { + this.cardStatusDtos = await this.cardStatusService.fetch(); + } + + async onCreate() { + const dialog = this.dialog.open(CardStatusDialogComponent, { + minWidth: '600px', + maxWidth: '800px', + width: '70%', + }); + dialog.beforeClosed().subscribe(async (result) => { + if (result) { + await this.fetch(); + } + }); + } + + async onEdit(cardStatusDto: CardStatusDto) { + const dialog = this.dialog.open(CardStatusDialogComponent, { + minWidth: '600px', + maxWidth: '800px', + width: '70%', + data: cardStatusDto, + }); + dialog.beforeClosed().subscribe(async (result) => { + if (result) { + await this.fetch(); + } + }); + } + + async onDelete(cardStatusDto: CardStatusDto) { + this.confirmationDialogService + .openDialog({ + body: `Are you sure you want to delete ${cardStatusDto.label}?`, + }) + .subscribe(async (answer) => { + if (answer) { + await this.cardStatusService.delete(cardStatusDto.code); + await this.fetch(); + } + }); + } +} diff --git a/alcs-frontend/src/app/services/card/card-status/card-status.service.spec.ts b/alcs-frontend/src/app/services/card/card-status/card-status.service.spec.ts new file mode 100644 index 0000000000..5afb06f74d --- /dev/null +++ b/alcs-frontend/src/app/services/card/card-status/card-status.service.spec.ts @@ -0,0 +1,154 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../../toast/toast.service'; +import { CardStatusService } from './card-status.service'; + +describe('CardStatusService', () => { + let service: CardStatusService; + let mockHttpClient: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(() => { + mockHttpClient = createMock(); + mockToastService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + ], + }); + service = TestBed.inject(CardStatusService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call post on create', async () => { + mockHttpClient.post.mockReturnValue( + of({ + code: 'fake', + }) + ); + + const res = await service.create({ + code: '', + label: '', + description: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res!.code).toEqual('fake'); + }); + + it('should show toast if create fails', async () => { + mockHttpClient.post.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.create({ + code: '', + label: '', + description: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeUndefined(); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should call patch on update', async () => { + mockHttpClient.patch.mockReturnValue( + of({ + code: 'fake', + }) + ); + + const res = await service.update('fake', { + code: '', + label: '', + description: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res!.code).toEqual('fake'); + }); + + it('should show toast if update fails', async () => { + mockHttpClient.patch.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.update('mock', { + code: '', + label: '', + description: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should call get on fetch', async () => { + mockHttpClient.get.mockReturnValue(of([])); + + await service.fetch(); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + }); + + it('should show toast if get fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.fetch(); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should call delete on delete', async () => { + mockHttpClient.delete.mockReturnValue( + of({ + code: 'fake', + }) + ); + + const res = await service.delete('fake'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res!.code).toEqual('fake'); + }); + + it('should show toast if delete fails', async () => { + mockHttpClient.delete.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.delete('mock'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/card/card-status/card-status.service.ts b/alcs-frontend/src/app/services/card/card-status/card-status.service.ts new file mode 100644 index 0000000000..53dfa4cd05 --- /dev/null +++ b/alcs-frontend/src/app/services/card/card-status/card-status.service.ts @@ -0,0 +1,65 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { CardStatusDto } from '../../application/application-code.dto'; +import { ToastService } from '../../toast/toast.service'; + +@Injectable({ + providedIn: 'root', +}) +export class CardStatusService { + private url = `${environment.apiUrl}/card-status`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetch() { + try { + return await firstValueFrom(this.http.get(`${this.url}`)); + } catch (err) { + console.error(err); + this.toastService.showErrorToast('Failed to fetch card statuses'); + } + return []; + } + + async create(createDto: CardStatusDto) { + try { + return await firstValueFrom(this.http.post(`${this.url}`, createDto)); + } catch (e) { + this.toastService.showErrorToast('Failed to create card status'); + console.log(e); + } + return; + } + + async update(code: string, updateDto: CardStatusDto) { + try { + return await firstValueFrom(this.http.patch(`${this.url}/${code}`, updateDto)); + } catch (e) { + this.toastService.showErrorToast('Failed to update ceo criterion'); + console.log(e); + } + return; + } + + async delete(code: string) { + try { + return await firstValueFrom(this.http.delete(`${this.url}/${code}`)); + } catch (e) { + this.toastService.showErrorToast('Failed to delete card status'); + console.log(e); + } + return; + } + + async canDelete(code: string) { + try { + return await firstValueFrom(this.http.get<{ canDelete: boolean; reason: string }>(`${this.url}/${code}`)); + } catch (e) { + this.toastService.showErrorToast('Failed to delete card status'); + console.log(e); + } + return; + } +} diff --git a/services/apps/alcs/src/alcs/admin/admin.module.ts b/services/apps/alcs/src/alcs/admin/admin.module.ts index 23e3cf81f2..9bce9fb8a6 100644 --- a/services/apps/alcs/src/alcs/admin/admin.module.ts +++ b/services/apps/alcs/src/alcs/admin/admin.module.ts @@ -3,6 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationDecisionConditionType } from '../application-decision/application-decision-condition/application-decision-condition-code.entity'; import { ApplicationDecisionMakerCode } from '../application-decision/application-decision-maker/application-decision-maker.entity'; import { ApplicationModule } from '../application/application.module'; +import { BoardModule } from '../board/board.module'; +import { CardModule } from '../card/card.module'; import { CovenantModule } from '../covenant/covenant.module'; import { ApplicationCeoCriterionCode } from '../application-decision/application-ceo-criterion/application-ceo-criterion.entity'; import { ApplicationDecisionModule } from '../application-decision/application-decision.module'; @@ -16,6 +18,7 @@ import { ApplicationDecisionConditionTypesController } from './application-decis import { ApplicationDecisionConditionTypesService } from './application-decision-condition-types/application-decision-condition-types.service'; import { ApplicationDecisionMakerController } from './application-decision-maker/application-decision-maker.controller'; import { ApplicationDecisionMakerService } from './application-decision-maker/application-decision-maker.service'; +import { CardStatusController } from './card-status/card-status.controller'; import { HolidayController } from './holiday/holiday.controller'; import { HolidayEntity } from './holiday/holiday.entity'; import { HolidayService } from './holiday/holiday.service'; @@ -40,6 +43,8 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service'; forwardRef(() => CovenantModule), NoticeOfIntentModule, NoticeOfIntentDecisionModule, + CardModule, + BoardModule, ], controllers: [ HolidayController, @@ -49,6 +54,7 @@ import { UnarchiveCardService } from './unarchive-card/unarchive-card.service'; NoiSubtypeController, ApplicationDecisionMakerController, ApplicationDecisionConditionTypesController, + CardStatusController, ], providers: [ HolidayService, diff --git a/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.spec.ts b/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.spec.ts new file mode 100644 index 0000000000..243b9ba436 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.spec.ts @@ -0,0 +1,125 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { ConfigModule } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { BoardStatus } from '../../board/board-status.entity'; +import { Board } from '../../board/board.entity'; +import { BoardService } from '../../board/board.service'; +import { + CARD_STATUS, + CardStatus, +} from '../../card/card-status/card-status.entity'; +import { CardStatusService } from '../../card/card-status/card-status.service'; +import { CardStatusController } from './card-status.controller'; + +describe('CardStatusController', () => { + let controller: CardStatusController; + let mockCardStatusService: DeepMocked; + let mockBoardService: DeepMocked; + + beforeEach(async () => { + mockCardStatusService = createMock(); + mockBoardService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [CardStatusController], + providers: [ + { + provide: CardStatusService, + useValue: mockCardStatusService, + }, + { + provide: BoardService, + useValue: mockBoardService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + imports: [ConfigModule], + }).compile(); + + controller = module.get(CardStatusController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call out to service when fetching card statuses', async () => { + mockCardStatusService.fetch.mockResolvedValue([]); + + const cardStatuses = await controller.fetch(); + + expect(cardStatuses).toBeDefined(); + expect(mockCardStatusService.fetch).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when updating card status', async () => { + mockCardStatusService.update.mockResolvedValue(new CardStatus()); + + const holiday = await controller.update('fake', new CardStatus()); + + expect(holiday).toBeDefined(); + expect(mockCardStatusService.update).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when creating a card status', async () => { + mockCardStatusService.create.mockResolvedValue(new CardStatus()); + + const holiday = await controller.create(new CardStatus()); + + expect(holiday).toBeDefined(); + expect(mockCardStatusService.create).toHaveBeenCalledTimes(1); + }); + + it('should return false when trying to delete a critical column', async () => { + const res = await controller.canDelete(CARD_STATUS.READY_FOR_REVIEW); + + expect(res).toBeDefined(); + expect(res.canDelete).toBeFalsy(); + }); + + it('should return false when trying to delete a column with cards in it', async () => { + mockCardStatusService.getCardCountByStatus.mockResolvedValue(3); + + const res = await controller.canDelete('FAKE-CODE' as CARD_STATUS); + + expect(res).toBeDefined(); + expect(res.canDelete).toBeFalsy(); + expect(mockCardStatusService.getCardCountByStatus).toHaveBeenCalledTimes(1); + }); + + it('should return false when it is the last status on a board', async () => { + mockCardStatusService.getCardCountByStatus.mockResolvedValue(0); + mockBoardService.getBoardsWithStatus.mockResolvedValue([ + new Board({ + statuses: [new BoardStatus()], + }), + ]); + + const res = await controller.canDelete('FAKE-CODE' as CARD_STATUS); + + expect(res).toBeDefined(); + expect(res.canDelete).toBeFalsy(); + expect(mockBoardService.getBoardsWithStatus).toHaveBeenCalledTimes(1); + }); + + it('should return true for the happy path', async () => { + mockCardStatusService.getCardCountByStatus.mockResolvedValue(0); + mockBoardService.getBoardsWithStatus.mockResolvedValue([ + new Board({ + statuses: [new BoardStatus(), new BoardStatus()], + }), + ]); + + const res = await controller.canDelete('FAKE-CODE' as CARD_STATUS); + + expect(res).toBeDefined(); + expect(res.canDelete).toBeTruthy(); + expect(mockBoardService.getBoardsWithStatus).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.ts b/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.ts new file mode 100644 index 0000000000..65de8f7ec0 --- /dev/null +++ b/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.ts @@ -0,0 +1,89 @@ +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { BoardService } from '../../board/board.service'; +import { CardStatusDto } from '../../card/card-status/card-status.dto'; +import { CARD_STATUS } from '../../card/card-status/card-status.entity'; +import { CardStatusService } from '../../card/card-status/card-status.service'; + +@Controller('card-status') +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(RolesGuard) +export class CardStatusController { + constructor( + private cardStatusService: CardStatusService, + private boardService: BoardService, + ) {} + + @Get() + @UserRoles(AUTH_ROLE.ADMIN) + async fetch() { + return await this.cardStatusService.fetch(); + } + + @Get('/:code') + @UserRoles(AUTH_ROLE.ADMIN) + async canDelete(@Param('code') code: CARD_STATUS) { + //If its Decision Released, Ready for Review, Cancelled + if ( + [ + CARD_STATUS.CANCELLED, + CARD_STATUS.DECISION_RELEASED, + CARD_STATUS.READY_FOR_REVIEW, + ].includes(code) + ) { + return { + canDelete: false, + reason: + 'Column is critical to application functionality and can never be deleted', + }; + } + + //If it has any cards + const cardCount = await this.cardStatusService.getCardCountByStatus(code); + if (cardCount > 0) { + return { + canDelete: false, + reason: 'Column has cards in it, please move cards in order to delete', + }; + } + + //Is boards only status + const boardsUsingStatus = await this.boardService.getBoardsWithStatus(code); + const boardsWithOnlyStatus = boardsUsingStatus.filter( + (board) => board.statuses.length === 1, + ); + if (boardsWithOnlyStatus.length > 0) { + return { + canDelete: false, + reason: `Column is only status on board ${boardsWithOnlyStatus[0].title} and cannot be deleted`, + }; + } + return { + canDelete: true, + }; + } + + @Patch('/:code') + @UserRoles(AUTH_ROLE.ADMIN) + async update(@Param('code') code: string, @Body() updateDto: CardStatusDto) { + return await this.cardStatusService.update(code, updateDto); + } + + @Post('') + @UserRoles(AUTH_ROLE.ADMIN) + async create(@Body() createDto: CardStatusDto) { + return await this.cardStatusService.create(createDto); + } +} diff --git a/services/apps/alcs/src/alcs/board/board.service.ts b/services/apps/alcs/src/alcs/board/board.service.ts index 34c8d40f40..2fb3fa6797 100644 --- a/services/apps/alcs/src/alcs/board/board.service.ts +++ b/services/apps/alcs/src/alcs/board/board.service.ts @@ -1,8 +1,9 @@ import { ServiceNotFoundException } from '@app/common/exceptions/base.exception'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { FindOptionsWhere, Repository } from 'typeorm'; +import { Any, FindOptionsWhere, Repository } from 'typeorm'; import { FindOptionsRelations } from 'typeorm/find-options/FindOptionsRelations'; +import { CARD_STATUS } from '../card/card-status/card-status.entity'; import { CardService } from '../card/card.service'; import { Board } from './board.entity'; @@ -71,4 +72,19 @@ export class BoardService { card.board = board; return this.cardService.save(card); } + + async getBoardsWithStatus(code: CARD_STATUS) { + return this.boardRepository.find({ + where: { + statuses: { + status: { + code, + }, + }, + }, + relations: { + statuses: true, + }, + }); + } } diff --git a/services/apps/alcs/src/alcs/card/card-status/card-status.controller.spec.ts b/services/apps/alcs/src/alcs/card/card-status/card-status.controller.spec.ts deleted file mode 100644 index d13a1896bb..0000000000 --- a/services/apps/alcs/src/alcs/card/card-status/card-status.controller.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ClsService } from 'nestjs-cls'; -import { initCardStatusMockEntity } from '../../../../test/mocks/mockEntities'; -import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; -import { CardStatusController } from './card-status.controller'; -import { CardStatusDto } from './card-status.dto'; -import { CardStatusService } from './card-status.service'; - -describe('CardStatusController', () => { - let controller: CardStatusController; - let mockCardStatusService: DeepMocked; - const mockCardStatusEntity = initCardStatusMockEntity(); - const cardStatusDto: CardStatusDto = { - code: mockCardStatusEntity.code, - description: mockCardStatusEntity.description, - label: mockCardStatusEntity.label, - }; - - beforeEach(async () => { - mockCardStatusService = createMock(); - - const module: TestingModule = await Test.createTestingModule({ - controllers: [CardStatusController], - providers: [ - { - provide: CardStatusService, - useValue: mockCardStatusService, - }, - { - provide: ClsService, - useValue: {}, - }, - ...mockKeyCloakProviders, - ], - }).compile(); - - controller = module.get(CardStatusController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - it('should add', async () => { - mockCardStatusService.create.mockResolvedValue(mockCardStatusEntity); - expect(await controller.add(cardStatusDto)).toStrictEqual(cardStatusDto); - }); -}); diff --git a/services/apps/alcs/src/alcs/card/card-status/card-status.controller.ts b/services/apps/alcs/src/alcs/card/card-status/card-status.controller.ts deleted file mode 100644 index d1898c2d82..0000000000 --- a/services/apps/alcs/src/alcs/card/card-status/card-status.controller.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Body, Controller, Post, UseGuards } from '@nestjs/common'; -import { ApiOAuth2 } from '@nestjs/swagger'; -import * as config from 'config'; -import { AUTH_ROLE } from '../../../common/authorization/roles'; -import { RolesGuard } from '../../../common/authorization/roles-guard.service'; -import { UserRoles } from '../../../common/authorization/roles.decorator'; -import { CardStatusDto } from './card-status.dto'; -import { CardStatusService } from './card-status.service'; - -@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) -@Controller('card-status') -@UseGuards(RolesGuard) -export class CardStatusController { - constructor(private cardStatusService: CardStatusService) {} - - @Post() - @UserRoles(AUTH_ROLE.ADMIN) - async add(@Body() card: CardStatusDto): Promise { - const newCard = await this.cardStatusService.create(card); - return { - code: newCard.code, - description: newCard.description, - label: newCard.label, - }; - } -} diff --git a/services/apps/alcs/src/alcs/card/card-status/card-status.entity.ts b/services/apps/alcs/src/alcs/card/card-status/card-status.entity.ts index 1d5e5be4f3..8679487ea7 100644 --- a/services/apps/alcs/src/alcs/card/card-status/card-status.entity.ts +++ b/services/apps/alcs/src/alcs/card/card-status/card-status.entity.ts @@ -4,6 +4,7 @@ import { BaseCodeEntity } from '../../../common/entities/base.code.entity'; export enum CARD_STATUS { CANCELLED = 'CNCL', DECISION_RELEASED = 'RELE', + READY_FOR_REVIEW = 'READ', } @Entity() diff --git a/services/apps/alcs/src/alcs/card/card-status/card-status.service.spec.ts b/services/apps/alcs/src/alcs/card/card-status/card-status.service.spec.ts index 25e8f57258..e91f407819 100644 --- a/services/apps/alcs/src/alcs/card/card-status/card-status.service.spec.ts +++ b/services/apps/alcs/src/alcs/card/card-status/card-status.service.spec.ts @@ -1,51 +1,88 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { initCardStatusMockEntity } from '../../../../test/mocks/mockEntities'; -import { CardStatusDto } from './card-status.dto'; +import { ApplicationDecisionMakerCode } from '../../application-decision/application-decision-maker/application-decision-maker.entity'; +import { CardService } from '../card.service'; import { CardStatus } from './card-status.entity'; import { CardStatusService } from './card-status.service'; describe('CardStatusService', () => { - let cardStatusService: CardStatusService; - let cardStatusRepositoryMock: DeepMocked>; + let service: CardStatusService; + let mockRepository: DeepMocked>; + let mockCardService: DeepMocked; - const cardStatusDto: CardStatusDto = { - code: 'app_1', - description: 'app desc 1', - label: 'app_label', - }; - const cardStatusMockEntity = initCardStatusMockEntity(); + const cardStatus = new CardStatus(); beforeEach(async () => { - cardStatusRepositoryMock = createMock>(); + mockRepository = createMock(); + mockCardService = createMock(); const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], providers: [ CardStatusService, { provide: getRepositoryToken(CardStatus), - useValue: cardStatusRepositoryMock, + useValue: mockRepository, + }, + { + provide: CardService, + useValue: mockCardService, }, ], }).compile(); - cardStatusRepositoryMock = module.get(getRepositoryToken(CardStatus)); - cardStatusService = module.get(CardStatusService); - - cardStatusRepositoryMock.findOne.mockResolvedValue(cardStatusMockEntity); - cardStatusRepositoryMock.save.mockResolvedValue(cardStatusMockEntity); - cardStatusRepositoryMock.find.mockResolvedValue([cardStatusMockEntity]); + service = module.get(CardStatusService); }); it('should be defined', () => { - expect(cardStatusService).toBeDefined(); + expect(service).toBeDefined(); + }); + + it('should successfully create card status', async () => { + mockRepository.save.mockResolvedValue(new ApplicationDecisionMakerCode()); + + const result = await service.create({ + code: '', + description: '', + label: '', + }); + + expect(mockRepository.save).toBeCalledTimes(1); + expect(result).toBeDefined(); }); - it('should create card_status', async () => { - expect(await cardStatusService.create(cardStatusDto)).toStrictEqual( - cardStatusMockEntity, - ); + it('should successfully update card status if it exists', async () => { + mockRepository.save.mockResolvedValue(cardStatus); + mockRepository.findOneOrFail.mockResolvedValue(cardStatus); + + const result = await service.update(cardStatus.code, { + code: '', + description: '', + label: '', + }); + + expect(mockRepository.save).toBeCalledTimes(1); + expect(mockRepository.findOneOrFail).toBeCalledTimes(1); + expect(mockRepository.findOneOrFail).toBeCalledWith({ + where: { uuid: cardStatus.code }, + }); + expect(result).toBeDefined(); + }); + + it('should successfully fetch card status', async () => { + mockRepository.find.mockResolvedValue([cardStatus]); + + const result = await service.fetch(); + + expect(mockRepository.find).toBeCalledTimes(1); + expect(result).toBeDefined(); }); }); diff --git a/services/apps/alcs/src/alcs/card/card-status/card-status.service.ts b/services/apps/alcs/src/alcs/card/card-status/card-status.service.ts index 43dcf7468d..85659cd05c 100644 --- a/services/apps/alcs/src/alcs/card/card-status/card-status.service.ts +++ b/services/apps/alcs/src/alcs/card/card-status/card-status.service.ts @@ -1,26 +1,56 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { CardService } from '../card.service'; import { CardStatusDto } from './card-status.dto'; -import { CardStatus } from './card-status.entity'; - -export const defaultApplicationStatus = { - id: '46235264-9529-4e52-9c2d-6ed2b8b9edb8', - code: 'TODO', -}; +import { CARD_STATUS, CardStatus } from './card-status.entity'; @Injectable() export class CardStatusService { constructor( @InjectRepository(CardStatus) private cardStatusRepository: Repository, + private cardService: CardService, ) {} - async create(card: CardStatusDto): Promise { - const cardEntity = new CardStatus(); - cardEntity.code = card.code; - cardEntity.description = card.description; + async fetch() { + return await this.cardStatusRepository.find({ + order: { label: 'ASC' }, + select: { + code: true, + label: true, + description: true, + }, + }); + } + + async getOneOrFail(code: string) { + return await this.cardStatusRepository.findOneOrFail({ + where: { code }, + }); + } + + async update(code: string, updateDto: CardStatusDto) { + const cardStatus = await this.getOneOrFail(code); + + cardStatus.description = updateDto.description; + cardStatus.label = updateDto.label; + + return await this.cardStatusRepository.save(cardStatus); + } + + async create(createDto: CardStatusDto) { + const cardStatus = new CardStatus(); + + cardStatus.code = createDto.code; + cardStatus.description = createDto.description; + cardStatus.label = createDto.label; + + return await this.cardStatusRepository.save(cardStatus); + } - return await this.cardStatusRepository.save(cardEntity); + async getCardCountByStatus(code: CARD_STATUS) { + const cards = await this.cardService.getByCardStatus(code); + return cards.length; } } diff --git a/services/apps/alcs/src/alcs/card/card.module.ts b/services/apps/alcs/src/alcs/card/card.module.ts index 0ef0061253..4cb2525d26 100644 --- a/services/apps/alcs/src/alcs/card/card.module.ts +++ b/services/apps/alcs/src/alcs/card/card.module.ts @@ -5,7 +5,6 @@ import { CardProfile } from '../../common/automapper/card.automapper.profile'; import { NotificationModule } from '../notification/notification.module'; import { CardHistory } from './card-history/card-history.entity'; import { CardSubscriber } from './card-history/card.subscriber'; -import { CardStatusController } from './card-status/card-status.controller'; import { CardStatus } from './card-status/card-status.entity'; import { CardStatusService } from './card-status/card-status.service'; import { CardSubtaskType } from './card-subtask/card-subtask-type/card-subtask-type.entity'; @@ -30,7 +29,7 @@ import { CardService } from './card.service'; CodeModule, NotificationModule, ], - controllers: [CardStatusController, CardSubtaskController, CardController], + controllers: [CardSubtaskController, CardController], providers: [ CardStatusService, CardService, diff --git a/services/apps/alcs/src/alcs/card/card.service.ts b/services/apps/alcs/src/alcs/card/card.service.ts index a72d682a7d..1cc3ff4ec7 100644 --- a/services/apps/alcs/src/alcs/card/card.service.ts +++ b/services/apps/alcs/src/alcs/card/card.service.ts @@ -200,4 +200,11 @@ export class CardService { await this.cardRepository.save(card); await this.cardRepository.recover(card); } + + async getByCardStatus(code: string) { + return this.cardRepository.find({ + where: { statusCode: code }, + relations: this.DEFAULT_RELATIONS, + }); + } } From c9691ba74ea78f3ff547fac982813585d85e51a4 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:46:15 -0700 Subject: [PATCH 058/954] Bugfix/alcs 502 (#764) updated colours for condition labels new logic for incomplete fix condition to component link on reset --- .../decision/conditions/condition/condition.component.html | 3 ++- .../decision/conditions/condition/condition.component.scss | 3 +++ .../decision/conditions/condition/condition.component.ts | 6 ++---- .../decision/conditions/conditions.component.scss | 1 + .../application-type-pill.constants.ts | 2 +- .../application-decision/application-decision-v2.service.ts | 2 +- 6 files changed, 10 insertions(+), 7 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html index c1fac6d8bd..c211c1c758 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html @@ -13,7 +13,8 @@

{{ condition.type.label }}

Component to Condition
- {{ condition.componentLabels }} + {{ condition.componentLabels }} +
diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss index e69de29bb2..b1976e0872 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.scss @@ -0,0 +1,3 @@ +.component-labels { + white-space: pre-wrap; +} diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts index d090247a4e..c9e640878f 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts @@ -39,7 +39,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { if (this.condition) { this.condition = { ...this.condition, - componentLabels: this.condition.conditionComponentsLabels?.join(', '), + componentLabels: this.condition.conditionComponentsLabels?.join(';\n'), }; } } @@ -87,10 +87,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { this.conditionStatus = CONDITION_STATUS.SUPERSEDED; } else if (this.condition.completionDate && this.condition.completionDate <= today) { this.conditionStatus = CONDITION_STATUS.COMPLETE; - } else if (this.isDraftDecision === false) { - this.conditionStatus = CONDITION_STATUS.INCOMPLETE; } else { - this.conditionStatus = ''; + this.conditionStatus = CONDITION_STATUS.INCOMPLETE; } } } diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss index cf53302c55..c1e422c958 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.scss @@ -80,6 +80,7 @@ p { display: grid; grid-template-columns: 33% 33% 33%; grid-row-gap: 24px; + grid-column-gap: 16px; .full-width { grid-column: 1/4; diff --git a/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.constants.ts b/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.constants.ts index 998afea8b4..9543a8ccfd 100644 --- a/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.constants.ts +++ b/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.constants.ts @@ -78,7 +78,7 @@ export const DECISION_CONDITION_COMPLETE_LABEL = { label: 'Complete', shortLabel: 'COMD', backgroundColor: '#fff', - borderColor: '#c08106', + borderColor: '#065a2f', textColor: '#000', }; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts index af671e4126..6fad7ff617 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts @@ -412,7 +412,7 @@ export class ApplicationDecisionV2Service { existingDecision.components ?? [], false, ); - } else if (existingDecision.conditions) { + } else if (updateDto.conditions === null && existingDecision.conditions) { await this.decisionConditionService.remove(existingDecision.conditions); } } From 20ac9602afde26917ea70b8390d0913b3933aeb0 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Mon, 10 Jul 2023 15:59:27 -0700 Subject: [PATCH 059/954] ALCS View of under/pending LFNG Review and returned applications (#762) * Retrieve app status and render lfng menu * Show lfng banner or comment by status * Rename variables to improve code readability * Update and pass frontend tests * Rename again to clarify intent --- .../application-details.component.spec.ts | 3 +- .../application/application.component.html | 17 +++++- .../application/application.component.spec.ts | 7 +++ .../application/application.component.ts | 61 +++++++++++++++++-- .../lfng-info/lfng-info.component.html | 10 ++- .../lfng-info/lfng-info.component.scss | 22 +++++++ .../lfng-info/lfng-info.component.spec.ts | 7 +++ .../lfng-info/lfng-info.component.ts | 14 ++++- .../application-submission.service.spec.ts | 3 +- .../services/application/application.dto.ts | 19 +++++- 10 files changed, 152 insertions(+), 11 deletions(-) diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts index c1821e6903..1b59d4f078 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts @@ -2,7 +2,7 @@ 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/application-document/application-document.service'; -import { SubmittedApplicationOwnerDto } from '../../../../services/application/application.dto'; +import { ApplicationStatus, SubmittedApplicationOwnerDto } from '../../../../services/application/application.dto'; import { ApplicationDetailsComponent } from './application-details.component'; @@ -45,6 +45,7 @@ describe('ApplicationDetailsComponent', () => { soilToPlaceVolume: null, soilToRemoveAverageDepth: null, soilToRemoveMaximumDepth: null, + status: {} as ApplicationStatus, type: '', updatedAt: '', uuid: '', diff --git a/alcs-frontend/src/app/features/application/application.component.html b/alcs-frontend/src/app/features/application/application.component.html index 73db1279f1..3be9c441d8 100644 --- a/alcs-frontend/src/app/features/application/application.component.html +++ b/alcs-frontend/src/app/features/application/application.component.html @@ -8,7 +8,7 @@ heading="Application" >
-

-
+
This section will update after the application is submitted.
-
+
Application not subject to Local/First Nation Government review.
@@ -50,8 +51,8 @@

Local/First Nation Gov Review

@@ -59,7 +60,11 @@

Local/First Nation Gov Review

{{ application.returnedComment }} No comment added
-
+

Contact Information

diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts index 7f0a03317b..2e50a0957b 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts +++ b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts @@ -11,7 +11,7 @@ import { ApplicationSubmissionReviewDto } from '../../../services/application-su import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionDetailedDto, - APPLICATION_STATUS, + SUBMISSION_STATUS, } from '../../../services/application-submission/application-submission.dto'; import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; @@ -30,7 +30,7 @@ export class LfngReviewComponent implements OnInit, OnDestroy { application: ApplicationSubmissionDetailedDto | undefined; applicationReview: ApplicationSubmissionReviewDto | undefined; - APPLICATION_STATUS = APPLICATION_STATUS; + SUBMISSION_STATUS = SUBMISSION_STATUS; staffReport: ApplicationDocumentDto[] = []; resolutionDocument: ApplicationDocumentDto[] = []; governmentOtherAttachments: ApplicationDocumentDto[] = []; @@ -84,10 +84,10 @@ export class LfngReviewComponent implements OnInit, OnDestroy { if ( this.application && this.application.typeCode !== 'TURP' && - ([APPLICATION_STATUS.SUBMITTED_TO_ALC, APPLICATION_STATUS.REFUSED_TO_FORWARD].includes( + ([SUBMISSION_STATUS.SUBMITTED_TO_ALC, SUBMISSION_STATUS.REFUSED_TO_FORWARD_LG].includes( this.application.status.code ) || - (this.application.status.code === APPLICATION_STATUS.IN_REVIEW && this.application.canReview)) + (this.application.status.code === SUBMISSION_STATUS.IN_REVIEW_BY_LG && this.application.canReview)) ) { await this.applicationReviewService.getByFileId(this.application.fileNumber); } else { @@ -103,7 +103,7 @@ export class LfngReviewComponent implements OnInit, OnDestroy { } async onReview(fileId: string) { - if (this.application?.status.code === APPLICATION_STATUS.SUBMITTED_TO_LG) { + if (this.application?.status.code === SUBMISSION_STATUS.SUBMITTED_TO_LG) { const review = await this.applicationReviewService.startReview(fileId); if (!review) { return; diff --git a/portal-frontend/src/app/features/view-submission/view-submission.component.html b/portal-frontend/src/app/features/view-submission/view-submission.component.html index 367e444f31..3990a744a8 100644 --- a/portal-frontend/src/app/features/view-submission/view-submission.component.html +++ b/portal-frontend/src/app/features/view-submission/view-submission.component.html @@ -243,13 +243,13 @@

Application

diff --git a/alcs-frontend/src/app/features/application/review/decision-meeting/decision-meeting.component.scss b/alcs-frontend/src/app/features/application/review/decision-meeting/decision-meeting.component.scss index ff7aaca559..c4b9b5fafb 100644 --- a/alcs-frontend/src/app/features/application/review/decision-meeting/decision-meeting.component.scss +++ b/alcs-frontend/src/app/features/application/review/decision-meeting/decision-meeting.component.scss @@ -38,3 +38,15 @@ width: 21%; text-align: right; } + +.icon { + margin-top: 4px; + height: 20px; + width: 20px; + font-size: 20px; +} + +.label-wrapper { + display: flex; + gap: 8px; +} diff --git a/alcs-frontend/src/app/features/application/review/review.component.html b/alcs-frontend/src/app/features/application/review/review.component.html index e3c4f11bc7..daec1fc246 100644 --- a/alcs-frontend/src/app/features/application/review/review.component.html +++ b/alcs-frontend/src/app/features/application/review/review.component.html @@ -1,10 +1,7 @@

Review

-
'Ready for Review' Notification Sent to Applicant
- +
Applicant Notified to Review Evidentiary Record
+
From 009de2ec11f54015b9e366ba84f3f47087ab3c4e Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:02:13 -0700 Subject: [PATCH 081/954] added statuses controller (#788) --- .../application-submission-status.dto.ts | 21 ++++++ ...lication-submission-status.service.spec.ts | 68 +++++++++++++++++++ .../application-submission-status.service.ts | 27 ++++++++ .../details-header.component.html | 4 ++ services/apps/alcs/src/alcs/alcs.module.ts | 2 + ...ation-submission-status.controller.spec.ts | 67 ++++++++++++++++++ ...pplication-submission-status.controller.ts | 31 +++++++++ .../application-submission-status.module.ts | 2 + .../submission-status.dto.ts | 18 +++++ ...plication-submission.automapper.profile.ts | 27 +++++++- 10 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.dto.ts create mode 100644 alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.spec.ts create mode 100644 alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.ts create mode 100644 services/apps/alcs/src/application-submission-status/application-submission-status.controller.spec.ts create mode 100644 services/apps/alcs/src/application-submission-status/application-submission-status.controller.ts diff --git a/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.dto.ts b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.dto.ts new file mode 100644 index 0000000000..41d2d17381 --- /dev/null +++ b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.dto.ts @@ -0,0 +1,21 @@ +import { BaseCodeDto } from '../../../shared/dto/base.dto'; + +export interface ApplicationStatusDto extends BaseCodeDto { + alcsBackgroundColor: string; + + alcsColor: string; + + portalBackgroundColor: string; + + portalColor: string; +} + +export interface ApplicationSubmissionToSubmissionStatusDto { + submissionUuid: string; + + effectiveDate: number; + + statusTypeCode: string; + + status: ApplicationStatusDto; +} diff --git a/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.spec.ts b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.spec.ts new file mode 100644 index 0000000000..56843beb9d --- /dev/null +++ b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.spec.ts @@ -0,0 +1,68 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../../toast/toast.service'; +import { ApplicationSubmissionStatusService } from './application-submission-status.service'; + +describe('ApplicationSubmissionStatusService', () => { + let service: ApplicationSubmissionStatusService; + let mockHttpClient: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + ], + }); + service = TestBed.inject(ApplicationSubmissionStatusService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should fetch statuses by fileNumber', async () => { + mockHttpClient.get.mockReturnValue( + of([ + { + submissionUuid: 'fake', + }, + ]) + ); + + const res = await service.fetchSubmissionStatusesByFileNumber('1'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(res.length).toEqual(1); + expect(res[0].submissionUuid).toEqual('fake'); + }); + + it('should show a toast message if fetch statuses by fileNumber fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + try { + await service.fetchSubmissionStatusesByFileNumber('1'); + } catch { + // suppress error message + } + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.ts b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.ts new file mode 100644 index 0000000000..ff9bcb460a --- /dev/null +++ b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.ts @@ -0,0 +1,27 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { ToastService } from '../../toast/toast.service'; +import { ApplicationSubmissionToSubmissionStatusDto } from './application-submission-status.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationSubmissionStatusService { + private baseUrl = `${environment.apiUrl}/application-submission-status`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetchSubmissionStatusesByFileNumber(fileNumber: string): Promise { + try { + const result = await firstValueFrom( + this.http.get(`${this.baseUrl}/${fileNumber}`) + ); + return result + } catch (e) { + this.toastService.showErrorToast('Failed to fetch Application Submission Statuses'); + throw e; + } + } +} diff --git a/alcs-frontend/src/app/shared/details-header/details-header.component.html b/alcs-frontend/src/app/shared/details-header/details-header.component.html index 4b92906c85..c6042d1263 100644 --- a/alcs-frontend/src/app/shared/details-header/details-header.component.html +++ b/alcs-frontend/src/app/shared/details-header/details-header.component.html @@ -49,5 +49,9 @@
{{ _application.fileNumber }} ({{ _application.applicant }})
{{ days }}:
+
+ + +
diff --git a/services/apps/alcs/src/alcs/alcs.module.ts b/services/apps/alcs/src/alcs/alcs.module.ts index e39d3c03ed..645cbe05ec 100644 --- a/services/apps/alcs/src/alcs/alcs.module.ts +++ b/services/apps/alcs/src/alcs/alcs.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { RouterModule } from '@nestjs/core'; +import { ApplicationSubmissionStatusModule } from '../application-submission-status/application-submission-status.module'; import { AdminModule } from './admin/admin.module'; import { ApplicationDecisionModule } from './application-decision/application-decision.module'; import { ApplicationModule } from './application/application.module'; @@ -54,6 +55,7 @@ import { StaffJournalModule } from './staff-journal/staff-journal.module'; { path: 'alcs', module: NoticeOfIntentDecisionModule }, { path: 'alcs', module: StaffJournalModule }, { path: 'alcs', module: SearchModule }, + { path: 'alcs', module: ApplicationSubmissionStatusModule }, ]), ], controllers: [], diff --git a/services/apps/alcs/src/application-submission-status/application-submission-status.controller.spec.ts b/services/apps/alcs/src/application-submission-status/application-submission-status.controller.spec.ts new file mode 100644 index 0000000000..c883e5ac65 --- /dev/null +++ b/services/apps/alcs/src/application-submission-status/application-submission-status.controller.spec.ts @@ -0,0 +1,67 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../test/mocks/mockTypes'; +import { ApplicationSubmissionProfile } from '../common/automapper/application-submission.automapper.profile'; +import { ApplicationSubmissionStatusController } from './application-submission-status.controller'; +import { ApplicationSubmissionStatusService } from './application-submission-status.service'; +import { ApplicationSubmissionToSubmissionStatus } from './submission-status.entity'; + +describe('ApplicationSubmissionStatusController', () => { + let controller: ApplicationSubmissionStatusController; + let mockApplicationSubmissionStatusService: DeepMocked; + + beforeEach(async () => { + mockApplicationSubmissionStatusService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [ApplicationSubmissionStatusController], + providers: [ + ApplicationSubmissionProfile, + { + provide: ApplicationSubmissionStatusService, + useValue: mockApplicationSubmissionStatusService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + }).compile(); + + controller = module.get( + ApplicationSubmissionStatusController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call service to get statuses by file number', async () => { + const fakeFileNumber = 'fake'; + + mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber.mockResolvedValue( + [new ApplicationSubmissionToSubmissionStatus()], + ); + + const result = await controller.getStatusesByFileNumber(fakeFileNumber); + + expect( + mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber, + ).toBeCalledTimes(1); + expect( + mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber, + ).toBeCalledWith(fakeFileNumber); + expect(result.length).toEqual(1); + expect(result).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/application-submission-status/application-submission-status.controller.ts b/services/apps/alcs/src/application-submission-status/application-submission-status.controller.ts new file mode 100644 index 0000000000..edb6112fdb --- /dev/null +++ b/services/apps/alcs/src/application-submission-status/application-submission-status.controller.ts @@ -0,0 +1,31 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Controller, Get, Param } from '@nestjs/common'; +import { ANY_AUTH_ROLE } from '../common/authorization/roles'; +import { UserRoles } from '../common/authorization/roles.decorator'; +import { ApplicationSubmissionStatusService } from './application-submission-status.service'; +import { ApplicationSubmissionToSubmissionStatusDto } from './submission-status.dto'; +import { ApplicationSubmissionToSubmissionStatus } from './submission-status.entity'; + +@Controller('application-submission-status') +@UserRoles(...ANY_AUTH_ROLE) +export class ApplicationSubmissionStatusController { + constructor( + private applicationSubmissionStatusService: ApplicationSubmissionStatusService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('/:fileNumber') + async getStatusesByFileNumber(@Param('fileNumber') fileNumber) { + const statuses = + await this.applicationSubmissionStatusService.getCurrentStatusesByFileNumber( + fileNumber, + ); + + return this.mapper.mapArrayAsync( + statuses, + ApplicationSubmissionToSubmissionStatus, + ApplicationSubmissionToSubmissionStatusDto, + ); + } +} diff --git a/services/apps/alcs/src/application-submission-status/application-submission-status.module.ts b/services/apps/alcs/src/application-submission-status/application-submission-status.module.ts index 201148ae4f..bc55e7395d 100644 --- a/services/apps/alcs/src/application-submission-status/application-submission-status.module.ts +++ b/services/apps/alcs/src/application-submission-status/application-submission-status.module.ts @@ -4,6 +4,7 @@ import { ApplicationSubmission } from '../portal/application-submission/applicat import { ApplicationSubmissionStatusService } from './application-submission-status.service'; import { ApplicationSubmissionStatusType } from './submission-status-type.entity'; import { ApplicationSubmissionToSubmissionStatus } from './submission-status.entity'; +import { ApplicationSubmissionStatusController } from './application-submission-status.controller'; @Module({ imports: [ @@ -15,5 +16,6 @@ import { ApplicationSubmissionToSubmissionStatus } from './submission-status.ent ], providers: [ApplicationSubmissionStatusService], exports: [ApplicationSubmissionStatusService], + controllers: [ApplicationSubmissionStatusController], }) export class ApplicationSubmissionStatusModule {} diff --git a/services/apps/alcs/src/application-submission-status/submission-status.dto.ts b/services/apps/alcs/src/application-submission-status/submission-status.dto.ts index b05a18baa1..17ae9b3b7d 100644 --- a/services/apps/alcs/src/application-submission-status/submission-status.dto.ts +++ b/services/apps/alcs/src/application-submission-status/submission-status.dto.ts @@ -16,6 +16,7 @@ export enum SUBMISSION_STATUS { CANCELLED = 'CANC', } +// TODO rename to better reflect the origin? export class ApplicationStatusDto extends BaseCodeDto { @AutoMap() alcsBackgroundColor: string; @@ -28,4 +29,21 @@ export class ApplicationStatusDto extends BaseCodeDto { @AutoMap() portalColor: string; + + @AutoMap() + weight: number; +} + +export class ApplicationSubmissionToSubmissionStatusDto { + @AutoMap() + submissionUuid: string; + + @AutoMap() + effectiveDate: number; + + @AutoMap() + statusTypeCode: string; + + @AutoMap(() => ApplicationStatusDto) + status: ApplicationStatusDto; } diff --git a/services/apps/alcs/src/common/automapper/application-submission.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-submission.automapper.profile.ts index eff296ef30..412c0bde4d 100644 --- a/services/apps/alcs/src/common/automapper/application-submission.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-submission.automapper.profile.ts @@ -3,7 +3,11 @@ import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { AlcsApplicationSubmissionDto } from '../../alcs/application/application.dto'; import { ApplicationSubmissionStatusType } from '../../application-submission-status/submission-status-type.entity'; -import { ApplicationStatusDto } from '../../application-submission-status/submission-status.dto'; +import { + ApplicationStatusDto, + ApplicationSubmissionToSubmissionStatusDto, +} from '../../application-submission-status/submission-status.dto'; +import { ApplicationSubmissionToSubmissionStatus } from '../../application-submission-status/submission-status.entity'; import { ApplicationOwnerDetailedDto, ApplicationOwnerDto, @@ -147,6 +151,27 @@ export class ApplicationSubmissionProfile extends AutomapperProfile { }), ), ); + createMap( + mapper, + ApplicationSubmissionToSubmissionStatus, + ApplicationSubmissionToSubmissionStatusDto, + forMember( + (a) => a.effectiveDate, + mapFrom((ad) => { + return ad.effectiveDate?.getTime(); + }), + ), + forMember( + (a) => a.status, + mapFrom((ad) => { + return this.mapper.map( + ad.statusType, + ApplicationSubmissionStatusType, + ApplicationStatusDto, + ); + }), + ), + ); }; } } From fc898217f9bd893711bd188eaeb86201222b29e9 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 17 Jul 2023 16:34:40 -0700 Subject: [PATCH 082/954] Add Cancel / Uncancel Application --- .../release-dialog.component.spec.ts | 4 +- .../release-dialog.component.ts | 5 +-- .../revert-to-draft-dialog.component.spec.ts | 4 +- .../revert-to-draft-dialog.component.ts | 4 +- .../overview/overview.component.html | 14 +++++- .../overview/overview.component.spec.ts | 10 +++++ .../overview/overview.component.ts | 45 ++++++++++++++++++- .../application/application-code.dto.ts | 7 ++- .../application/application-detail.service.ts | 8 ++++ .../application-reconsideration.dto.ts | 8 +++- .../services/application/application.dto.ts | 1 + .../application/application.service.ts | 26 +++++++++-- .../application-type-pill.component.ts | 2 +- .../confirmation-dialog.component.html | 4 +- .../confirmation-dialog.component.ts | 2 + .../application.controller.spec.ts | 14 ++++++ .../application/application.controller.ts | 12 +++++ .../application/application.service.spec.ts | 29 ++++++++++++ .../alcs/application/application.service.ts | 17 +++++++ 19 files changed, 194 insertions(+), 22 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.spec.ts index 5592ff43ef..8d84a61e44 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationStatusTypeDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; +import { ApplicationStatusDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; import { ApplicationService } from '../../../../../services/application/application.service'; import { ReleaseDialogComponent } from './release-dialog.component'; @@ -15,7 +15,7 @@ describe('ReleaseDialogComponent', () => { beforeEach(async () => { mockApplicationService = createMock(); - mockApplicationService.$applicationStatuses = new BehaviorSubject([]); + mockApplicationService.$applicationStatuses = new BehaviorSubject([]); await TestBed.configureTestingModule({ declarations: [ReleaseDialogComponent], diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.ts index 8030370c17..7f4a3dddbf 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.ts @@ -1,9 +1,8 @@ import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Subject, takeUntil } from 'rxjs'; -import { ApplicationStatusTypeDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; +import { ApplicationStatusDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; import { ApplicationService } from '../../../../../services/application/application.service'; -import { CeoCriterionDto } from '../../../../../services/application/decision/application-decision-v1/application-decision.dto'; @Component({ selector: 'app-release-dialog', @@ -13,7 +12,7 @@ import { CeoCriterionDto } from '../../../../../services/application/decision/ap export class ReleaseDialogComponent implements OnInit, OnDestroy { $destroy = new Subject(); - statuses: ApplicationStatusTypeDto[] = []; + statuses: ApplicationStatusDto[] = []; selectedApplicationStatus = ''; wasReleased = false; diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.spec.ts index 217f6750bc..e35aa1b6ad 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationStatusTypeDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; +import { ApplicationStatusDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; import { ApplicationService } from '../../../../../services/application/application.service'; import { RevertToDraftDialogComponent } from './revert-to-draft-dialog.component'; @@ -15,7 +15,7 @@ describe('RevertToDraftDialogComponent', () => { beforeEach(async () => { mockApplicationService = createMock(); - mockApplicationService.$applicationStatuses = new BehaviorSubject([]); + mockApplicationService.$applicationStatuses = new BehaviorSubject([]); await TestBed.configureTestingModule({ declarations: [RevertToDraftDialogComponent], diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts index 248d3abe08..f19da034e5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialogRef } from '@angular/material/dialog'; import { Subject, takeUntil } from 'rxjs'; -import { ApplicationStatusTypeDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; +import { ApplicationStatusDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; import { ApplicationService } from '../../../../../services/application/application.service'; @Component({ @@ -12,7 +12,7 @@ import { ApplicationService } from '../../../../../services/application/applicat export class RevertToDraftDialogComponent implements OnInit, OnDestroy { $destroy = new Subject(); - statuses: ApplicationStatusTypeDto[] = []; + statuses: ApplicationStatusDto[] = []; selectedApplicationStatus = ''; constructor( diff --git a/alcs-frontend/src/app/features/application/overview/overview.component.html b/alcs-frontend/src/app/features/application/overview/overview.component.html index b0245dfe5a..939a9f5ed1 100644 --- a/alcs-frontend/src/app/features/application/overview/overview.component.html +++ b/alcs-frontend/src/app/features/application/overview/overview.component.html @@ -1,4 +1,14 @@ -

Overview

+
+

Overview

+
+ + +
+
Proposal Summary
Proposal Summary >
- +
Application Event Timeline
diff --git a/alcs-frontend/src/app/features/application/overview/overview.component.spec.ts b/alcs-frontend/src/app/features/application/overview/overview.component.spec.ts index 5dc73f26e6..260dcac99c 100644 --- a/alcs-frontend/src/app/features/application/overview/overview.component.spec.ts +++ b/alcs-frontend/src/app/features/application/overview/overview.component.spec.ts @@ -1,5 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; @@ -7,6 +8,7 @@ import { ApplicationMeetingService } from '../../../services/application/applica import { ApplicationModificationService } from '../../../services/application/application-modification/application-modification.service'; import { ApplicationReconsiderationService } from '../../../services/application/application-reconsideration/application-reconsideration.service'; import { ApplicationReviewService } from '../../../services/application/application-review/application-review.service'; +import { ApplicationSubmissionStatusService } from '../../../services/application/application-submission-status/application-submission-status.service'; import { ApplicationDto } from '../../../services/application/application.dto'; import { ApplicationDecisionService } from '../../../services/application/decision/application-decision-v1/application-decision.service'; @@ -49,6 +51,14 @@ describe('OverviewComponent', () => { fetch: jest.fn(), }, }, + { + provide: MatDialog, + useValue: {}, + }, + { + provide: ApplicationSubmissionStatusService, + useValue: {}, + }, ], declarations: [OverviewComponent], schemas: [NO_ERRORS_SCHEMA], diff --git a/alcs-frontend/src/app/features/application/overview/overview.component.ts b/alcs-frontend/src/app/features/application/overview/overview.component.ts index c53ea841f0..e1b9537014 100644 --- a/alcs-frontend/src/app/features/application/overview/overview.component.ts +++ b/alcs-frontend/src/app/features/application/overview/overview.component.ts @@ -8,9 +8,11 @@ import { ApplicationModificationService } from '../../../services/application/ap import { ApplicationReconsiderationDto } from '../../../services/application/application-reconsideration/application-reconsideration.dto'; import { ApplicationReconsiderationService } from '../../../services/application/application-reconsideration/application-reconsideration.service'; import { ApplicationReviewService } from '../../../services/application/application-review/application-review.service'; -import { ApplicationDto, ApplicationReviewDto } from '../../../services/application/application.dto'; +import { ApplicationSubmissionStatusService } from '../../../services/application/application-submission-status/application-submission-status.service'; +import { ApplicationDto, ApplicationReviewDto, SUBMISSION_STATUS } from '../../../services/application/application.dto'; import { ApplicationDecisionDto } from '../../../services/application/decision/application-decision-v1/application-decision.dto'; import { ApplicationDecisionService } from '../../../services/application/decision/application-decision-v1/application-decision.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { TimelineEvent } from '../../../shared/timeline/timeline.component'; const editLink = new Map([ @@ -48,6 +50,7 @@ export class OverviewComponent implements OnInit, OnDestroy { private $review = new BehaviorSubject(undefined); events: TimelineEvent[] = []; summary = ''; + isCancelled = false; constructor( private applicationDetailService: ApplicationDetailService, @@ -55,7 +58,9 @@ export class OverviewComponent implements OnInit, OnDestroy { private decisionService: ApplicationDecisionService, private reconsiderationService: ApplicationReconsiderationService, private modificationService: ApplicationModificationService, - private reviewService: ApplicationReviewService + private reviewService: ApplicationReviewService, + private confirmationDialogService: ConfirmationDialogService, + private applicationSubmissionStatusService: ApplicationSubmissionStatusService ) {} ngOnInit(): void { @@ -66,6 +71,7 @@ export class OverviewComponent implements OnInit, OnDestroy { if (app) { this.clearComponentData(); + this.loadStatusHistory(app.fileNumber); this.meetingService.fetch(app.fileNumber); this.decisionService.fetchByApplication(app.fileNumber).then((res) => { this.$decisions.next(res); @@ -114,6 +120,34 @@ export class OverviewComponent implements OnInit, OnDestroy { this.$review.next(undefined); } + async onCancelApplication() { + this.confirmationDialogService + .openDialog({ + body: `Are you sure you want to cancel this Application?`, + cancelButtonText: 'No', + }) + .subscribe(async (didConfirm) => { + if (didConfirm && this.application) { + await this.applicationDetailService.cancelApplication(this.application.fileNumber); + await this.loadStatusHistory(this.application.fileNumber); + } + }); + } + + async onUncancelApplication() { + this.confirmationDialogService + .openDialog({ + body: `Are you sure you want to uncancel this Application?`, + cancelButtonText: 'No', + }) + .subscribe(async (didConfirm) => { + if (didConfirm && this.application) { + await this.applicationDetailService.uncancelApplication(this.application.fileNumber); + await this.loadStatusHistory(this.application.fileNumber); + } + }); + } + private mapApplicationToEvents( application: ApplicationDto, meetings: ApplicationMeetingDto[], @@ -310,4 +344,11 @@ export class OverviewComponent implements OnInit, OnDestroy { }); } } + + private async loadStatusHistory(fileNumber: string) { + const statusHistory = await this.applicationSubmissionStatusService.fetchSubmissionStatusesByFileNumber(fileNumber); + this.isCancelled = + statusHistory.filter((status) => status.effectiveDate && status.statusTypeCode === SUBMISSION_STATUS.CANCELLED) + .length > 0; + } } diff --git a/alcs-frontend/src/app/services/application/application-code.dto.ts b/alcs-frontend/src/app/services/application/application-code.dto.ts index cd1b3c5b80..670140a87f 100644 --- a/alcs-frontend/src/app/services/application/application-code.dto.ts +++ b/alcs-frontend/src/app/services/application/application-code.dto.ts @@ -1,5 +1,8 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; -import { ApplicationStatusTypeDto, ReconsiderationTypeDto } from './application-reconsideration/application-reconsideration.dto'; +import { + ApplicationStatusDto, + ReconsiderationTypeDto, +} from './application-reconsideration/application-reconsideration.dto'; export interface CardStatusDto extends BaseCodeDto {} export interface ApplicationRegionDto extends BaseCodeDto {} @@ -15,5 +18,5 @@ export interface ApplicationMasterCodesDto { status: CardStatusDto[]; region: ApplicationRegionDto[]; reconsiderationType: ReconsiderationTypeDto[]; - applicationStatusType: ApplicationStatusTypeDto[]; + applicationStatusType: ApplicationStatusDto[]; } diff --git a/alcs-frontend/src/app/services/application/application-detail.service.ts b/alcs-frontend/src/app/services/application/application-detail.service.ts index 28c887eaaf..98a3619396 100644 --- a/alcs-frontend/src/app/services/application/application-detail.service.ts +++ b/alcs-frontend/src/app/services/application/application-detail.service.ts @@ -31,4 +31,12 @@ export class ApplicationDetailService { } return updatedApp; } + + cancelApplication(fileNumber: string) { + return this.applicationService.cancelApplication(fileNumber); + } + + uncancelApplication(fileNumber: string) { + return this.applicationService.uncancelApplication(fileNumber); + } } diff --git a/alcs-frontend/src/app/services/application/application-reconsideration/application-reconsideration.dto.ts b/alcs-frontend/src/app/services/application/application-reconsideration/application-reconsideration.dto.ts index 8be327efb9..f88364d34e 100644 --- a/alcs-frontend/src/app/services/application/application-reconsideration/application-reconsideration.dto.ts +++ b/alcs-frontend/src/app/services/application/application-reconsideration/application-reconsideration.dto.ts @@ -16,7 +16,13 @@ export interface ReconsiderationTypeDto extends BaseCodeDto { export interface ReconsiderationReviewOutcomeTypeDto extends BaseCodeDto {} -export interface ApplicationStatusTypeDto extends BaseCodeDto {} +export interface ApplicationStatusDto extends BaseCodeDto { + alcsBackgroundColor: string; + alcsColor: string; + portalBackgroundColor: string; + portalColor: string; + weight: number; +} export interface ApplicationForReconsiderationDto { fileNumber: string; diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 21fff0b0a2..2054b4edcc 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -26,6 +26,7 @@ export enum SUBMISSION_STATUS { IN_REVIEW_BY_ALC = 'REVA', //new Under Review by ALC ALC_DECISION = 'ALCD', // Decision Released REFUSED_TO_FORWARD_LG = 'RFFG', //new L/FNG Refused to Forward + CANCELLED = 'CANC', //Cancelled } export interface ApplicationStatus extends BaseCodeDto { diff --git a/alcs-frontend/src/app/services/application/application.service.ts b/alcs-frontend/src/app/services/application/application.service.ts index b3ad66e892..67e5d5f54d 100644 --- a/alcs-frontend/src/app/services/application/application.service.ts +++ b/alcs-frontend/src/app/services/application/application.service.ts @@ -9,7 +9,7 @@ import { ApplicationTypeDto, CardStatusDto, } from './application-code.dto'; -import { ApplicationStatusTypeDto } from './application-reconsideration/application-reconsideration.dto'; +import { ApplicationStatusDto } from './application-reconsideration/application-reconsideration.dto'; import { ApplicationDto, CreateApplicationDto, UpdateApplicationDto } from './application.dto'; @Injectable({ @@ -21,13 +21,13 @@ export class ApplicationService { public $cardStatuses = new BehaviorSubject([]); public $applicationTypes = new BehaviorSubject([]); public $applicationRegions = new BehaviorSubject([]); - public $applicationStatuses = new BehaviorSubject([]); + public $applicationStatuses = new BehaviorSubject([]); private baseUrl = `${environment.apiUrl}/application`; private statuses: CardStatusDto[] = []; private types: ApplicationTypeDto[] = []; private regions: ApplicationRegionDto[] = []; - private applicationStatuses: ApplicationStatusTypeDto[] = []; + private applicationStatuses: ApplicationStatusDto[] = []; private isInitialized = false; async fetchApplication(fileNumber: string): Promise { @@ -100,4 +100,24 @@ export class ApplicationService { this.applicationStatuses = codes.applicationStatusType; this.$applicationStatuses.next(this.applicationStatuses); } + + async cancelApplication(fileNumber: string) { + await this.setup(); + try { + return await firstValueFrom(this.http.post(`${this.baseUrl}/${fileNumber}/cancel`, {})); + } catch (e) { + this.toastService.showErrorToast('Failed to cancel Application'); + } + return; + } + + async uncancelApplication(fileNumber: string) { + await this.setup(); + try { + return await firstValueFrom(this.http.post(`${this.baseUrl}/${fileNumber}/uncancel`, {})); + } catch (e) { + this.toastService.showErrorToast('Failed to uncancel Application'); + } + return; + } } diff --git a/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.component.ts b/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.component.ts index 76f52e6d01..ca9df0977f 100644 --- a/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.component.ts +++ b/alcs-frontend/src/app/shared/application-type-pill/application-type-pill.component.ts @@ -9,7 +9,7 @@ export type ApplicationPill = { }; @Component({ - selector: 'app-application-type-pill', + selector: 'app-application-type-pill[type]', templateUrl: './application-type-pill.component.html', styleUrls: ['./application-type-pill.component.scss'], }) diff --git a/alcs-frontend/src/app/shared/confirmation-dialog/confirmation-dialog.component.html b/alcs-frontend/src/app/shared/confirmation-dialog/confirmation-dialog.component.html index a7c1a758a2..1385a75812 100644 --- a/alcs-frontend/src/app/shared/confirmation-dialog/confirmation-dialog.component.html +++ b/alcs-frontend/src/app/shared/confirmation-dialog/confirmation-dialog.component.html @@ -2,6 +2,6 @@

{{ data.body }}

- - + +
diff --git a/alcs-frontend/src/app/shared/confirmation-dialog/confirmation-dialog.component.ts b/alcs-frontend/src/app/shared/confirmation-dialog/confirmation-dialog.component.ts index cca86acfcf..4795215ad0 100644 --- a/alcs-frontend/src/app/shared/confirmation-dialog/confirmation-dialog.component.ts +++ b/alcs-frontend/src/app/shared/confirmation-dialog/confirmation-dialog.component.ts @@ -3,6 +3,8 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; export interface DialogData { body: string; + yesButtonText?: string; + cancelButtonText?: string; } @Component({ diff --git a/services/apps/alcs/src/alcs/application/application.controller.spec.ts b/services/apps/alcs/src/alcs/application/application.controller.spec.ts index a58326ec84..b519566dbe 100644 --- a/services/apps/alcs/src/alcs/application/application.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application/application.controller.spec.ts @@ -367,4 +367,18 @@ describe('ApplicationController', () => { expect(applicationService.getOrFail).toBeCalledTimes(1); expect(applicationService.mapToDtos).toBeCalledTimes(1); }); + + it('should call through for cancel', async () => { + applicationService.cancel.mockResolvedValue(); + await controller.cancel(mockApplicationEntity.uuid); + + expect(applicationService.cancel).toBeCalledTimes(1); + }); + + it('should call through for uncancel', async () => { + applicationService.uncancel.mockResolvedValue(); + await controller.uncancel(mockApplicationEntity.uuid); + + expect(applicationService.uncancel).toBeCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/alcs/application/application.controller.ts b/services/apps/alcs/src/alcs/application/application.controller.ts index 1f44f4fc41..0dc5422c0e 100644 --- a/services/apps/alcs/src/alcs/application/application.controller.ts +++ b/services/apps/alcs/src/alcs/application/application.controller.ts @@ -118,6 +118,18 @@ export class ApplicationController { await this.applicationService.delete(applicationNumber); } + @Post('/:fileNumber/cancel') + @UserRoles(...ROLES_ALLOWED_APPLICATIONS) + async cancel(@Param('fileNumber') fileNumber): Promise { + await this.applicationService.cancel(fileNumber); + } + + @Post('/:fileNumber/uncancel') + @UserRoles(...ROLES_ALLOWED_APPLICATIONS) + async uncancel(@Param('fileNumber') fileNumber): Promise { + await this.applicationService.uncancel(fileNumber); + } + @Get('/card/:uuid') @UserRoles(...ROLES_ALLOWED_APPLICATIONS) async getByCardUuid(@Param('uuid') cardUuid): Promise { diff --git a/services/apps/alcs/src/alcs/application/application.service.spec.ts b/services/apps/alcs/src/alcs/application/application.service.spec.ts index a35a18f8ee..68fbb111ac 100644 --- a/services/apps/alcs/src/alcs/application/application.service.spec.ts +++ b/services/apps/alcs/src/alcs/application/application.service.spec.ts @@ -7,6 +7,9 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { FindOptionsRelations, Repository } from 'typeorm'; import { initApplicationMockEntity } from '../../../test/mocks/mockEntities'; import { ApplicationSubmissionStatusService } from '../../application-submission-status/application-submission-status.service'; +import { ApplicationSubmissionStatusType } from '../../application-submission-status/submission-status-type.entity'; +import { SUBMISSION_STATUS } from '../../application-submission-status/submission-status.dto'; +import { ApplicationSubmissionToSubmissionStatus } from '../../application-submission-status/submission-status.entity'; import { FileNumberService } from '../../file-number/file-number.service'; import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; @@ -311,4 +314,30 @@ describe('ApplicationService', () => { relations: BOARD_RELATIONS, }); }); + + it('should set the cancelled status for cancel', async () => { + mockApplicationSubmissionStatusService.setStatusDateByFileNumber.mockResolvedValue( + new ApplicationSubmissionToSubmissionStatus(), + ); + await applicationService.cancel(''); + expect( + mockApplicationSubmissionStatusService.setStatusDateByFileNumber, + ).toHaveBeenCalledTimes(1); + expect( + mockApplicationSubmissionStatusService.setStatusDateByFileNumber, + ).toHaveBeenCalledWith('', SUBMISSION_STATUS.CANCELLED); + }); + + it('should clear the cancelled status for uncancel', async () => { + mockApplicationSubmissionStatusService.setStatusDateByFileNumber.mockResolvedValue( + new ApplicationSubmissionToSubmissionStatus(), + ); + await applicationService.uncancel(''); + expect( + mockApplicationSubmissionStatusService.setStatusDateByFileNumber, + ).toHaveBeenCalledTimes(1); + expect( + mockApplicationSubmissionStatusService.setStatusDateByFileNumber, + ).toHaveBeenCalledWith('', SUBMISSION_STATUS.CANCELLED, null); + }); }); diff --git a/services/apps/alcs/src/alcs/application/application.service.ts b/services/apps/alcs/src/alcs/application/application.service.ts index eca2c3b770..7ef4744334 100644 --- a/services/apps/alcs/src/alcs/application/application.service.ts +++ b/services/apps/alcs/src/alcs/application/application.service.ts @@ -241,6 +241,23 @@ export class ApplicationService { return; } + async cancel(fileNumber: string): Promise { + await this.applicationSubmissionStatusService.setStatusDateByFileNumber( + fileNumber, + SUBMISSION_STATUS.CANCELLED, + ); + return; + } + + async uncancel(fileNumber: string) { + await this.applicationSubmissionStatusService.setStatusDateByFileNumber( + fileNumber, + SUBMISSION_STATUS.CANCELLED, + null, + ); + return; + } + async getMany( findOptions?: FindOptionsWhere, sortOptions?: FindOptionsOrder, From 27a5ba7d3f24d7abb160c53135bd98cc82d63291 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Tue, 18 Jul 2023 10:49:53 -0700 Subject: [PATCH 083/954] Feature/alcs 909 (#790) - Portal statuses in alcs - card detailed view - application detailed view - Status pill --- .../application/application.component.html | 1 + .../application-dialog.component.html | 4 +++ .../application-dialog.component.ts | 18 +++++++++-- ...lication-submission-status.service.spec.ts | 30 +++++++++++++++++++ .../application-submission-status.service.ts | 14 ++++++++- ...submission-status-type-pill.component.html | 12 ++++++++ ...submission-status-type-pill.component.scss | 15 ++++++++++ ...mission-status-type-pill.component.spec.ts | 27 +++++++++++++++++ ...n-submission-status-type-pill.component.ts | 20 +++++++++++++ .../details-header.component.html | 14 +++++++-- .../details-header.component.scss | 9 +++++- .../details-header.component.spec.ts | 11 +++++++ .../details-header.component.ts | 17 ++++++++++- alcs-frontend/src/app/shared/shared.module.ts | 3 ++ ...ation-submission-status.controller.spec.ts | 20 +++++++++++++ ...pplication-submission-status.controller.ts | 14 +++++++++ ...lication-submission-status.service.spec.ts | 28 +++++++++++++++++ .../application-submission-status.service.ts | 6 ++++ 18 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 alcs-frontend/src/app/shared/application-submission-status-type-pill/application-submission-status-type-pill.component.html create mode 100644 alcs-frontend/src/app/shared/application-submission-status-type-pill/application-submission-status-type-pill.component.scss create mode 100644 alcs-frontend/src/app/shared/application-submission-status-type-pill/application-submission-status-type-pill.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/application-submission-status-type-pill/application-submission-status-type-pill.component.ts diff --git a/alcs-frontend/src/app/features/application/application.component.html b/alcs-frontend/src/app/features/application/application.component.html index 3be9c441d8..9fa5a76717 100644 --- a/alcs-frontend/src/app/features/application/application.component.html +++ b/alcs-frontend/src/app/features/application/application.component.html @@ -6,6 +6,7 @@ [modifications]="modifications" days="Business Days" heading="Application" + [showStatus]="true" >
- + + @@ -109,19 +127,25 @@

Primary Contact Information

Primary Contact Authorization Letters

An authorization letter must be provided if: -
    -
  1. the parcel under application is owned by more than one person;
  2. -
  3. the parcel(s) is owned by an organization; or
  4. -
  5. the parcel(s) is owned by a corporation (private, Crown, local government, First Nations); or
  6. -
  7. the application is being submitted by a third-party agent on behalf of the land owner(s)
  8. -
-

- The authorization letter must be signed by all individual land owners and the majority of directors in - organization land owners listed in Step 1. Please consult the Supporting Documentation page of ALC website for - further instruction and an Authorization Letter template. -

+ +
    +
  1. the parcel under application is owned by more than one person;
  2. +
  3. the parcel(s) is owned by an organization; or
  4. +
  5. the parcel(s) is owned by a corporation (private, Crown, local government, First Nations); or
  6. +
  7. the application is being submitted by a third-party agent on behalf of the land owner(s)
  8. +
+

+ The authorization letter must be signed by all individual land owners and the majority of directors in + organization land owners listed in Step 1. Please consult the Supporting Documentation page of ALC website for + further instruction and an Authorization Letter template. +

+
+ + An authorization letter must be provided only if the application is being submitted by a third-party agent. Please + consult the Supporting Documentation page of the
TODO: FIX THIS: ALC website for further instruction. +
-
Authorization Letters
+
Authorization Letters (if applicable)
{ let mockAppService: DeepMocked; let mockAppDocumentService: DeepMocked; let mockAppOwnerService: DeepMocked; + let mockAuthService: DeepMocked; let applicationDocumentPipe = new BehaviorSubject([]); @@ -24,6 +27,9 @@ describe('PrimaryContactComponent', () => { mockAppService = createMock(); mockAppDocumentService = createMock(); mockAppOwnerService = createMock(); + mockAuthService = createMock(); + + mockAuthService.$currentProfile = new BehaviorSubject(undefined); await TestBed.configureTestingModule({ providers: [ @@ -43,6 +49,10 @@ describe('PrimaryContactComponent', () => { provide: MatDialog, useValue: {}, }, + { + provide: AuthenticationService, + useValue: mockAuthService, + }, ], declarations: [PrimaryContactComponent], schemas: [NO_ERRORS_SCHEMA], diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index 4e8871f591..71d0d984a6 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -1,19 +1,16 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; -import { BehaviorSubject, takeUntil } from 'rxjs'; +import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { FileHandle } from '../../../shared/file-drag-drop/drag-drop.directive'; -import { RemoveFileConfirmationDialogComponent } from '../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; +import { AuthenticationService } from '../../../services/authentication/authentication.service'; import { EditApplicationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; -import { StepComponent } from '../step.partial'; @Component({ selector: 'app-primary-contact', @@ -23,14 +20,17 @@ import { StepComponent } from '../step.partial'; export class PrimaryContactComponent extends FilesStepComponent implements OnInit, OnDestroy { currentStep = EditApplicationSteps.PrimaryContact; - nonAgentOwners: ApplicationOwnerDto[] = []; + parcelOwners: ApplicationOwnerDto[] = []; owners: ApplicationOwnerDto[] = []; files: (ApplicationDocumentDto & { errorMessage?: string })[] = []; needsAuthorizationLetter = false; selectedThirdPartyAgent = false; + selectedLocalGovernment = false; selectedOwnerUuid: string | undefined = undefined; isCrownOwner = false; + isLocalGovernmentUser = false; + governmentName: string | undefined; firstName = new FormControl('', [Validators.required]); lastName = new FormControl('', [Validators.required]); @@ -53,6 +53,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni private applicationService: ApplicationSubmissionService, applicationDocumentService: ApplicationDocumentService, private applicationOwnerService: ApplicationOwnerService, + private authenticationService: AuthenticationService, dialog: MatDialog ) { super(applicationDocumentService, dialog); @@ -67,6 +68,14 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } }); + this.authenticationService.$currentProfile.pipe(takeUntil(this.$destroy)).subscribe((profile) => { + this.isLocalGovernmentUser = !!profile?.government; + this.governmentName = profile?.government; + if (this.isLocalGovernmentUser) { + this.prepareGovernmentOwners(); + } + }); + this.$applicationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { this.files = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.AUTHORIZATION_LETTER); }); @@ -76,41 +85,28 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni await this.save(); } - protected async save() { - let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( - (owner) => owner.uuid === this.selectedOwnerUuid - ); + onSelectAgent() { + this.onSelectOwner('agent'); + } - if (this.selectedThirdPartyAgent) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - agentOrganization: this.organizationName.getRawValue() ?? '', - agentFirstName: this.firstName.getRawValue() ?? '', - agentLastName: this.lastName.getRawValue() ?? '', - agentEmail: this.email.getRawValue() ?? '', - agentPhoneNumber: this.phoneNumber.getRawValue() ?? '', - ownerUuid: selectedOwner?.uuid, - }); - } else if (selectedOwner) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - ownerUuid: selectedOwner.uuid, - }); - } + onSelectGovernment() { + this.onSelectOwner('government'); } onSelectOwner(uuid: string) { this.selectedOwnerUuid = uuid; - const selectedOwner = this.nonAgentOwners.find((owner) => owner.uuid === uuid); - this.nonAgentOwners = this.nonAgentOwners.map((owner) => ({ + const selectedOwner = this.parcelOwners.find((owner) => owner.uuid === uuid); + this.parcelOwners = this.parcelOwners.map((owner) => ({ ...owner, isSelected: owner.uuid === uuid, })); - const hasSelectedAgent = (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.AGENT) || uuid == 'agent'; - this.selectedThirdPartyAgent = hasSelectedAgent; + this.selectedThirdPartyAgent = + (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.AGENT) || uuid == 'agent'; + this.selectedLocalGovernment = + (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT) || uuid == 'government'; this.form.reset(); - if (hasSelectedAgent) { + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { this.firstName.enable(); this.lastName.enable(); this.organizationName.enable(); @@ -135,10 +131,17 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.isCrownOwner = selectedOwner.type.code === APPLICATION_OWNER.CROWN; } } + + const isSelfApplicant = + this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || + this.owners[0].type.code === APPLICATION_OWNER.GOVERNMENT; + this.needsAuthorizationLetter = !( - this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL && + isSelfApplicant && (this.owners.length === 1 || - (this.owners.length === 2 && this.owners[1].type.code === APPLICATION_OWNER.AGENT && !hasSelectedAgent)) + (this.owners.length === 2 && + this.owners[1].type.code === APPLICATION_OWNER.AGENT && + !this.selectedThirdPartyAgent)) ); this.files = this.files.map((file) => ({ ...file, @@ -148,20 +151,46 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni })); } - onSelectAgent() { - this.onSelectOwner('agent'); + protected async save() { + let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( + (owner) => owner.uuid === this.selectedOwnerUuid + ); + + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + organization: this.organizationName.getRawValue() ?? '', + firstName: this.firstName.getRawValue() ?? '', + lastName: this.lastName.getRawValue() ?? '', + email: this.email.getRawValue() ?? '', + phoneNumber: this.phoneNumber.getRawValue() ?? '', + ownerUuid: selectedOwner?.uuid, + type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, + }); + } else if (selectedOwner) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + ownerUuid: selectedOwner.uuid, + }); + } } private async loadOwners(submissionUuid: string, primaryContactOwnerUuid?: string) { const owners = await this.applicationOwnerService.fetchBySubmissionId(submissionUuid); if (owners) { const selectedOwner = owners.find((owner) => owner.uuid === primaryContactOwnerUuid); - this.nonAgentOwners = owners.filter((owner) => owner.type.code !== APPLICATION_OWNER.AGENT); + this.parcelOwners = owners.filter( + (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + ); this.owners = owners; - if (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.AGENT) { + if (selectedOwner) { + this.selectedThirdPartyAgent = selectedOwner.type.code === APPLICATION_OWNER.AGENT; + this.selectedLocalGovernment = selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT; + } + + if (selectedOwner && (this.selectedThirdPartyAgent || this.selectedLocalGovernment)) { this.selectedOwnerUuid = selectedOwner.uuid; - this.selectedThirdPartyAgent = true; this.form.patchValue({ firstName: selectedOwner.firstName, lastName: selectedOwner.lastName, @@ -179,9 +208,17 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.phoneNumber.disable(); } + if (this.isLocalGovernmentUser) { + this.prepareGovernmentOwners(); + } + if (this.showErrors) { this.form.markAllAsTouched(); } } } + + private prepareGovernmentOwners() { + this.parcelOwners = []; + } } diff --git a/portal-frontend/src/app/services/application-owner/application-owner.dto.ts b/portal-frontend/src/app/services/application-owner/application-owner.dto.ts index c053632910..090698c4d0 100644 --- a/portal-frontend/src/app/services/application-owner/application-owner.dto.ts +++ b/portal-frontend/src/app/services/application-owner/application-owner.dto.ts @@ -7,6 +7,7 @@ export enum APPLICATION_OWNER { ORGANIZATION = 'ORGZ', AGENT = 'AGEN', CROWN = 'CRWN', + GOVERNMENT = 'GOVR', } export interface ApplicationOwnerTypeDto extends BaseCodeDto { @@ -45,11 +46,12 @@ export interface ApplicationOwnerCreateDto extends ApplicationOwnerUpdateDto { } export interface SetPrimaryContactDto { - agentFirstName?: string; - agentLastName?: string; - agentOrganization?: string; - agentPhoneNumber?: string; - agentEmail?: string; + firstName?: string; + lastName?: string; + organization?: string; + phoneNumber?: string; + email?: string; + type?: APPLICATION_OWNER; ownerUuid?: string; applicationSubmissionUuid: string; } diff --git a/portal-frontend/src/app/services/authentication/auth-interceptor.service.spec.ts b/portal-frontend/src/app/services/authentication/auth-interceptor.service.spec.ts index 372ca00461..4c59fd8cc1 100644 --- a/portal-frontend/src/app/services/authentication/auth-interceptor.service.spec.ts +++ b/portal-frontend/src/app/services/authentication/auth-interceptor.service.spec.ts @@ -13,7 +13,7 @@ describe('AuthInterceptorService', () => { beforeEach(() => { mockAuthService = createMock(); - mockAuthService.$currentUser = new BehaviorSubject(undefined); + mockAuthService.$currentTokenUser = new BehaviorSubject(undefined); TestBed.configureTestingModule({ providers: [ diff --git a/portal-frontend/src/app/services/authentication/authentication.dto.ts b/portal-frontend/src/app/services/authentication/authentication.dto.ts new file mode 100644 index 0000000000..c62bc621f6 --- /dev/null +++ b/portal-frontend/src/app/services/authentication/authentication.dto.ts @@ -0,0 +1,10 @@ +export interface UserDto { + uuid: string; + initials: string; + name: string; + identityProvider: string; + idirUserName?: string | null; + bceidUserName?: string | null; + prettyName?: string | null; + government?: string; +} diff --git a/portal-frontend/src/app/services/authentication/authentication.service.ts b/portal-frontend/src/app/services/authentication/authentication.service.ts index 4705cf01d5..f531c799c1 100644 --- a/portal-frontend/src/app/services/authentication/authentication.service.ts +++ b/portal-frontend/src/app/services/authentication/authentication.service.ts @@ -4,6 +4,7 @@ import { Router } from '@angular/router'; import jwtDecode, { JwtPayload } from 'jwt-decode'; import { BehaviorSubject, firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { UserDto } from './authentication.dto'; const JWT_TOKEN_KEY = 'jwt_token'; const REFRESH_TOKEN_KEY = 'refresh_token'; @@ -23,7 +24,8 @@ export class AuthenticationService { private refreshExpires: number | undefined; isInitialized = false; - $currentUser = new BehaviorSubject(undefined); + $currentTokenUser = new BehaviorSubject(undefined); + $currentProfile = new BehaviorSubject(undefined); currentUser: ICurrentUser | undefined; constructor(private http: HttpClient, private router: Router) {} @@ -41,7 +43,8 @@ export class AuthenticationService { this.refreshExpires = decodedRefreshToken.exp! * 1000; this.expires = decodedToken.exp! * 1000; this.currentUser = decodedToken as ICurrentUser; - this.$currentUser.next(this.currentUser); + this.$currentTokenUser.next(this.currentUser); + this.loadUser(); } clearTokens() { @@ -137,4 +140,9 @@ export class AuthenticationService { private async getLogoutUrl() { return firstValueFrom(this.http.get<{ url: string }>(`${environment.authUrl}/logout/portal`)); } + + private async loadUser() { + const user = await firstValueFrom(this.http.get(`${environment.authUrl}/user/profile`)); + this.$currentProfile.next(user); + } } diff --git a/portal-frontend/src/app/services/authentication/token-refresh.service.ts b/portal-frontend/src/app/services/authentication/token-refresh.service.ts index 1d8177ee0e..96e420c40a 100644 --- a/portal-frontend/src/app/services/authentication/token-refresh.service.ts +++ b/portal-frontend/src/app/services/authentication/token-refresh.service.ts @@ -12,7 +12,7 @@ export class TokenRefreshService { constructor(private authenticationService: AuthenticationService) {} init() { - this.authenticationService.$currentUser.subscribe((user) => { + this.authenticationService.$currentTokenUser.subscribe((user) => { if (user) { if (this.interval) { clearInterval(this.interval); diff --git a/portal-frontend/src/app/shared/header/header.component.spec.ts b/portal-frontend/src/app/shared/header/header.component.spec.ts index bfbbe7d67a..cc8c34d530 100644 --- a/portal-frontend/src/app/shared/header/header.component.spec.ts +++ b/portal-frontend/src/app/shared/header/header.component.spec.ts @@ -12,7 +12,7 @@ describe('HeaderComponent', () => { beforeEach(async () => { mockAuthService = createMock(); - mockAuthService.$currentUser = new BehaviorSubject(undefined); + mockAuthService.$currentTokenUser = new BehaviorSubject(undefined); await TestBed.configureTestingModule({ declarations: [HeaderComponent], diff --git a/portal-frontend/src/app/shared/header/header.component.ts b/portal-frontend/src/app/shared/header/header.component.ts index b695a7cb75..79817826b0 100644 --- a/portal-frontend/src/app/shared/header/header.component.ts +++ b/portal-frontend/src/app/shared/header/header.component.ts @@ -16,7 +16,7 @@ export class HeaderComponent implements OnInit, OnDestroy { constructor(private authenticationService: AuthenticationService, private router: Router) {} ngOnInit(): void { - this.authenticationService.$currentUser.pipe(takeUntil(this.$destroy)).subscribe((user) => { + this.authenticationService.$currentTokenUser.pipe(takeUntil(this.$destroy)).subscribe((user) => { if (user) { this.isAuthenticated = true; } diff --git a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts b/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts index 4d4a126dd4..c9f542e873 100644 --- a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts +++ b/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts @@ -1,7 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsWhere, ILike, Repository } from 'typeorm'; -import { ApplicationSubmissionReview } from '../../../../portal/application-submission-review/application-submission-review.entity'; import { HolidayEntity } from '../../../admin/holiday/holiday.entity'; import { LocalGovernmentCreateDto, diff --git a/services/apps/alcs/src/main.module.ts b/services/apps/alcs/src/main.module.ts index 3200e044c1..045e0d91b7 100644 --- a/services/apps/alcs/src/main.module.ts +++ b/services/apps/alcs/src/main.module.ts @@ -11,7 +11,6 @@ import { ClsModule } from 'nestjs-cls'; import { LoggerModule } from 'nestjs-pino'; import { CdogsModule } from '../../../libs/common/src/cdogs/cdogs.module'; import { AlcsModule } from './alcs/alcs.module'; -import { ApplicationSubmissionStatusModule } from './application-submission-status/application-submission-status.module'; import { AuthorizationFilter } from './common/authorization/authorization.filter'; import { AuthorizationModule } from './common/authorization/authorization.module'; import { AuditSubscriber } from './common/entities/audit.subscriber'; @@ -23,15 +22,13 @@ import { MainController } from './main.controller'; import { MainService } from './main.service'; import { PortalModule } from './portal/portal.module'; import { TypeormConfigService } from './providers/typeorm/typeorm.service'; -import { User } from './user/user.entity'; import { UserModule } from './user/user.module'; -import { UserService } from './user/user.service'; @Module({ imports: [ ConfigModule, TypeOrmModule.forRootAsync({ useClass: TypeormConfigService }), - TypeOrmModule.forFeature([HealthCheck, User]), + TypeOrmModule.forFeature([HealthCheck]), AutomapperModule.forRoot({ strategyInitializer: classes(), }), @@ -69,7 +66,6 @@ import { UserService } from './user/user.service'; controllers: [MainController, LogoutController], providers: [ MainService, - UserService, AuditSubscriber, { provide: APP_GUARD, diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts index fdd19fd8df..8aa0a7ddcb 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts @@ -255,7 +255,7 @@ describe('ApplicationOwnerController', () => { }); it('should create a new owner when setting primary contact to third party agent that doesnt exist', async () => { - mockAppOwnerService.deleteAgents.mockResolvedValue([]); + mockAppOwnerService.deleteNonParcelOwners.mockResolvedValue([]); mockAppOwnerService.create.mockResolvedValue(new ApplicationOwner()); mockAppOwnerService.setPrimaryContact.mockResolvedValue(); mockApplicationSubmissionService.verifyAccessByUuid.mockResolvedValue( @@ -271,7 +271,7 @@ describe('ApplicationOwnerController', () => { }, ); - expect(mockAppOwnerService.deleteAgents).toHaveBeenCalledTimes(1); + expect(mockAppOwnerService.deleteNonParcelOwners).toHaveBeenCalledTimes(1); expect(mockAppOwnerService.create).toHaveBeenCalledTimes(1); expect(mockAppOwnerService.setPrimaryContact).toHaveBeenCalledTimes(1); expect( @@ -288,7 +288,7 @@ describe('ApplicationOwnerController', () => { }), ); mockAppOwnerService.setPrimaryContact.mockResolvedValue(); - mockAppOwnerService.deleteAgents.mockResolvedValue({} as any); + mockAppOwnerService.deleteNonParcelOwners.mockResolvedValue({} as any); mockApplicationSubmissionService.verifyAccessByUuid.mockResolvedValue( new ApplicationSubmission(), ); @@ -306,7 +306,7 @@ describe('ApplicationOwnerController', () => { expect( mockApplicationSubmissionService.verifyAccessByUuid, ).toHaveBeenCalledTimes(1); - expect(mockAppOwnerService.deleteAgents).toHaveBeenCalledTimes(1); + expect(mockAppOwnerService.deleteNonParcelOwners).toHaveBeenCalledTimes(1); }); it('should update the agent owner when calling set primary contact', async () => { diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts index 1349a1d89f..03b0576c78 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts @@ -161,41 +161,44 @@ export class ApplicationOwnerController { //Create Owner if (!data.ownerUuid) { - await this.ownerService.deleteAgents(applicationSubmission); - const agentOwner = await this.ownerService.create( + await this.ownerService.deleteNonParcelOwners(applicationSubmission); + const newOwner = await this.ownerService.create( { - email: data.agentEmail, - typeCode: APPLICATION_OWNER.AGENT, - lastName: data.agentLastName, - firstName: data.agentFirstName, - phoneNumber: data.agentPhoneNumber, - organizationName: data.agentOrganization, + email: data.email, + typeCode: data.type, + lastName: data.lastName, + firstName: data.firstName, + phoneNumber: data.phoneNumber, + organizationName: data.organization, applicationSubmissionUuid: data.applicationSubmissionUuid, }, applicationSubmission, ); await this.ownerService.setPrimaryContact( applicationSubmission.uuid, - agentOwner, + newOwner, ); } else if (data.ownerUuid) { const primaryContactOwner = await this.ownerService.getOwner( data.ownerUuid, ); - if (primaryContactOwner.type.code === APPLICATION_OWNER.AGENT) { - //Update Fields for existing agent + if ( + primaryContactOwner.type.code === APPLICATION_OWNER.AGENT || + primaryContactOwner.type.code === APPLICATION_OWNER.GOVERNMENT + ) { + //Update Fields for non parcel owners await this.ownerService.update(primaryContactOwner.uuid, { - email: data.agentEmail, - typeCode: APPLICATION_OWNER.AGENT, - lastName: data.agentLastName, - firstName: data.agentFirstName, - phoneNumber: data.agentPhoneNumber, - organizationName: data.agentOrganization, + email: data.email, + typeCode: primaryContactOwner.type.code, + lastName: data.lastName, + firstName: data.firstName, + phoneNumber: data.phoneNumber, + organizationName: data.organization, }); } else { - //Delete Agents if we aren't using one - await this.ownerService.deleteAgents(applicationSubmission); + //Delete Non parcel owners if we aren't using one + await this.ownerService.deleteNonParcelOwners(applicationSubmission); } await this.ownerService.setPrimaryContact( diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts index 09d3377c24..10ec1bdac7 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts @@ -6,7 +6,6 @@ import { IsUUID, Matches, } from 'class-validator'; -import { DOCUMENT_TYPE } from '../../../alcs/application/application-document/application-document-code.entity'; import { ApplicationDocumentDto } from '../../../alcs/application/application-document/application-document.dto'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; import { emailRegex } from '../../../utils/email.helper'; @@ -17,6 +16,7 @@ export enum APPLICATION_OWNER { ORGANIZATION = 'ORGZ', AGENT = 'AGEN', CROWN = 'CRWN', + GOVERNMENT = 'GOVR', } export class ApplicationOwnerTypeDto extends BaseCodeDto {} @@ -97,23 +97,27 @@ export class ApplicationOwnerCreateDto extends ApplicationOwnerUpdateDto { export class SetPrimaryContactDto { @IsString() @IsOptional() - agentFirstName?: string; + firstName?: string; @IsString() @IsOptional() - agentLastName?: string; + lastName?: string; @IsString() @IsOptional() - agentOrganization?: string; + organization?: string; @IsString() @IsOptional() - agentPhoneNumber?: string; + phoneNumber?: string; + + @IsString() + @IsOptional() + email?: string; @IsString() @IsOptional() - agentEmail?: string; + type?: APPLICATION_OWNER; @IsUUID() @IsOptional() diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts index b46252a209..51abcd1584 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts @@ -214,16 +214,26 @@ export class ApplicationOwnerService { }); } - async deleteAgents(application: ApplicationSubmission) { + async deleteNonParcelOwners(application: ApplicationSubmission) { const agentOwners = await this.repository.find({ - where: { - applicationSubmission: { - fileNumber: application.fileNumber, + where: [ + { + applicationSubmission: { + fileNumber: application.fileNumber, + }, + type: { + code: APPLICATION_OWNER.AGENT, + }, }, - type: { - code: APPLICATION_OWNER.AGENT, + { + applicationSubmission: { + fileNumber: application.fileNumber, + }, + type: { + code: APPLICATION_OWNER.GOVERNMENT, + }, }, - }, + ], }); return await this.repository.remove(agentOwners); } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts index 0d5cd4e44f..d9937bf19c 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts @@ -314,6 +314,40 @@ describe('ApplicationSubmissionValidatorService', () => { ).toBe(false); }); + it('should not require an authorization letter when contact is goverment', async () => { + const mockOwner = new ApplicationOwner({ + uuid: 'owner-uuid', + type: new ApplicationOwnerType({ + code: APPLICATION_OWNER.INDIVIDUAL, + }), + firstName: 'Bruce', + lastName: 'Wayne', + }); + + const governmentOwner = new ApplicationOwner({ + uuid: 'government-owner-uuid', + type: new ApplicationOwnerType({ + code: APPLICATION_OWNER.GOVERNMENT, + }), + firstName: 'Govern', + lastName: 'Ment', + }); + + const applicationSubmission = new ApplicationSubmission({ + owners: [mockOwner, governmentOwner], + primaryContactOwnerUuid: governmentOwner.uuid, + }); + + const res = await service.validateSubmission(applicationSubmission); + + expect( + includesError( + res.errors, + new Error(`Application has no authorization letters`), + ), + ).toBe(false); + }); + it('should not have an authorization letter error when one is provided', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts index c2238c4a71..4f9305f0e3 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts @@ -243,7 +243,10 @@ export class ApplicationSubmissionValidatorService { applicationSubmission.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL; - if (!onlyHasIndividualOwner) { + const isGovernmentContact = + primaryOwner.type.code === APPLICATION_OWNER.GOVERNMENT; + + if (!onlyHasIndividualOwner && !isGovernmentContact) { const authorizationLetters = documents.filter( (document) => document.type?.code === DOCUMENT_TYPE.AUTHORIZATION_LETTER, @@ -257,7 +260,10 @@ export class ApplicationSubmissionValidatorService { } } - if (primaryOwner.type.code === APPLICATION_OWNER.AGENT) { + if ( + primaryOwner.type.code === APPLICATION_OWNER.AGENT || + isGovernmentContact + ) { if ( !primaryOwner.firstName || !primaryOwner.lastName || diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1689806720586-add_government_owner_type.ts b/services/apps/alcs/src/providers/typeorm/migrations/1689806720586-add_government_owner_type.ts new file mode 100644 index 0000000000..26c7609782 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1689806720586-add_government_owner_type.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addGovernmentOwnerType1689806720586 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO "alcs"."application_owner_type" + ("audit_deleted_date_at", "audit_created_at", "audit_updated_at", "audit_created_by", "audit_updated_by", "label", "code", "description") VALUES + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Government', 'GOVR', 'For use by LFNG to select themselves'); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "alcs"."application_owner_type" WHERE "code" = 'GOVR'`, + ); + } +} diff --git a/services/apps/alcs/src/user/user.controller.spec.ts b/services/apps/alcs/src/user/user.controller.spec.ts index be89cfd84c..f4390447e1 100644 --- a/services/apps/alcs/src/user/user.controller.spec.ts +++ b/services/apps/alcs/src/user/user.controller.spec.ts @@ -3,6 +3,7 @@ import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; +import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; import { UserProfile } from '../common/automapper/user.automapper.profile'; import { initMockUserDto, @@ -16,13 +17,13 @@ import { UserService } from './user.service'; describe('UserController', () => { let controller: UserController; - let mockService: DeepMocked; + let mockUserService: DeepMocked; let mockUser: Partial; let mockUserDto: UserDto; let request; beforeEach(async () => { - mockService = createMock(); + mockUserService = createMock(); const module: TestingModule = await Test.createTestingModule({ controllers: [UserController, UserProfile], @@ -35,7 +36,7 @@ describe('UserController', () => { //Keep this below mockKeyCloak as it overrides the one from there { provide: UserService, - useValue: mockService, + useValue: mockUserService, }, ], imports: [ @@ -86,37 +87,37 @@ describe('UserController', () => { expect(controller).toBeDefined(); }); it('should call getAssignableUsers on the service', async () => { - mockService.getAssignableUsers.mockResolvedValue([mockUser as User]); + mockUserService.getAssignableUsers.mockResolvedValue([mockUser as User]); const res = await controller.getAssignableUsers(); expect(res[0].name).toEqual(mockUserDto.name); expect(res[0].initials).toEqual(mockUserDto.initials); - expect(mockService.getAssignableUsers).toHaveBeenCalledTimes(1); + expect(mockUserService.getAssignableUsers).toHaveBeenCalledTimes(1); }); it('should call deleteUser on the service', async () => { - mockService.delete.mockResolvedValue(mockUser as User); + mockUserService.delete.mockResolvedValue(mockUser as User); const res = await controller.deleteUser(''); expect(res).toBeTruthy(); - expect(mockService.delete).toHaveBeenCalledTimes(1); + expect(mockUserService.delete).toHaveBeenCalledTimes(1); }); it('should call update user on the service', async () => { const mockUserDto = initMockUserDto(); - mockService.getByUuid.mockResolvedValueOnce(mockUser as User); - mockService.update.mockResolvedValueOnce({} as any); + mockUserService.getByUuid.mockResolvedValueOnce(mockUser as User); + mockUserService.update.mockResolvedValueOnce({} as any); request.user.entity.uuid = mockUser.uuid = mockUserDto.uuid; await controller.update(mockUserDto.uuid, mockUserDto, request); - expect(mockService.update).toBeCalledTimes(1); + expect(mockUserService.update).toBeCalledTimes(1); }); it('should fail on user update if user not found', async () => { - mockService.getByUuid.mockResolvedValueOnce(null); + mockUserService.getByUuid.mockResolvedValueOnce(null); const mockUserDto = initMockUserDto(); request.user.entity.uuid = mockUser.uuid = mockUserDto.uuid; @@ -129,8 +130,8 @@ describe('UserController', () => { it('should fail on user update if current user does not mach updating user', async () => { const mockUserDto = initMockUserDto(); - mockService.getByUuid.mockResolvedValueOnce(mockUser as User); - mockService.update.mockResolvedValueOnce({} as any); + mockUserService.getByUuid.mockResolvedValueOnce(mockUser as User); + mockUserService.update.mockResolvedValueOnce({} as any); await expect( controller.update(mockUserDto.uuid, mockUserDto, request), @@ -141,6 +142,13 @@ describe('UserController', () => { it('return the current user', async () => { const mockEntity = initUserMockEntity(); + const governmentName = 'Government'; + + mockUserService.getUserLocalGovernment.mockResolvedValue( + new ApplicationLocalGovernment({ + name: governmentName, + }), + ); const res = await controller.getMyself({ user: { @@ -148,6 +156,7 @@ describe('UserController', () => { }, }); expect(res.name).toEqual(mockEntity.name); + expect(res.government).toEqual(governmentName); expect(res.identityProvider).toEqual(mockEntity.identityProvider); }); }); diff --git a/services/apps/alcs/src/user/user.controller.ts b/services/apps/alcs/src/user/user.controller.ts index be0737da54..f7cf70c275 100644 --- a/services/apps/alcs/src/user/user.controller.ts +++ b/services/apps/alcs/src/user/user.controller.ts @@ -31,10 +31,13 @@ export class UserController { ) {} @Get('/profile') - @UserRoles(...ANY_AUTH_ROLE) + @UserRoles() async getMyself(@Req() req) { const user = req.user.entity; - return this.userMapper.mapAsync(user, User, UserDto); + const mappedUser = await this.userMapper.mapAsync(user, User, UserDto); + const government = await this.userService.getUserLocalGovernment(user); + mappedUser.government = government ? government.name : undefined; + return mappedUser; } @Get('/assignable') diff --git a/services/apps/alcs/src/user/user.dto.ts b/services/apps/alcs/src/user/user.dto.ts index 9c1f895d7d..4b768f00d7 100644 --- a/services/apps/alcs/src/user/user.dto.ts +++ b/services/apps/alcs/src/user/user.dto.ts @@ -38,6 +38,8 @@ export class UserDto extends UpdateUserDto { @AutoMap() prettyName?: string | null; + + government?: string; } export class CreateUserDto { diff --git a/services/apps/alcs/src/user/user.module.ts b/services/apps/alcs/src/user/user.module.ts index 7acae6e908..9269c64bdb 100644 --- a/services/apps/alcs/src/user/user.module.ts +++ b/services/apps/alcs/src/user/user.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; import { UserProfile } from '../common/automapper/user.automapper.profile'; import { EmailModule } from '../providers/email/email.module'; import { UserController } from './user.controller'; @@ -7,7 +8,10 @@ import { User } from './user.entity'; import { UserService } from './user.service'; @Module({ - imports: [TypeOrmModule.forFeature([User]), EmailModule], + imports: [ + TypeOrmModule.forFeature([ApplicationLocalGovernment, User]), + EmailModule, + ], providers: [UserService, UserProfile], exports: [UserService, EmailModule], controllers: [UserController], diff --git a/services/apps/alcs/src/user/user.service.spec.ts b/services/apps/alcs/src/user/user.service.spec.ts index aa3fc94031..fe94069eb8 100644 --- a/services/apps/alcs/src/user/user.service.spec.ts +++ b/services/apps/alcs/src/user/user.service.spec.ts @@ -8,6 +8,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import * as config from 'config'; import { Repository } from 'typeorm'; import { initUserMockEntity } from '../../test/mocks/mockEntities'; +import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; import { UserProfile } from '../common/automapper/user.automapper.profile'; import { EmailService } from '../providers/email/email.service'; import { User } from './user.entity'; @@ -15,7 +16,10 @@ import { UserService } from './user.service'; describe('UserService', () => { let service: UserService; - let repositoryMock = createMock>(); + let mockUserRepository: DeepMocked>; + let mockGovernmentRepository: DeepMocked< + Repository + >; let emailServiceMock: DeepMocked; const email = 'bruce.wayne@gotham.com'; @@ -23,14 +27,20 @@ describe('UserService', () => { mockUser.email = email; beforeEach(async () => { - emailServiceMock = createMock(); + emailServiceMock = createMock(); + mockUserRepository = createMock(); + mockGovernmentRepository = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: getRepositoryToken(User), - useValue: repositoryMock, + useValue: mockUserRepository, + }, + { + provide: getRepositoryToken(ApplicationLocalGovernment), + useValue: mockGovernmentRepository, }, { provide: EmailService, useValue: emailServiceMock }, UserProfile, @@ -46,13 +56,13 @@ describe('UserService', () => { ], }).compile(); - repositoryMock = module.get(getRepositoryToken(User)); + mockUserRepository = module.get(getRepositoryToken(User)); service = module.get(UserService); - repositoryMock.findOne.mockResolvedValue(mockUser); - repositoryMock.save.mockResolvedValue(mockUser); - repositoryMock.find.mockResolvedValue([mockUser]); - repositoryMock.softRemove.mockResolvedValue(mockUser); + mockUserRepository.findOne.mockResolvedValue(mockUser); + mockUserRepository.save.mockResolvedValue(mockUser); + mockUserRepository.find.mockResolvedValue([mockUser]); + mockUserRepository.softRemove.mockResolvedValue(mockUser); emailServiceMock.sendEmail.mockResolvedValue(); }); @@ -72,12 +82,12 @@ describe('UserService', () => { describe('createUser', () => { it('should save a user when user does not exist', async () => { - repositoryMock.findOne.mockResolvedValue(null); + mockUserRepository.findOne.mockResolvedValue(null); const user = await service.create(mockUser); expect(user).toEqual(mockUser); - expect(repositoryMock.save).toHaveBeenCalledTimes(1); + expect(mockUserRepository.save).toHaveBeenCalledTimes(1); }); it('should reject if user already exists', async () => { @@ -91,12 +101,12 @@ describe('UserService', () => { it('should call delete user on the repository', async () => { await service.delete(mockUser.uuid); - expect(repositoryMock.softRemove).toHaveBeenCalledTimes(1); - expect(repositoryMock.softRemove).toHaveBeenCalledWith(mockUser); + expect(mockUserRepository.softRemove).toHaveBeenCalledTimes(1); + expect(mockUserRepository.softRemove).toHaveBeenCalledWith(mockUser); }); it('should reject when user does not exist', async () => { - repositoryMock.findOne.mockResolvedValue(null); + mockUserRepository.findOne.mockResolvedValue(null); await expect(service.delete(mockUser.uuid)).rejects.toMatchObject( new Error(`User with provided uuid ${mockUser.uuid} was not found`), @@ -116,7 +126,7 @@ describe('UserService', () => { }); it('should fail when user does not exist', async () => { - repositoryMock.findOne.mockResolvedValue(null); + mockUserRepository.findOne.mockResolvedValue(null); await expect(service.update('fake-uuid', mockUser)).rejects.toMatchObject( new ServiceNotFoundException(`User not found fake-uuid`), @@ -130,7 +140,7 @@ describe('UserService', () => { const prefix = env === 'production' ? '' : `[${env}]`; const subject = `${prefix} Access Requested to ALCS`; const body = `A new user ${email}: ${userIdentifier} has requested access to ALCS.
-CSS`; +CSS`; await service.sendNewUserRequestEmail(email, userIdentifier); @@ -140,4 +150,30 @@ describe('UserService', () => { subject, }); }); + + it('should not call repository if user does not have a bc business guid', async () => { + mockGovernmentRepository.findOne.mockResolvedValue( + new ApplicationLocalGovernment(), + ); + + const res = await service.getUserLocalGovernment(new User()); + + expect(res).toBeUndefined(); + expect(mockGovernmentRepository.findOne).toHaveBeenCalledTimes(0); + }); + + it('should call repository if user has a bc business guid', async () => { + mockGovernmentRepository.findOne.mockResolvedValue( + new ApplicationLocalGovernment(), + ); + + const res = await service.getUserLocalGovernment( + new User({ + bceidBusinessGuid: 'guid', + }), + ); + + expect(res).toBeDefined(); + expect(mockGovernmentRepository.findOne).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/user/user.service.ts b/services/apps/alcs/src/user/user.service.ts index 3a66b707f1..d5a9e99450 100644 --- a/services/apps/alcs/src/user/user.service.ts +++ b/services/apps/alcs/src/user/user.service.ts @@ -6,6 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IConfig } from 'config'; import { Repository } from 'typeorm'; +import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; import { EmailService } from '../providers/email/email.service'; import { CreateUserDto } from './user.dto'; import { User } from './user.entity'; @@ -22,6 +23,8 @@ export class UserService { private userRepository: Repository, @InjectMapper() private userMapper: Mapper, private emailService: EmailService, + @InjectRepository(ApplicationLocalGovernment) + private localGovernmentRepository: Repository, @Inject(CONFIG_TOKEN) private config: IConfig, ) {} @@ -88,12 +91,23 @@ export class UserService { return this.userRepository.save(updatedUser); } + async getUserLocalGovernment(user: User) { + if (user.bceidBusinessGuid) { + return await this.localGovernmentRepository.findOne({ + where: { bceidBusinessGuid: user.bceidBusinessGuid }, + select: { + name: true, + }, + }); + } + } + async sendNewUserRequestEmail(email: string, userIdentifier: string) { const env = this.config.get('ENV'); const prefix = env === 'production' ? '' : `[${env}]`; const subject = `${prefix} Access Requested to ALCS`; const body = `A new user ${email}: ${userIdentifier} has requested access to ALCS.
-CSS`; +CSS`; await this.emailService.sendEmail({ to: this.config.get('EMAIL.DEFAULT_ADMINS'), From ab7fb70b6cecee4f6847121df4817ab1bfb0388e Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 20 Jul 2023 10:46:29 -0700 Subject: [PATCH 095/954] Show warning banner when user selects their own government --- .../application-details.component.html | 8 ++++ .../select-government.component.html | 5 +++ .../select-government.component.ts | 4 ++ .../src/app/services/code/code.dto.ts | 1 + .../src/portal/code/code.controller.spec.ts | 42 +++++++++++++++++-- .../alcs/src/portal/code/code.controller.ts | 15 +++++-- 6 files changed, 69 insertions(+), 6 deletions(-) diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/application-details/application-details.component.html index 6dee41c5ba..46d5a225fb 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/application-details/application-details.component.html @@ -93,6 +93,14 @@

4. Government

>ALC.Portal@gov.bc.ca / 236-468-3342 + + You're logged in with a Business BCeID that is associated with the government selected above. You will have the + opportunity to complete the local or first nation government review form immediately after this application is + submitted. +
diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.html b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.html index 1fb17aad70..3ad9accac8 100644 --- a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.html +++ b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.html @@ -45,6 +45,11 @@

Government

will need to contact the ALC directly:  ALC.Portal@gov.bc.ca / 236-468-3342 + + You're logged in with a Business BCeID that is associated with the government selected above. You will have the + opportunity to complete the local or first nation government review form immediately after this application is + submitted. +

Please Note: If your Local or First Nation Government is not listed, please contact the ALC directly. ALC.Portal@gov.bc.ca / 236-468-3342 diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts index 11ab4211a1..ef052485f1 100644 --- a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts +++ b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts @@ -22,6 +22,7 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, localGovernment = new FormControl('', [Validators.required]); showWarning = false; + selectedOwnGovernment = false; selectGovernmentUuid = ''; localGovernments: LocalGovernmentDto[] = []; filteredLocalGovernments!: Observable; @@ -73,6 +74,8 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, } else { this.localGovernment.setErrors({ invalid: localGovernment.hasGuid }); } + + this.selectedOwnGovernment = localGovernment.matchesUserGuid; } } } @@ -134,6 +137,7 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, if (!lg.hasGuid) { this.localGovernment.setErrors({ invalid: true }); } + this.selectedOwnGovernment = lg.matchesUserGuid; } } } diff --git a/portal-frontend/src/app/services/code/code.dto.ts b/portal-frontend/src/app/services/code/code.dto.ts index a9054735ba..e00e33c8ea 100644 --- a/portal-frontend/src/app/services/code/code.dto.ts +++ b/portal-frontend/src/app/services/code/code.dto.ts @@ -4,6 +4,7 @@ export interface LocalGovernmentDto { uuid: string; name: string; hasGuid: boolean; + matchesUserGuid: boolean; } export interface ApplicationTypeDto { diff --git a/services/apps/alcs/src/portal/code/code.controller.spec.ts b/services/apps/alcs/src/portal/code/code.controller.spec.ts index 7898cf4884..671d02300c 100644 --- a/services/apps/alcs/src/portal/code/code.controller.spec.ts +++ b/services/apps/alcs/src/portal/code/code.controller.spec.ts @@ -10,6 +10,7 @@ import { ApplicationDocumentService } from '../../alcs/application/application-d import { ApplicationService } from '../../alcs/application/application.service'; import { CardType } from '../../alcs/card/card-type/card-type.entity'; import { CardService } from '../../alcs/card/card.service'; +import { User } from '../../user/user.entity'; import { ApplicationSubmissionService } from '../application-submission/application-submission.service'; import { CodeController } from './code.controller'; @@ -89,15 +90,50 @@ describe('CodeController', () => { }); it('should call out to local government service for fetching codes', async () => { - const codes = await portalController.loadCodes(); + const codes = await portalController.loadCodes({ + user: { + entity: new User(), + }, + }); expect(codes.localGovernments).toBeDefined(); expect(codes.localGovernments.length).toBe(1); expect(codes.localGovernments[0].name).toEqual('fake-name'); - expect(mockAppService.fetchApplicationTypes).toHaveBeenCalledTimes(1); + expect(codes.localGovernments[0].matchesUserGuid).toBeFalsy(); + expect(mockLgService.listActive).toHaveBeenCalledTimes(1); + }); + + it('should set the matches flag correctly when users guid matches government', async () => { + const matchingGuid = 'guid'; + mockLgService.listActive.mockResolvedValue([ + new ApplicationLocalGovernment({ + uuid: 'fake-uuid', + name: 'fake-name', + isFirstNation: false, + bceidBusinessGuid: matchingGuid, + }), + ]); + + const codes = await portalController.loadCodes({ + user: { + entity: new User({ + bceidBusinessGuid: matchingGuid, + }), + }, + }); + + expect(codes.localGovernments).toBeDefined(); + expect(codes.localGovernments.length).toBe(1); + expect(codes.localGovernments[0].name).toEqual('fake-name'); + expect(codes.localGovernments[0].matchesUserGuid).toBeTruthy(); + expect(mockLgService.listActive).toHaveBeenCalledTimes(1); }); it('should call out to local submission service for fetching codes', async () => { - const codes = await portalController.loadCodes(); + const codes = await portalController.loadCodes({ + user: { + entity: new User(), + }, + }); expect(codes.submissionTypes).toBeDefined(); expect(codes.submissionTypes.length).toBe(1); expect(codes.submissionTypes[0].code).toEqual('fake-code'); diff --git a/services/apps/alcs/src/portal/code/code.controller.ts b/services/apps/alcs/src/portal/code/code.controller.ts index 228a6967ea..e77d619fed 100644 --- a/services/apps/alcs/src/portal/code/code.controller.ts +++ b/services/apps/alcs/src/portal/code/code.controller.ts @@ -1,6 +1,6 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { Controller, Get, UseGuards } from '@nestjs/common'; +import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; import { ApplicationDocumentCode } from '../../alcs/application/application-document/application-document-code.entity'; @@ -9,12 +9,14 @@ import { ApplicationDocumentService } from '../../alcs/application/application-d import { ApplicationService } from '../../alcs/application/application.service'; import { CardService } from '../../alcs/card/card.service'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; +import { User } from '../../user/user.entity'; import { ApplicationSubmissionService } from '../application-submission/application-submission.service'; export interface LocalGovernmentDto { uuid: string; name: string; hasGuid: boolean; + matchesUserGuid: boolean; } @Controller('/portal/code') @@ -30,7 +32,7 @@ export class CodeController { ) {} @Get() - async loadCodes() { + async loadCodes(@Req() req) { const localGovernments = await this.localGovernmentService.listActive(); const applicationTypes = await this.applicationService.fetchApplicationTypes(); @@ -51,7 +53,10 @@ export class CodeController { ); }); return { - localGovernments: this.mapLocalGovernments(localGovernments), + localGovernments: this.mapLocalGovernments( + localGovernments, + req.user.entity, + ), applicationTypes, submissionTypes, applicationDocumentTypes: mappedDocTypes, @@ -61,11 +66,15 @@ export class CodeController { private mapLocalGovernments( governments: ApplicationLocalGovernment[], + user: User, ): LocalGovernmentDto[] { return governments.map((government) => ({ name: government.name, uuid: government.uuid, hasGuid: government.bceidBusinessGuid !== null, + matchesUserGuid: + !!government.bceidBusinessGuid && + government.bceidBusinessGuid === user.bceidBusinessGuid, })); } } From 8c6301a3243a2a2c1c87d9718243dfc7dc9d78df Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 20 Jul 2023 11:36:29 -0700 Subject: [PATCH 096/954] Add Confirmation Dialog to Portal Submission --- .../edit-submission-base.module.ts | 2 + .../review-and-submit.component.spec.ts | 5 +++ .../review-and-submit.component.ts | 14 +++++- .../submit-confirmation-dialog.component.html | 43 +++++++++++++++++++ .../submit-confirmation-dialog.component.scss | 18 ++++++++ ...bmit-confirmation-dialog.component.spec.ts | 35 +++++++++++++++ .../submit-confirmation-dialog.component.ts | 19 ++++++++ 7 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html create mode 100644 portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss create mode 100644 portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts index 24936e0644..13b593d51f 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts @@ -34,6 +34,7 @@ import { SoilTableComponent } from './proposal/soil-table/soil-table.component'; import { SubdProposalComponent } from './proposal/subd-proposal/subd-proposal.component'; import { TurProposalComponent } from './proposal/tur-proposal/tur-proposal.component'; import { ReviewAndSubmitComponent } from './review-and-submit/review-and-submit.component'; +import { SubmitConfirmationDialogComponent } from './review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; @NgModule({ @@ -54,6 +55,7 @@ import { SelectGovernmentComponent } from './select-government/select-government OtherAttachmentsComponent, PrimaryContactComponent, ReviewAndSubmitComponent, + SubmitConfirmationDialogComponent, OtherParcelConfirmationDialogComponent, NfuProposalComponent, TurProposalComponent, diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts index 78f8be8cca..c1072da656 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts @@ -1,5 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; @@ -42,6 +43,10 @@ describe('ReviewAndSubmitComponent', () => { provide: PdfGenerationService, useValue: {}, }, + { + provide: MatDialog, + useValue: {}, + }, ], }).compileComponents(); diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts index 824735471b..ba85493352 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts @@ -1,4 +1,5 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { BehaviorSubject, takeUntil } from 'rxjs'; import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; @@ -7,6 +8,7 @@ import { ApplicationSubmissionService } from '../../../services/application-subm import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; import { ToastService } from '../../../services/toast/toast.service'; import { StepComponent } from '../step.partial'; +import { SubmitConfirmationDialogComponent } from './submit-confirmation-dialog/submit-confirmation-dialog.component'; @Component({ selector: 'app-review-and-submit', @@ -25,7 +27,8 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O private router: Router, private toastService: ToastService, private applicationService: ApplicationSubmissionService, - private pdfGenerationService: PdfGenerationService + private pdfGenerationService: PdfGenerationService, + private dialog: MatDialog ) { super(); } @@ -55,7 +58,14 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O }); this.toastService.showErrorToast('Please correct all errors before submitting the form'); } else { - this.submit.emit(); + this.dialog + .open(SubmitConfirmationDialogComponent) + .beforeClosed() + .subscribe((didConfirm) => { + if (didConfirm) { + this.submit.emit(); + } + }); } } diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html new file mode 100644 index 0000000000..16b1d105f9 --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html @@ -0,0 +1,43 @@ +

+

Submit Application

+
+ +
+

+ Your application will be submitted to the [L/FNG]. After submission, you will be auto-directed to complete the local + or first nation government review form in order to submit this application to the Agricultural Land Commission. +

+
+
Terms and Conditions:
+ + I/we consent to the use of the information provided in the application and all supporting documents to process the + application in accordance with the Agricultural Land Commission Act, the Agricultural Land Reserve General + Regulation, and the Agricultural Land Reserve Use Regulation. + + + I/we declare that the information provided in the application and all the supporting documents are, to the best of + my/our knowledge, true and correct. + + + I/we understand that the Agricultural Land Commission will take the steps necessary to confirm the accuracy of the + information and documents provided. This information will be available for review by any member of the public. + +
+

+ If you have any questions about the collection or use of this information, please contact the Agricultural Land + Commission. +

+
+
+ + +
+
+
diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss new file mode 100644 index 0000000000..d4f7e0ae48 --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss @@ -0,0 +1,18 @@ +@use '../../../../../styles/functions' as *; + +.checkbox { + margin: rem(10) 0; +} + +p { + margin: rem(28) 0 !important; +} + +.step-controls { + display: flex; + justify-content: flex-end; + + button:not(:last-child) { + margin-right: rem(16) !important; + } +} diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts new file mode 100644 index 0000000000..0f7741443b --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { SubmitConfirmationDialogComponent } from './submit-confirmation-dialog.component'; + +describe('OtherParcelConfirmationDialogComponent', () => { + let component: SubmitConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SubmitConfirmationDialogComponent], + providers: [ + { + provide: MatDialog, + useValue: {}, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmitConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts new file mode 100644 index 0000000000..fc8779c3f5 --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'app-submit-confirmation-dialog', + templateUrl: './submit-confirmation-dialog.component.html', + styleUrls: ['./submit-confirmation-dialog.component.scss'], +}) +export class SubmitConfirmationDialogComponent { + constructor(private dialogRef: MatDialogRef) {} + + async onCancel(dialogResult: boolean = false) { + this.dialogRef.close(dialogResult); + } + + async onSubmit() { + this.dialogRef.close(true); + } +} From 0c456862eab3c33576da0a1e715ecb5e66b4d306 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Thu, 20 Jul 2023 11:44:10 -0700 Subject: [PATCH 097/954] Bugix/alcs 908 various fixes (#800) fix edit submission from alcs fix wrong government and incomplete statuses fix to include mask date into decision released status date (V2 only) --- ...plication-submission-draft.service.spec.ts | 2 - .../application-decision-v2.service.ts | 22 ++++- ...lication-submission-status.service.spec.ts | 80 +++++++++++++++++++ .../application-submission-status.service.ts | 34 +++++++- .../application-submission-draft.module.ts | 2 + ...plication-submission-draft.service.spec.ts | 28 ++++++- .../application-submission-draft.service.ts | 24 +++++- ...ation-submission-review.controller.spec.ts | 7 +- ...pplication-submission-review.controller.ts | 39 ++++++--- 9 files changed, 211 insertions(+), 27 deletions(-) diff --git a/portal-frontend/src/app/services/application-submission/application-submission-draft.service.spec.ts b/portal-frontend/src/app/services/application-submission/application-submission-draft.service.spec.ts index b1e4071c77..2f9f7b164d 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission-draft.service.spec.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission-draft.service.spec.ts @@ -6,8 +6,6 @@ import { of, throwError } from 'rxjs'; import { ToastService } from '../toast/toast.service'; import { ApplicationSubmissionDraftService } from './application-submission-draft.service'; -import { ApplicationSubmissionService } from './application-submission.service'; - describe('ApplicationSubmissionDraftService', () => { let service: ApplicationSubmissionDraftService; let mockToastService: DeepMocked; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts index 1b49a2621e..b67a513d27 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts @@ -701,12 +701,26 @@ export class ApplicationDecisionV2Service { }, ); - await this.applicationSubmissionStatusService.setStatusDateByFileNumber( - applicationDecision.application.fileNumber, - SUBMISSION_STATUS.ALC_DECISION, - decisionDate, + await this.setDecisionReleasedStatus(decisionDate, applicationDecision); + } + } + + private async setDecisionReleasedStatus( + decisionDate: Date | null, + applicationDecision: ApplicationDecision, + ) { + const statusDate = decisionDate; + if (applicationDecision.daysHideFromPublic) { + statusDate?.setDate( + statusDate.getDate() + applicationDecision.daysHideFromPublic, ); } + + await this.applicationSubmissionStatusService.setStatusDateByFileNumber( + applicationDecision.application.fileNumber, + SUBMISSION_STATUS.ALC_DECISION, + statusDate, + ); } private async getDecisionDocumentOrFail(decisionDocumentUuid: string) { diff --git a/services/apps/alcs/src/application-submission-status/application-submission-status.service.spec.ts b/services/apps/alcs/src/application-submission-status/application-submission-status.service.spec.ts index e45d5da18a..8186208768 100644 --- a/services/apps/alcs/src/application-submission-status/application-submission-status.service.spec.ts +++ b/services/apps/alcs/src/application-submission-status/application-submission-status.service.spec.ts @@ -353,4 +353,84 @@ describe('ApplicationSubmissionStatusService', () => { fileNumber: fakeFileNumber, }); }); + + it('Should remove statuses', async () => { + const fakeSubmissionUuid = 'fake'; + const mockStatuses = [ + new ApplicationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: SUBMISSION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }), + ]; + + mockApplicationSubmissionToSubmissionStatusRepository.findBy.mockResolvedValue( + mockStatuses, + ); + mockApplicationSubmissionToSubmissionStatusRepository.remove.mockResolvedValue( + {} as any, + ); + + await service.removeStatuses(fakeSubmissionUuid); + + expect( + mockApplicationSubmissionToSubmissionStatusRepository.findBy, + ).toHaveBeenCalledTimes(1); + expect( + mockApplicationSubmissionToSubmissionStatusRepository.findBy, + ).toHaveBeenCalledWith({ + submissionUuid: fakeSubmissionUuid, + }); + expect( + mockApplicationSubmissionToSubmissionStatusRepository.remove, + ).toBeCalledTimes(1); + expect( + mockApplicationSubmissionToSubmissionStatusRepository.remove, + ).toBeCalledWith(mockStatuses); + }); + + it('should return copied statuses', async () => { + const fakeSubmissionUuid = 'fake'; + const fakeUpdatedSubmissionUuid = 'fake-updated'; + + const mockStatuses = [ + new ApplicationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: SUBMISSION_STATUS.ALC_DECISION, + }), + new ApplicationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: SUBMISSION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }), + ]; + + const copiedStatuses = mockStatuses.map( + (s) => + new ApplicationSubmissionToSubmissionStatus({ + ...s, + submissionUuid: fakeUpdatedSubmissionUuid, + }), + ); + + mockApplicationSubmissionToSubmissionStatusRepository.find.mockResolvedValue( + mockStatuses, + ); + + const result = await service.getCopiedStatuses( + fakeSubmissionUuid, + fakeUpdatedSubmissionUuid, + ); + + expect( + mockApplicationSubmissionToSubmissionStatusRepository.find, + ).toBeCalledTimes(1); + expect( + mockApplicationSubmissionToSubmissionStatusRepository.find, + ).toBeCalledWith({ + where: { submissionUuid: fakeSubmissionUuid }, + }); + + expect(result).toMatchObject(copiedStatuses); + }); }); diff --git a/services/apps/alcs/src/application-submission-status/application-submission-status.service.ts b/services/apps/alcs/src/application-submission-status/application-submission-status.service.ts index aeb2b7b8d4..a1c31bc8a7 100644 --- a/services/apps/alcs/src/application-submission-status/application-submission-status.service.ts +++ b/services/apps/alcs/src/application-submission-status/application-submission-status.service.ts @@ -24,7 +24,7 @@ export class ApplicationSubmissionStatusService { private applicationSubmissionRepository: Repository, ) {} - async setInitialStatuses(submissionUuid: string) { + async setInitialStatuses(submissionUuid: string, persist = true) { const statuses = await this.submissionStatusTypeRepository.find(); const newStatuses: ApplicationSubmissionToSubmissionStatus[] = []; @@ -42,7 +42,11 @@ export class ApplicationSubmissionStatusService { newStatuses.push(newStatus); } - return await this.statusesRepository.save(newStatuses); + if (persist) { + return await this.statusesRepository.save(newStatuses); + } + + return newStatuses; } async setStatusDate( @@ -113,4 +117,30 @@ export class ApplicationSubmissionStatusService { return submission; } + + //Note: do not use fileNumber as identifier since there maybe multiple submissions with + // the same fileNumber due to isDraft flag + async removeStatuses(submissionUuid: string) { + const statusesToRemove = await this.getCurrentStatusesBy(submissionUuid); + + return await this.statusesRepository.remove(statusesToRemove); + } + + async getCopiedStatuses( + sourceSubmissionUuid: string, + destinationSubmissionUuid, + ) { + const statuses = await this.statusesRepository.find({ + where: { submissionUuid: sourceSubmissionUuid }, + }); + const newStatuses = statuses.map( + (s) => + new ApplicationSubmissionToSubmissionStatus({ + ...s, + submissionUuid: destinationSubmissionUuid, + }), + ); + + return newStatuses; + } } diff --git a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts index 8a77f5d9bf..6856f35af0 100644 --- a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts +++ b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationSubmissionStatusModule } from '../../application-submission-status/application-submission-status.module'; import { ApplicationSubmissionStatusType } from '../../application-submission-status/submission-status-type.entity'; import { ApplicationOwnerType } from '../application-submission/application-owner/application-owner-type/application-owner-type.entity'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; @@ -23,6 +24,7 @@ import { ApplicationSubmissionDraftService } from './application-submission-draf ]), ApplicationSubmissionModule, PdfGenerationModule, + ApplicationSubmissionStatusModule, ], providers: [ApplicationSubmissionDraftService], controllers: [ApplicationSubmissionDraftController], diff --git a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.spec.ts b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.spec.ts index 885eb11c03..10d11cf2c2 100644 --- a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.spec.ts @@ -2,13 +2,13 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { ApplicationSubmissionStatusService } from '../../application-submission-status/application-submission-status.service'; import { User } from '../../user/user.entity'; import { ApplicationOwnerService } from '../application-submission/application-owner/application-owner.service'; import { ApplicationParcelService } from '../application-submission/application-parcel/application-parcel.service'; import { ApplicationSubmission } from '../application-submission/application-submission.entity'; import { ApplicationSubmissionService } from '../application-submission/application-submission.service'; import { GenerateSubmissionDocumentService } from '../pdf-generation/generate-submission-document.service'; -import { ApplicationSubmissionDraftModule } from './application-submission-draft.module'; import { ApplicationSubmissionDraftService } from './application-submission-draft.service'; describe('ApplicationSubmissionDraftService', () => { @@ -18,6 +18,7 @@ describe('ApplicationSubmissionDraftService', () => { let mockParcelService: DeepMocked; let mockAppOwnerService: DeepMocked; let mockGenerateSubmissionDocumentService: DeepMocked; + let mockApplicationSubmissionStatusService: DeepMocked; beforeEach(async () => { mockSubmissionRepo = createMock(); @@ -25,6 +26,7 @@ describe('ApplicationSubmissionDraftService', () => { mockParcelService = createMock(); mockAppOwnerService = createMock(); mockGenerateSubmissionDocumentService = createMock(); + mockApplicationSubmissionStatusService = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -49,6 +51,10 @@ describe('ApplicationSubmissionDraftService', () => { provide: GenerateSubmissionDocumentService, useValue: mockGenerateSubmissionDocumentService, }, + { + provide: ApplicationSubmissionStatusService, + useValue: mockApplicationSubmissionStatusService, + }, ], }).compile(); @@ -83,11 +89,19 @@ describe('ApplicationSubmissionDraftService', () => { mockSubmissionRepo.save.mockResolvedValue(new ApplicationSubmission()); mockParcelService.update.mockResolvedValue([]); + const mockTransaction = jest.fn(); + mockSubmissionRepo.manager.transaction = mockTransaction; + mockApplicationSubmissionStatusService.getCopiedStatuses.mockResolvedValue( + [], + ); const draft = await service.getOrCreateDraft('fileNumber'); expect(mockSubmissionRepo.findOne).toHaveBeenCalledTimes(3); expect(mockSubmissionRepo.save).toHaveBeenCalledTimes(1); + expect( + mockApplicationSubmissionStatusService.getCopiedStatuses, + ).toHaveBeenCalledTimes(1); expect(draft).toBeDefined(); }); @@ -112,22 +126,30 @@ describe('ApplicationSubmissionDraftService', () => { it('should load two submissions and save one as not draft when publishing', async () => { mockSubmissionRepo.findOne.mockResolvedValue( new ApplicationSubmission({ + uuid: 'fake', owners: [], parcels: [], }), ); - mockSubmissionRepo.remove.mockResolvedValue(new ApplicationSubmission()); + mockSubmissionRepo.delete.mockResolvedValue({} as any); mockSubmissionRepo.save.mockResolvedValue(new ApplicationSubmission()); mockParcelService.deleteMany.mockResolvedValueOnce([]); mockGenerateSubmissionDocumentService.generateUpdate.mockResolvedValue(); + mockApplicationSubmissionStatusService.removeStatuses.mockResolvedValue( + {} as any, + ); await service.publish('fileNumber', new User()); expect(mockSubmissionRepo.findOne).toHaveBeenCalledTimes(2); - expect(mockSubmissionRepo.remove).toHaveBeenCalledTimes(1); + expect(mockSubmissionRepo.delete).toHaveBeenCalledTimes(1); + expect(mockSubmissionRepo.delete).toBeCalledWith({ uuid: 'fake' }); expect(mockParcelService.deleteMany).toHaveBeenCalledTimes(1); expect(mockSubmissionRepo.save).toHaveBeenCalledTimes(1); + expect( + mockApplicationSubmissionStatusService.removeStatuses, + ).toHaveBeenCalledTimes(1); expect(mockSubmissionRepo.save.mock.calls[0][0].isDraft).toEqual(false); expect( mockGenerateSubmissionDocumentService.generateUpdate, diff --git a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.ts b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.ts index f58ab6066c..d044c6bef4 100644 --- a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.ts +++ b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.service.ts @@ -2,6 +2,7 @@ import { BaseServiceException } from '@app/common/exceptions/base.exception'; import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { ApplicationSubmissionStatusService } from '../../application-submission-status/application-submission-status.service'; import { User } from '../../user/user.entity'; import { ApplicationOwnerService } from '../application-submission/application-owner/application-owner.service'; import { ApplicationParcelUpdateDto } from '../application-submission/application-parcel/application-parcel.dto'; @@ -21,6 +22,7 @@ export class ApplicationSubmissionDraftService { private applicationParcelService: ApplicationParcelService, private applicationOwnerService: ApplicationOwnerService, private generateSubmissionDocumentService: GenerateSubmissionDocumentService, + private applicationSubmissionStatusService: ApplicationSubmissionStatusService, ) {} async getOrCreateDraft(fileNumber: string) { @@ -79,7 +81,7 @@ export class ApplicationSubmissionDraftService { ); } - const newReview = new ApplicationSubmission({ + const newSubmission = new ApplicationSubmission({ ...originalSubmission, uuid: undefined, auditCreatedAt: undefined, @@ -89,7 +91,20 @@ export class ApplicationSubmissionDraftService { owners: [], }); const savedSubmission = await this.applicationSubmissionRepository.save( - newReview, + newSubmission, + ); + const statuses = + await this.applicationSubmissionStatusService.getCopiedStatuses( + originalSubmission.uuid, + newSubmission.uuid, + ); + + this.applicationSubmissionRepository.manager.transaction( + async (transactionalEntityManager) => { + await transactionalEntityManager.save(newSubmission); + + await transactionalEntityManager.save(statuses); + }, ); const ownerUuidMap = new Map(); @@ -176,6 +191,7 @@ export class ApplicationSubmissionDraftService { relations: { owners: true, parcels: true, + createdBy: true, }, }); @@ -191,8 +207,10 @@ export class ApplicationSubmissionDraftService { } const parcelUuids = original.parcels.map((parcel) => parcel.uuid); await this.applicationParcelService.deleteMany(parcelUuids); - await this.applicationSubmissionRepository.remove(original); + await this.applicationSubmissionStatusService.removeStatuses(original.uuid); + await this.applicationSubmissionRepository.delete({ uuid: original.uuid }); + draft.createdBy = original.createdBy; draft.isDraft = false; await this.applicationSubmissionRepository.save(draft); diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts index 676789998b..066087443d 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts @@ -509,12 +509,15 @@ describe('ApplicationSubmissionReviewController', () => { mockAppSubmissionService.getForGovernmentByFileId, ).toHaveBeenCalledTimes(1); expect(mockAppReviewService.getByFileNumber).toHaveBeenCalledTimes(1); - expect(mockAppSubmissionService.updateStatus).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.updateStatus).toHaveBeenCalledTimes(2); expect(mockAppDocService.delete).toHaveBeenCalledTimes(1); expect(mockAppReviewService.delete).toHaveBeenCalledTimes(1); - expect(mockAppSubmissionService.updateStatus.mock.calls[0][1]).toEqual( + expect(mockAppSubmissionService.updateStatus.mock.calls[1][1]).toEqual( SUBMISSION_STATUS.INCOMPLETE, ); + expect(mockAppSubmissionService.updateStatus.mock.calls[0][1]).toEqual( + SUBMISSION_STATUS.WRONG_GOV, + ); expect(mockAppSubmissionService.update).toHaveBeenCalledTimes(1); expect(mockAppSubmissionService.update.mock.calls[0][0]).toEqual( 'submission-uuid', diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index 801b05692f..c64d4ce2c2 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -371,22 +371,39 @@ export class ApplicationSubmissionReviewController { ); } - if (returnDto.reasonForReturn === 'incomplete') { - await this.applicationSubmissionService.updateStatus( - applicationSubmission, - SUBMISSION_STATUS.INCOMPLETE, - ); - } else { - await this.applicationSubmissionService.updateStatus( - applicationSubmission, - SUBMISSION_STATUS.WRONG_GOV, - ); - } + await this.setReturnedStatus(returnDto, applicationSubmission); } else { throw new BaseServiceException('Application not in correct status'); } } + private async setReturnedStatus( + returnDto: ReturnApplicationSubmissionDto, + applicationSubmission: ApplicationSubmission, + ) { + if (returnDto.reasonForReturn === 'incomplete') { + await this.applicationSubmissionService.updateStatus( + applicationSubmission, + SUBMISSION_STATUS.WRONG_GOV, + null, + ); + await this.applicationSubmissionService.updateStatus( + applicationSubmission, + SUBMISSION_STATUS.INCOMPLETE, + ); + } else { + await this.applicationSubmissionService.updateStatus( + applicationSubmission, + SUBMISSION_STATUS.INCOMPLETE, + null, + ); + await this.applicationSubmissionService.updateStatus( + applicationSubmission, + SUBMISSION_STATUS.WRONG_GOV, + ); + } + } + private async getUserGovernmentOrFail(user: User) { const userGovernment = await this.getUserGovernment(user); if (!userGovernment) { From e6f2b76cc7a4e3389b069cfb44bfdb24e446d28a Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 20 Jul 2023 12:26:06 -0700 Subject: [PATCH 098/954] Code Review Feedback * Add test for deleteNonParcelOwners * Change to use submission uuid to not impact draft/non-draft submissions --- .../application-owner/application-owner.controller.ts | 6 ++++-- .../application-owner.service.spec.ts | 10 ++++++++++ .../application-owner/application-owner.service.ts | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts index 03b0576c78..4a900a8353 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts @@ -161,7 +161,7 @@ export class ApplicationOwnerController { //Create Owner if (!data.ownerUuid) { - await this.ownerService.deleteNonParcelOwners(applicationSubmission); + await this.ownerService.deleteNonParcelOwners(applicationSubmission.uuid); const newOwner = await this.ownerService.create( { email: data.email, @@ -198,7 +198,9 @@ export class ApplicationOwnerController { }); } else { //Delete Non parcel owners if we aren't using one - await this.ownerService.deleteNonParcelOwners(applicationSubmission); + await this.ownerService.deleteNonParcelOwners( + applicationSubmission.uuid, + ); } await this.ownerService.setPrimaryContact( diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts index 48ef867b65..b78ca5feb0 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts @@ -329,4 +329,14 @@ describe('ApplicationOwnerService', () => { 'A et al.', ); }); + + it('should load then delete non application owners', async () => { + mockRepo.find.mockResolvedValue([new ApplicationOwner()]); + mockRepo.remove.mockResolvedValue([] as any); + + await service.deleteNonParcelOwners('uuid'); + + expect(mockRepo.find).toHaveBeenCalledTimes(1); + expect(mockRepo.remove).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts index 51abcd1584..ddc745c66a 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts @@ -214,12 +214,12 @@ export class ApplicationOwnerService { }); } - async deleteNonParcelOwners(application: ApplicationSubmission) { + async deleteNonParcelOwners(submissionUuid: string) { const agentOwners = await this.repository.find({ where: [ { applicationSubmission: { - fileNumber: application.fileNumber, + uuid: submissionUuid, }, type: { code: APPLICATION_OWNER.AGENT, @@ -227,7 +227,7 @@ export class ApplicationOwnerService { }, { applicationSubmission: { - fileNumber: application.fileNumber, + uuid: submissionUuid, }, type: { code: APPLICATION_OWNER.GOVERNMENT, From ae96a15f0eabc49bf2d9d1422234318e6e101ed5 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 20 Jul 2023 12:39:11 -0700 Subject: [PATCH 099/954] Code Review Feedback --- .../review-and-submit.component.spec.ts | 5 ++ .../review-and-submit.component.ts | 53 +++++++++++++------ .../submit-confirmation-dialog.component.html | 5 +- ...bmit-confirmation-dialog.component.spec.ts | 8 ++- .../submit-confirmation-dialog.component.ts | 19 +++---- .../select-government.component.ts | 6 +-- 6 files changed, 59 insertions(+), 37 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts index c1072da656..69089069ef 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts @@ -6,6 +6,7 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../services/code/code.service'; import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; import { ToastService } from '../../../services/toast/toast.service'; @@ -47,6 +48,10 @@ describe('ReviewAndSubmitComponent', () => { provide: MatDialog, useValue: {}, }, + { + provide: CodeService, + useValue: {}, + }, ], }).compileComponents(); diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts index ba85493352..6c69c84fd8 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts @@ -5,6 +5,7 @@ import { BehaviorSubject, takeUntil } from 'rxjs'; import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../services/code/code.service'; import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; import { ToastService } from '../../../services/toast/toast.service'; import { StepComponent } from '../step.partial'; @@ -28,14 +29,15 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O private toastService: ToastService, private applicationService: ApplicationSubmissionService, private pdfGenerationService: PdfGenerationService, + private codeService: CodeService, private dialog: MatDialog ) { super(); } ngOnInit(): void { - this.$applicationSubmission.pipe(takeUntil(this.$destroy)).subscribe((app) => { - this.applicationSubmission = app; + this.$applicationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + this.applicationSubmission = submission; }); } @@ -50,22 +52,29 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O } async onSubmitToAlcs() { - const el = document.getElementsByClassName('error'); - if (el && el.length > 0) { - el[0].scrollIntoView({ - behavior: 'smooth', - block: 'center', - }); - this.toastService.showErrorToast('Please correct all errors before submitting the form'); - } else { - this.dialog - .open(SubmitConfirmationDialogComponent) - .beforeClosed() - .subscribe((didConfirm) => { - if (didConfirm) { - this.submit.emit(); - } + if (this.applicationSubmission) { + const el = document.getElementsByClassName('error'); + if (el && el.length > 0) { + el[0].scrollIntoView({ + behavior: 'smooth', + block: 'center', }); + this.toastService.showErrorToast('Please correct all errors before submitting the form'); + } else { + const governmentName = await this.loadGovernmentName(this.applicationSubmission.localGovernmentUuid); + this.dialog + .open(SubmitConfirmationDialogComponent, { + data: { + governmentName: governmentName, + }, + }) + .beforeClosed() + .subscribe((didConfirm) => { + if (didConfirm) { + this.submit.emit(); + } + }); + } } } @@ -74,4 +83,14 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O await this.pdfGenerationService.generateSubmission(fileNumber); } } + + private async loadGovernmentName(uuid: string) { + const codes = await this.codeService.loadCodes(); + const localGovernment = codes.localGovernments.find((a) => a.uuid === uuid); + if (localGovernment) { + return localGovernment.name; + } else { + return 'selected local / first nation government'; + } + } } diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html index 16b1d105f9..7ffc11dbaf 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html @@ -4,8 +4,9 @@

Submit Application

- Your application will be submitted to the [L/FNG]. After submission, you will be auto-directed to complete the local - or first nation government review form in order to submit this application to the Agricultural Land Commission. + Your application will be submitted to the {{ data.governmentName }}. After submission, you will be auto-directed to + complete the local or first nation government review form in order to submit this application to the Agricultural + Land Commission.

Terms and Conditions:
diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts index 0f7741443b..7d6d06ed68 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts @@ -1,10 +1,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { SubmitConfirmationDialogComponent } from './submit-confirmation-dialog.component'; -describe('OtherParcelConfirmationDialogComponent', () => { +describe('SubmitConfirmationDialogComponent', () => { let component: SubmitConfirmationDialogComponent; let fixture: ComponentFixture; @@ -20,6 +20,10 @@ describe('OtherParcelConfirmationDialogComponent', () => { provide: MatDialogRef, useValue: {}, }, + { + provide: MAT_DIALOG_DATA, + useValue: {}, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts index fc8779c3f5..cf84e55daa 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts @@ -1,5 +1,5 @@ -import { Component } from '@angular/core'; -import { MatDialogRef } from '@angular/material/dialog'; +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; @Component({ selector: 'app-submit-confirmation-dialog', @@ -7,13 +7,10 @@ import { MatDialogRef } from '@angular/material/dialog'; styleUrls: ['./submit-confirmation-dialog.component.scss'], }) export class SubmitConfirmationDialogComponent { - constructor(private dialogRef: MatDialogRef) {} - - async onCancel(dialogResult: boolean = false) { - this.dialogRef.close(dialogResult); - } - - async onSubmit() { - this.dialogRef.close(true); - } + constructor( + @Inject(MAT_DIALOG_DATA) + protected data: { + governmentName: string; + } + ) {} } diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts index 11ab4211a1..9d7ad511a5 100644 --- a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts +++ b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts @@ -30,11 +30,7 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, localGovernment: this.localGovernment, }); - constructor( - private codeService: CodeService, - private applicationSubmissionService: ApplicationSubmissionService, - private router: Router - ) { + constructor(private codeService: CodeService, private applicationSubmissionService: ApplicationSubmissionService) { super(); } From 4d3fb7f31a876a85100a4c2e37bd2a8652e0728b Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Thu, 20 Jul 2023 14:11:27 -0700 Subject: [PATCH 100/954] Show uneditable app info if app was submitted to lfng (#803) * Show uneditable app info if app was submitted to lfng * Use onChanges to handle input property changes * Add onChanges fn argument * Rename variable to clarify app status * Refactor code --- .../applicant-info.component.html | 5 ++-- .../applicant-info.component.ts | 19 +++++++++++---- .../application-details.component.html | 24 ++++++++++++------- .../application-details.component.ts | 14 ++++++++--- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.html b/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.html index fd5c9dff83..3bcbdddc5c 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.html @@ -4,8 +4,9 @@ [submission]="submission" [fileNumber]="fileNumber" [applicationType]="application.type.code" - [isSubmitted]='isSubmitted' - [showEdit]='true' + [isSubmittedToAlc]="isSubmittedToAlc" + [wasSubmittedToLfng]="wasSubmittedToLfng" + [showEdit]="true" >
diff --git a/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts b/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts index 8fd982baba..583d97e0c9 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts @@ -1,5 +1,4 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; import { DOCUMENT_TYPE } from '../../../services/application/application-document/application-document.service'; @@ -8,6 +7,7 @@ import { APPLICATION_SYSTEM_SOURCE_TYPES, ApplicationDto, ApplicationSubmissionDto, + SUBMISSION_STATUS, } from '../../../services/application/application.dto'; @Component({ @@ -22,7 +22,8 @@ export class ApplicantInfoComponent implements OnInit, OnDestroy { DOCUMENT_TYPE = DOCUMENT_TYPE; application: ApplicationDto | undefined; submission?: ApplicationSubmissionDto = undefined; - isSubmitted = false; + isSubmittedToAlc = false; + wasSubmittedToLfng = false; constructor( private applicationDetailService: ApplicationDetailService, @@ -36,8 +37,18 @@ export class ApplicantInfoComponent implements OnInit, OnDestroy { this.fileNumber = application.fileNumber; this.submission = await this.applicationSubmissionService.fetchSubmission(this.fileNumber); - this.isSubmitted = - application.source === APPLICATION_SYSTEM_SOURCE_TYPES.APPLICANT ? !!application.dateSubmittedToAlc : true; + const isApplicantSubmission = application.source === APPLICATION_SYSTEM_SOURCE_TYPES.APPLICANT; + + this.isSubmittedToAlc = isApplicantSubmission ? !!application.dateSubmittedToAlc : true; + + this.wasSubmittedToLfng = + isApplicantSubmission && + [ + SUBMISSION_STATUS.SUBMITTED_TO_LG, + SUBMISSION_STATUS.IN_REVIEW_BY_LG, + SUBMISSION_STATUS.WRONG_GOV, + SUBMISSION_STATUS.INCOMPLETE, + ].includes(this.submission?.status?.code); } }); } diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html index c95a6a337d..cda1a72718 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html @@ -7,7 +7,7 @@ >
-
@@ -17,7 +17,7 @@
-
@@ -63,13 +63,13 @@

Primary Contact Information

-
-
+

Land Use

@@ -130,11 +130,13 @@

Land Use of Adjacent Parcels

- +
-
+

Proposal

Proposal >
- +
-
+

Optional Documents

@@ -197,7 +201,9 @@

Optional Documents

No optional attachments
- +
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.ts index 420725db83..c176d6e37a 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { Subject } from 'rxjs'; import { environment } from '../../../../../environments/environment'; import { ApplicationDocumentDto } from '../../../../services/application/application-document/application-document.dto'; @@ -13,18 +13,21 @@ import { ApplicationSubmissionDto } from '../../../../services/application/appli templateUrl: './application-details.component.html', styleUrls: ['./application-details.component.scss'], }) -export class ApplicationDetailsComponent implements OnInit, OnDestroy { +export class ApplicationDetailsComponent implements OnInit, OnChanges, OnDestroy { $destroy = new Subject(); @Input() submission!: ApplicationSubmissionDto; @Input() applicationType!: string; @Input() fileNumber!: string; @Input() showEdit = false; - @Input() isSubmitted = true; + @Input() isSubmittedToAlc = true; + @Input() wasSubmittedToLfng = false; authorizationLetters: ApplicationDocumentDto[] = []; otherFiles: ApplicationDocumentDto[] = []; files: ApplicationDocumentDto[] | undefined; + disableEdit = false; + showFullApp = false; constructor(private applicationDocumentService: ApplicationDocumentService) {} @@ -32,6 +35,11 @@ export class ApplicationDetailsComponent implements OnInit, OnDestroy { this.loadDocuments(); } + ngOnChanges(changes: SimpleChanges): void { + this.disableEdit = this.wasSubmittedToLfng || !this.isSubmittedToAlc; + this.showFullApp = this.wasSubmittedToLfng || this.isSubmittedToAlc; + } + ngOnDestroy(): void { this.$destroy.next(); this.$destroy.complete(); From 9b77e7345eb4f31913460e9dfa3f1396ea7db7ed Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 20 Jul 2023 14:34:50 -0700 Subject: [PATCH 101/954] upload non-noi files to application folder --- bin/migrate-files/app_docs.sql | 72 ++++++++++++++++++++ bin/migrate-files/migrate-files.py | 19 +++++- bin/migrate-files/noi_docs.sql | 104 +++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 bin/migrate-files/app_docs.sql create mode 100644 bin/migrate-files/noi_docs.sql diff --git a/bin/migrate-files/app_docs.sql b/bin/migrate-files/app_docs.sql new file mode 100644 index 0000000000..15a6592c3a --- /dev/null +++ b/bin/migrate-files/app_docs.sql @@ -0,0 +1,72 @@ +DROP TABLE IF EXISTS oats.alcs_etl_app_docs; + +CREATE TABLE + oats.alcs_etl_app_docs ( + document_id INT, + document_code VARCHAR, + alr_application_id INT, + issue_id INT, + planning_review_id INT, + file_name VARCHAR, + "description" VARCHAR, + document_blob BYTEA, + referenced_in_staff_rpt_ind VARCHAR, + document_source_code VARCHAR, + subject_property_id INT, + publicly_viewable_ind VARCHAR, + app_lg_viewable_ind VARCHAR, + uploaded_date TIMESTAMP, + who_created VARCHAR, + when_created TIMESTAMP, + who_updated VARCHAR, + when_updated TIMESTAMP, + revision_count INT + ); + +INSERT INTO + oats.alcs_etl_app_docs ( + document_id, + document_code, + alr_application_id, + issue_id, + planning_review_id, + file_name, + "description", + document_blob, + referenced_in_staff_rpt_ind, + document_source_code, + subject_property_id, + publicly_viewable_ind, + app_lg_viewable_ind, + uploaded_date, + who_created, + when_created, + who_updated, + when_updated, + revision_count + ) +select + ad.document_id, + ad.document_code, + ad.alr_application_id, + ad.issue_id, + ad.planning_review_id, + ad.file_name, + ad."description", + ad.document_blob, + ad.referenced_in_staff_rpt_ind, + ad.document_source_code, + ad.subject_property_id, + ad.publicly_viewable_ind, + ad.app_lg_viewable_ind, + ad.uploaded_date, + ad.who_created, + ad.when_created, + ad.who_updated, + ad.when_updated, + ad.revision_count +from + oats.alcs_etl_noi_docs nd + right join oats.oats_documents ad on nd.document_id = ad.document_id +where + nd.document_id is null \ No newline at end of file diff --git a/bin/migrate-files/migrate-files.py b/bin/migrate-files/migrate-files.py index e3aef390aa..0ca8743905 100644 --- a/bin/migrate-files/migrate-files.py +++ b/bin/migrate-files/migrate-files.py @@ -54,10 +54,25 @@ count = cursor.fetchone()[0] print('Count =', count) + +with open( + "noi_docs.sql", "r", encoding="utf-8" +) as sql_file: + create_noi_table = sql_file.read() + cursor.execute(create_noi_table) +conn.commit() + +with open( + "app_docs.sql", "r", encoding="utf-8" +) as sql_file: + create_app_table = sql_file.read() + cursor.execute(create_app_table) +conn.commit() + # # Execute the SQL query to retrieve the BLOB data and key column cursor.execute(f""" SELECT DOCUMENT_ID, ALR_APPLICATION_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH -FROM OATS.OATS_DOCUMENTS +FROM OATS.ALCS_ETL_APP_DOCS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND DOCUMENT_ID > {starting_document_id} ORDER BY DOCUMENT_ID ASC @@ -82,7 +97,7 @@ tqdm.write(f"{application_id}/{document_id}_{filename}") with tqdm(total=length, unit="B", unit_scale=True, desc=filename) as pbar2: - s3.upload_fileobj(file, ecs_bucket, f"migrate/{application_id}/{document_id}_{filename}", + s3.upload_fileobj(file, ecs_bucket, f"migrate/application/{application_id}/{document_id}_{filename}", Callback=lambda bytes_transferred: pbar2.update(bytes_transferred),) pbar.update(1) last_document_id = document_id diff --git a/bin/migrate-files/noi_docs.sql b/bin/migrate-files/noi_docs.sql new file mode 100644 index 0000000000..c543831a15 --- /dev/null +++ b/bin/migrate-files/noi_docs.sql @@ -0,0 +1,104 @@ +DROP TABLE IF EXISTS oats.alcs_etl_noi_docs; + +CREATE TABLE + oats.alcs_etl_noi_docs ( + document_id INT, + document_code VARCHAR, + alr_application_id INT, + issue_id INT, + planning_review_id INT, + file_name VARCHAR, + "description" VARCHAR, + document_blob BYTEA, + referenced_in_staff_rpt_ind VARCHAR, + document_source_code VARCHAR, + subject_property_id INT, + publicly_viewable_ind VARCHAR, + app_lg_viewable_ind VARCHAR, + uploaded_date TIMESTAMP, + who_created VARCHAR, + when_created TIMESTAMP, + who_updated VARCHAR, + when_updated TIMESTAMP, + revision_count INT + ); + +INSERT INTO + oats.alcs_etl_noi_docs ( + document_id, + document_code, + alr_application_id, + issue_id, + planning_review_id, + file_name, + "description", + document_blob, + referenced_in_staff_rpt_ind, + document_source_code, + subject_property_id, + publicly_viewable_ind, + app_lg_viewable_ind, + uploaded_date, + who_created, + when_created, + who_updated, + when_updated, + revision_count + ) +WITH + noi AS ( + SELECT + od.alr_application_id AS app_id, + od.issue_id AS tissue_id + FROM + oats.oats_documents od + WHERE + document_code = 'NOI' + ), + appl AS ( + SELECT + * + FROM + oats.oats_documents od2 + JOIN noi ON od2.alr_application_id = noi.app_id + ), + issue AS ( + SELECT + * + FROM + oats.oats_documents od3 + JOIN noi ON od3.issue_id = noi.tissue_id + ), + noi_docs AS ( + SELECT + * + FROM + appl + UNION + SELECT + * + FROM + issue + ) +SELECT + nd.document_id, + nd.document_code, + nd.alr_application_id, + nd.issue_id, + nd.planning_review_id, + nd.file_name, + nd."description", + nd.document_blob, + nd.referenced_in_staff_rpt_ind, + nd.document_source_code, + nd.subject_property_id, + nd.publicly_viewable_ind, + nd.app_lg_viewable_ind, + nd.uploaded_date, + nd.who_created, + nd.when_created, + nd.who_updated, + nd.when_updated, + nd.revision_count +FROM + noi_docs nd \ No newline at end of file From 72552b00f0bea6e1085e6bc943b5efdb232474fc Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 20 Jul 2023 15:22:32 -0700 Subject: [PATCH 102/954] Start FNLG review when submitting to yourself * Immediately redirect to start review URL when submitting to your own LG * When creating a review if the primary contact is Government, copy over the contact details --- .../application-details.component.html | 5 +- .../edit-submission.component.spec.ts | 5 ++ .../edit-submission.component.ts | 30 +++++--- .../review-and-submit.component.ts | 12 ++-- .../submit-confirmation-dialog.component.html | 8 ++- .../submit-confirmation-dialog.component.ts | 1 + ...ation-submission-review.controller.spec.ts | 2 + ...pplication-submission-review.controller.ts | 72 ++++++++++++------- ...lication-submission-review.service.spec.ts | 4 +- .../application-submission-review.service.ts | 5 +- .../application-submission.service.ts | 2 +- 11 files changed, 93 insertions(+), 53 deletions(-) diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/application-details/application-details.component.html index 1acd593578..2f955132ec 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/application-details/application-details.component.html @@ -100,10 +100,7 @@

4. Government

>ALC.Portal@gov.bc.ca / 236-468-3342 - + You're logged in with a Business BCeID that is associated with the government selected above. You will have the opportunity to complete the local or first nation government review form immediately after this application is submitted. diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.spec.ts index 6261818bec..4c6636f950 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { RouterTestingModule } from '@angular/router/testing'; import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewService } from '../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; import { CodeService } from '../../services/code/code.service'; import { MatDialogModule } from '@angular/material/dialog'; @@ -38,6 +39,10 @@ describe('EditSubmissionComponent', () => { provide: PdfGenerationService, useValue: {}, }, + { + provide: ApplicationSubmissionReviewService, + useValue: {}, + }, ], imports: [RouterTestingModule, MatAutocompleteModule, MatDialogModule], schemas: [NO_ERRORS_SCHEMA], diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index 67d10f5442..dd8608f3b0 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -5,28 +5,29 @@ import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; import { ApplicationDocumentDto } from '../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; +import { APPLICATION_OWNER } from '../../services/application-owner/application-owner.dto'; +import { ApplicationSubmissionReviewService } from '../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionDetailedDto } from '../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { ApplicationTypeDto } from '../../services/code/code.dto'; import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; import { ToastService } from '../../services/toast/toast.service'; import { CustomStepperComponent } from '../../shared/custom-stepper/custom-stepper.component'; import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { scrollToElement } from '../../shared/utils/scroll-helper'; import { ChangeApplicationTypeDialogComponent } from './change-application-type-dialog/change-application-type-dialog.component'; import { LandUseComponent } from './land-use/land-use.component'; -import { NaruProposalComponent } from './proposal/naru-proposal/naru-proposal.component'; -import { NfuProposalComponent } from './proposal/nfu-proposal/nfu-proposal.component'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { OtherParcelsComponent } from './other-parcels/other-parcels.component'; import { ParcelDetailsComponent } from './parcel-details/parcel-details.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { NaruProposalComponent } from './proposal/naru-proposal/naru-proposal.component'; +import { NfuProposalComponent } from './proposal/nfu-proposal/nfu-proposal.component'; import { PfrsProposalComponent } from './proposal/pfrs-proposal/pfrs-proposal.component'; import { PofoProposalComponent } from './proposal/pofo-proposal/pofo-proposal.component'; import { RosoProposalComponent } from './proposal/roso-proposal/roso-proposal.component'; import { SubdProposalComponent } from './proposal/subd-proposal/subd-proposal.component'; -import { SelectGovernmentComponent } from './select-government/select-government.component'; import { TurProposalComponent } from './proposal/tur-proposal/tur-proposal.component'; -import { scrollToElement } from '../../shared/utils/scroll-helper'; +import { SelectGovernmentComponent } from './select-government/select-government.component'; export enum EditApplicationSteps { AppParcel = 0, @@ -82,7 +83,8 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit private toastService: ToastService, private overlayService: OverlaySpinnerService, private router: Router, - private pdfGenerationService: PdfGenerationService + private pdfGenerationService: PdfGenerationService, + private applicationReviewService: ApplicationSubmissionReviewService ) {} ngOnInit(): void { @@ -247,9 +249,19 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit } async onSubmit() { - if (this.applicationSubmission) { - await this.applicationSubmissionService.submitToAlcs(this.applicationSubmission.uuid); - await this.router.navigateByUrl(`/application/${this.applicationSubmission?.fileNumber}`); + const submission = this.applicationSubmission; + if (submission) { + await this.applicationSubmissionService.submitToAlcs(submission.uuid); + + const primaryContact = submission.owners.find((owner) => owner.uuid === submission?.primaryContactOwnerUuid); + if (primaryContact && primaryContact.type.code === APPLICATION_OWNER.GOVERNMENT) { + const review = await this.applicationReviewService.startReview(submission.fileNumber); + if (review) { + await this.router.navigateByUrl(`/application/${submission?.fileNumber}/review`); + } + } else { + await this.router.navigateByUrl(`/application/${submission?.fileNumber}`); + } } } } diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts index 6c69c84fd8..5331a49f0b 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts @@ -61,11 +61,12 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O }); this.toastService.showErrorToast('Please correct all errors before submitting the form'); } else { - const governmentName = await this.loadGovernmentName(this.applicationSubmission.localGovernmentUuid); + const government = await this.loadGovernment(this.applicationSubmission.localGovernmentUuid); this.dialog .open(SubmitConfirmationDialogComponent, { data: { - governmentName: governmentName, + governmentName: government?.name ?? 'selected local / first nation government', + userIsGovernment: government?.matchesUserGuid ?? false, }, }) .beforeClosed() @@ -84,13 +85,12 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O } } - private async loadGovernmentName(uuid: string) { + private async loadGovernment(uuid: string) { const codes = await this.codeService.loadCodes(); const localGovernment = codes.localGovernments.find((a) => a.uuid === uuid); if (localGovernment) { - return localGovernment.name; - } else { - return 'selected local / first nation government'; + return localGovernment; } + return; } } diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html index 7ffc11dbaf..493c5eb5d2 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html @@ -4,9 +4,11 @@

Submit Application

- Your application will be submitted to the {{ data.governmentName }}. After submission, you will be auto-directed to - complete the local or first nation government review form in order to submit this application to the Agricultural - Land Commission. + Your application will be submitted to the {{ data.governmentName }}. + After submission, you will be auto-directed to complete the local or first nation government review form in order + to submit this application to the Agricultural Land Commission. +

Terms and Conditions:
diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts index cf84e55daa..8fc05d73b8 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts +++ b/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts @@ -11,6 +11,7 @@ export class SubmitConfirmationDialogComponent { @Inject(MAT_DIALOG_DATA) protected data: { governmentName: string; + userIsGovernment: boolean; } ) {} } diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts index 066087443d..0d752399b0 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts @@ -21,6 +21,7 @@ import { DOCUMENT_SOURCE } from '../../document/document.dto'; import { Document } from '../../document/document.entity'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; +import { ApplicationOwnerType } from '../application-submission/application-owner/application-owner-type/application-owner-type.entity'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; import { ApplicationSubmissionValidatorService, @@ -243,6 +244,7 @@ describe('ApplicationSubmissionReviewController', () => { new ApplicationOwner({ uuid: 'uuid', email: 'fake-email', + type: new ApplicationOwnerType(), }), ], primaryContactOwnerUuid: 'uuid', diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index c64d4ce2c2..3448114556 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -24,6 +24,7 @@ import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.se import { DOCUMENT_SOURCE } from '../../document/document.dto'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; +import { APPLICATION_OWNER } from '../application-submission/application-owner/application-owner.dto'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; import { ApplicationSubmissionValidatorService } from '../application-submission/application-submission-validator.service'; import { ApplicationSubmission } from '../application-submission/application-submission.entity'; @@ -43,7 +44,7 @@ export class ApplicationSubmissionReviewController { constructor( private applicationSubmissionService: ApplicationSubmissionService, - private applicationReviewService: ApplicationSubmissionReviewService, + private applicationSubmissionReviewService: ApplicationSubmissionReviewService, private applicationDocumentService: ApplicationDocumentService, private localGovernmentService: ApplicationLocalGovernmentService, private applicationValidatorService: ApplicationSubmissionValidatorService, @@ -69,7 +70,9 @@ export class ApplicationSubmissionReviewController { } const applicationReview = - await this.applicationReviewService.getByFileNumber(fileNumber); + await this.applicationSubmissionReviewService.getByFileNumber( + fileNumber, + ); if (applicationReview.createdBy) { const reviewGovernment = await this.localGovernmentService.getByGuid( @@ -77,21 +80,21 @@ export class ApplicationSubmissionReviewController { ); if (reviewGovernment) { - return this.applicationReviewService.mapToDto( + return this.applicationSubmissionReviewService.mapToDto( applicationReview, reviewGovernment, ); } } - return this.applicationReviewService.mapToDto( + return this.applicationSubmissionReviewService.mapToDto( applicationReview, userLocalGovernment, ); } const applicationReview = - await this.applicationReviewService.getByFileNumber(fileNumber); + await this.applicationSubmissionReviewService.getByFileNumber(fileNumber); const applicationSubmission = await this.applicationSubmissionService.getByFileNumber( @@ -124,7 +127,7 @@ export class ApplicationSubmissionReviewController { throw new BaseServiceException('Failed to load Local Government'); } - return this.applicationReviewService.mapToDto( + return this.applicationSubmissionReviewService.mapToDto( applicationReview, matchingGovernment, ); @@ -153,13 +156,13 @@ export class ApplicationSubmissionReviewController { ); } - const applicationReview = await this.applicationReviewService.update( - fileNumber, - userLocalGovernment, - updateDto, - ); + const applicationReview = + await this.applicationSubmissionReviewService.update( + fileNumber, + updateDto, + ); - return this.applicationReviewService.mapToDto( + return this.applicationSubmissionReviewService.mapToDto( applicationReview, userLocalGovernment, ); @@ -177,10 +180,11 @@ export class ApplicationSubmissionReviewController { userLocalGovernment, ); - const applicationReview = await this.applicationReviewService.startReview( - applicationSubmission, - req.user.entity, - ); + const applicationReview = + await this.applicationSubmissionReviewService.startReview( + applicationSubmission, + req.user.entity, + ); await this.applicationSubmissionService.updateStatus( applicationSubmission, @@ -200,7 +204,24 @@ export class ApplicationSubmissionReviewController { ); } - return this.applicationReviewService.mapToDto( + if ( + primaryContact && + primaryContact.type.code === APPLICATION_OWNER.GOVERNMENT + ) { + //Copy contact details over to government form + await this.applicationSubmissionReviewService.update( + applicationSubmission.fileNumber, + { + firstName: primaryContact.firstName, + lastName: primaryContact.lastName, + email: primaryContact.email, + department: primaryContact.organizationName, + phoneNumber: primaryContact.phoneNumber, + }, + ); + } + + return this.applicationSubmissionReviewService.mapToDto( applicationReview, userLocalGovernment, ); @@ -258,7 +279,7 @@ export class ApplicationSubmissionReviewController { ); const applicationReview = - await this.applicationReviewService.getByFileNumber( + await this.applicationSubmissionReviewService.getByFileNumber( application.fileNumber, ); @@ -270,11 +291,12 @@ export class ApplicationSubmissionReviewController { applicationReview.applicationFileNumber, ); - const completedReview = this.applicationReviewService.verifyComplete( - applicationReview, - applicationDocuments, - userLocalGovernment.isFirstNation, - ); + const completedReview = + this.applicationSubmissionReviewService.verifyComplete( + applicationReview, + applicationDocuments, + userLocalGovernment.isFirstNation, + ); const validationResult = await this.applicationValidatorService.validateSubmission(application); @@ -326,7 +348,7 @@ export class ApplicationSubmissionReviewController { ); const applicationReview = - await this.applicationReviewService.getByFileNumber( + await this.applicationSubmissionReviewService.getByFileNumber( applicationSubmission.fileNumber, ); @@ -348,7 +370,7 @@ export class ApplicationSubmissionReviewController { await this.applicationDocumentService.delete(document); } - await this.applicationReviewService.delete(applicationReview); + await this.applicationSubmissionReviewService.delete(applicationReview); await this.applicationSubmissionStatusService.setStatusDate( applicationSubmission.uuid, diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.spec.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.spec.ts index be98c241a1..62e0f2ce73 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.spec.ts @@ -97,7 +97,7 @@ describe('ApplicationSubmissionReviewService', () => { mockRepository.findOneOrFail.mockResolvedValue(appReview); mockRepository.save.mockResolvedValue({} as any); - const res = await service.update('', mockLocalGovernment, {}); + const res = await service.update('', {}); expect(res).toBeDefined(); expect(mockRepository.save).toHaveBeenCalledTimes(1); @@ -112,7 +112,7 @@ describe('ApplicationSubmissionReviewService', () => { mockRepository.save.mockResolvedValue({} as any); mockAppService.getUuid.mockResolvedValue(''); - const res = await service.update('', mockLocalGovernment, { + const res = await service.update('', { isOCPDesignation: false, isSubjectToZoning: false, }); diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.ts index da5a5c9d7a..e0753d92e1 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.ts @@ -41,9 +41,9 @@ export class ApplicationSubmissionReviewService { }); } - async startReview(application: ApplicationSubmission, createdBy: User) { + async startReview(submission: ApplicationSubmission, createdBy: User) { const applicationReview = new ApplicationSubmissionReview({ - applicationFileNumber: application.fileNumber, + applicationFileNumber: submission.fileNumber, createdBy, }); return await this.applicationSubmissionReviewRepository.save( @@ -53,7 +53,6 @@ export class ApplicationSubmissionReviewService { async update( fileNumber: string, - localGovernment: ApplicationLocalGovernment, updateDto: UpdateApplicationSubmissionReviewDto, ) { const applicationReview = await this.getByFileNumber(fileNumber); diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index 8410a7ea9b..871274d24a 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -188,7 +188,7 @@ export class ApplicationSubmissionService { } async submitToLg(submission: ApplicationSubmission) { - this.updateStatus(submission, SUBMISSION_STATUS.SUBMITTED_TO_LG); + await this.updateStatus(submission, SUBMISSION_STATUS.SUBMITTED_TO_LG); } async updateStatus( From 35c540682bbb0c767fef9c9020963c188df47651 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Fri, 21 Jul 2023 10:02:06 -0700 Subject: [PATCH 103/954] Feature/alcs 879 (#805) no data on decision v2 fields no conditions no components --- .../condition/condition.component.html | 8 ++-- .../conditions/conditions.component.html | 3 ++ .../conditions/conditions.component.scss | 5 +++ .../basic/basic.component.html | 2 + .../naru/naru.component.html | 3 ++ .../nfup/nfup.component.html | 3 ++ .../pfrs/pfrs.component.html | 11 +++++ .../pofo/pofo.component.html | 6 +++ .../roso/roso.component.html | 6 +++ .../turp/turp.component.html | 1 + .../decision-v2/decision-v2.component.html | 45 ++++++++++++++----- .../decision-v2/decision-v2.component.scss | 5 +++ .../app/shared/no-data/no-data.component.html | 2 +- .../app/shared/no-data/no-data.component.scss | 3 +- 14 files changed, 87 insertions(+), 16 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html index c211c1c758..a4b9466b4c 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html @@ -20,18 +20,19 @@

{{ condition.type.label }}

Approval Dependent
{{ condition.approvalDependant | booleanToString }} +
Security Amount
{{ condition.securityAmount }} - +
Admin Fee
{{ condition.administrativeFee }} - +
@@ -55,8 +56,7 @@

{{ condition.type.label }}

{{ condition.description }} - - +
+
+ +
Ag Cap Map
{{ component.agCapMap }} +
Ag Cap Consultant
{{ component.agCapConsultant }} +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.html index c5b1b488c0..32fd73fb85 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.html @@ -5,14 +5,17 @@
Residential Use Type
{{ component.naruSubtype?.label }} +
Expiry Date
{{ component.expiryDate | date }} +
Use End Date
{{ component.endDate | date }} +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.html index 17ca43885d..f1651e9ac5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.html @@ -4,12 +4,15 @@
Non-Farm Use Type
{{ component.nfuType }} +
Non-Farm Use Sub-Type
{{ component.nfuSubType }} +
Use End Date
{{ component.endDate | date }} +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html index 9ab242d759..076a4c03bf 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html @@ -9,15 +9,18 @@
Use End Date
{{ component.endDate | date }} +
Type of soil approved to be removed.
{{ component.soilTypeRemoved }}
+
Type, origin and quality of fill proposed to be placed.
{{ component.soilFillTypeToPlace }}
+
@@ -30,7 +33,9 @@ + + + + + + + +
Volume {{ component.soilToRemoveVolume }} m3 {{ component.soilToPlaceVolume }} m3
@@ -38,17 +43,23 @@
Note: 0.01 ha is 100m2
{{ component.soilToRemoveArea }} ha {{ component.soilToPlaceArea }} ha
Maximum Depth {{ component.soilToRemoveMaximumDepth }} m {{ component.soilToPlaceMaximumDepth }} m
Average Depth {{ component.soilToRemoveAverageDepth }} m {{ component.soilToPlaceAverageDepth }} m
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html index ebd7029307..9cda6ebd15 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html @@ -9,11 +9,13 @@
Use End Date
{{ component.endDate | date }} +
Type, origin and quality of fill proposed to be placed.
{{ component.soilFillTypeToPlace }}
+
@@ -25,6 +27,7 @@ + + + +
Volume {{ component.soilToPlaceVolume }} m3
@@ -32,14 +35,17 @@
Note: 0.01 ha is 100m2
{{ component.soilToPlaceArea }} ha
Maximum Depth {{ component.soilToPlaceMaximumDepth }} m
Average Depth {{ component.soilToPlaceAverageDepth }} m
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html index d23dc50c2b..14ae67a69d 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html @@ -9,11 +9,13 @@
Use End Date
{{ component.endDate | date }} +
Type of soil approved to be removed.
{{ component.soilTypeRemoved }}
+
@@ -25,18 +27,22 @@ + + + +
Volume {{ component.soilToRemoveVolume }} m3
Area {{ component.soilToRemoveArea }} ha
Maximum Depth {{ component.soilToRemoveMaximumDepth }} m
Average Depth {{ component.soilToRemoveAverageDepth }} m
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.html index d4a34841d6..d6fc3ab2e5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.html @@ -9,5 +9,6 @@
Expiry Date
{{ component.expiryDate | date }} +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html index e5f6fdde97..1e62004cb3 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html @@ -98,10 +98,14 @@

Condition Compliance

Decision Date
{{ decision.date | momentFormat }} +
Decision Maker
- {{ decision.decisionMaker ? decision.decisionMaker.label : '(Unset)' }} + {{ decision.decisionMaker?.label }} +
@@ -123,7 +127,10 @@

Condition Compliance

Modification Outcome Reconsideration Outcome
- {{ decision.linkedResolutionOutcome ? decision.linkedResolutionOutcome.label : 'No Data' }} + {{ decision.linkedResolutionOutcome?.label }} + {{ decision.modifies?.linkedResolutions?.join(', ') }} {{ decision.reconsiders?.linkedResolutions?.join(', ') }}
@@ -131,13 +138,16 @@

Condition Compliance

Decision Description
- {{ decision.decisionDescription ? decision.decisionDescription : 'No Data' }} + {{ decision.decisionDescription }} +
Days To Hide From Public
- {{ decision.daysHideFromPublic ? decision.daysHideFromPublic : 'No Data' }} + {{ decision.daysHideFromPublic }}
Stats Required
@@ -149,13 +159,18 @@

Condition Compliance

Rescinded Date
{{ decision.rescindedDate | momentFormat }} - No Data +
Rescinded Comment
- {{ decision.rescindedComment ? decision.rescindedComment : 'No Data' }} + {{ decision.rescindedComment }} +
@@ -167,11 +182,17 @@

Condition Compliance

-
+
Decision Components and Conditions
- +
{{ component.applicationDecisionComponentType?.label }} Component @@ -188,10 +209,14 @@
Decision Components and Conditions
Agri Cap
{{ component.agCap }} +
Agri Cap Source
{{ component.agCapSource }} +
@@ -235,7 +260,7 @@
Decision Components and Conditions

Conditions

@@ -266,7 +291,7 @@

Conditions

Chair Review Outcome
{{ decision.chairReviewOutcome.label }} - No Data +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss index 501bd5b8e1..3700bfe1df 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss @@ -83,6 +83,11 @@ font-size: 16px; } + .components-wrapper, + .conditions-wrapper { + margin-bottom: 36px; + } + .subheading2 { margin-bottom: 6px !important; } diff --git a/alcs-frontend/src/app/shared/no-data/no-data.component.html b/alcs-frontend/src/app/shared/no-data/no-data.component.html index ad21442101..f9145f7327 100644 --- a/alcs-frontend/src/app/shared/no-data/no-data.component.html +++ b/alcs-frontend/src/app/shared/no-data/no-data.component.html @@ -1,5 +1,5 @@
-
No Data
+
{{ text ? text : 'No Data' }}
warning Required Field diff --git a/alcs-frontend/src/app/shared/no-data/no-data.component.scss b/alcs-frontend/src/app/shared/no-data/no-data.component.scss index 6f25dbaad2..f3f71b5650 100644 --- a/alcs-frontend/src/app/shared/no-data/no-data.component.scss +++ b/alcs-frontend/src/app/shared/no-data/no-data.component.scss @@ -1,7 +1,8 @@ @use '../../../styles/colors'; .text { - color: colors.$grey-dark; + color: colors.$grey; + font-weight: 400; } .error { From 3a7b491325a4ac373467f54aad1d3bd2470601d4 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 21 Jul 2023 13:06:42 -0700 Subject: [PATCH 104/954] revamped logic --- bin/migrate-files/migrate-files.py | 185 +++++++++++++++++++++++++---- 1 file changed, 164 insertions(+), 21 deletions(-) diff --git a/bin/migrate-files/migrate-files.py b/bin/migrate-files/migrate-files.py index 0ca8743905..f117a06971 100644 --- a/bin/migrate-files/migrate-files.py +++ b/bin/migrate-files/migrate-files.py @@ -39,40 +39,45 @@ with open('last-file.pickle', 'rb') as file: starting_document_id = pickle.load(file) -print('Starting from:', starting_document_id) +print('Starting applications from:', starting_document_id) + +starting_planning_document_id = 0 +# Determine job resume status +if os.path.isfile('last-planning-file.pickle'): + with open('last-planning-file.pickle', 'rb') as file: + starting_planning_document_id = pickle.load(file) + #ignore imported files before failure + starting_document_id = 999999 + +print('Starting planning_review from:', starting_planning_document_id) + +starting_issue_document_id = 0 +# Determine job resume status +if os.path.isfile('last-issue-file.pickle'): + with open('last-issue-file.pickle', 'rb') as file: + starting_issue_document_id = pickle.load(file) + #ignore imported files before failure + starting_document_id = 999999 + starting_planning_document_id = 999999 +print('Starting issues from:', starting_issue_document_id) # Get total number of files cursor = conn.cursor() try: - cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0') + cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND ALR_APPLICATION_ID IS NOT NULL') except cx_Oracle.Error as e: error, = e.args print(error.message) -count = cursor.fetchone()[0] -print('Count =', count) - - -with open( - "noi_docs.sql", "r", encoding="utf-8" -) as sql_file: - create_noi_table = sql_file.read() - cursor.execute(create_noi_table) -conn.commit() - -with open( - "app_docs.sql", "r", encoding="utf-8" -) as sql_file: - create_app_table = sql_file.read() - cursor.execute(create_app_table) -conn.commit() +application_count = cursor.fetchone()[0] +print('Count =', application_count) # # Execute the SQL query to retrieve the BLOB data and key column cursor.execute(f""" SELECT DOCUMENT_ID, ALR_APPLICATION_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH -FROM OATS.ALCS_ETL_APP_DOCS +FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND DOCUMENT_ID > {starting_document_id} ORDER BY DOCUMENT_ID ASC @@ -84,6 +89,7 @@ # Track progress documents_processed = 0 last_document_id = 0 +max_file = 0 try: with tqdm(total=count, unit="file", desc="Uploading files to S3") as pbar: @@ -102,6 +108,9 @@ pbar.update(1) last_document_id = document_id documents_processed += 1 + max_file += 1 + if max_file > 4: + break except Exception as e: print("Something went wrong:",e) print("Processed", documents_processed, "files") @@ -117,6 +126,140 @@ # Display results print("Process complete: Successfully migrated", documents_processed, "files.") +# Close the database cursor and connection +# cursor.close() +# conn.close() + +# cursor = conn.cursor() + +try: + cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND PLANNING_REVIEW_ID IS NOT NULL') +except cx_Oracle.Error as e: + error, = e.args + print(error.message) + +planning_review_count = cursor.fetchone()[0] +print('Count =', planning_review_count) + +# # Execute the SQL query to retrieve the BLOB data and key column +cursor.execute(f""" +SELECT DOCUMENT_ID, PLANNING_REVIEW_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH +FROM OATS.OATS_DOCUMENTS +WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 +AND DOCUMENT_ID > {starting_planning_document_id} +ORDER BY DOCUMENT_ID ASC +""") + +# Set the batch size +BATCH_SIZE = 10 + +# Track progress +documents_processed = 0 +last_planning_document_id = 0 +max_file = 0 + +try: + with tqdm(total=planning_review_count, unit="file", desc="Uploading files to S3") as pbar: + while True: + # Fetch the next batch of BLOB data + data = cursor.fetchmany(BATCH_SIZE) + if not data: + break + # Upload the batch to S3 with a progress bar + for document_id, planning_review_id, filename, file, code, description, source, created, updated, revision, length in data: + tqdm.write(f"{planning_review_id}/{document_id}_{filename}") + + with tqdm(total=length, unit="B", unit_scale=True, desc=filename) as pbar2: + s3.upload_fileobj(file, ecs_bucket, f"migrate/planning_review/{planning_review_id}/{document_id}_{filename}", + Callback=lambda bytes_transferred: pbar2.update(bytes_transferred),) + pbar.update(1) + last_planning_document_id = document_id + documents_processed += 1 + max_file += 1 + if max_file > 4: + break +except Exception as e: + print("Something went wrong:",e) + print("Processed", documents_processed, "files") + + # Set resume status in case of interuption + with open('last-planning-file.pickle', 'wb') as file: + pickle.dump(last_planning_document_id, file) + + cursor.close() + conn.close() + exit() + +# Display results +print("Process complete: Successfully migrated", documents_processed, "files.") + +# Close the database cursor and connection +# cursor.close() +# conn.close() + +# cursor = conn.cursor() + +try: + cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND ISSUE_ID IS NOT NULL') +except cx_Oracle.Error as e: + error, = e.args + print(error.message) + +issue_count = cursor.fetchone()[0] +print('Count =', issue_count) + +# # Execute the SQL query to retrieve the BLOB data and key column +cursor.execute(f""" +SELECT DOCUMENT_ID, ISSUE_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH +FROM OATS.OATS_DOCUMENTS +WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 +AND DOCUMENT_ID > {starting_issue_document_id} +ORDER BY DOCUMENT_ID ASC +""") + +# Set the batch size +BATCH_SIZE = 10 + +# Track progress +documents_processed = 0 +last_issue_document_id = 0 +max_file = 0 + +try: + with tqdm(total=issue_count, unit="file", desc="Uploading files to S3") as pbar: + while True: + # Fetch the next batch of BLOB data + data = cursor.fetchmany(BATCH_SIZE) + if not data: + break + # Upload the batch to S3 with a progress bar + for document_id, issue_id, filename, file, code, description, source, created, updated, revision, length in data: + tqdm.write(f"{issue_id}/{document_id}_{filename}") + + with tqdm(total=length, unit="B", unit_scale=True, desc=filename) as pbar2: + s3.upload_fileobj(file, ecs_bucket, f"migrate/issue/{issue_id}/{document_id}_{filename}", + Callback=lambda bytes_transferred: pbar2.update(bytes_transferred),) + pbar.update(1) + last_issue_document_id = document_id + documents_processed += 1 + max_file += 1 + if max_file > 4: + break +except Exception as e: + print("Something went wrong:",e) + print("Processed", documents_processed, "files") + + # Set resume status in case of interuption + with open('last-issue-file.pickle', 'wb') as file: + pickle.dump(last_issue_document_id, file) + + cursor.close() + conn.close() + exit() + +# Display results +print("Process complete: Successfully migrated", documents_processed, "files.") + # Close the database cursor and connection cursor.close() -conn.close() +conn.close() \ No newline at end of file From 53ac5f646b5e3f5023c0efc772fdcf40f712fce6 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 21 Jul 2023 13:16:24 -0700 Subject: [PATCH 105/954] Add Prescribed Body step to Create & Change Dialogs * Add new step to both create and change application dialogs * Hardcode government options * Send to frontend whether use isFirstNation or isLocalGovernment --- .../create-application-dialog.component.html | 149 ++++++++++-------- .../create-application-dialog.component.ts | 21 ++- ...nge-application-type-dialog.component.html | 13 +- ...hange-application-type-dialog.component.ts | 24 ++- .../application-submission.dto.ts | 3 + .../application-submission.service.ts | 3 +- .../authentication/authentication.dto.ts | 2 + .../presribed-body.component.html | 23 +++ .../presribed-body.component.scss | 8 + .../presribed-body.component.spec.ts | 36 +++++ .../presribed-body.component.ts | 77 +++++++++ .../src/app/shared/shared.module.ts | 32 ++-- portal-frontend/src/styles/typography.scss | 1 + .../alcs/src/user/user.controller.spec.ts | 2 + .../apps/alcs/src/user/user.controller.ts | 3 + services/apps/alcs/src/user/user.dto.ts | 2 + services/apps/alcs/src/user/user.service.ts | 1 + 17 files changed, 317 insertions(+), 83 deletions(-) create mode 100644 portal-frontend/src/app/shared/presribed-body/presribed-body.component.html create mode 100644 portal-frontend/src/app/shared/presribed-body/presribed-body.component.scss create mode 100644 portal-frontend/src/app/shared/presribed-body/presribed-body.component.spec.ts create mode 100644 portal-frontend/src/app/shared/presribed-body/presribed-body.component.ts diff --git a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html index c48fb7c1e3..8b840722d3 100644 --- a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html +++ b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html @@ -1,81 +1,94 @@
-

Create New

+

Create New

+

+ The governmental or prescribed public body that is applying to exclude land: +

-
- Select an option to learn more about the {{ currentStepIndex === 0 ? 'submission type' : 'application type' }}. -
-
- - - {{ subType.label }} - - -
-
- - - {{ appType.portalLabel }} - - -
- -
-
- {{ - selectedSubmissionType?.label - }} - -
+ +
+ Select an option to learn more about the {{ currentStepIndex === 0 ? 'submission type' : 'application type' }}. +
+
+ + + {{ subType.label }} + +
-
- {{ selectedAppType?.portalLabel }} + + + {{ appType.portalLabel }} + +
+
- Read {{ readMoreClicked ? 'Less' : 'More' }} -
+
+ {{ + selectedSubmissionType?.label + }} + +
+
+
+ {{ selectedAppType?.portalLabel }} +
+
+ Read {{ readMoreClicked ? 'Less' : 'More' }} +
+ + + +
-
+
@@ -88,7 +101,19 @@

Create New

(click)="onSubmit()" [disabled]="!selectedAppType || !selectedSubmissionType" > - create + next + create + +
+
+ +
diff --git a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts index 3ce1bf8725..6a59ee5472 100644 --- a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts +++ b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts @@ -10,6 +10,7 @@ import { scrollToElement } from '../../shared/utils/scroll-helper'; export enum ApplicationCreateDialogStepsEnum { submissionType = 0, applicationType = 1, + prescribedBody = 2, } @Component({ @@ -21,6 +22,7 @@ export enum ApplicationCreateDialogStepsEnum { export class CreateApplicationDialogComponent implements OnInit, AfterViewChecked { submissionStep = ApplicationCreateDialogStepsEnum.submissionType; applicationStep = ApplicationCreateDialogStepsEnum.applicationType; + prescribedBodyStep = ApplicationCreateDialogStepsEnum.prescribedBody; applicationTypes: ApplicationTypeDto[] = []; selectedAppType: ApplicationTypeDto | undefined = undefined; @@ -31,6 +33,7 @@ export class CreateApplicationDialogComponent implements OnInit, AfterViewChecke readMoreClicked: boolean = false; isReadMoreVisible: boolean = false; currentStepIndex: number = 0; + prescribedBody: string | undefined; constructor( private dialogRef: MatDialogRef, @@ -61,7 +64,19 @@ export class CreateApplicationDialogComponent implements OnInit, AfterViewChecke } async onSubmit() { - const res = await this.applicationService.create(this.selectedAppType!.code); + if (this.selectedAppType! && ['INCL', 'EXCL'].includes(this.selectedAppType!.code)) { + this.currentStepIndex++; + } else { + await this.createApplication(); + } + } + + async onSubmitInlcExcl() { + await this.createApplication(); + } + + private async createApplication() { + const res = await this.applicationService.create(this.selectedAppType!.code, this.prescribedBody); if (res) { await this.router.navigateByUrl(`/application/${res.fileId}/edit`); this.dialogRef.close(true); @@ -111,4 +126,8 @@ export class CreateApplicationDialogComponent implements OnInit, AfterViewChecke return true; } } + + onSelectPrescribedBody(name: string) { + this.prescribedBody = name; + } } diff --git a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html b/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html index 1aa97808ef..001d0a0494 100644 --- a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html +++ b/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html @@ -53,12 +53,16 @@

Change Application Type

Are you sure you want to change your application type?
+ + + +
- +
@@ -78,4 +82,11 @@
Are you sure you want to change your application type?
+ +
+ + +
diff --git a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts b/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts index 8e8916ce50..a39aeae077 100644 --- a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts +++ b/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts @@ -9,7 +9,8 @@ import { scrollToElement } from '../../../shared/utils/scroll-helper'; export enum ApplicationChangeTypeStepsEnum { warning = 0, applicationType = 1, - confirmation = 2, + prescribedBody = 2, + confirmation = 3, } @Component({ @@ -30,8 +31,10 @@ export class ChangeApplicationTypeDialogComponent implements OnInit, AfterViewCh warningStep = ApplicationChangeTypeStepsEnum.warning; applicationTypeStep = ApplicationChangeTypeStepsEnum.applicationType; confirmationStep = ApplicationChangeTypeStepsEnum.confirmation; + prescribedBodyStep = ApplicationChangeTypeStepsEnum.prescribedBody; stepIdx = 0; + prescribedBody: string | undefined; constructor( private dialogRef: MatDialogRef, @@ -59,16 +62,17 @@ export class ChangeApplicationTypeDialogComponent implements OnInit, AfterViewCh .sort((a, b) => (a.portalLabel > b.portalLabel ? 1 : -1)); } - async onCancel(dialogResult: boolean = false) { + async closeDialog(dialogResult: boolean = false) { this.dialogRef.close(dialogResult); } async onSubmit() { const result = await this.applicationSubmissionService.updatePending(this.submissionUuid, { typeCode: this.selectedAppType!.code, + prescribedBody: this.prescribedBody, }); if (result) { - this.onCancel(true); + await this.closeDialog(true); } } @@ -101,10 +105,22 @@ export class ChangeApplicationTypeDialogComponent implements OnInit, AfterViewCh } async next() { - this.stepIdx += 1; + if (this.stepIdx === ApplicationChangeTypeStepsEnum.applicationType) { + if (this.selectedAppType && ['INCL', 'EXCL'].includes(this.selectedAppType.code)) { + this.stepIdx = ApplicationChangeTypeStepsEnum.prescribedBody; + } else { + this.stepIdx = ApplicationChangeTypeStepsEnum.confirmation; + } + } else { + this.stepIdx += 1; + } } async back() { this.stepIdx -= 1; } + + onSelectPrescribedBody(name: string) { + this.prescribedBody = name; + } } diff --git a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts index c852ad723e..39f4700d61 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts @@ -243,4 +243,7 @@ export interface ApplicationSubmissionUpdateDto { naruToPlaceAverageDepth?: number | null; naruSleepingUnits?: number | null; naruAgriTourism?: string | null; + + //Inclusion / Exclusion Fields + prescribedBody?: string | null; } diff --git a/portal-frontend/src/app/services/application-submission/application-submission.service.ts b/portal-frontend/src/app/services/application-submission/application-submission.service.ts index 1f31776d7f..0a48f3faa2 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.service.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.service.ts @@ -54,12 +54,13 @@ export class ApplicationSubmissionService { } } - async create(type: string) { + async create(type: string, prescribedBody?: string) { try { this.overlayService.showSpinner(); return await firstValueFrom( this.httpClient.post<{ fileId: string }>(`${this.serviceUrl}`, { type, + prescribedBody, }) ); } catch (e) { diff --git a/portal-frontend/src/app/services/authentication/authentication.dto.ts b/portal-frontend/src/app/services/authentication/authentication.dto.ts index c62bc621f6..0ca335f035 100644 --- a/portal-frontend/src/app/services/authentication/authentication.dto.ts +++ b/portal-frontend/src/app/services/authentication/authentication.dto.ts @@ -7,4 +7,6 @@ export interface UserDto { bceidUserName?: string | null; prettyName?: string | null; government?: string; + isLocalGovernment: boolean; + isFirstNationGovernment: boolean; } diff --git a/portal-frontend/src/app/shared/presribed-body/presribed-body.component.html b/portal-frontend/src/app/shared/presribed-body/presribed-body.component.html new file mode 100644 index 0000000000..6ca6072e75 --- /dev/null +++ b/portal-frontend/src/app/shared/presribed-body/presribed-body.component.html @@ -0,0 +1,23 @@ +

Individual landowners may not submit exclusion applications to the ALC.

+
+ + {{ body.label }} + +
+ + This option is selected because your BCeID is associated with a local government. + + + This option is selected because your BCeID is associated with a first nation government. + + + In order to proceed, you must either use a BCeID that is associated with a {{ selectedValue }} or change your + selection above. + diff --git a/portal-frontend/src/app/shared/presribed-body/presribed-body.component.scss b/portal-frontend/src/app/shared/presribed-body/presribed-body.component.scss new file mode 100644 index 0000000000..fcb5d63cc2 --- /dev/null +++ b/portal-frontend/src/app/shared/presribed-body/presribed-body.component.scss @@ -0,0 +1,8 @@ +@use '../../../styles/functions' as *; + +.radio-group { + display: flex; + flex-direction: column; + margin: rem(8) 0; + align-items: flex-start; +} diff --git a/portal-frontend/src/app/shared/presribed-body/presribed-body.component.spec.ts b/portal-frontend/src/app/shared/presribed-body/presribed-body.component.spec.ts new file mode 100644 index 0000000000..3c301dfc04 --- /dev/null +++ b/portal-frontend/src/app/shared/presribed-body/presribed-body.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { UserDto } from '../../services/authentication/authentication.dto'; +import { AuthenticationService } from '../../services/authentication/authentication.service'; + +import { PresribedBodyComponent } from './presribed-body.component'; + +describe('PresribedBodyComponent', () => { + let component: PresribedBodyComponent; + let fixture: ComponentFixture; + let mockAuthService: DeepMocked; + + beforeEach(async () => { + mockAuthService = createMock(); + mockAuthService.$currentProfile = new BehaviorSubject(undefined); + + await TestBed.configureTestingModule({ + declarations: [PresribedBodyComponent], + providers: [ + { + provide: AuthenticationService, + useValue: mockAuthService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(PresribedBodyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/shared/presribed-body/presribed-body.component.ts b/portal-frontend/src/app/shared/presribed-body/presribed-body.component.ts new file mode 100644 index 0000000000..0fd431de42 --- /dev/null +++ b/portal-frontend/src/app/shared/presribed-body/presribed-body.component.ts @@ -0,0 +1,77 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { AuthenticationService } from '../../services/authentication/authentication.service'; + +const FIRST_NATION = 'First Nation Government'; +const LOCAL = 'Local Government'; + +@Component({ + selector: 'app-presribed-body', + templateUrl: './presribed-body.component.html', + styleUrls: ['./presribed-body.component.scss'], +}) +export class PresribedBodyComponent implements OnInit { + @Output() select = new EventEmitter(); + + PRESCRIBED_BODIES = [ + 'Regional Health Board (as per s.4(1) Health Authorities Act)', + 'Educational Body (as per the Freedom of Information and Protection of Privacy Act)', + 'An Improvement District (as per the Local Government Act)', + 'The BC Transportation Financing Authority', + 'The BC Housing Management Commission', + 'The BC Hydro and Power Authority', + 'The South Coast BC Transportation Authority', + 'The BC Transit Corporation', + 'The Columbia Power Corporation', + 'The Province of BC', + FIRST_NATION, + LOCAL, + ]; + prescribedBodies: { label: string; enabled: boolean; selected: boolean }[] = []; + isLocalGovernmentUser = false; + isFirstNationUser = false; + showGovernmentWarning = false; + selectedValue: string | undefined; + + constructor(private authenticationService: AuthenticationService) {} + + ngOnInit(): void { + this.prescribedBodies = this.PRESCRIBED_BODIES.map((body) => ({ + label: body, + enabled: true, + selected: false, + })); + + this.authenticationService.$currentProfile.subscribe((profile) => { + if (profile && profile.government) { + this.isLocalGovernmentUser = profile.isLocalGovernment; + this.isFirstNationUser = profile.isFirstNationGovernment; + + if (this.isLocalGovernmentUser) { + this.onSelect(LOCAL); + } else { + this.onSelect(FIRST_NATION); + } + + this.prescribedBodies = this.PRESCRIBED_BODIES.map((body) => ({ + label: body, + enabled: body === this.selectedValue, + selected: body === this.selectedValue, + })); + } + }); + } + + onSelect(chosenValue: string) { + this.selectedValue = chosenValue; + if ( + (chosenValue === LOCAL && !this.isLocalGovernmentUser) || + (chosenValue === FIRST_NATION && !this.isFirstNationUser) + ) { + this.showGovernmentWarning = true; + this.select.emit(undefined); + } else { + this.showGovernmentWarning = false; + this.select.emit(this.selectedValue); + } + } +} diff --git a/portal-frontend/src/app/shared/shared.module.ts b/portal-frontend/src/app/shared/shared.module.ts index 1a229de1cf..3d55310071 100644 --- a/portal-frontend/src/app/shared/shared.module.ts +++ b/portal-frontend/src/app/shared/shared.module.ts @@ -35,6 +35,7 @@ import { EmailValidPipe } from './pipes/emailValid.pipe'; import { FileSizePipe } from './pipes/fileSize.pipe'; import { MomentPipe } from './pipes/moment.pipe'; import { PhoneValidPipe } from './pipes/phoneValid.pipe'; +import { PresribedBodyComponent } from './presribed-body/presribed-body.component'; import { UpdatedBannerComponent } from './updated-banner/updated-banner.component'; import { DATE_FORMATS } from './utils/date-format'; import { ValidationErrorComponent } from './validation-error/validation-error.component'; @@ -57,6 +58,22 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen CdkStepperModule, NgxMaskDirective, NgxMaskPipe, + MatRadioModule, + ], + declarations: [ + FileDragDropComponent, + FileSizePipe, + EmailValidPipe, + PhoneValidPipe, + DragDropDirective, + WarningBannerComponent, + InfoBannerComponent, + NoDataComponent, + UpdatedBannerComponent, + ValidationErrorComponent, + CustomStepperComponent, + MomentPipe, + PresribedBodyComponent, ], exports: [ CommonModule, @@ -98,20 +115,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen NgxMaskDirective, NgxMaskPipe, MomentPipe, - ], - declarations: [ - FileDragDropComponent, - FileSizePipe, - EmailValidPipe, - PhoneValidPipe, - DragDropDirective, - WarningBannerComponent, - InfoBannerComponent, - NoDataComponent, - UpdatedBannerComponent, - ValidationErrorComponent, - CustomStepperComponent, - MomentPipe, + PresribedBodyComponent, ], }) export class SharedModule { diff --git a/portal-frontend/src/styles/typography.scss b/portal-frontend/src/styles/typography.scss index 914bac6533..8e965ee917 100644 --- a/portal-frontend/src/styles/typography.scss +++ b/portal-frontend/src/styles/typography.scss @@ -33,6 +33,7 @@ h3 { h4 { @include typography-style(rem(18), 700); + line-height: 1.5rem !important; } h5 { diff --git a/services/apps/alcs/src/user/user.controller.spec.ts b/services/apps/alcs/src/user/user.controller.spec.ts index f4390447e1..25e4bffa35 100644 --- a/services/apps/alcs/src/user/user.controller.spec.ts +++ b/services/apps/alcs/src/user/user.controller.spec.ts @@ -78,6 +78,8 @@ describe('UserController', () => { settings: { favoriteBoards: ['cats'], }, + isFirstNationGovernment: false, + isLocalGovernment: false, }; controller = module.get(UserController); diff --git a/services/apps/alcs/src/user/user.controller.ts b/services/apps/alcs/src/user/user.controller.ts index f7cf70c275..b82a71d37e 100644 --- a/services/apps/alcs/src/user/user.controller.ts +++ b/services/apps/alcs/src/user/user.controller.ts @@ -37,6 +37,9 @@ export class UserController { const mappedUser = await this.userMapper.mapAsync(user, User, UserDto); const government = await this.userService.getUserLocalGovernment(user); mappedUser.government = government ? government.name : undefined; + mappedUser.isLocalGovernment = !!government && !government.isFirstNation; + mappedUser.isFirstNationGovernment = + !!government && government.isFirstNation; return mappedUser; } diff --git a/services/apps/alcs/src/user/user.dto.ts b/services/apps/alcs/src/user/user.dto.ts index 4b768f00d7..b14b6b22d0 100644 --- a/services/apps/alcs/src/user/user.dto.ts +++ b/services/apps/alcs/src/user/user.dto.ts @@ -40,6 +40,8 @@ export class UserDto extends UpdateUserDto { prettyName?: string | null; government?: string; + isLocalGovernment: boolean; + isFirstNationGovernment: boolean; } export class CreateUserDto { diff --git a/services/apps/alcs/src/user/user.service.ts b/services/apps/alcs/src/user/user.service.ts index d5a9e99450..be498d7f86 100644 --- a/services/apps/alcs/src/user/user.service.ts +++ b/services/apps/alcs/src/user/user.service.ts @@ -97,6 +97,7 @@ export class UserService { where: { bceidBusinessGuid: user.bceidBusinessGuid }, select: { name: true, + isFirstNation: true, }, }); } From 44087d6d8347f810f1275f56a827f9452b1769dd Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Fri, 21 Jul 2023 13:54:42 -0700 Subject: [PATCH 106/954] Fix portal LFNG review tab and update banner (#806) * Add icon to portal lfng banner matching alcs * Show LFNG review or banner for apps submitted to ALC * Remove @extend as per MR feedback --- .../lfng-info/lfng-info.component.scss | 7 +++-- .../lfng-review/lfng-review.component.html | 18 ++++++++----- .../lfng-review/lfng-review.component.scss | 26 +++++++++++++++++++ .../lfng-review/lfng-review.component.ts | 8 +++--- .../application-submission.dto.ts | 8 ++++++ 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/alcs-frontend/src/app/features/application/lfng-info/lfng-info.component.scss b/alcs-frontend/src/app/features/application/lfng-info/lfng-info.component.scss index 3e371bf572..dfcc95d763 100644 --- a/alcs-frontend/src/app/features/application/lfng-info/lfng-info.component.scss +++ b/alcs-frontend/src/app/features/application/lfng-info/lfng-info.component.scss @@ -21,9 +21,12 @@ h4 { } .comment-container { - @extend .warning; - flex-direction: column; align-items: flex-start; + background-color: colors.$secondary-color-light; + border-radius: 4px; + display: flex; + flex-direction: column; + padding: 16px; div { margin-bottom: 4px; diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html index e32262058f..5afec266c1 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html +++ b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html @@ -35,10 +35,10 @@

Local/First Nation Gov Review

- This section will update after the application is submitted. + infoThis section will update after the application is submitted.
-
- Application not subject to Local/First Nation Government review. +
+ infoApplication not subject to Local/First Nation Government review.
- Pending Local/First Nation Government review. + infoPending Local/First Nation Government review.
Comment for Applicant
{{ application.returnedComment }} @@ -259,7 +259,13 @@

Attachments

-
+
Resolution Document
{{ diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.scss b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.scss index b94d3abdeb..9d4b7c0db9 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.scss +++ b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.scss @@ -2,9 +2,35 @@ @use '../../../../styles/colors'; .warning { + align-items: center; background-color: rgba(colors.$accent-color-light, 0.5); + border-radius: rem(4); + display: flex; padding: rem(16); margin-bottom: rem(24); + + mat-icon { + margin-right: 12px; + } +} + +.comment-container { + align-items: flex-start; + background-color: rgba(colors.$accent-color-light, 0.5); + border-radius: rem(4); + display: flex; + flex-direction: column; + margin-bottom: rem(24); + padding: rem(16); + + div { + margin-bottom: rem(4); + } +} + +.no-comment { + color: colors.$grey-dark; + font-style: italic; } .link { diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts index 26df0a2c84..8305093ae2 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts +++ b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts @@ -35,6 +35,7 @@ export class LfngReviewComponent implements OnInit, OnDestroy { resolutionDocument: ApplicationDocumentDto[] = []; governmentOtherAttachments: ApplicationDocumentDto[] = []; hasCompletedStepsBeforeDocuments = false; + submittedToAlcStatus = false; constructor( private applicationReviewService: ApplicationSubmissionReviewService, @@ -61,6 +62,9 @@ export class LfngReviewComponent implements OnInit, OnDestroy { this.$application.pipe(takeUntil(this.$destroy)).subscribe((application) => { this.application = application; + this.submittedToAlcStatus = !!this.application?.submissionStatuses.find( + (s) => s.statusTypeCode === SUBMISSION_STATUS.SUBMITTED_TO_ALC && !!s.effectiveDate + ); this.loadReview(); }); @@ -85,9 +89,7 @@ export class LfngReviewComponent implements OnInit, OnDestroy { if ( this.application && this.application.typeCode !== 'TURP' && - ([SUBMISSION_STATUS.SUBMITTED_TO_ALC, SUBMISSION_STATUS.REFUSED_TO_FORWARD_LG].includes( - this.application.status.code - ) || + (this.submittedToAlcStatus || (this.application.status.code === SUBMISSION_STATUS.IN_REVIEW_BY_LG && this.application.canReview)) ) { await this.applicationReviewService.getByFileId(this.application.fileNumber); diff --git a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts index c852ad723e..1ce7a5e57e 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts @@ -22,6 +22,13 @@ export interface ApplicationStatusDto extends BaseCodeDto { portalColor: string; } +export interface ApplicationSubmissionToSubmissionStatusDto { + submissionUuid: string; + effectiveDate: number | null; + statusTypeCode: string; + status: ApplicationStatusDto; +} + export interface NaruSubtypeDto extends BaseCodeDto {} export interface ProposedLot { @@ -46,6 +53,7 @@ export interface ApplicationSubmissionDto { owners: ApplicationOwnerDetailedDto[]; hasOtherParcelsInCommunity?: boolean | null; returnedComment?: string; + submissionStatuses: ApplicationSubmissionToSubmissionStatusDto[]; } export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { From 0a98416e4de84dc0c0d2498096a228ec1f9a2bdd Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 21 Jul 2023 14:41:39 -0700 Subject: [PATCH 107/954] Combine Purpose Fields * Add incl/excl fields --- .../application-details.component.spec.ts | 6 +- .../naru-details/naru-details.component.html | 2 +- .../nfu-details/nfu-details.component.html | 2 +- .../pfrs-details/pfrs-details.component.html | 2 +- .../pofo-details/pofo-details.component.html | 2 +- .../roso-details/roso-details.component.html | 2 +- .../subd-details/subd-details.component.html | 2 +- .../tur-details/tur-details.component.html | 2 +- .../application-submission.service.spec.ts | 6 +- .../services/application/application.dto.ts | 6 +- .../naru-details/naru-details.component.html | 6 +- .../nfu-details/nfu-details.component.html | 6 +- .../pfrs-details/pfrs-details.component.html | 6 +- .../pofo-details/pofo-details.component.html | 6 +- .../roso-details/roso-details.component.html | 6 +- .../subd-details/subd-details.component.html | 6 +- .../tur-details/tur-details.component.html | 6 +- .../naru-proposal/naru-proposal.component.ts | 4 +- .../nfu-proposal/nfu-proposal.component.ts | 6 +- .../pfrs-proposal/pfrs-proposal.component.ts | 6 +- .../pofo-proposal/pofo-proposal.component.ts | 6 +- .../roso-proposal/roso-proposal.component.ts | 6 +- .../subd-proposal/subd-proposal.component.ts | 6 +- .../tur-proposal/tur-proposal.component.ts | 6 +- .../application-submission.dto.ts | 12 +-- ...ation-submission-validator.service.spec.ts | 23 +++-- ...pplication-submission-validator.service.ts | 10 +-- .../application-submission.dto.ts | 47 ++-------- .../application-submission.entity.ts | 48 +++++++---- .../application-submission.service.ts | 14 +-- .../generate-submission-document.service.ts | 4 +- ...55105-add_inclexcl_fields_to_submission.ts | 85 +++++++++++++++++++ 32 files changed, 197 insertions(+), 160 deletions(-) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1689973655105-add_inclexcl_fields_to_submission.ts diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts index a72111b842..4607f83461 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts @@ -29,6 +29,7 @@ describe('ApplicationDetailsComponent', () => { component = fixture.componentInstance; component.submission = { applicant: '', + purpose: '', canEdit: false, canReview: false, canView: false, @@ -55,7 +56,6 @@ describe('ApplicationDetailsComponent', () => { naruLocationRationale: null, naruProjectDurationAmount: null, naruProjectDurationUnit: null, - naruPurpose: null, naruResidenceNecessity: null, naruSubtype: null, naruToPlaceArea: null, @@ -74,7 +74,6 @@ describe('ApplicationDetailsComponent', () => { nfuOutsideLands: null, nfuProjectDurationAmount: null, nfuProjectDurationUnit: null, - nfuPurpose: null, nfuTotalFillPlacement: null, nfuWillImportFill: null, soilAlreadyPlacedArea: null, @@ -91,7 +90,6 @@ describe('ApplicationDetailsComponent', () => { soilNOIIDs: null, soilProjectDurationAmount: null, soilProjectDurationUnit: null, - soilPurpose: null, soilReduceNegativeImpacts: null, soilToPlaceAverageDepth: null, soilToPlaceMaximumDepth: null, @@ -100,11 +98,9 @@ describe('ApplicationDetailsComponent', () => { soilTypeRemoved: null, subdAgricultureSupport: null, subdIsHomeSiteSeverance: null, - subdPurpose: null, subdSuitability: null, turAgriculturalActivities: null, turOutsideLands: null, - turPurpose: null, turReduceNegativeImpacts: null, turTotalCorridorArea: null, primaryContact: { diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html index b7979c06cf..e3ab6ad57b 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.html @@ -5,7 +5,7 @@
What is the purpose of the proposal?
- {{ _applicationSubmission.naruPurpose }} + {{ _applicationSubmission.purpose }}
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html index 734caeec4a..ac66739155 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html @@ -5,7 +5,7 @@
What is the purpose of the proposal?
- {{ applicationSubmission.nfuPurpose }} + {{ applicationSubmission.purpose }}
Could this proposal be accommodated on lands outside of the ALR? Please justify why the proposal cannot be carried diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.html index 82060ed931..882787ab3d 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/pfrs-details/pfrs-details.component.html @@ -19,7 +19,7 @@
What is the purpose of the proposal?
- {{ _applicationSubmission.soilPurpose }} + {{ _applicationSubmission.purpose }}
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.html index 05507c46ef..4cb7818358 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/pofo-details/pofo-details.component.html @@ -19,7 +19,7 @@
What is the purpose of the proposal?
- {{ _applicationSubmission.soilPurpose }} + {{ _applicationSubmission.purpose }}
Fill to be Placed
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.html index e353058262..4a9638f911 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/roso-details/roso-details.component.html @@ -19,7 +19,7 @@
What is the purpose of the proposal?
- {{ _applicationSubmission.soilPurpose }} + {{ _applicationSubmission.purpose }}
Soil to be Removed
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/subd-details/subd-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/subd-details/subd-details.component.html index f472075d88..c5a0d3c446 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/subd-details/subd-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/subd-details/subd-details.component.html @@ -18,7 +18,7 @@
What is the purpose of the proposal?
- {{ _applicationSubmission.subdPurpose }} + {{ _applicationSubmission.purpose }}
Why do you believe this parcel is suitable for subdivision?
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/tur-details/tur-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/tur-details/tur-details.component.html index 787d8f97b0..f71a0bd172 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/tur-details/tur-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/tur-details/tur-details.component.html @@ -1,7 +1,7 @@
What is the purpose of the proposal?
- {{ _application.turPurpose }} + {{ _application.purpose }}
Specify any agricultural activities such as livestock operations, greenhouses or horticultural activities in diff --git a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts index 7de907cfb9..61863069f0 100644 --- a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts @@ -40,7 +40,6 @@ describe('ApplicationSubmissionService', () => { naruLocationRationale: null, naruProjectDurationAmount: null, naruProjectDurationUnit: null, - naruPurpose: null, naruResidenceNecessity: null, naruSubtype: null, naruToPlaceArea: null, @@ -74,7 +73,7 @@ describe('ApplicationSubmissionService', () => { nfuOutsideLands: null, nfuProjectDurationAmount: null, nfuProjectDurationUnit: null, - nfuPurpose: null, + purpose: '', nfuTotalFillPlacement: null, nfuWillImportFill: null, soilAlreadyPlacedArea: null, @@ -90,7 +89,6 @@ describe('ApplicationSubmissionService', () => { soilNOIIDs: null, soilProjectDurationAmount: null, soilProjectDurationUnit: null, - soilPurpose: null, soilReduceNegativeImpacts: null, soilToPlaceAverageDepth: null, soilToPlaceMaximumDepth: null, @@ -99,11 +97,9 @@ describe('ApplicationSubmissionService', () => { soilTypeRemoved: null, subdAgricultureSupport: null, subdIsHomeSiteSeverance: null, - subdPurpose: null, subdSuitability: null, turAgriculturalActivities: null, turOutsideLands: null, - turPurpose: null, turReduceNegativeImpacts: null, turTotalCorridorArea: null, submissionStatuses: [], diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 85dafca899..7775c4c030 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -105,6 +105,7 @@ export interface ApplicationSubmissionDto { fileNumber: string; lastStatusUpdate: number; applicant: string; + purpose: string | null; type: string; status: ApplicationStatus; typeCode: string; @@ -134,7 +135,6 @@ export interface ApplicationSubmissionDto { //NFU Specific Fields nfuHectares: number | null; - nfuPurpose: string | null; nfuOutsideLands: string | null; nfuAgricultureSupport: string | null; nfuWillImportFill: boolean | null; @@ -147,7 +147,6 @@ export interface ApplicationSubmissionDto { nfuFillOriginDescription: string | null; //TUR Fields - turPurpose: string | null; turAgriculturalActivities: string | null; turReduceNegativeImpacts: string | null; turOutsideLands: string | null; @@ -155,7 +154,6 @@ export interface ApplicationSubmissionDto { turAllOwnersNotified?: boolean | null; //Subdivision Fields - subdPurpose: string | null; subdSuitability: string | null; subdAgricultureSupport: string | null; subdIsHomeSiteSeverance: boolean | null; @@ -166,7 +164,6 @@ export interface ApplicationSubmissionDto { soilNOIIDs: string | null; soilHasPreviousALCAuthorization: boolean | null; soilApplicationIDs: string | null; - soilPurpose: string | null; soilTypeRemoved: string | null; soilReduceNegativeImpacts: string | null; soilToRemoveVolume: number | null; @@ -194,7 +191,6 @@ export interface ApplicationSubmissionDto { //NARU Fields naruSubtype: BaseCodeDto | null; - naruPurpose: string | null; naruFloorArea: number | null; naruResidenceNecessity: string | null; naruLocationRationale: string | null; diff --git a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html b/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html index addd2ceb07..92acfe1eec 100644 --- a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html +++ b/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html @@ -12,11 +12,11 @@
What is the purpose of the proposal? - +
- {{ _applicationSubmission.naruPurpose }} - + {{ _applicationSubmission.purpose }} +
diff --git a/portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.html b/portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.html index e03a7f7493..5af77d22fa 100644 --- a/portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.html +++ b/portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.html @@ -8,12 +8,12 @@
What is the purpose of the proposal? - +
- {{ applicationSubmission.nfuPurpose }} + {{ applicationSubmission.purpose }} - +
Could this proposal be accommodated on lands outside of the ALR? Please justify why the proposal cannot be carried diff --git a/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.html b/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.html index b4b82a1100..a36bbf8c87 100644 --- a/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.html +++ b/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.html @@ -44,11 +44,11 @@
What is the purpose of the proposal? - +
- {{ _applicationSubmission.soilPurpose }} - + {{ _applicationSubmission.purpose }} +
diff --git a/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.html b/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.html index bbd093de01..21b534141a 100644 --- a/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.html +++ b/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.html @@ -44,11 +44,11 @@
What is the purpose of the proposal? - +
- {{ _applicationSubmission.soilPurpose }} - + {{ _applicationSubmission.purpose }} +
diff --git a/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.html b/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.html index ac1f26938f..792234b5c2 100644 --- a/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.html +++ b/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.html @@ -44,11 +44,11 @@
What is the purpose of the proposal? - +
- {{ _applicationSubmission.soilPurpose }} - + {{ _applicationSubmission.purpose }} +
diff --git a/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.html b/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.html index b147b001f4..b3ee95d67a 100644 --- a/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.html +++ b/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.html @@ -30,11 +30,11 @@
What is the purpose of the proposal? - +
- {{ _applicationSubmission.subdPurpose }} - + {{ _applicationSubmission.purpose }} +
Why do you believe this parcel is suitable for subdivision? diff --git a/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.html b/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.html index ca804d4077..c3a2d98db8 100644 --- a/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.html +++ b/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.html @@ -1,11 +1,11 @@
What is the purpose of the proposal? - +
- {{ _applicationSubmission.turPurpose }} - + {{ _applicationSubmission.purpose }} +
Specify any agricultural activities such as livestock operations, greenhouses or horticultural activities in diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts index 7fcb9cf724..74642f793d 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts @@ -98,7 +98,7 @@ export class NaruProposalComponent extends FilesStepComponent implements OnInit, ? applicationSubmission.naruProjectDurationAmount.toString() : null, projectDurationUnit: applicationSubmission.naruProjectDurationUnit, - purpose: applicationSubmission.naruPurpose, + purpose: applicationSubmission.purpose, residenceNecessity: applicationSubmission.naruResidenceNecessity, agriTourism: applicationSubmission.naruAgriTourism, sleepingUnits: applicationSubmission.naruSleepingUnits @@ -206,7 +206,7 @@ export class NaruProposalComponent extends FilesStepComponent implements OnInit, naruLocationRationale: locationRationale, naruProjectDurationAmount: projectDurationAmount ? parseFloat(projectDurationAmount) : null, naruProjectDurationUnit: projectDurationUnit, - naruPurpose: purpose, + purpose: purpose, naruResidenceNecessity: residenceNecessity, naruSubtypeCode: subtype, naruSleepingUnits: sleepingUnits ? parseFloat(sleepingUnits) : null, diff --git a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts index da80e5605e..4cd97a36c1 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts @@ -61,7 +61,7 @@ export class NfuProposalComponent extends StepComponent implements OnInit, OnDes this.form.patchValue({ hectares: applicationSubmission.nfuHectares?.toString(), - purpose: applicationSubmission.nfuPurpose, + purpose: applicationSubmission.purpose, outsideLands: applicationSubmission.nfuOutsideLands, agricultureSupport: applicationSubmission.nfuAgricultureSupport, totalFillPlacement: applicationSubmission.nfuTotalFillPlacement?.toString(), @@ -92,7 +92,7 @@ export class NfuProposalComponent extends StepComponent implements OnInit, OnDes private async save() { if (this.fileId) { const nfuHectares = this.hectares.getRawValue(); - const nfuPurpose = this.purpose.getRawValue(); + const purpose = this.purpose.getRawValue(); const nfuOutsideLands = this.outsideLands.getRawValue(); const nfuAgricultureSupport = this.agricultureSupport.getRawValue(); const nfuWillImportFill = this.willImportFill.getRawValue(); @@ -106,7 +106,7 @@ export class NfuProposalComponent extends StepComponent implements OnInit, OnDes const updateDto: ApplicationSubmissionUpdateDto = { nfuHectares: nfuHectares ? parseFloat(nfuHectares) : null, - nfuPurpose, + purpose, nfuOutsideLands, nfuAgricultureSupport, nfuWillImportFill: parseStringToBoolean(nfuWillImportFill), diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts index b8627b09f2..042cafd6b4 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts @@ -157,7 +157,7 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, hasALCAuthorization: hasALCAuthorization, NOIIDs: applicationSubmission.soilNOIIDs, applicationIDs: applicationSubmission.soilApplicationIDs, - purpose: applicationSubmission.soilPurpose, + purpose: applicationSubmission.purpose, soilTypeRemoved: applicationSubmission.soilTypeRemoved, reduceNegativeImpacts: applicationSubmission.soilReduceNegativeImpacts, alternativeMeasures: applicationSubmission.soilAlternativeMeasures, @@ -191,14 +191,14 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, const soilNOIIDs = this.NOIIDs.getRawValue(); const hasALCAuthorization = this.hasALCAuthorization.getRawValue(); const soilApplicationIDs = this.applicationIDs.getRawValue(); - const soilPurpose = this.purpose.getRawValue(); + const purpose = this.purpose.getRawValue(); const soilTypeRemoved = this.soilTypeRemoved.getRawValue(); const soilReduceNegativeImpacts = this.reduceNegativeImpacts.getRawValue(); const soilFillTypeToPlace = this.fillTypeToPlace.getRawValue(); const soilAlternativeMeasures = this.alternativeMeasures.getRawValue(); const updateDto: ApplicationSubmissionUpdateDto = { - soilPurpose, + purpose, soilTypeRemoved, soilFillTypeToPlace, soilReduceNegativeImpacts, diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts index 41824b14c9..5536e09cec 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts @@ -106,7 +106,7 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, hasALCAuthorization: hasALCAuthorization, NOIIDs: applicationSubmission.soilNOIIDs, applicationIDs: applicationSubmission.soilApplicationIDs, - purpose: applicationSubmission.soilPurpose, + purpose: applicationSubmission.purpose, fillTypeToPlace: applicationSubmission.soilFillTypeToPlace, alternativeMeasures: applicationSubmission.soilAlternativeMeasures, reduceNegativeImpacts: applicationSubmission.soilReduceNegativeImpacts, @@ -136,13 +136,13 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, const soilNOIIDs = this.NOIIDs.getRawValue(); const hasALCAuthorization = this.hasALCAuthorization.getRawValue(); const soilApplicationIDs = this.applicationIDs.getRawValue(); - const soilPurpose = this.purpose.getRawValue(); + const purpose = this.purpose.getRawValue(); const soilFillTypeToPlace = this.fillTypeToPlace.getRawValue(); const soilAlternativeMeasures = this.alternativeMeasures.getRawValue(); const soilReduceNegativeImpacts = this.reduceNegativeImpacts.getRawValue(); const updateDto: ApplicationSubmissionUpdateDto = { - soilPurpose, + purpose, soilFillTypeToPlace, soilAlternativeMeasures, soilReduceNegativeImpacts, diff --git a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts index 72ad4ee6a0..216e12b954 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts @@ -105,7 +105,7 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, hasALCAuthorization: hasALCAuthorization, NOIIDs: applicationSubmission.soilNOIIDs, applicationIDs: applicationSubmission.soilApplicationIDs, - purpose: applicationSubmission.soilPurpose, + purpose: applicationSubmission.purpose, soilTypeRemoved: applicationSubmission.soilTypeRemoved, reduceNegativeImpacts: applicationSubmission.soilReduceNegativeImpacts, projectDurationAmount: applicationSubmission.soilProjectDurationAmount?.toString() ?? null, @@ -134,12 +134,12 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, const soilNOIIDs = this.NOIIDs.getRawValue(); const hasALCAuthorization = this.hasALCAuthorization.getRawValue(); const soilApplicationIDs = this.applicationIDs.getRawValue(); - const soilPurpose = this.purpose.getRawValue(); + const purpose = this.purpose.getRawValue(); const soilTypeRemoved = this.soilTypeRemoved.getRawValue(); const soilReduceNegativeImpacts = this.reduceNegativeImpacts.getRawValue(); const updateDto: ApplicationSubmissionUpdateDto = { - soilPurpose, + purpose, soilTypeRemoved, soilReduceNegativeImpacts, soilIsNOIFollowUp: parseStringToBoolean(isNOIFollowUp), diff --git a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts index 9d02217609..6e73edff80 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts @@ -72,7 +72,7 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, } this.form.patchValue({ - purpose: applicationSubmission.subdPurpose, + purpose: applicationSubmission.purpose, suitability: applicationSubmission.subdSuitability, agriculturalSupport: applicationSubmission.subdAgricultureSupport, lotsProposed: applicationSubmission.subdProposedLots.length.toString(10), @@ -105,13 +105,13 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, protected async save() { if (this.fileId) { - const subdPurpose = this.purpose.getRawValue(); + const purpose = this.purpose.getRawValue(); const subdSuitability = this.suitability.getRawValue(); const subdAgricultureSupport = this.agriculturalSupport.getRawValue(); const subdIsHomeSiteSeverance = this.isHomeSiteSeverance.getRawValue(); const updateDto: ApplicationSubmissionUpdateDto = { - subdPurpose, + purpose, subdSuitability, subdAgricultureSupport, subdIsHomeSiteSeverance: subdIsHomeSiteSeverance !== null ? subdIsHomeSiteSeverance === 'true' : null, diff --git a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts index b67e523f9e..30b4b08fa0 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts @@ -59,7 +59,7 @@ export class TurProposalComponent extends FilesStepComponent implements OnInit, this.submissionUuid = applicationSubmission.uuid; this.form.patchValue({ - purpose: applicationSubmission.turPurpose, + purpose: applicationSubmission.purpose, outsideLands: applicationSubmission.turOutsideLands, agriculturalActivities: applicationSubmission.turAgriculturalActivities, reduceNegativeImpacts: applicationSubmission.turReduceNegativeImpacts, @@ -85,7 +85,7 @@ export class TurProposalComponent extends FilesStepComponent implements OnInit, protected async save() { if (this.fileId) { - const turPurpose = this.purpose.getRawValue(); + const purpose = this.purpose.getRawValue(); const turOutsideLands = this.outsideLands.getRawValue(); const turAgriculturalActivities = this.agriculturalActivities.getRawValue(); const turReduceNegativeImpacts = this.reduceNegativeImpacts.getRawValue(); @@ -93,7 +93,7 @@ export class TurProposalComponent extends FilesStepComponent implements OnInit, const turAllOwnersNotified = this.allOwnersNotified.getRawValue(); const updateDto: ApplicationSubmissionUpdateDto = { - turPurpose, + purpose, turOutsideLands, turAgriculturalActivities, turReduceNegativeImpacts, diff --git a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts index 39f4700d61..513e8b1965 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts @@ -36,6 +36,7 @@ export interface ApplicationSubmissionDto { updatedAt: string; lastStatusUpdate: number; applicant: string; + purpose: string | null; type: string; typeCode: string; localGovernmentUuid: string; @@ -65,7 +66,6 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD //NFU Specific Fields nfuHectares: number | null; - nfuPurpose: string | null; nfuOutsideLands: string | null; nfuAgricultureSupport: string | null; nfuWillImportFill: boolean | null; @@ -78,7 +78,6 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD nfuFillOriginDescription: string | null; //TUR Fields - turPurpose: string | null; turAgriculturalActivities: string | null; turReduceNegativeImpacts: string | null; turOutsideLands: string | null; @@ -86,7 +85,6 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD turAllOwnersNotified?: boolean | null; //Subdivision Fields - subdPurpose: string | null; subdSuitability: string | null; subdAgricultureSupport: string | null; subdIsHomeSiteSeverance: boolean | null; @@ -97,7 +95,6 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD soilNOIIDs: string | null; soilHasPreviousALCAuthorization: boolean | null; soilApplicationIDs: string | null; - soilPurpose: string | null; soilTypeRemoved: string | null; soilReduceNegativeImpacts: string | null; soilToRemoveVolume: number | null; @@ -125,7 +122,6 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD //NARU Fields naruSubtype: NaruSubtypeDto | null; - naruPurpose: string | null; naruFloorArea: number | null; naruResidenceNecessity: string | null; naruLocationRationale: string | null; @@ -146,6 +142,7 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD export interface ApplicationSubmissionUpdateDto { applicant?: string; + purpose?: string | null; localGovernmentUuid?: string; typeCode?: string; primaryContactOwnerUuid?: string; @@ -166,7 +163,6 @@ export interface ApplicationSubmissionUpdateDto { //NFU Specific Fields nfuHectares?: number | null; - nfuPurpose?: string | null; nfuOutsideLands?: string | null; nfuAgricultureSupport?: string | null; nfuWillImportFill?: boolean | null; @@ -179,7 +175,6 @@ export interface ApplicationSubmissionUpdateDto { nfuFillOriginDescription?: string | null; //TUR Fields - turPurpose?: string | null; turAgriculturalActivities?: string | null; turReduceNegativeImpacts?: string | null; turOutsideLands?: string | null; @@ -187,7 +182,6 @@ export interface ApplicationSubmissionUpdateDto { turAllOwnersNotified?: boolean | null; //Subdivision Fields - subdPurpose?: string | null; subdSuitability?: string | null; subdAgricultureSupport?: string | null; subdIsHomeSiteSeverance?: boolean | null; @@ -198,7 +192,6 @@ export interface ApplicationSubmissionUpdateDto { soilNOIIDs?: string | null; soilHasPreviousALCAuthorization?: boolean | null; soilApplicationIDs?: string | null; - soilPurpose?: string | null; soilTypeRemoved?: string | null; soilReduceNegativeImpacts?: string | null; soilToRemoveVolume?: number | null; @@ -226,7 +219,6 @@ export interface ApplicationSubmissionUpdateDto { //NARU Fields naruSubtypeCode?: string | null; - naruPurpose?: string | null; naruFloorArea?: number | null; naruResidenceNecessity?: string | null; naruLocationRationale?: string | null; diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts index d9937bf19c..d8ed4a4351 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts @@ -78,6 +78,16 @@ describe('ApplicationSubmissionValidatorService', () => { ); }); + it('should return an error for missing purpose', async () => { + const applicationSubmission = new ApplicationSubmission({ + owners: [], + }); + + const res = await service.validateSubmission(applicationSubmission); + + expect(includesError(res.errors, new Error('Missing purpose'))).toBe(true); + }); + it('should return an error for no parcels', async () => { const applicationSubmission = new ApplicationSubmission({ owners: [], @@ -531,7 +541,7 @@ describe('ApplicationSubmissionValidatorService', () => { const application = new ApplicationSubmission({ owners: [], nfuHectares: 1.5125, - nfuPurpose: 'VALID', + purpose: 'VALID', nfuOutsideLands: 'VALID', nfuAgricultureSupport: 'VALID', nfuWillImportFill: true, @@ -559,7 +569,7 @@ describe('ApplicationSubmissionValidatorService', () => { const application = new ApplicationSubmission({ owners: [], nfuHectares: 1.5125, - nfuPurpose: 'VALID', + purpose: 'VALID', nfuOutsideLands: 'VALID', nfuAgricultureSupport: 'VALID', nfuWillImportFill: false, @@ -580,7 +590,6 @@ describe('ApplicationSubmissionValidatorService', () => { const application = new ApplicationSubmission({ owners: [], nfuHectares: null, - nfuPurpose: 'VALID', nfuOutsideLands: 'VALID', nfuAgricultureSupport: 'VALID', nfuWillImportFill: true, @@ -627,7 +636,6 @@ describe('ApplicationSubmissionValidatorService', () => { it('should not have an error when base information is filled correctly', async () => { const application = new ApplicationSubmission({ owners: [], - soilPurpose: 'soilPurpose', soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', soilHasPreviousALCAuthorization: false, soilIsNOIFollowUp: false, @@ -653,7 +661,6 @@ describe('ApplicationSubmissionValidatorService', () => { it('should report errors when information is missing', async () => { const application = new ApplicationSubmission({ owners: [], - soilPurpose: 'soilPurpose', soilReduceNegativeImpacts: null, soilToRemoveVolume: null, typeCode: 'ROSO', @@ -729,7 +736,6 @@ describe('ApplicationSubmissionValidatorService', () => { it('should not have errors when base information is filled correctly', async () => { const application = new ApplicationSubmission({ owners: [], - soilPurpose: 'soilPurpose', soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', soilHasPreviousALCAuthorization: false, soilIsNOIFollowUp: false, @@ -760,7 +766,6 @@ describe('ApplicationSubmissionValidatorService', () => { it('should report errors when information is missing', async () => { const application = new ApplicationSubmission({ owners: [], - soilPurpose: 'soilPurpose', soilFillTypeToPlace: null, soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', soilToPlaceArea: null, @@ -837,7 +842,7 @@ describe('ApplicationSubmissionValidatorService', () => { it('should not have errors when base information is filled correctly', async () => { const application = new ApplicationSubmission({ owners: [], - soilPurpose: 'soilPurpose', + purpose: 'purpose', soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', soilHasPreviousALCAuthorization: false, soilIsNOIFollowUp: false, @@ -868,7 +873,7 @@ describe('ApplicationSubmissionValidatorService', () => { it('should report errors when information is missing', async () => { const application = new ApplicationSubmission({ owners: [], - soilPurpose: 'soilPurpose', + purpose: 'purpose', soilFillTypeToPlace: null, soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', soilToPlaceArea: null, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts index 4f9305f0e3..b902f52f8d 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts @@ -49,6 +49,10 @@ export class ApplicationSubmissionValidatorService { errors.push(new ServiceValidationException('Missing applicant')); } + if (!applicationSubmission.purpose) { + errors.push(new ServiceValidationException('Missing purpose')); + } + const validatedParcels = await this.validateParcels( applicationSubmission, errors, @@ -381,7 +385,6 @@ export class ApplicationSubmissionValidatorService { ) { if ( !applicationSubmission.nfuHectares || - !applicationSubmission.nfuPurpose || !applicationSubmission.nfuOutsideLands || !applicationSubmission.nfuAgricultureSupport || applicationSubmission.nfuWillImportFill === null @@ -411,7 +414,6 @@ export class ApplicationSubmissionValidatorService { errors: Error[], ) { if ( - !applicationSubmission.turPurpose || !applicationSubmission.turOutsideLands || !applicationSubmission.turAgriculturalActivities || !applicationSubmission.turReduceNegativeImpacts || @@ -433,7 +435,6 @@ export class ApplicationSubmissionValidatorService { ); } if ( - !applicationSubmission.subdPurpose || !applicationSubmission.subdSuitability || !applicationSubmission.subdAgricultureSupport ) { @@ -470,7 +471,6 @@ export class ApplicationSubmissionValidatorService { errors: Error[], ) { if ( - applicationSubmission.soilPurpose === null || applicationSubmission.soilTypeRemoved === null || applicationSubmission.soilReduceNegativeImpacts === null ) { @@ -511,7 +511,6 @@ export class ApplicationSubmissionValidatorService { errors: Error[], ) { if ( - applicationSubmission.soilPurpose === null || applicationSubmission.soilFillTypeToPlace === null || applicationSubmission.soilAlternativeMeasures === null || applicationSubmission.soilReduceNegativeImpacts === null @@ -560,7 +559,6 @@ export class ApplicationSubmissionValidatorService { if ( applicationSubmission.soilIsNOIFollowUp === null || applicationSubmission.soilHasPreviousALCAuthorization === null || - !applicationSubmission.soilPurpose || applicationSubmission.soilReduceNegativeImpacts === null ) { errors.push( diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts index 5789d698dc..6883fb6f1e 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts @@ -61,6 +61,8 @@ export class ApplicationSubmissionDto { } export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { + @AutoMap(() => String) + purpose: string | null; @AutoMap() parcelsAgricultureDescription: string; @AutoMap() @@ -90,9 +92,6 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { @AutoMap(() => Number) nfuHectares?: number | null; - @AutoMap(() => String) - nfuPurpose?: string | null; - @AutoMap(() => String) nfuOutsideLands?: string | null; @@ -124,9 +123,6 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { nfuFillOriginDescription?: string | null; //TUR Fields - @AutoMap(() => String) - turPurpose?: string | null; - @AutoMap(() => String) turAgriculturalActivities?: string | null; @@ -143,9 +139,6 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { turAllOwnersNotified?: boolean | null; //Subdivision Fields - @AutoMap(() => String) - subdPurpose?: string | null; - @AutoMap(() => String) subdSuitability?: string | null; @@ -170,9 +163,6 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { @AutoMap(() => String) soilApplicationIDs: string | null; - @AutoMap(() => String) - soilPurpose: string | null; - @AutoMap(() => String) soilTypeRemoved: string | null; @@ -249,9 +239,6 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { @AutoMap(() => [NaruSubtypeDto]) naruSubtype: NaruSubtypeDto | null; - @AutoMap(() => String) - naruPurpose: string | null; - @AutoMap(() => Number) naruFloorArea: number | null; @@ -315,6 +302,11 @@ export class ApplicationSubmissionUpdateDto { @IsOptional() applicant?: string; + @IsString() + @IsOptional() + @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) + purpose?: string; + @IsUUID() @IsOptional() localGovernmentUuid?: string; @@ -383,11 +375,6 @@ export class ApplicationSubmissionUpdateDto { @IsOptional() nfuHectares?: number | null; - @IsString() - @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) - @IsOptional() - nfuPurpose?: string | null; - @IsString() @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) @IsOptional() @@ -432,12 +419,6 @@ export class ApplicationSubmissionUpdateDto { @IsOptional() nfuFillOriginDescription?: string | null; - //TUR Fields - @IsString() - @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) - @IsOptional() - turPurpose?: string | null; - @IsString() @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) @IsOptional() @@ -462,11 +443,6 @@ export class ApplicationSubmissionUpdateDto { turAllOwnersNotified?: boolean | null; //Subdivision Fields - @IsString() - @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) - @IsOptional() - subdPurpose?: string | null; - @IsString() @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) @IsOptional() @@ -504,11 +480,6 @@ export class ApplicationSubmissionUpdateDto { @IsOptional() soilApplicationIDs?: string | null; - @IsString() - @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) - @IsOptional() - soilPurpose?: string | null; - @IsString() @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) @IsOptional() @@ -612,10 +583,6 @@ export class ApplicationSubmissionUpdateDto { @IsOptional() naruSubtypeCode?: string | null; - @IsString() - @IsOptional() - naruPurpose?: string | null; - @IsNumber() @IsOptional() naruFloorArea?: number | null; diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts index 670bb36d17..5893038f79 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts @@ -214,7 +214,7 @@ export class ApplicationSubmission extends Base { @AutoMap(() => String) @Column({ type: 'text', nullable: true }) - nfuPurpose: string | null; + purpose: string | null; @AutoMap(() => String) @Column({ type: 'text', nullable: true }) @@ -281,10 +281,6 @@ export class ApplicationSubmission extends Base { nfuFillOriginDescription: string | null; //TUR Specific Fields - @AutoMap(() => String) - @Column({ type: 'text', nullable: true }) - turPurpose: string | null; - @AutoMap(() => String) @Column({ type: 'text', nullable: true }) turAgriculturalActivities: string | null; @@ -315,10 +311,6 @@ export class ApplicationSubmission extends Base { turAllOwnersNotified?: boolean | null; //Subdivision Fields - @AutoMap(() => String) - @Column({ type: 'text', nullable: true }) - subdPurpose: string | null; - @AutoMap(() => String) @Column({ type: 'text', nullable: true }) subdSuitability: string | null; @@ -357,10 +349,6 @@ export class ApplicationSubmission extends Base { @Column({ type: 'text', nullable: true, name: 'soil_application_ids' }) soilApplicationIDs: string | null; - @AutoMap(() => String) - @Column({ type: 'text', nullable: true }) - soilPurpose: string | null; - @AutoMap(() => String) @Column({ type: 'text', nullable: true }) soilTypeRemoved: string | null; @@ -568,10 +556,6 @@ export class ApplicationSubmission extends Base { @Column({ type: 'text', nullable: true }) naruSubtypeCode: string | null; - @AutoMap(() => String) - @Column({ type: 'text', nullable: true }) - naruPurpose: string | null; - @AutoMap(() => Number) @Column({ type: 'decimal', @@ -678,6 +662,36 @@ export class ApplicationSubmission extends Base { @Column({ type: 'text', nullable: true }) naruAgriTourism: string | null; + //Inclusion / Exclusion Fields + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + prescribedBody: string | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + inclExclHectares: number | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + exclWhyLand: string | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + inclAgricultureSupport: string | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + inclImprovements: string | null; + + //END SUBMISSION FIELDS + @AutoMap(() => Application) @ManyToOne(() => Application) @JoinColumn({ diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index 871274d24a..41ae69b8da 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -153,6 +153,8 @@ export class ApplicationSubmissionService { const applicationSubmission = await this.getOrFailByUuid(submissionUuid); applicationSubmission.applicant = updateDto.applicant; + applicationSubmission.purpose = + updateDto.purpose || applicationSubmission.purpose; applicationSubmission.typeCode = updateDto.typeCode || applicationSubmission.typeCode; applicationSubmission.localGovernmentUuid = updateDto.localGovernmentUuid; @@ -636,7 +638,6 @@ export class ApplicationSubmissionService { updateDto: ApplicationSubmissionUpdateDto, ) { application.nfuHectares = updateDto.nfuHectares || application.nfuHectares; - application.nfuPurpose = updateDto.nfuPurpose || application.nfuPurpose; application.nfuOutsideLands = updateDto.nfuOutsideLands || application.nfuOutsideLands; application.nfuAgricultureSupport = @@ -681,7 +682,6 @@ export class ApplicationSubmissionService { application: ApplicationSubmission, updateDto: ApplicationSubmissionUpdateDto, ) { - application.turPurpose = updateDto.turPurpose || application.turPurpose; application.turAgriculturalActivities = updateDto.turAgriculturalActivities || application.turAgriculturalActivities; @@ -703,8 +703,6 @@ export class ApplicationSubmissionService { applicationSubmission: ApplicationSubmission, updateDto: ApplicationSubmissionUpdateDto, ) { - applicationSubmission.subdPurpose = - updateDto.subdPurpose || applicationSubmission.subdPurpose; applicationSubmission.subdSuitability = updateDto.subdSuitability || applicationSubmission.subdSuitability; applicationSubmission.subdAgricultureSupport = @@ -748,10 +746,6 @@ export class ApplicationSubmissionService { updateDto.soilApplicationIDs, applicationSubmission.soilApplicationIDs, ); - applicationSubmission.soilPurpose = filterUndefined( - updateDto.soilPurpose, - applicationSubmission.soilPurpose, - ); applicationSubmission.soilTypeRemoved = filterUndefined( updateDto.soilTypeRemoved, applicationSubmission.soilTypeRemoved, @@ -874,10 +868,6 @@ export class ApplicationSubmissionService { updateDto.naruSubtypeCode, applicationSubmission.naruSubtypeCode, ); - applicationSubmission.naruPurpose = filterUndefined( - updateDto.naruPurpose, - applicationSubmission.naruPurpose, - ); applicationSubmission.naruFloorArea = filterUndefined( updateDto.naruFloorArea, applicationSubmission.naruFloorArea, diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts index bab896b69a..4b69a074bb 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts @@ -211,6 +211,7 @@ export class GenerateSubmissionDocumentService { .tz(new Date(), 'Canada/Pacific') .format('MMM DD, YYYY hh:mm:ss Z'), + purpose: submission.purpose, fileNumber: submission.fileNumber, localGovernment: localGovernment?.name, status: submission.status.statusType, @@ -279,7 +280,6 @@ export class GenerateSubmissionDocumentService { // NFU Proposal nfuHectares: submission.nfuHectares, - nfuPurpose: submission.nfuPurpose, nfuOutsideLands: submission.nfuOutsideLands, nfuAgricultureSupport: submission.nfuAgricultureSupport, showImportFill: submission.nfuWillImportFill, @@ -313,7 +313,6 @@ export class GenerateSubmissionDocumentService { ...pdfData, // TUR Proposal - turPurpose: submission.turPurpose, turAgriculturalActivities: submission.turAgriculturalActivities, turReduceNegativeImpacts: submission.turReduceNegativeImpacts, turOutsideLands: submission.turOutsideLands, @@ -341,7 +340,6 @@ export class GenerateSubmissionDocumentService { ...pdfData, // SUBD Proposal - subdPurpose: submission.subdPurpose, subdSuitability: submission.subdSuitability, subdAgricultureSupport: submission.subdAgricultureSupport, subdIsHomeSiteSeverance: formatBooleanToYesNoString( diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1689973655105-add_inclexcl_fields_to_submission.ts b/services/apps/alcs/src/providers/typeorm/migrations/1689973655105-add_inclexcl_fields_to_submission.ts new file mode 100644 index 0000000000..2961622db3 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1689973655105-add_inclexcl_fields_to_submission.ts @@ -0,0 +1,85 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addInclexclFieldsToSubmission1689973655105 + implements MigrationInterface +{ + name = 'addInclexclFieldsToSubmission1689973655105'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "purpose" text`, + ); + + await queryRunner.query( + `UPDATE "alcs"."application_submission" SET "purpose" = CONCAT(nfu_purpose, tur_purpose, subd_purpose, soil_purpose, naru_purpose)`, + ); + + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "nfu_purpose"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "tur_purpose"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "subd_purpose"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "soil_purpose"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "naru_purpose"`, + ); + + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "prescribed_body" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "incl_excl_hectares" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "excl_why_land" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "incl_agriculture_support" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "incl_improvements" text`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "incl_improvements"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "incl_agriculture_support"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "excl_why_land"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "incl_excl_hectares"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "prescribed_body"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "purpose"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "naru_purpose" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "soil_purpose" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "subd_purpose" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "tur_purpose" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "nfu_purpose" text`, + ); + } +} From 8e73c23e49bdc6ebec95b00b57507c4d424c8716 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 21 Jul 2023 17:12:22 -0700 Subject: [PATCH 108/954] upload is working --- bin/migrate-files/migrate-files.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/bin/migrate-files/migrate-files.py b/bin/migrate-files/migrate-files.py index f117a06971..1940cd65d9 100644 --- a/bin/migrate-files/migrate-files.py +++ b/bin/migrate-files/migrate-files.py @@ -12,8 +12,10 @@ db_username = os.getenv("DB_USERNAME") db_password = os.getenv("DB_PASSWORD") db_dsn = os.getenv("DB_DSN") +db_path = os.getenv("DB_PATH") # Connect to the Oracle database +cx_Oracle.init_oracle_client(lib_dir=db_path) conn = cx_Oracle.connect( user=db_username, password=db_password, dsn=db_dsn, encoding="UTF-8" ) @@ -29,7 +31,7 @@ "s3", aws_access_key_id=ecs_access_key, aws_secret_access_key=ecs_secret_key, - use_ssl=True, + use_ssl=False, endpoint_url=ecs_host, ) @@ -38,7 +40,7 @@ if os.path.isfile('last-file.pickle'): with open('last-file.pickle', 'rb') as file: starting_document_id = pickle.load(file) - +# starting_document_id = 999999 print('Starting applications from:', starting_document_id) starting_planning_document_id = 0 @@ -80,6 +82,7 @@ FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND DOCUMENT_ID > {starting_document_id} +AND ALR_APPLICATION_ID IS NOT NULL ORDER BY DOCUMENT_ID ASC """) @@ -90,9 +93,10 @@ documents_processed = 0 last_document_id = 0 max_file = 0 +breakout = False try: - with tqdm(total=count, unit="file", desc="Uploading files to S3") as pbar: + with tqdm(total=application_count, unit="file", desc="Uploading files to S3") as pbar: while True: # Fetch the next batch of BLOB data data = cursor.fetchmany(BATCH_SIZE) @@ -109,8 +113,11 @@ last_document_id = document_id documents_processed += 1 max_file += 1 + print("number of files", max_file) if max_file > 4: - break + breakout = True + if breakout: + break except Exception as e: print("Something went wrong:",e) print("Processed", documents_processed, "files") @@ -147,6 +154,7 @@ FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND DOCUMENT_ID > {starting_planning_document_id} +AND PLANNING_REVIEW_ID IS NOT NULL ORDER BY DOCUMENT_ID ASC """) @@ -176,6 +184,7 @@ last_planning_document_id = document_id documents_processed += 1 max_file += 1 + print("number of files", max_file) if max_file > 4: break except Exception as e: @@ -214,6 +223,7 @@ FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND DOCUMENT_ID > {starting_issue_document_id} +AND ISSUE_ID IS NOT NULL ORDER BY DOCUMENT_ID ASC """) @@ -243,6 +253,7 @@ last_issue_document_id = document_id documents_processed += 1 max_file += 1 + print("number of files", max_file) if max_file > 4: break except Exception as e: From 5d4f8415897fab719aa227c94186d18b8ae38da2 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 24 Jul 2023 09:28:27 -0700 Subject: [PATCH 109/954] Add Exclusion Step 6 to Portal * Add Step 6 with Validation to Portal * Hook up new document types * Add migration for missed field in last MR --- .../application-details.component.spec.ts | 6 + .../application-submission.service.spec.ts | 6 + .../services/application/application.dto.ts | 8 + .../edit-submission-base.module.ts | 2 + .../edit-submission.component.html | 8 + .../edit-submission.component.ts | 9 +- .../excl-proposal.component.html | 199 ++++++++++++++++++ .../excl-proposal.component.scss | 5 + .../excl-proposal.component.spec.ts | 59 ++++++ .../excl-proposal/excl-proposal.component.ts | 111 ++++++++++ .../nfu-proposal/nfu-proposal.component.ts | 9 +- .../application-document.dto.ts | 3 + .../application-submission.dto.ts | 14 ++ .../application-document-code.entity.ts | 3 + .../application-submission.controller.ts | 3 +- .../application-submission.dto.ts | 48 +++++ .../application-submission.entity.ts | 4 + .../application-submission.service.ts | 34 ++- ...291162-add_share_property_to_submission.ts | 19 ++ 19 files changed, 541 insertions(+), 9 deletions(-) create mode 100644 portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html create mode 100644 portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.scss create mode 100644 portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts create mode 100644 portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1689980291162-add_share_property_to_submission.ts diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts index 4607f83461..33e28b6222 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts @@ -28,6 +28,12 @@ describe('ApplicationDetailsComponent', () => { fixture = TestBed.createComponent(ApplicationDetailsComponent); component = fixture.componentInstance; component.submission = { + exclShareGovernmentBorders: null, + exclWhyLand: null, + inclAgricultureSupport: null, + inclExclHectares: null, + inclImprovements: null, + prescribedBody: null, applicant: '', purpose: '', canEdit: false, diff --git a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts index 61863069f0..2db85d84bd 100644 --- a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts @@ -102,6 +102,12 @@ describe('ApplicationSubmissionService', () => { turOutsideLands: null, turReduceNegativeImpacts: null, turTotalCorridorArea: null, + inclAgricultureSupport: null, + inclImprovements: null, + exclShareGovernmentBorders: null, + exclWhyLand: null, + inclExclHectares: null, + prescribedBody: null, submissionStatuses: [], }; diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 7775c4c030..90421efad0 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -207,6 +207,14 @@ export interface ApplicationSubmissionDto { naruToPlaceAverageDepth: number | null; naruSleepingUnits: number | null; naruAgriTourism: string | null; + + //Inclusion / Exclusion Fields + prescribedBody: string | null; + inclExclHectares: number | null; + exclWhyLand: string | null; + inclAgricultureSupport: string | null; + inclImprovements: string | null; + exclShareGovernmentBorders: boolean | null; } export interface ApplicationDto { diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts index 13b593d51f..6f0889f931 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts @@ -24,6 +24,7 @@ import { ParcelEntryConfirmationDialogComponent } from './parcel-details/parcel- import { ParcelEntryComponent } from './parcel-details/parcel-entry/parcel-entry.component'; import { ParcelOwnersComponent } from './parcel-details/parcel-owners/parcel-owners.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { ExclProposalComponent } from './proposal/excl-proposal/excl-proposal.component'; import { ChangeSubtypeConfirmationDialogComponent } from './proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component'; import { NaruProposalComponent } from './proposal/naru-proposal/naru-proposal.component'; import { NfuProposalComponent } from './proposal/nfu-proposal/nfu-proposal.component'; @@ -66,6 +67,7 @@ import { SelectGovernmentComponent } from './select-government/select-government NaruProposalComponent, ChangeSubtypeConfirmationDialogComponent, SoilTableComponent, + ExclProposalComponent, ], imports: [ CommonModule, diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html index b7eb604fdc..f484aff4db 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html @@ -146,6 +146,14 @@
(navigateToStep)="onBeforeSwitchStep($event)" (exit)="onExit()" > +
diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index dd8608f3b0..9034b4075d 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -1,5 +1,5 @@ import { StepperSelectionEvent } from '@angular/cdk/stepper'; -import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ContentChildren, OnDestroy, OnInit, ViewChild, ViewChildren } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; @@ -20,6 +20,7 @@ import { OtherAttachmentsComponent } from './other-attachments/other-attachments import { OtherParcelsComponent } from './other-parcels/other-parcels.component'; import { ParcelDetailsComponent } from './parcel-details/parcel-details.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { ExclProposalComponent } from './proposal/excl-proposal/excl-proposal.component'; import { NaruProposalComponent } from './proposal/naru-proposal/naru-proposal.component'; import { NfuProposalComponent } from './proposal/nfu-proposal/nfu-proposal.component'; import { PfrsProposalComponent } from './proposal/pfrs-proposal/pfrs-proposal.component'; @@ -28,6 +29,7 @@ import { RosoProposalComponent } from './proposal/roso-proposal/roso-proposal.co import { SubdProposalComponent } from './proposal/subd-proposal/subd-proposal.component'; import { TurProposalComponent } from './proposal/tur-proposal/tur-proposal.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; +import { StepComponent } from './step.partial'; export enum EditApplicationSteps { AppParcel = 0, @@ -73,6 +75,7 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit @ViewChild(PofoProposalComponent) pofoProposalComponent?: RosoProposalComponent; @ViewChild(PfrsProposalComponent) pfrsProposalComponent?: PfrsProposalComponent; @ViewChild(NaruProposalComponent) naruProposalComponent?: NaruProposalComponent; + @ViewChild(ExclProposalComponent) exclProposalComponent?: ExclProposalComponent; @ViewChild(OtherAttachmentsComponent) otherAttachmentsComponent!: OtherAttachmentsComponent; constructor( @@ -205,6 +208,7 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit } private async saveProposalSteps() { + debugger; if (this.nfuProposalComponent) { await this.nfuProposalComponent.onSave(); } @@ -226,6 +230,9 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit if (this.naruProposalComponent) { await this.naruProposalComponent.onSave(); } + if (this.exclProposalComponent) { + await this.exclProposalComponent.onSave(); + } } async onBeforeSwitchStep(index: number) { diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html new file mode 100644 index 0000000000..0949656217 --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html @@ -0,0 +1,199 @@ +
+ + Please consult the ' + + What the Commission Considers + + ' page of the ALC website for more information. + +
+
+
+ +
+ Note: To change this selection, you will need to use the 'Change Application Type' button at the top right of + the form. +
+ + + +
+
+ + + + ha + +
+ warning +
This field is required
+
+
+
+ + + Yes + No + +
+ warning +
This field is required
+
+
+
+ + + + + Characters left: {{ 4000 - purposeText.textLength }} +
+ warning +
This field is required
+
+
+
+ + + + + Characters left: {{ 4000 - outsideLandsText.textLength }} +
+ warning +
This field is required
+
+
+
+ +
A visual representation of your proposal.
+ +
+
+
+
+

Notification and Public Hearing Requirements

+

+ A printed copy of the application will need to be used for notification. Please ensure all prior fields are complete + and correct before downloading the PDF of the application (Step 8 of this application form will flag outstanding + fields). +

+ Please refer to Exclusion page on the ALC website for more information. + + + You will not be able to complete the remaining portion of the application until the notification and public hearing + process is complete. + +
+
+
+ +
+ Proof that notice of the application was provided in a form and manner acceptable to the Commission +
+
+
+ +
+
+ +
+ Proof that a sign, in a form and manner acceptable to the Commission, was posted on the land that is the + subject of the application +
+ +
+
+ +
Public hearing report and any other public comments received
+ +
+
+
+
+
+ + +
+ + +
+
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.scss b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.scss new file mode 100644 index 0000000000..8223c57759 --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.scss @@ -0,0 +1,5 @@ +@use '../../../../../styles/functions' as *; + +section { + margin-top: rem(36); +} diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts new file mode 100644 index 0000000000..38c8bc753e --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts @@ -0,0 +1,59 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; + +import { ExclProposalComponent } from './excl-proposal.component'; + +describe('ExclProposalComponent', () => { + let component: ExclProposalComponent; + let fixture: ComponentFixture; + let mockApplicationService: DeepMocked; + let mockAppDocumentService: DeepMocked; + let mockRouter: DeepMocked; + + beforeEach(async () => { + mockApplicationService = createMock(); + mockAppDocumentService = createMock(); + mockRouter = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: ApplicationSubmissionService, + useValue: mockApplicationService, + }, + { + provide: ApplicationDocumentService, + useValue: mockAppDocumentService, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + declarations: [ExclProposalComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ExclProposalComponent); + component = fixture.componentInstance; + component.$applicationSubmission = new BehaviorSubject(undefined); + component.$applicationDocuments = new BehaviorSubject([]); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts new file mode 100644 index 0000000000..aa6e46f03a --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts @@ -0,0 +1,111 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { takeUntil } from 'rxjs'; +import { + ApplicationDocumentDto, + DOCUMENT_TYPE, +} from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +import { EditApplicationSteps } from '../../edit-submission.component'; +import { FilesStepComponent } from '../../files-step.partial'; + +@Component({ + selector: 'app-excl-proposal', + templateUrl: './excl-proposal.component.html', + styleUrls: ['./excl-proposal.component.scss'], +}) +export class ExclProposalComponent extends FilesStepComponent implements OnInit, OnDestroy { + DOCUMENT = DOCUMENT_TYPE; + + currentStep = EditApplicationSteps.Proposal; + prescribedBody: string | null = null; + + hectares = new FormControl(null, [Validators.required]); + shareProperty = new FormControl(null, [Validators.required]); + purpose = new FormControl(null, [Validators.required]); + whyExclude = new FormControl(null, [Validators.required]); + + form = new FormGroup({ + hectares: this.hectares, + purpose: this.purpose, + shareProperty: this.shareProperty, + whyExclude: this.whyExclude, + }); + private submissionUuid = ''; + proposalMap: ApplicationDocumentDto[] = []; + noticeOfPublicHearing: ApplicationDocumentDto[] = []; + proofOfSignage: ApplicationDocumentDto[] = []; + reportOfPublicHearing: ApplicationDocumentDto[] = []; + + constructor( + private router: Router, + private applicationSubmissionService: ApplicationSubmissionService, + applicationDocumentService: ApplicationDocumentService, + dialog: MatDialog + ) { + super(applicationDocumentService, dialog); + } + + ngOnInit(): void { + this.$applicationSubmission.pipe(takeUntil(this.$destroy)).subscribe((applicationSubmission) => { + if (applicationSubmission) { + this.fileId = applicationSubmission.fileNumber; + this.submissionUuid = applicationSubmission.uuid; + this.prescribedBody = applicationSubmission.prescribedBody; + + this.form.patchValue({ + hectares: applicationSubmission.inclExclHectares?.toString(), + purpose: applicationSubmission.purpose, + whyExclude: applicationSubmission.exclWhyLand, + }); + + if (applicationSubmission.exclShareGovernmentBorders !== null) { + this.shareProperty.setValue(applicationSubmission.exclShareGovernmentBorders ? 'true' : 'false'); + } + + if (this.showErrors) { + this.form.markAllAsTouched(); + } + } + }); + + this.$applicationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.noticeOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_ADVERTISING + ); + this.proofOfSignage = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_SIGNAGE); + this.reportOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING + ); + }); + } + + async onSave() { + await this.save(); + } + + protected async save() { + if (this.fileId) { + const inclExclHectares = this.hectares.value; + const purpose = this.purpose.value; + const exclWhyLand = this.whyExclude.value; + const shareProperty = this.shareProperty.value; + + const updateDto: ApplicationSubmissionUpdateDto = { + inclExclHectares: inclExclHectares ? parseFloat(inclExclHectares) : null, + purpose, + exclWhyLand, + exclShareGovernmentBorders: parseStringToBoolean(shareProperty), + }; + + const updatedApp = await this.applicationSubmissionService.updatePending(this.submissionUuid, updateDto); + this.$applicationSubmission.next(updatedApp); + } + } +} diff --git a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts index 4cd97a36c1..c2f9e5573a 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts @@ -1,11 +1,8 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; -import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { - ApplicationSubmissionDetailedDto, - ApplicationSubmissionUpdateDto, -} from '../../../../services/application-submission/application-submission.dto'; +import { takeUntil } from 'rxjs'; +import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; diff --git a/portal-frontend/src/app/services/application-document/application-document.dto.ts b/portal-frontend/src/app/services/application-document/application-document.dto.ts index 0fdb44eaa1..f5762d92fe 100644 --- a/portal-frontend/src/app/services/application-document/application-document.dto.ts +++ b/portal-frontend/src/app/services/application-document/application-document.dto.ts @@ -23,6 +23,9 @@ export enum DOCUMENT_TYPE { CROSS_SECTIONS = 'SPCS', RECLAMATION_PLAN = 'RECP', NOTICE_OF_WORK = 'NOWE', + PROOF_OF_SIGNAGE = 'POSA', + REPORT_OF_PUBLIC_HEARING = 'ROPH', + PROOF_OF_ADVERTISING = 'POAA', } export enum DOCUMENT_SOURCE { diff --git a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts index 513e8b1965..60fa775b03 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts @@ -138,6 +138,14 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD naruToPlaceAverageDepth: number | null; naruSleepingUnits: number | null; naruAgriTourism: string | null; + + //Inclusion / Exclusion Fields + prescribedBody: string | null; + inclExclHectares: number | null; + exclWhyLand: string | null; + inclAgricultureSupport: string | null; + inclImprovements: string | null; + exclShareGovernmentBorders: boolean | null; } export interface ApplicationSubmissionUpdateDto { @@ -236,6 +244,12 @@ export interface ApplicationSubmissionUpdateDto { naruSleepingUnits?: number | null; naruAgriTourism?: string | null; + //Inclusion / Exclusion Fields //Inclusion / Exclusion Fields prescribedBody?: string | null; + inclExclHectares?: number | null; + exclWhyLand?: string | null; + inclAgricultureSupport?: string | null; + inclImprovements?: string | null; + exclShareGovernmentBorders?: boolean | null; } diff --git a/services/apps/alcs/src/alcs/application/application-document/application-document-code.entity.ts b/services/apps/alcs/src/alcs/application/application-document/application-document-code.entity.ts index ed3b75e5d0..3e5ddb7e44 100644 --- a/services/apps/alcs/src/alcs/application/application-document/application-document-code.entity.ts +++ b/services/apps/alcs/src/alcs/application/application-document/application-document-code.entity.ts @@ -23,6 +23,9 @@ export enum DOCUMENT_TYPE { CROSS_SECTIONS = 'SPCS', RECLAMATION_PLAN = 'RECP', NOTICE_OF_WORK = 'NOWE', + PROOF_OF_SIGNAGE = 'POSA', + REPORT_OF_PUBLIC_HEARING = 'ROPH', + PROOF_OF_ADVERTISING = 'POAA', ORIGINAL_SUBMISSION = 'SUBO', UPDATED_SUBMISSION = 'SUBU', diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts index f1b14ac2b0..70defa84b2 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts @@ -117,11 +117,12 @@ export class ApplicationSubmissionController { @Post() async create(@Req() req, @Body() body: ApplicationSubmissionCreateDto) { - const { type } = body; + const { type, prescribedBody } = body; const user = req.user.entity as User; const newFileNumber = await this.applicationSubmissionService.create( type, user, + prescribedBody, ); return { fileId: newFileNumber, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts index 6883fb6f1e..2959014317 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts @@ -289,12 +289,35 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { @AutoMap(() => ApplicationSubmissionToSubmissionStatusDto) submissionStatuses: ApplicationSubmissionToSubmissionStatusDto[]; + + //Inclusion / Exclusion Fields + @AutoMap(() => String) + prescribedBody: string | null; + + @AutoMap(() => Number) + inclExclHectares: number | null; + + @AutoMap(() => String) + exclWhyLand: string | null; + + @AutoMap(() => String) + inclAgricultureSupport: string | null; + + @AutoMap(() => String) + inclImprovements: string | null; + + @AutoMap(() => Boolean) + exclShareGovernmentBorders: boolean | null; } export class ApplicationSubmissionCreateDto { @IsString() @IsNotEmpty() type: string; + + @IsString() + @IsOptional() + prescribedBody?: string; } export class ApplicationSubmissionUpdateDto { @@ -646,4 +669,29 @@ export class ApplicationSubmissionUpdateDto { @IsString() @IsOptional() naruAgriTourism?: string | null; + + //Inclusion / Exclusion Fields + @IsString() + @IsOptional() + prescribedBody?: string | null; + + @IsNumber() + @IsOptional() + inclExclHectares?: number | null; + + @IsString() + @IsOptional() + exclWhyLand?: string | null; + + @IsString() + @IsOptional() + inclAgricultureSupport?: string | null; + + @IsString() + @IsOptional() + inclImprovements?: string | null; + + @IsBoolean() + @IsOptional() + exclShareGovernmentBorders?: boolean | null; } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts index 5893038f79..41c10c042e 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts @@ -690,6 +690,10 @@ export class ApplicationSubmission extends Base { @Column({ type: 'text', nullable: true }) inclImprovements: string | null; + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + exclShareGovernmentBorders: boolean | null; + //END SUBMISSION FIELDS @AutoMap(() => Application) diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index 41ae69b8da..b87504f608 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -102,7 +102,7 @@ export class ApplicationSubmissionService { return application; } - async create(type: string, createdBy: User) { + async create(type: string, createdBy: User, prescribedBody?: string) { const fileNumber = await this.applicationService.generateNextFileNumber(); await this.applicationService.create( @@ -132,6 +132,7 @@ export class ApplicationSubmissionService { fileNumber, typeCode: type, createdBy, + prescribedBody, }); const submission = await this.applicationSubmissionRepository.save( @@ -168,6 +169,7 @@ export class ApplicationSubmissionService { await this.setSUBDFields(applicationSubmission, updateDto); await this.setSoilFields(applicationSubmission, updateDto); this.setNARUFields(applicationSubmission, updateDto); + this.setInclusionExclusionFields(applicationSubmission, updateDto); await this.applicationSubmissionRepository.save(applicationSubmission); @@ -934,6 +936,36 @@ export class ApplicationSubmissionService { ); } + private setInclusionExclusionFields( + applicationSubmission: ApplicationSubmission, + updateDto: ApplicationSubmissionUpdateDto, + ) { + applicationSubmission.prescribedBody = filterUndefined( + updateDto.prescribedBody, + applicationSubmission.prescribedBody, + ); + applicationSubmission.inclExclHectares = filterUndefined( + updateDto.inclExclHectares, + applicationSubmission.inclExclHectares, + ); + applicationSubmission.exclWhyLand = filterUndefined( + updateDto.exclWhyLand, + applicationSubmission.exclWhyLand, + ); + applicationSubmission.inclAgricultureSupport = filterUndefined( + updateDto.inclAgricultureSupport, + applicationSubmission.inclAgricultureSupport, + ); + applicationSubmission.inclImprovements = filterUndefined( + updateDto.inclImprovements, + applicationSubmission.inclImprovements, + ); + applicationSubmission.exclShareGovernmentBorders = filterUndefined( + updateDto.exclShareGovernmentBorders, + applicationSubmission.exclShareGovernmentBorders, + ); + } + async listNaruSubtypes() { return this.naruSubtypeRepository.find({ select: { diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1689980291162-add_share_property_to_submission.ts b/services/apps/alcs/src/providers/typeorm/migrations/1689980291162-add_share_property_to_submission.ts new file mode 100644 index 0000000000..92421374b2 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1689980291162-add_share_property_to_submission.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addSharePropertyToSubmission1689980291162 + implements MigrationInterface +{ + name = 'addSharePropertyToSubmission1689980291162'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "excl_share_government_borders" boolean`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "excl_share_government_borders"`, + ); + } +} From 89dc139ce7f55839a8fc9ada0d90ef9986bb22da Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Mon, 24 Jul 2023 09:32:37 -0700 Subject: [PATCH 110/954] Feature/alcs 880 decision v2 errors (#809) errors on decision v2 highlight buttons scroll to error update SUBM status colour --- .../decision-documents.component.html | 12 ++- .../decision-documents.component.ts | 1 + .../decision-components.component.html | 21 ++++- .../decision-components.component.ts | 5 ++ .../decision-conditions.component.html | 11 ++- .../decision-conditions.component.ts | 5 ++ .../decision-input-v2.component.html | 85 ++++++++++--------- .../decision-input-v2.component.scss | 17 ++++ .../decision-input-v2.component.ts | 84 ++++++++++++++---- .../error-message.component.html | 4 + .../error-message.component.scss | 17 ++++ .../error-message.component.spec.ts | 23 +++++ .../error-message/error-message.component.ts | 10 +++ alcs-frontend/src/app/shared/shared.module.ts | 3 + alcs-frontend/src/styles/ngselect.scss | 20 +++++ ...0214633898-alcs_submission_status_color.ts | 20 +++++ 16 files changed, 276 insertions(+), 62 deletions(-) create mode 100644 alcs-frontend/src/app/shared/error-message/error-message.component.html create mode 100644 alcs-frontend/src/app/shared/error-message/error-message.component.scss create mode 100644 alcs-frontend/src/app/shared/error-message/error-message.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/error-message/error-message.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1690214633898-alcs_submission_status_color.ts diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html index 9298139c2f..78377b19d1 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html @@ -3,7 +3,17 @@
Decision Documents *
- + +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts index c5a3c8356c..2010e9202f 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts @@ -21,6 +21,7 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { @Input() editable = true; @Input() loadData = true; @Input() decision: ApplicationDecisionDto | undefined; + @Input() showError = false; @Output() beforeDocumentUpload = new EventEmitter(); displayedColumns: string[] = ['type', 'fileName', 'source', 'visibilityFlags', 'uploadedAt', 'actions']; diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.html index 8074dc379e..b3a243ca19 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.html @@ -4,7 +4,11 @@
Decision Components
- +
- +
+ + +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index 403da2fa6b..8fa1564740 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -34,6 +34,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView @Input() codes!: DecisionCodesDto; @Input() fileNumber!: string; + @Input() showError = false; @Input() components: DecisionComponentDto[] = []; @Output() componentsChange = new EventEmitter<{ @@ -225,4 +226,8 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView isValid, }); } + + onValidate() { + this.childComponents.forEach((component) => component.form.markAllAsTouched()); + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html index f31d852659..34dd8b0936 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html @@ -12,6 +12,15 @@
Decision Conditions
- + +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 6fcf5886e3..0bf0b99623 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -34,6 +34,7 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy @Input() codes!: DecisionCodesDto; @Input() components: DecisionComponentDto[] = []; @Input() conditions: ApplicationDecisionConditionDto[] = []; + @Input() showError = false; @ViewChildren(DecisionConditionComponent) conditionComponents: DecisionConditionComponent[] = []; @Output() conditionsChange = new EventEmitter<{ @@ -197,4 +198,8 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy isValid: this.conditionComponents.reduce((isValid, component) => isValid && component.form.valid, true), }); } + + onValidate() { + this.conditionComponents.forEach((component) => component.form.markAllAsTouched()); + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html index 4c425eb490..f0bb7692b7 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html @@ -18,17 +18,22 @@

Decisions > Edit Decision Draft

- +
+ + +
Res #{{ resolutionNumberControl.getRawValue() }} / {{ resolutionYearControl.getRawValue() }} @@ -138,16 +143,21 @@

Decisions > Edit Decision Draft

>
- Subject to Conditions* - - Yes - No - +
+ Subject to Conditions* + + Yes + No + +
@@ -159,7 +169,14 @@

Decisions > Edit Decision Draft

Stats required* - + Yes No @@ -191,7 +208,10 @@

Decisions > Edit Decision Draft

- +
Decisions > Edit Decision Draft [fileNumber]="fileNumber" [components]="components" (componentsChange)="onComponentChange($event)" + [showError]="form.touched && components.length < 1" > @@ -210,6 +231,7 @@

Decisions > Edit Decision Draft

[components]="components" [conditions]="conditions" (conditionsChange)="onConditionsChange($event)" + [showError]="form.touched && conditionUpdates.length < 1 && showConditions" >
@@ -270,25 +292,12 @@

Audit and Chair Review Info

- +
-
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss index 9ab9319b14..e6e203b157 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss @@ -26,6 +26,9 @@ .resolution-number-wrapper { display: grid; grid-template-columns: 1fr 16px 0.7fr; + .resolution-number-btn-wrapper { + width: 100%; + } } .row-no-flex, @@ -143,4 +146,18 @@ color: #565656 !important; margin: 24px 0 !important; } + + .error-field-outlined.ng-invalid { + border-color: colors.$error-color !important; + + .mat-button-toggle { + border-color: colors.$error-color; + color: colors.$error-color !important; + } + + &.upload-button { + border: 2px solid colors.$error-color; + margin-bottom: 0px !important; + } + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index 014e30bed1..f2e92007fa 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MatDialog } from '@angular/material/dialog'; @@ -10,7 +10,6 @@ import { ApplicationModificationDto } from '../../../../../services/application/ import { ApplicationModificationService } from '../../../../../services/application/application-modification/application-modification.service'; import { ApplicationReconsiderationDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; import { ApplicationReconsiderationService } from '../../../../../services/application/application-reconsideration/application-reconsideration.service'; -import { ApplicationSubmissionService } from '../../../../../services/application/application-submission/application-submission.service'; import { ApplicationDecisionConditionDto, ApplicationDecisionDto, @@ -30,6 +29,8 @@ import { ToastService } from '../../../../../services/toast/toast.service'; import { formatDateForApi } from '../../../../../shared/utils/api-date-formatter'; import { parseBooleanToString, parseStringToBoolean } from '../../../../../shared/utils/boolean-helper'; import { ReleaseDialogComponent } from '../release-dialog/release-dialog.component'; +import { DecisionComponentsComponent } from './decision-components/decision-components.component'; +import { DecisionConditionsComponent } from './decision-conditions/decision-conditions.component'; export enum PostDecisionType { Modification = 'modification', @@ -76,6 +77,9 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { conditions: ApplicationDecisionConditionDto[] = []; conditionUpdates: UpdateApplicationDecisionConditionDto[] = []; + @ViewChild(DecisionComponentsComponent) decisionComponentsComponent?: DecisionComponentsComponent; + @ViewChild(DecisionConditionsComponent) decisionConditionsComponent?: DecisionConditionsComponent; + form = new FormGroup({ outcome: new FormControl(null, [Validators.required]), date: new FormControl(undefined, [Validators.required]), @@ -105,7 +109,6 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { public router: Router, private route: ActivatedRoute, private toastService: ToastService, - private applicationSubmissionService: ApplicationSubmissionService, private applicationService: ApplicationDetailService, public dialog: MatDialog ) {} @@ -523,22 +526,66 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { this.resolutionYearControl.enable(); } + private runValidation() { + this.form.markAllAsTouched(); + this.componentsValid = this.componentsValid && this.components.length > 0; + this.conditionsValid = this.conditionsValid && this.conditionUpdates.length > 0; + + if (this.decisionComponentsComponent) { + this.decisionComponentsComponent.onValidate(); + } + + if (this.decisionConditionsComponent) { + this.decisionConditionsComponent.onValidate(); + } + + if ( + !this.form.valid || + !this.conditionsValid || + !this.componentsValid || + (this.components.length === 0 && this.showComponents) || + (this.conditionUpdates.length === 0 && this.showConditions) + ) { + this.form.controls.decisionMaker.markAsDirty(); + this.toastService.showErrorToast('Please correct all errors before submitting the form'); + + // this will ensure that error rendering complete + setTimeout(() => this.scrollToError()); + + return false; + } else { + return true; + } + } + + private scrollToError() { + let elements = document.getElementsByClassName('ng-invalid'); + let elArray = Array.from(elements).filter((el) => el.nodeName !== 'FORM'); + + elArray[0]?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + } + async onRelease() { - this.dialog - .open(ReleaseDialogComponent, { - minWidth: '600px', - maxWidth: '900px', - maxHeight: '80vh', - width: '90%', - autoFocus: false, - }) - .afterClosed() - .subscribe(async (didAccept) => { - if (didAccept) { - await this.onSubmit(false, false); - await this.applicationService.loadApplication(this.fileNumber); - } - }); + if (this.runValidation()) { + this.dialog + .open(ReleaseDialogComponent, { + minWidth: '600px', + maxWidth: '900px', + maxHeight: '80vh', + width: '90%', + autoFocus: false, + }) + .afterClosed() + .subscribe(async (didAccept) => { + if (didAccept) { + await this.onSubmit(false, false); + await this.applicationService.loadApplication(this.fileNumber); + } + }); + } } onComponentChange($event: { components: DecisionComponentDto[]; isValid: boolean }) { @@ -549,6 +596,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { onConditionsChange($event: { conditions: UpdateApplicationDecisionConditionDto[]; isValid: boolean }) { this.conditionUpdates = $event.conditions; this.conditionsValid = $event.isValid; + this.conditionUpdates = Array.from($event.conditions); } onChangeDecisionOutcome(selectedOutcome: DecisionOutcomeCodeDto) { diff --git a/alcs-frontend/src/app/shared/error-message/error-message.component.html b/alcs-frontend/src/app/shared/error-message/error-message.component.html new file mode 100644 index 0000000000..0d7922d2e4 --- /dev/null +++ b/alcs-frontend/src/app/shared/error-message/error-message.component.html @@ -0,0 +1,4 @@ +
+ warning + This field is required +
diff --git a/alcs-frontend/src/app/shared/error-message/error-message.component.scss b/alcs-frontend/src/app/shared/error-message/error-message.component.scss new file mode 100644 index 0000000000..e0fdc68d29 --- /dev/null +++ b/alcs-frontend/src/app/shared/error-message/error-message.component.scss @@ -0,0 +1,17 @@ +@use '../../../styles/colors'; + +.error { + margin-top: 4px !important; + color: colors.$error-color; + display: flex; + align-items: center; + font-weight: 700 !important; + font-size: 14px; + + .mat-icon { + margin-right: 4px; + font-size: 14px; + width: 14px; + height: 14px; + } +} diff --git a/alcs-frontend/src/app/shared/error-message/error-message.component.spec.ts b/alcs-frontend/src/app/shared/error-message/error-message.component.spec.ts new file mode 100644 index 0000000000..8a9bf041c0 --- /dev/null +++ b/alcs-frontend/src/app/shared/error-message/error-message.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ErrorMessageComponent } from './error-message.component'; + +describe('ErrorMessageComponent', () => { + let component: ErrorMessageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ErrorMessageComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ErrorMessageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/error-message/error-message.component.ts b/alcs-frontend/src/app/shared/error-message/error-message.component.ts new file mode 100644 index 0000000000..f8b636425f --- /dev/null +++ b/alcs-frontend/src/app/shared/error-message/error-message.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-error-message', + templateUrl: './error-message.component.html', + styleUrls: ['./error-message.component.scss'] +}) +export class ErrorMessageComponent { + +} diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 7b44c0669f..53d08a1cb0 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -38,6 +38,7 @@ import { ApplicationSubmissionStatusTypePillComponent } from './application-subm import { ApplicationTypePillComponent } from './application-type-pill/application-type-pill.component'; import { AvatarCircleComponent } from './avatar-circle/avatar-circle.component'; import { DetailsHeaderComponent } from './details-header/details-header.component'; +import { ErrorMessageComponent } from './error-message/error-message.component'; import { FavoriteButtonComponent } from './favorite-button/favorite-button.component'; import { InlineBooleanComponent } from './inline-boolean/inline-boolean.component'; import { InlineDatepickerComponent } from './inline-datepicker/inline-datepicker.component'; @@ -90,6 +91,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen NoDataComponent, ApplicationSubmissionStatusTypePillComponent, WarningBannerComponent, + ErrorMessageComponent, ], imports: [ CommonModule, @@ -174,6 +176,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen NoDataComponent, ApplicationSubmissionStatusTypePillComponent, WarningBannerComponent, + ErrorMessageComponent, ], }) export class SharedModule { diff --git a/alcs-frontend/src/styles/ngselect.scss b/alcs-frontend/src/styles/ngselect.scss index 5ec0ce3465..fe0fb471c4 100644 --- a/alcs-frontend/src/styles/ngselect.scss +++ b/alcs-frontend/src/styles/ngselect.scss @@ -76,3 +76,23 @@ .ng-select.ng-select-single .ng-select-container:not(.ng-appearance-outline) .ng-arrow-wrapper { bottom: 15px !important; } + +.ng-select.ng-invalid.ng-touched { + border-color: colors.$error-color !important; +} + +.ng-select.ng-select-focused.ng-invalid.ng-touched .ng-select-container:after { + border-color: colors.$error-color; +} + +.ng-select.ng-invalid.ng-touched .ng-select-container { + border-color: colors.$error-color !important; +} + +.ng-select.ng-invalid.ng-touched .ng-select-container.ng-appearance-outline:after { + border-color: colors.$error-color !important; +} + +.ng-select.ng-invalid.ng-touched .ng-value-container .ng-placeholder { + color: colors.$error-color !important; +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690214633898-alcs_submission_status_color.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690214633898-alcs_submission_status_color.ts new file mode 100644 index 0000000000..210d78c0c3 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690214633898-alcs_submission_status_color.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class alcsSubmissionStatusColor1690214633898 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + UPDATE "alcs"."application_submission_status_type" + SET "alcs_background_color" = '#94c6ac', + "alcs_color" = '#002f17' + WHERE "code" IN ('SUBM'); + `, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // nope + } +} From 224ab8d957c004fbd64520c6e5c2df49ba160935 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 24 Jul 2023 09:58:40 -0700 Subject: [PATCH 111/954] Code Review Feedback --- .../app/features/edit-submission/edit-submission.component.ts | 1 - .../application-submission/application-submission.dto.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index 9034b4075d..60790c333b 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -208,7 +208,6 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit } private async saveProposalSteps() { - debugger; if (this.nfuProposalComponent) { await this.nfuProposalComponent.onSave(); } diff --git a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts index 60fa775b03..44fb25220b 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts @@ -244,7 +244,6 @@ export interface ApplicationSubmissionUpdateDto { naruSleepingUnits?: number | null; naruAgriTourism?: string | null; - //Inclusion / Exclusion Fields //Inclusion / Exclusion Fields prescribedBody?: string | null; inclExclHectares?: number | null; From 9de0b64299d667dadb0e2b07c341d0f0da6fde87 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 24 Jul 2023 10:29:04 -0700 Subject: [PATCH 112/954] added break out logig to not download everything on local --- bin/migrate-files/migrate-files.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bin/migrate-files/migrate-files.py b/bin/migrate-files/migrate-files.py index 1940cd65d9..caf86efabb 100644 --- a/bin/migrate-files/migrate-files.py +++ b/bin/migrate-files/migrate-files.py @@ -165,6 +165,7 @@ documents_processed = 0 last_planning_document_id = 0 max_file = 0 +breakout = False try: with tqdm(total=planning_review_count, unit="file", desc="Uploading files to S3") as pbar: @@ -186,7 +187,9 @@ max_file += 1 print("number of files", max_file) if max_file > 4: - break + breakout = True + if breakout: + break except Exception as e: print("Something went wrong:",e) print("Processed", documents_processed, "files") @@ -234,6 +237,7 @@ documents_processed = 0 last_issue_document_id = 0 max_file = 0 +breakout = False try: with tqdm(total=issue_count, unit="file", desc="Uploading files to S3") as pbar: @@ -255,7 +259,9 @@ max_file += 1 print("number of files", max_file) if max_file > 4: - break + breakout = True + if breakout: + break except Exception as e: print("Something went wrong:",e) print("Processed", documents_processed, "files") From 4614e022d46cc0d8ea4a428132a845a9dcf1d0c1 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 24 Jul 2023 11:02:35 -0700 Subject: [PATCH 113/954] removed local db restrictions --- bin/migrate-files/migrate-files.py | 47 +++++------------------------- 1 file changed, 7 insertions(+), 40 deletions(-) diff --git a/bin/migrate-files/migrate-files.py b/bin/migrate-files/migrate-files.py index caf86efabb..edb46e8745 100644 --- a/bin/migrate-files/migrate-files.py +++ b/bin/migrate-files/migrate-files.py @@ -12,10 +12,10 @@ db_username = os.getenv("DB_USERNAME") db_password = os.getenv("DB_PASSWORD") db_dsn = os.getenv("DB_DSN") -db_path = os.getenv("DB_PATH") +# db_path = os.getenv("DB_PATH") # only necessary if running on local M1 # Connect to the Oracle database -cx_Oracle.init_oracle_client(lib_dir=db_path) +# cx_Oracle.init_oracle_client(lib_dir=db_path) # only necessary if running on local M1 conn = cx_Oracle.connect( user=db_username, password=db_password, dsn=db_dsn, encoding="UTF-8" ) @@ -31,7 +31,7 @@ "s3", aws_access_key_id=ecs_access_key, aws_secret_access_key=ecs_secret_key, - use_ssl=False, + use_ssl=True, endpoint_url=ecs_host, ) @@ -40,7 +40,7 @@ if os.path.isfile('last-file.pickle'): with open('last-file.pickle', 'rb') as file: starting_document_id = pickle.load(file) -# starting_document_id = 999999 + print('Starting applications from:', starting_document_id) starting_planning_document_id = 0 @@ -92,8 +92,6 @@ # Track progress documents_processed = 0 last_document_id = 0 -max_file = 0 -breakout = False try: with tqdm(total=application_count, unit="file", desc="Uploading files to S3") as pbar: @@ -112,12 +110,7 @@ pbar.update(1) last_document_id = document_id documents_processed += 1 - max_file += 1 - print("number of files", max_file) - if max_file > 4: - breakout = True - if breakout: - break + except Exception as e: print("Something went wrong:",e) print("Processed", documents_processed, "files") @@ -133,12 +126,6 @@ # Display results print("Process complete: Successfully migrated", documents_processed, "files.") -# Close the database cursor and connection -# cursor.close() -# conn.close() - -# cursor = conn.cursor() - try: cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND PLANNING_REVIEW_ID IS NOT NULL') except cx_Oracle.Error as e: @@ -164,8 +151,6 @@ # Track progress documents_processed = 0 last_planning_document_id = 0 -max_file = 0 -breakout = False try: with tqdm(total=planning_review_count, unit="file", desc="Uploading files to S3") as pbar: @@ -184,12 +169,7 @@ pbar.update(1) last_planning_document_id = document_id documents_processed += 1 - max_file += 1 - print("number of files", max_file) - if max_file > 4: - breakout = True - if breakout: - break + except Exception as e: print("Something went wrong:",e) print("Processed", documents_processed, "files") @@ -205,12 +185,6 @@ # Display results print("Process complete: Successfully migrated", documents_processed, "files.") -# Close the database cursor and connection -# cursor.close() -# conn.close() - -# cursor = conn.cursor() - try: cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND ISSUE_ID IS NOT NULL') except cx_Oracle.Error as e: @@ -236,8 +210,6 @@ # Track progress documents_processed = 0 last_issue_document_id = 0 -max_file = 0 -breakout = False try: with tqdm(total=issue_count, unit="file", desc="Uploading files to S3") as pbar: @@ -256,12 +228,7 @@ pbar.update(1) last_issue_document_id = document_id documents_processed += 1 - max_file += 1 - print("number of files", max_file) - if max_file > 4: - breakout = True - if breakout: - break + except Exception as e: print("Something went wrong:",e) print("Processed", documents_processed, "files") From 11e30fa36f46de735da77dd5ecac061a3b846a29 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 24 Jul 2023 11:14:28 -0700 Subject: [PATCH 114/954] Bug Fixes for L/FNG as Applicant * Only redirect to LFNG review is submitting as same government * Make department mandatory on Step 3 * Always show authorization letter errors --- .../edit-submission.component.ts | 38 ++++-- .../primary-contact.component.html | 25 ++-- .../primary-contact.component.ts | 48 +++++--- .../application-document.service.ts | 2 +- .../application-submission.service.ts | 4 +- .../application-submission.controller.spec.ts | 110 +++++++++++------- .../application-submission.service.ts | 7 +- 7 files changed, 150 insertions(+), 84 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index 60790c333b..59dfe11d14 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -1,14 +1,14 @@ import { StepperSelectionEvent } from '@angular/cdk/stepper'; -import { AfterViewInit, Component, ContentChildren, OnDestroy, OnInit, ViewChild, ViewChildren } from '@angular/core'; +import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; import { ApplicationDocumentDto } from '../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { APPLICATION_OWNER } from '../../services/application-owner/application-owner.dto'; import { ApplicationSubmissionReviewService } from '../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionDetailedDto } from '../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; +import { CodeService } from '../../services/code/code.service'; import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; import { ToastService } from '../../services/toast/toast.service'; import { CustomStepperComponent } from '../../shared/custom-stepper/custom-stepper.component'; @@ -29,7 +29,6 @@ import { RosoProposalComponent } from './proposal/roso-proposal/roso-proposal.co import { SubdProposalComponent } from './proposal/subd-proposal/subd-proposal.component'; import { TurProposalComponent } from './proposal/tur-proposal/tur-proposal.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; -import { StepComponent } from './step.partial'; export enum EditApplicationSteps { AppParcel = 0, @@ -87,7 +86,8 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit private overlayService: OverlaySpinnerService, private router: Router, private pdfGenerationService: PdfGenerationService, - private applicationReviewService: ApplicationSubmissionReviewService + private applicationReviewService: ApplicationSubmissionReviewService, + private codeService: CodeService ) {} ngOnInit(): void { @@ -257,17 +257,31 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit async onSubmit() { const submission = this.applicationSubmission; if (submission) { - await this.applicationSubmissionService.submitToAlcs(submission.uuid); + const didSubmit = await this.applicationSubmissionService.submitToAlcs(submission.uuid); + if (didSubmit) { + let government; + if (this.applicationSubmission?.localGovernmentUuid) { + government = await this.loadGovernment(this.applicationSubmission?.localGovernmentUuid); + } - const primaryContact = submission.owners.find((owner) => owner.uuid === submission?.primaryContactOwnerUuid); - if (primaryContact && primaryContact.type.code === APPLICATION_OWNER.GOVERNMENT) { - const review = await this.applicationReviewService.startReview(submission.fileNumber); - if (review) { - await this.router.navigateByUrl(`/application/${submission?.fileNumber}/review`); + if (government && government.matchesUserGuid) { + const review = await this.applicationReviewService.startReview(submission.fileNumber); + if (review) { + await this.router.navigateByUrl(`/application/${submission?.fileNumber}/review`); + } + } else { + await this.router.navigateByUrl(`/application/${submission?.fileNumber}`); } - } else { - await this.router.navigateByUrl(`/application/${submission?.fileNumber}`); } } } + + private async loadGovernment(uuid: string) { + const codes = await this.codeService.loadCodes(); + const localGovernment = codes.localGovernments.find((a) => a.uuid === uuid); + if (localGovernment) { + return localGovernment; + } + return; + } } diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html index 313af24e69..dd83477975 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html @@ -1,7 +1,7 @@

Primary Contact

-

Select from the listed parcel owners or identify a third party agent

-

+

Select from the listed parcel owners or identify a third party agent

+

Identify staff from the local or first nation government listed below or a third-party agent to act as the primary contact.

@@ -28,8 +28,8 @@
{{ owner.displayName }}
checkPrimary Contact
- -
{{governmentName}} Staff
+ +
{{ governmentName }} Staff
@@ -126,8 +133,8 @@

Primary Contact Information

Primary Contact Authorization Letters

- An authorization letter must be provided if: - + + An authorization letter must be provided if:
  1. the parcel under application is owned by more than one person;
  2. the parcel(s) is owned by an organization; or
  3. @@ -140,7 +147,7 @@

    Primary Contact Authorization Letters

    further instruction and an Authorization Letter template.

    - + An authorization letter must be provided only if the application is being submitted by a third-party agent. Please consult the Supporting Documentation page of the TODO: FIX THIS: ALC website for further instruction. @@ -153,8 +160,8 @@
    Authorization Letters (if applicable)
    (uploadFiles)="attachFile($event, DOCUMENT_TYPE.AUTHORIZATION_LETTER)" (deleteFile)="onDeleteFile($event)" (openFile)="openFile($event)" - [showErrors]="showErrors" - [isRequired]="needsAuthorizationLetter || selectedThirdPartyAgent" + [showErrors]="showErrors || selectedLocalGovernment" + [isRequired]="needsAuthorizationLetter" >
diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index 71d0d984a6..7efc0ae9f5 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -29,7 +29,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni selectedLocalGovernment = false; selectedOwnerUuid: string | undefined = undefined; isCrownOwner = false; - isLocalGovernmentUser = false; + isGovernmentUser = false; governmentName: string | undefined; firstName = new FormControl('', [Validators.required]); @@ -69,9 +69,9 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni }); this.authenticationService.$currentProfile.pipe(takeUntil(this.$destroy)).subscribe((profile) => { - this.isLocalGovernmentUser = !!profile?.government; + this.isGovernmentUser = !!profile?.isLocalGovernment || !!profile?.isFirstNationGovernment; this.governmentName = profile?.government; - if (this.isLocalGovernmentUser) { + if (this.isGovernmentUser) { this.prepareGovernmentOwners(); } }); @@ -106,6 +106,12 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT) || uuid == 'government'; this.form.reset(); + if (this.selectedLocalGovernment) { + this.organizationName.setValidators([Validators.required]); + } else { + this.organizationName.setValidators([]); + } + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { this.firstName.enable(); this.lastName.enable(); @@ -131,23 +137,29 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.isCrownOwner = selectedOwner.type.code === APPLICATION_OWNER.CROWN; } } + this.calculateLetterRequired(); + } + private calculateLetterRequired() { const isSelfApplicant = this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || this.owners[0].type.code === APPLICATION_OWNER.GOVERNMENT; - this.needsAuthorizationLetter = !( - isSelfApplicant && - (this.owners.length === 1 || - (this.owners.length === 2 && - this.owners[1].type.code === APPLICATION_OWNER.AGENT && - !this.selectedThirdPartyAgent)) - ); + this.needsAuthorizationLetter = + this.selectedThirdPartyAgent || + !( + isSelfApplicant && + (this.owners.length === 1 || + (this.owners.length === 2 && + this.owners[1].type.code === APPLICATION_OWNER.AGENT && + !this.selectedThirdPartyAgent)) + ); + this.files = this.files.map((file) => ({ ...file, - errorMessage: this.needsAuthorizationLetter - ? undefined - : 'Authorization Letter not required. Please remove this file.', + errorMessage: !this.needsAuthorizationLetter + ? 'Authorization Letter not required. Please remove this file.' + : undefined, })); } @@ -189,6 +201,12 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.selectedLocalGovernment = selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT; } + if (this.selectedLocalGovernment) { + this.organizationName.setValidators([Validators.required]); + } else { + this.organizationName.setValidators([]); + } + if (selectedOwner && (this.selectedThirdPartyAgent || this.selectedLocalGovernment)) { this.selectedOwnerUuid = selectedOwner.uuid; this.form.patchValue({ @@ -198,6 +216,8 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni phoneNumber: selectedOwner.phoneNumber, email: selectedOwner.email, }); + + this.calculateLetterRequired(); } else if (selectedOwner) { this.onSelectOwner(selectedOwner.uuid); } else { @@ -208,7 +228,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.phoneNumber.disable(); } - if (this.isLocalGovernmentUser) { + if (this.isGovernmentUser) { this.prepareGovernmentOwners(); } diff --git a/portal-frontend/src/app/services/application-document/application-document.service.ts b/portal-frontend/src/app/services/application-document/application-document.service.ts index 9990a477fa..34020c3ebe 100644 --- a/portal-frontend/src/app/services/application-document/application-document.service.ts +++ b/portal-frontend/src/app/services/application-document/application-document.service.ts @@ -88,7 +88,7 @@ export class ApplicationDocumentService { ); } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to update documents, please try again'); + this.toastService.showErrorToast('Failed to fetch documents, please try again'); } return undefined; } diff --git a/portal-frontend/src/app/services/application-submission/application-submission.service.ts b/portal-frontend/src/app/services/application-submission/application-submission.service.ts index 0a48f3faa2..47c54836c5 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.service.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.service.ts @@ -104,9 +104,10 @@ export class ApplicationSubmissionService { } async submitToAlcs(uuid: string) { + let res; try { this.overlayService.showSpinner(); - await firstValueFrom( + res = await firstValueFrom( this.httpClient.post(`${this.serviceUrl}/alcs/submit/${uuid}`, {}) ); this.toastService.showSuccessToast('Application Submitted'); @@ -116,5 +117,6 @@ export class ApplicationSubmissionService { } finally { this.overlayService.hideSpinner(); } + return res; } } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts index 19fe26901d..cc28740ba7 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts @@ -28,7 +28,7 @@ import { ApplicationSubmissionService } from './application-submission.service'; describe('ApplicationSubmissionController', () => { let controller: ApplicationSubmissionController; - let mockAppService: DeepMocked; + let mockAppSubmissionService: DeepMocked; let mockDocumentService: DeepMocked; let mockLgService: DeepMocked; let mockAppValidationService: DeepMocked; @@ -38,7 +38,7 @@ describe('ApplicationSubmissionController', () => { const bceidBusinessGuid = 'business-guid'; beforeEach(async () => { - mockAppService = createMock(); + mockAppSubmissionService = createMock(); mockDocumentService = createMock(); mockLgService = createMock(); mockAppValidationService = createMock(); @@ -49,7 +49,7 @@ describe('ApplicationSubmissionController', () => { ApplicationProfile, { provide: ApplicationSubmissionService, - useValue: mockAppService, + useValue: mockAppSubmissionService, }, { provide: ApplicationDocumentService, @@ -80,25 +80,25 @@ describe('ApplicationSubmissionController', () => { ApplicationSubmissionController, ); - mockAppService.update.mockResolvedValue( + mockAppSubmissionService.update.mockResolvedValue( new ApplicationSubmission({ applicant: applicant, localGovernmentUuid, }), ); - mockAppService.create.mockResolvedValue('2'); - mockAppService.getIfCreatorByFileNumber.mockResolvedValue( + mockAppSubmissionService.create.mockResolvedValue('2'); + mockAppSubmissionService.getIfCreatorByFileNumber.mockResolvedValue( new ApplicationSubmission(), ); - mockAppService.verifyAccessByFileId.mockResolvedValue( + mockAppSubmissionService.verifyAccessByFileId.mockResolvedValue( new ApplicationSubmission(), ); - mockAppService.verifyAccessByUuid.mockResolvedValue( + mockAppSubmissionService.verifyAccessByUuid.mockResolvedValue( new ApplicationSubmission(), ); - mockAppService.mapToDTOs.mockResolvedValue([]); + mockAppSubmissionService.mapToDTOs.mockResolvedValue([]); mockLgService.list.mockResolvedValue([ new ApplicationLocalGovernment({ uuid: localGovernmentUuid, @@ -114,7 +114,7 @@ describe('ApplicationSubmissionController', () => { }); it('should call out to service when fetching applications', async () => { - mockAppService.getByUser.mockResolvedValue([]); + mockAppSubmissionService.getByUser.mockResolvedValue([]); const applications = await controller.getApplications({ user: { @@ -123,11 +123,11 @@ describe('ApplicationSubmissionController', () => { }); expect(applications).toBeDefined(); - expect(mockAppService.getByUser).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.getByUser).toHaveBeenCalledTimes(1); }); it('should fetch by bceid if user has same guid as a local government', async () => { - mockAppService.getForGovernment.mockResolvedValue([]); + mockAppSubmissionService.getForGovernment.mockResolvedValue([]); const applications = await controller.getApplications({ user: { @@ -138,7 +138,7 @@ describe('ApplicationSubmissionController', () => { }); expect(applications).toBeDefined(); - expect(mockAppService.getForGovernment).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.getForGovernment).toHaveBeenCalledTimes(1); }); it('should call out to service when cancelling an application', async () => { @@ -150,11 +150,13 @@ describe('ApplicationSubmissionController', () => { }), }); - mockAppService.mapToDTOs.mockResolvedValue([ + mockAppSubmissionService.mapToDTOs.mockResolvedValue([ {} as ApplicationSubmissionDto, ]); - mockAppService.verifyAccessByUuid.mockResolvedValue(mockApplication); - mockAppService.cancel.mockResolvedValue(); + mockAppSubmissionService.verifyAccessByUuid.mockResolvedValue( + mockApplication, + ); + mockAppSubmissionService.cancel.mockResolvedValue(); const application = await controller.cancel('file-id', { user: { @@ -163,9 +165,11 @@ describe('ApplicationSubmissionController', () => { }); expect(application).toBeDefined(); - expect(mockAppService.cancel).toHaveBeenCalledTimes(1); - expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledTimes(1); - expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledWith( + expect(mockAppSubmissionService.cancel).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledWith( 'file-id', new User(), ); @@ -173,7 +177,7 @@ describe('ApplicationSubmissionController', () => { it('should throw an exception when trying to cancel an application that is not in progress', async () => { const mockApp = new ApplicationSubmission(); - mockAppService.verifyAccessByUuid.mockResolvedValue({ + mockAppSubmissionService.verifyAccessByUuid.mockResolvedValue({ ...mockApp, status: new ApplicationSubmissionStatusType({ code: SUBMISSION_STATUS.CANCELLED, @@ -189,16 +193,18 @@ describe('ApplicationSubmissionController', () => { await expect(promise).rejects.toMatchObject( new BadRequestException('Can only cancel in progress Applications'), ); - expect(mockAppService.cancel).toHaveBeenCalledTimes(0); - expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledTimes(1); - expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledWith( + expect(mockAppSubmissionService.cancel).toHaveBeenCalledTimes(0); + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledWith( 'file-id', new User(), ); }); it('should call out to service when fetching an application', async () => { - mockAppService.mapToDetailedDTO.mockResolvedValue( + mockAppSubmissionService.mapToDetailedDTO.mockResolvedValue( {} as ApplicationSubmissionDetailedDto, ); @@ -212,14 +218,16 @@ describe('ApplicationSubmissionController', () => { ); expect(application).toBeDefined(); - expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); }); it('should fetch application by bceid if user has same guid as a local government', async () => { - mockAppService.mapToDetailedDTO.mockResolvedValue( + mockAppSubmissionService.mapToDetailedDTO.mockResolvedValue( {} as ApplicationSubmissionDetailedDto, ); - mockAppService.verifyAccessByUuid.mockResolvedValue( + mockAppSubmissionService.verifyAccessByUuid.mockResolvedValue( new ApplicationSubmission({ localGovernmentUuid: '', }), @@ -237,12 +245,14 @@ describe('ApplicationSubmissionController', () => { ); expect(application).toBeDefined(); - expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); }); it('should call out to service when creating an application', async () => { - mockAppService.create.mockResolvedValue(''); - mockAppService.mapToDTOs.mockResolvedValue([ + mockAppSubmissionService.create.mockResolvedValue(''); + mockAppSubmissionService.mapToDTOs.mockResolvedValue([ {} as ApplicationSubmissionDto, ]); @@ -258,11 +268,11 @@ describe('ApplicationSubmissionController', () => { ); expect(application).toBeDefined(); - expect(mockAppService.create).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.create).toHaveBeenCalledTimes(1); }); it('should call out to service for update and map', async () => { - mockAppService.mapToDetailedDTO.mockResolvedValue( + mockAppSubmissionService.mapToDetailedDTO.mockResolvedValue( {} as ApplicationSubmissionDetailedDto, ); @@ -279,19 +289,23 @@ describe('ApplicationSubmissionController', () => { }, ); - expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledTimes(1); - expect(mockAppService.mapToDetailedDTO).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockAppSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(1); }); it('should call out to service on submitAlcs if application type is TURP', async () => { const mockFileId = 'file-id'; - mockAppService.submitToAlcs.mockResolvedValue(new Application()); - mockAppService.getIfCreatorByUuid.mockResolvedValue( + mockAppSubmissionService.submitToAlcs.mockResolvedValue(new Application()); + mockAppSubmissionService.getIfCreatorByUuid.mockResolvedValue( new ApplicationSubmission({ typeCode: 'TURP', }), ); - mockAppService.updateStatus.mockResolvedValue(); + mockAppSubmissionService.updateStatus.mockResolvedValue( + new ApplicationSubmissionToSubmissionStatus(), + ); mockAppValidationService.validateSubmission.mockResolvedValue({ application: new ApplicationSubmission({ typeCode: 'TURP', @@ -305,15 +319,19 @@ describe('ApplicationSubmissionController', () => { }, }); - expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledTimes(1); - expect(mockAppService.submitToAlcs).toHaveBeenCalledTimes(1); - expect(mockAppService.updateStatus).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockAppSubmissionService.submitToAlcs).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.updateStatus).toHaveBeenCalledTimes(1); }); it('should submit to LG if application type is NOT-TURP', async () => { const mockFileId = 'file-id'; - mockAppService.submitToLg.mockResolvedValue(); - mockAppService.getIfCreatorByUuid.mockResolvedValue( + mockAppSubmissionService.submitToLg.mockResolvedValue( + new ApplicationSubmissionToSubmissionStatus(), + ); + mockAppSubmissionService.getIfCreatorByUuid.mockResolvedValue( new ApplicationSubmission({ typeCode: 'NOT-TURP', localGovernmentUuid, @@ -331,13 +349,15 @@ describe('ApplicationSubmissionController', () => { }, }); - expect(mockAppService.verifyAccessByUuid).toHaveBeenCalledTimes(1); - expect(mockAppService.submitToLg).toHaveBeenCalledTimes(1); + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockAppSubmissionService.submitToLg).toHaveBeenCalledTimes(1); }); it('should throw an exception if application fails validation', async () => { const mockFileId = 'file-id'; - mockAppService.verifyAccessByUuid.mockResolvedValue( + mockAppSubmissionService.verifyAccessByUuid.mockResolvedValue( new ApplicationSubmission({ typeCode: 'NOT-TURP', }), diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index b87504f608..764b880842 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -192,7 +192,10 @@ export class ApplicationSubmissionService { } async submitToLg(submission: ApplicationSubmission) { - await this.updateStatus(submission, SUBMISSION_STATUS.SUBMITTED_TO_LG); + return await this.updateStatus( + submission, + SUBMISSION_STATUS.SUBMITTED_TO_LG, + ); } async updateStatus( @@ -200,7 +203,7 @@ export class ApplicationSubmissionService { statusCode: SUBMISSION_STATUS, effectiveDate?: Date | null, ) { - await this.applicationSubmissionStatusService.setStatusDate( + return await this.applicationSubmissionStatusService.setStatusDate( applicationSubmission.uuid, statusCode, effectiveDate, From b8dd7335acac9a9e36e1a9c9a25f1af77c167d1e Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 24 Jul 2023 11:25:43 -0700 Subject: [PATCH 115/954] updated readme --- bin/migrate-files/README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/bin/migrate-files/README.md b/bin/migrate-files/README.md index 51bb0bd6ce..691a63087d 100644 --- a/bin/migrate-files/README.md +++ b/bin/migrate-files/README.md @@ -2,9 +2,11 @@ This is a Python script for uploading files from the OATS database to the Dell ECS object storage service. -The files are uploaded in the format `/migrate/{application_id}/{document_id}_{filename}` where: +The files are uploaded in the format `/migrate/application||issue||planning_review/{application_id}||{issue_id}||{planning_review_id}/{document_id}_{filename}` where: - `application_id` is the associated OATS application ID +- `issue_id` is the associated OATS issue ID +- `planning_review_id` is the associated OATS planning_review ID - `document_id` is the primary key from the documents table - `filename` is the filename metadata from the documents table @@ -32,10 +34,24 @@ ECS_ACCESS_KEY: Dell ECS access key ECS_SECRET_KEY: Dell ECS secret key These variables can be stored in the .env file. +## Arm64 quirks + +### If running on M1: + +DB_PATH: Path to oracle instant client driver folder +example `DB_PATH=/Users/user/Downloads/instantclient_19_8` + +Force run python in emulation mode and reinstall requirements +`python3-intel64 -m pip install -r requirements.txt` + ## Running the Script To run the script, run the following command: `python migrate-files.py` +M1: + +`python3-intel64 migrate-files.py` + The script will start uploading files from the Oracle database to DELL ECS. The upload progress will be displayed in a progress bar. The script will also save the last uploaded document id, so the upload process can be resumed from where it left off in case of any interruption. From e22e72d78bae45ba8ce9189c67e0a4558811abd8 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 24 Jul 2023 11:28:01 -0700 Subject: [PATCH 116/954] remove unused files --- bin/migrate-files/app_docs.sql | 72 ----------------------- bin/migrate-files/noi_docs.sql | 104 --------------------------------- 2 files changed, 176 deletions(-) delete mode 100644 bin/migrate-files/app_docs.sql delete mode 100644 bin/migrate-files/noi_docs.sql diff --git a/bin/migrate-files/app_docs.sql b/bin/migrate-files/app_docs.sql deleted file mode 100644 index 15a6592c3a..0000000000 --- a/bin/migrate-files/app_docs.sql +++ /dev/null @@ -1,72 +0,0 @@ -DROP TABLE IF EXISTS oats.alcs_etl_app_docs; - -CREATE TABLE - oats.alcs_etl_app_docs ( - document_id INT, - document_code VARCHAR, - alr_application_id INT, - issue_id INT, - planning_review_id INT, - file_name VARCHAR, - "description" VARCHAR, - document_blob BYTEA, - referenced_in_staff_rpt_ind VARCHAR, - document_source_code VARCHAR, - subject_property_id INT, - publicly_viewable_ind VARCHAR, - app_lg_viewable_ind VARCHAR, - uploaded_date TIMESTAMP, - who_created VARCHAR, - when_created TIMESTAMP, - who_updated VARCHAR, - when_updated TIMESTAMP, - revision_count INT - ); - -INSERT INTO - oats.alcs_etl_app_docs ( - document_id, - document_code, - alr_application_id, - issue_id, - planning_review_id, - file_name, - "description", - document_blob, - referenced_in_staff_rpt_ind, - document_source_code, - subject_property_id, - publicly_viewable_ind, - app_lg_viewable_ind, - uploaded_date, - who_created, - when_created, - who_updated, - when_updated, - revision_count - ) -select - ad.document_id, - ad.document_code, - ad.alr_application_id, - ad.issue_id, - ad.planning_review_id, - ad.file_name, - ad."description", - ad.document_blob, - ad.referenced_in_staff_rpt_ind, - ad.document_source_code, - ad.subject_property_id, - ad.publicly_viewable_ind, - ad.app_lg_viewable_ind, - ad.uploaded_date, - ad.who_created, - ad.when_created, - ad.who_updated, - ad.when_updated, - ad.revision_count -from - oats.alcs_etl_noi_docs nd - right join oats.oats_documents ad on nd.document_id = ad.document_id -where - nd.document_id is null \ No newline at end of file diff --git a/bin/migrate-files/noi_docs.sql b/bin/migrate-files/noi_docs.sql deleted file mode 100644 index c543831a15..0000000000 --- a/bin/migrate-files/noi_docs.sql +++ /dev/null @@ -1,104 +0,0 @@ -DROP TABLE IF EXISTS oats.alcs_etl_noi_docs; - -CREATE TABLE - oats.alcs_etl_noi_docs ( - document_id INT, - document_code VARCHAR, - alr_application_id INT, - issue_id INT, - planning_review_id INT, - file_name VARCHAR, - "description" VARCHAR, - document_blob BYTEA, - referenced_in_staff_rpt_ind VARCHAR, - document_source_code VARCHAR, - subject_property_id INT, - publicly_viewable_ind VARCHAR, - app_lg_viewable_ind VARCHAR, - uploaded_date TIMESTAMP, - who_created VARCHAR, - when_created TIMESTAMP, - who_updated VARCHAR, - when_updated TIMESTAMP, - revision_count INT - ); - -INSERT INTO - oats.alcs_etl_noi_docs ( - document_id, - document_code, - alr_application_id, - issue_id, - planning_review_id, - file_name, - "description", - document_blob, - referenced_in_staff_rpt_ind, - document_source_code, - subject_property_id, - publicly_viewable_ind, - app_lg_viewable_ind, - uploaded_date, - who_created, - when_created, - who_updated, - when_updated, - revision_count - ) -WITH - noi AS ( - SELECT - od.alr_application_id AS app_id, - od.issue_id AS tissue_id - FROM - oats.oats_documents od - WHERE - document_code = 'NOI' - ), - appl AS ( - SELECT - * - FROM - oats.oats_documents od2 - JOIN noi ON od2.alr_application_id = noi.app_id - ), - issue AS ( - SELECT - * - FROM - oats.oats_documents od3 - JOIN noi ON od3.issue_id = noi.tissue_id - ), - noi_docs AS ( - SELECT - * - FROM - appl - UNION - SELECT - * - FROM - issue - ) -SELECT - nd.document_id, - nd.document_code, - nd.alr_application_id, - nd.issue_id, - nd.planning_review_id, - nd.file_name, - nd."description", - nd.document_blob, - nd.referenced_in_staff_rpt_ind, - nd.document_source_code, - nd.subject_property_id, - nd.publicly_viewable_ind, - nd.app_lg_viewable_ind, - nd.uploaded_date, - nd.who_created, - nd.when_created, - nd.who_updated, - nd.when_updated, - nd.revision_count -FROM - noi_docs nd \ No newline at end of file From f919cd04fecd5b2cabb1681ef616828622ab023f Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Mon, 24 Jul 2023 13:29:12 -0700 Subject: [PATCH 117/954] Add Inclusion form fields to portal and update description (#813) * Create inclusion proposal component and template * Save inclusion proposal fields and document on form edit * Add migration script for inclusion text update * Update inclusion portal frontend test * Fix typo --- .../edit-submission-base.module.ts | 2 + .../edit-submission.component.html | 8 ++ .../edit-submission.component.ts | 5 + .../incl-proposal.component.html | 135 ++++++++++++++++++ .../incl-proposal.component.scss | 0 .../incl-proposal.component.spec.ts | 48 +++++++ .../incl-proposal/incl-proposal.component.ts | 93 ++++++++++++ ...-change_inclusion_legislative_reference.ts | 21 +++ 8 files changed, 312 insertions(+) create mode 100644 portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html create mode 100644 portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss create mode 100644 portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts create mode 100644 portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1690225882322-change_inclusion_legislative_reference.ts diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts index 6f0889f931..acea5917c7 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts @@ -25,6 +25,7 @@ import { ParcelEntryComponent } from './parcel-details/parcel-entry/parcel-entry import { ParcelOwnersComponent } from './parcel-details/parcel-owners/parcel-owners.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; import { ExclProposalComponent } from './proposal/excl-proposal/excl-proposal.component'; +import { InclProposalComponent } from './proposal/incl-proposal/incl-proposal.component'; import { ChangeSubtypeConfirmationDialogComponent } from './proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component'; import { NaruProposalComponent } from './proposal/naru-proposal/naru-proposal.component'; import { NfuProposalComponent } from './proposal/nfu-proposal/nfu-proposal.component'; @@ -68,6 +69,7 @@ import { SelectGovernmentComponent } from './select-government/select-government ChangeSubtypeConfirmationDialogComponent, SoilTableComponent, ExclProposalComponent, + InclProposalComponent, ], imports: [ CommonModule, diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html index f484aff4db..ce6c77d023 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html @@ -154,6 +154,14 @@
(navigateToStep)="onBeforeSwitchStep($event)" (exit)="onExit()" > +
diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index 59dfe11d14..5e42bb0b17 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -21,6 +21,7 @@ import { OtherParcelsComponent } from './other-parcels/other-parcels.component'; import { ParcelDetailsComponent } from './parcel-details/parcel-details.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; import { ExclProposalComponent } from './proposal/excl-proposal/excl-proposal.component'; +import { InclProposalComponent } from './proposal/incl-proposal/incl-proposal.component'; import { NaruProposalComponent } from './proposal/naru-proposal/naru-proposal.component'; import { NfuProposalComponent } from './proposal/nfu-proposal/nfu-proposal.component'; import { PfrsProposalComponent } from './proposal/pfrs-proposal/pfrs-proposal.component'; @@ -75,6 +76,7 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit @ViewChild(PfrsProposalComponent) pfrsProposalComponent?: PfrsProposalComponent; @ViewChild(NaruProposalComponent) naruProposalComponent?: NaruProposalComponent; @ViewChild(ExclProposalComponent) exclProposalComponent?: ExclProposalComponent; + @ViewChild(InclProposalComponent) inclProposalComponent?: InclProposalComponent; @ViewChild(OtherAttachmentsComponent) otherAttachmentsComponent!: OtherAttachmentsComponent; constructor( @@ -232,6 +234,9 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit if (this.exclProposalComponent) { await this.exclProposalComponent.onSave(); } + if (this.inclProposalComponent) { + await this.inclProposalComponent.onSave(); + } } async onBeforeSwitchStep(index: number) { diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html new file mode 100644 index 0000000000..17d3efc0de --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html @@ -0,0 +1,135 @@ +
+

Proposal

+

+ Considerations are subject to + Section 6 of the ALC Act. +

+

*All fields are required unless stated optional.

+
+ + Please consult the ' + + What the Commission Considers + + ' page of the ALC website for more information. + +
+
+
+ +
Note: 0.01 ha is 100m2
+ + + ha + +
+ warning +
This field is required
+
+
+
+ +
+ Be clear and concise in describing the proposal. Include why you are applying for inclusion and what the + proposal will achieve. +
+ + + + Characters left: {{ 4000 - purposeText.textLength }} +
+ warning +
This field is required
+
+
+
+ + + + + Characters left: {{ 4000 - outsideLandsText.textLength }} +
+ warning +
This field is required
+
+
+
+ +
+ Describe any irrigation, drainage, fencing, material enhancement, clearing, or other improvements that have been + completed, are in progress, or are planned for the subject parcel(s). If there have not been any agricultural + improvements on the parcel(s), please specify "No Agricultural Improvements". +
+ + + +
Example: 40 ha of grazing land fenced in 2010.
+ Characters left: {{ 4000 - outsideLandsText.textLength }} +
+ warning +
This field is required
+
+
+
+ +
A visual representation of your proposal.
+ +
+
+ + +
+ + +
+ + +
+
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts new file mode 100644 index 0000000000..a0b183b23d --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts @@ -0,0 +1,48 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { InclProposalComponent } from './incl-proposal.component'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; + +describe('InclProposalComponent', () => { + let component: InclProposalComponent; + let fixture: ComponentFixture; + let mockApplicationService: DeepMocked; + let mockAppDocumentService: DeepMocked; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + { + provide: ApplicationSubmissionService, + useValue: mockApplicationService, + }, + { + provide: ApplicationDocumentService, + useValue: mockAppDocumentService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + declarations: [InclProposalComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(InclProposalComponent); + component = fixture.componentInstance; + component.$applicationSubmission = new BehaviorSubject(undefined); + component.$applicationDocuments = new BehaviorSubject([]); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts new file mode 100644 index 0000000000..6f320455a1 --- /dev/null +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts @@ -0,0 +1,93 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FilesStepComponent } from '../../files-step.partial'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { MatDialog } from '@angular/material/dialog'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { + ApplicationDocumentDto, + DOCUMENT_TYPE, +} from '../../../../services/application-document/application-document.dto'; +import { EditApplicationSteps } from '../../edit-submission.component'; +import { takeUntil } from 'rxjs'; +import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; + +@Component({ + selector: 'app-incl-proposal', + templateUrl: './incl-proposal.component.html', + styleUrls: ['./incl-proposal.component.scss'], +}) +export class InclProposalComponent extends FilesStepComponent implements OnInit, OnDestroy { + DOCUMENT = DOCUMENT_TYPE; + + currentStep = EditApplicationSteps.Proposal; + + hectares = new FormControl(null, [Validators.required]); + purpose = new FormControl(null, [Validators.required]); + agSupport = new FormControl(null, [Validators.required]); + improvements = new FormControl(null, [Validators.required]); + + form = new FormGroup({ + hectares: this.hectares, + purpose: this.purpose, + agSupport: this.agSupport, + improvements: this.improvements, + }); + private submissionUuid = ''; + proposalMap: ApplicationDocumentDto[] = []; + + constructor( + private applicationSubmissionService: ApplicationSubmissionService, + applicationDocumentService: ApplicationDocumentService, + dialog: MatDialog + ) { + super(applicationDocumentService, dialog); + } + + ngOnInit(): void { + this.$applicationSubmission.pipe(takeUntil(this.$destroy)).subscribe((applicationSubmission) => { + if (applicationSubmission) { + this.fileId = applicationSubmission.fileNumber; + this.submissionUuid = applicationSubmission.uuid; + + this.form.patchValue({ + hectares: applicationSubmission.inclExclHectares?.toString(), + purpose: applicationSubmission.purpose, + agSupport: applicationSubmission.inclAgricultureSupport, + improvements: applicationSubmission.inclImprovements, + }); + + if (this.showErrors) { + this.form.markAllAsTouched(); + } + } + }); + + this.$applicationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + }); + } + + async onSave() { + await this.save(); + } + + protected async save() { + if (this.fileId) { + const inclExclHectares = this.hectares.value; + const purpose = this.purpose.value; + const inclAgricultureSupport = this.agSupport.value; + const inclImprovements = this.improvements.value; + + const updateDto: ApplicationSubmissionUpdateDto = { + inclExclHectares: inclExclHectares ? parseFloat(inclExclHectares) : null, + purpose, + inclAgricultureSupport, + inclImprovements, + }; + + const updatedApp = await this.applicationSubmissionService.updatePending(this.submissionUuid, updateDto); + this.$applicationSubmission.next(updatedApp); + } + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690225882322-change_inclusion_legislative_reference.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690225882322-change_inclusion_legislative_reference.ts new file mode 100644 index 0000000000..38760fc0be --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690225882322-change_inclusion_legislative_reference.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class changeInclusionLegislativeReference1690225882322 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE "alcs"."application_type" SET "html_description"='Choose this option if you are proposing to include land into the Agricultural Land Reserve under + Section 17 of the Agricultural Land Commission Act. + ' WHERE "code"='INCL'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `UPDATE "alcs"."application_type" SET "html_description"='Choose this option if you are proposing to include land into the Agricultural Land Reserve under + Section 17(3) of the Agricultural Land Commission Act. + ' WHERE "code"='INCL'`, + ); + } +} From 65bbbaf9dd86c63958d2f8269447b28b4d82cf29 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 24 Jul 2023 15:40:41 -0700 Subject: [PATCH 118/954] Reduce Network Calls / Loads for Edit Submission * Only save/load the application when components are dirty * Remove logic to auto-save on every step navigation * Add logging to help debug stepper issues --- .../edit-submission.component.html | 2 + .../edit-submission.component.ts | 25 +++++---- .../land-use/land-use.component.ts | 33 +++++++----- .../other-parcels/other-parcels.component.ts | 52 ++++++++++--------- .../parcel-details.component.ts | 40 +++++++------- .../primary-contact.component.ts | 49 ++++++++++------- .../select-government.component.ts | 20 ++++--- .../review-submission.component.ts | 2 +- .../application-submission.service.ts | 6 ++- 9 files changed, 133 insertions(+), 96 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html index f484aff4db..3e7250145e 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html @@ -41,6 +41,7 @@
[showErrors]="showValidationErrors" (navigateToStep)="onBeforeSwitchStep($event)" (componentInitialized)="onParcelDetailsInitialized()" + (exit)="onExit()" >
@@ -51,6 +52,7 @@
[showErrors]="showValidationErrors" [$applicationSubmission]="$applicationSubmission" (navigateToStep)="onBeforeSwitchStep($event)" + (exit)="onExit()" >
diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index 60790c333b..6dfbfba6c9 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -64,7 +64,7 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; - @ViewChild(OtherParcelsComponent) otherParcelsComponent!: OtherAttachmentsComponent; + @ViewChild(OtherParcelsComponent) otherParcelsComponent!: OtherParcelsComponent; @ViewChild(PrimaryContactComponent) primaryContactComponent!: PrimaryContactComponent; @ViewChild(SelectGovernmentComponent) selectGovernmentComponent!: SelectGovernmentComponent; @ViewChild(LandUseComponent) landUseComponent!: LandUseComponent; @@ -109,10 +109,12 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit } if (stepInd) { + const randomInt = Math.random() * 100; + console.debug(`Setting timeout for navigation ${randomInt}`); // setTimeout is required for stepper to be initialized setTimeout(() => { this.customStepper.navigateToStep(parseInt(stepInd), true); - + console.debug(`Emitted step ${randomInt}`); if (parcelUuid) { this.expandedParcelUuid = parcelUuid; } @@ -133,15 +135,17 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit } private async loadApplication(fileId: string) { - this.overlayService.showSpinner(); - this.applicationSubmission = await this.applicationSubmissionService.getByFileId(fileId); - const documents = await this.applicationDocumentService.getByFileId(fileId); - if (documents) { - this.$applicationDocuments.next(documents); + if (!this.applicationSubmission) { + this.overlayService.showSpinner(); + this.applicationSubmission = await this.applicationSubmissionService.getByFileId(fileId); + const documents = await this.applicationDocumentService.getByFileId(fileId); + if (documents) { + this.$applicationDocuments.next(documents); + } + this.fileId = fileId; + this.$applicationSubmission.next(this.applicationSubmission); + this.overlayService.hideSpinner(); } - this.fileId = fileId; - this.$applicationSubmission.next(this.applicationSubmission); - this.overlayService.hideSpinner(); } async onApplicationTypeChangeClicked() { @@ -167,6 +171,7 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit // this gets fired whenever applicant navigates away from edit page async canDeactivate(): Promise> { + console.debug('DEACTIVATING'); await this.saveApplication(this.customStepper.selectedIndex); return of(true); diff --git a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts index ffe5cea54e..6701fb7145 100644 --- a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts +++ b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts @@ -92,20 +92,25 @@ export class LandUseComponent extends StepComponent implements OnInit, OnDestroy } async saveProgress() { - const formValues = this.landUseForm.getRawValue(); - await this.applicationService.updatePending(this.submissionUuid, { - parcelsAgricultureDescription: formValues.parcelsAgricultureDescription, - parcelsAgricultureImprovementDescription: formValues.parcelsAgricultureImprovementDescription, - parcelsNonAgricultureUseDescription: formValues.parcelsNonAgricultureUseDescription, - northLandUseType: formValues.northLandUseType, - northLandUseTypeDescription: formValues.northLandUseTypeDescription, - eastLandUseType: formValues.eastLandUseType, - eastLandUseTypeDescription: formValues.eastLandUseTypeDescription, - southLandUseType: formValues.southLandUseType, - southLandUseTypeDescription: formValues.southLandUseTypeDescription, - westLandUseType: formValues.westLandUseType, - westLandUseTypeDescription: formValues.westLandUseTypeDescription, - }); + if (this.landUseForm.dirty) { + const formValues = this.landUseForm.getRawValue(); + const updatedSubmission = await this.applicationService.updatePending(this.submissionUuid, { + parcelsAgricultureDescription: formValues.parcelsAgricultureDescription, + parcelsAgricultureImprovementDescription: formValues.parcelsAgricultureImprovementDescription, + parcelsNonAgricultureUseDescription: formValues.parcelsNonAgricultureUseDescription, + northLandUseType: formValues.northLandUseType, + northLandUseTypeDescription: formValues.northLandUseTypeDescription, + eastLandUseType: formValues.eastLandUseType, + eastLandUseTypeDescription: formValues.eastLandUseTypeDescription, + southLandUseType: formValues.southLandUseType, + southLandUseTypeDescription: formValues.southLandUseTypeDescription, + westLandUseType: formValues.westLandUseType, + westLandUseTypeDescription: formValues.westLandUseTypeDescription, + }); + if (updatedSubmission) { + this.$applicationSubmission.next(updatedSubmission); + } + } } async onSave() { diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts b/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts index b576c4b562..0af4fccb8a 100644 --- a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts +++ b/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts @@ -129,35 +129,37 @@ export class OtherParcelsComponent extends StepComponent implements OnInit, OnDe // replace placeholder uuid with the real one before saving await this.replacePlaceholderParcel(); - // delete all OTHER parcels if user answered 'NO' on 'Is there other parcels in the community' - if (!parseStringToBoolean(this.hasOtherParcelsInCommunity.getRawValue())) { - if (this.otherParcels.some((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL)) { - await this.applicationParcelService.deleteMany( - this.otherParcels.filter((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL).map((e) => e.uuid) - ); + if (this.parcelEntryChanged) { + // delete all OTHER parcels if user answered 'NO' on 'Is there other parcels in the community' + if (!parseStringToBoolean(this.hasOtherParcelsInCommunity.getRawValue())) { + if (this.otherParcels.some((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL)) { + await this.applicationParcelService.deleteMany( + this.otherParcels.filter((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL).map((e) => e.uuid) + ); + } + + return; } - return; - } + for (const parcel of this.otherParcels) { + parcelsToUpdate.push({ + uuid: parcel.uuid, + pid: parcel.pid?.toString() || null, + pin: parcel.pin?.toString() || null, + legalDescription: parcel.legalDescription, + civicAddress: parcel.civicAddress, + isFarm: parcel.isFarm, + purchasedDate: parcel.purchasedDate, + mapAreaHectares: parcel.mapAreaHectares, + ownershipTypeCode: parcel.ownershipTypeCode, + crownLandOwnerType: parcel.crownLandOwnerType, + isConfirmedByApplicant: false, + ownerUuids: parcel.owners.map((owner) => owner.uuid), + }); + } - for (const parcel of this.otherParcels) { - parcelsToUpdate.push({ - uuid: parcel.uuid, - pid: parcel.pid?.toString() || null, - pin: parcel.pin?.toString() || null, - legalDescription: parcel.legalDescription, - civicAddress: parcel.civicAddress, - isFarm: parcel.isFarm, - purchasedDate: parcel.purchasedDate, - mapAreaHectares: parcel.mapAreaHectares, - ownershipTypeCode: parcel.ownershipTypeCode, - crownLandOwnerType: parcel.crownLandOwnerType, - isConfirmedByApplicant: false, - ownerUuids: parcel.owners.map((owner) => owner.uuid), - }); + await this.applicationParcelService.update(parcelsToUpdate); } - - await this.applicationParcelService.update(parcelsToUpdate); } async onSave() { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts index dcc2ee3388..52fb6bbc48 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts +++ b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts @@ -31,6 +31,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft parcels: ApplicationParcelDto[] = []; $owners = new BehaviorSubject([]); newParcelAdded = false; + isDirty = false; constructor( private router: Router, @@ -93,6 +94,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft return; } + this.isDirty = true; parcel.pid = formData.pid !== undefined ? formData.pid : parcel.pid; parcel.pin = formData.pid !== undefined ? formData.pin : parcel.pin; parcel.civicAddress = formData.civicAddress !== undefined ? formData.civicAddress : parcel.civicAddress; @@ -113,25 +115,27 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft } private async saveProgress() { - const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; - for (const parcel of this.parcels) { - parcelsToUpdate.push({ - uuid: parcel.uuid, - pid: parcel.pid?.toString() || null, - pin: parcel.pin?.toString() || null, - civicAddress: parcel.civicAddress ?? null, - legalDescription: parcel.legalDescription, - isFarm: parcel.isFarm, - purchasedDate: parcel.purchasedDate, - mapAreaHectares: parcel.mapAreaHectares, - ownershipTypeCode: parcel.ownershipTypeCode, - isConfirmedByApplicant: parcel.isConfirmedByApplicant, - crownLandOwnerType: parcel.crownLandOwnerType, - ownerUuids: parcel.owners.map((owner) => owner.uuid), - }); + if (this.isDirty || this.newParcelAdded) { + const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; + for (const parcel of this.parcels) { + parcelsToUpdate.push({ + uuid: parcel.uuid, + pid: parcel.pid?.toString() || null, + pin: parcel.pin?.toString() || null, + civicAddress: parcel.civicAddress ?? null, + legalDescription: parcel.legalDescription, + isFarm: parcel.isFarm, + purchasedDate: parcel.purchasedDate, + mapAreaHectares: parcel.mapAreaHectares, + ownershipTypeCode: parcel.ownershipTypeCode, + isConfirmedByApplicant: parcel.isConfirmedByApplicant, + crownLandOwnerType: parcel.crownLandOwnerType, + ownerUuids: parcel.owners.map((owner) => owner.uuid), + }); + } + await this.applicationParcelService.update(parcelsToUpdate); + //TODO: Do we need to reload submission? } - - await this.applicationParcelService.update(parcelsToUpdate); } async onSave() { diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index 71d0d984a6..5993e5c9c3 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -31,6 +31,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni isCrownOwner = false; isLocalGovernmentUser = false; governmentName: string | undefined; + isDirty = false; firstName = new FormControl('', [Validators.required]); lastName = new FormControl('', [Validators.required]); @@ -94,6 +95,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } onSelectOwner(uuid: string) { + this.isDirty = true; this.selectedOwnerUuid = uuid; const selectedOwner = this.parcelOwners.find((owner) => owner.uuid === uuid); this.parcelOwners = this.parcelOwners.map((owner) => ({ @@ -152,29 +154,37 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } protected async save() { - let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( - (owner) => owner.uuid === this.selectedOwnerUuid - ); + if (this.isDirty || this.form.dirty) { + let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( + (owner) => owner.uuid === this.selectedOwnerUuid + ); - if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - organization: this.organizationName.getRawValue() ?? '', - firstName: this.firstName.getRawValue() ?? '', - lastName: this.lastName.getRawValue() ?? '', - email: this.email.getRawValue() ?? '', - phoneNumber: this.phoneNumber.getRawValue() ?? '', - ownerUuid: selectedOwner?.uuid, - type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, - }); - } else if (selectedOwner) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - ownerUuid: selectedOwner.uuid, - }); + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + organization: this.organizationName.getRawValue() ?? '', + firstName: this.firstName.getRawValue() ?? '', + lastName: this.lastName.getRawValue() ?? '', + email: this.email.getRawValue() ?? '', + phoneNumber: this.phoneNumber.getRawValue() ?? '', + ownerUuid: selectedOwner?.uuid, + type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, + }); + } else if (selectedOwner) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + ownerUuid: selectedOwner.uuid, + }); + } + await this.reloadApplication(); } } + private async reloadApplication() { + const application = await this.applicationService.getByUuid(this.submissionUuid); + this.$applicationSubmission.next(application); + } + private async loadOwners(submissionUuid: string, primaryContactOwnerUuid?: string) { const owners = await this.applicationOwnerService.fetchBySubmissionId(submissionUuid); if (owners) { @@ -215,6 +225,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni if (this.showErrors) { this.form.markAllAsTouched(); } + this.isDirty = false; } } diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts index 830d2a8ae5..9acdf56fd4 100644 --- a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts +++ b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts @@ -26,6 +26,7 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, selectGovernmentUuid = ''; localGovernments: LocalGovernmentDto[] = []; filteredLocalGovernments!: Observable; + isDirty = false; form = new FormGroup({ localGovernment: this.localGovernment, @@ -58,6 +59,7 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, } onChange($event: MatAutocompleteSelectedEvent) { + this.isDirty = true; const localGovernmentName = $event.option.value; if (localGovernmentName) { const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); @@ -95,15 +97,19 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, } private async save() { - const localGovernmentName = this.localGovernment.getRawValue(); - if (localGovernmentName) { - const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + if (this.isDirty) { + const localGovernmentName = this.localGovernment.getRawValue(); + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); - if (localGovernment) { - await this.applicationSubmissionService.updatePending(this.submissionUuid, { - localGovernmentUuid: localGovernment.uuid, - }); + if (localGovernment) { + const res = await this.applicationSubmissionService.updatePending(this.submissionUuid, { + localGovernmentUuid: localGovernment.uuid, + }); + this.$applicationSubmission.next(res); + } } + this.isDirty = false; } } diff --git a/portal-frontend/src/app/features/review-submission/review-submission.component.ts b/portal-frontend/src/app/features/review-submission/review-submission.component.ts index f5ba423738..dc6a512380 100644 --- a/portal-frontend/src/app/features/review-submission/review-submission.component.ts +++ b/portal-frontend/src/app/features/review-submission/review-submission.component.ts @@ -97,7 +97,7 @@ export class ReviewSubmissionComponent implements OnInit, OnDestroy { // setTimeout is required for stepper to be initialized setTimeout(() => { const stepInt = parseInt(stepInd); - this.customStepper.navigateToStep(stepInt, true); + //this.customStepper.navigateToStep(stepInt, true); this.showDownloadPdf = this.isFirstNationGovernment ? stepInt === ReviewApplicationFngSteps.ReviewAndSubmitFng diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index b87504f608..db0be19151 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -154,8 +154,10 @@ export class ApplicationSubmissionService { const applicationSubmission = await this.getOrFailByUuid(submissionUuid); applicationSubmission.applicant = updateDto.applicant; - applicationSubmission.purpose = - updateDto.purpose || applicationSubmission.purpose; + applicationSubmission.purpose = filterUndefined( + updateDto.purpose, + applicationSubmission.purpose, + ); applicationSubmission.typeCode = updateDto.typeCode || applicationSubmission.typeCode; applicationSubmission.localGovernmentUuid = updateDto.localGovernmentUuid; From 5492018a12e3b9d2de4f5b36a36cdbf41c0cb13d Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 24 Jul 2023 16:41:50 -0700 Subject: [PATCH 119/954] Copy over TempID when re-mapping and simplify filter logic * Copy over temp ID when we re-map components so that we can easily filter using TempID whenever their inputs change. --- .../decision-condition.component.ts | 1 + .../decision-conditions.component.ts | 24 +++++++------------ .../application-decision-v2.dto.ts | 2 +- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts index 1cf99cb340..fc566f39e6 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts @@ -67,6 +67,7 @@ export class DecisionConditionComponent implements OnInit, OnChanges { .map((e) => ({ componentDecisionUuid: e.decisionUuid, componentToConditionType: e.code, + tempId: e.tempId, })); this.dataChange.emit({ diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 0bf0b99623..631a758310 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -117,24 +117,15 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy this.decision.resolutionYear ); this.selectableComponents = [...this.allComponents, ...updatedComponents]; + const validComponentIds = this.selectableComponents.map((component) => component.tempId); this.mappedConditions = this.mappedConditions.map((condition) => { - const selectedComponents = this.selectableComponents - .filter((component) => - condition.componentsToCondition - ?.map((conditionComponent) => conditionComponent.tempId) - .includes(component.tempId) - ) - .map((e) => ({ - componentDecisionUuid: e.decisionUuid, - componentToConditionType: e.code, - tempId: e.tempId, - })); - - return { - ...condition, - componentsToCondition: selectedComponents, - }; + if (condition.componentsToCondition) { + condition.componentsToCondition = condition.componentsToCondition.filter((component) => + validComponentIds.includes(component.tempId) + ); + } + return condition; }); this.onChanges(); } @@ -155,6 +146,7 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy componentDecisionUuid: e.decisionUuid, componentToConditionType: e.code, tempId: e.tempId, + uuid: e.uuid, })); return { diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts index da363fc6d8..40183d8d27 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts @@ -203,7 +203,7 @@ export interface ApplicationDecisionConditionDto { export interface ComponentToCondition { componentDecisionUuid?: string; componentToConditionType?: string; - tempId?: string; + tempId: string; } export interface UpdateApplicationDecisionConditionDto { uuid?: string; From 1380ebe009fdb39e51a28acca669368e2eaf0926 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 24 Jul 2023 16:43:00 -0700 Subject: [PATCH 120/954] changed to functions --- bin/migrate-files/migrate-files.py | 384 +++++++++++++++-------------- 1 file changed, 196 insertions(+), 188 deletions(-) diff --git a/bin/migrate-files/migrate-files.py b/bin/migrate-files/migrate-files.py index edb46e8745..92a04ff354 100644 --- a/bin/migrate-files/migrate-files.py +++ b/bin/migrate-files/migrate-files.py @@ -5,6 +5,188 @@ from tqdm import tqdm import pickle +def application_docs(starting_document_id,batch): + # Get total number of files + try: + cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND ALR_APPLICATION_ID IS NOT NULL') + except cx_Oracle.Error as e: + error, = e.args + print(error.message) + + application_count = cursor.fetchone()[0] + print('Count =', application_count) + + # # Execute the SQL query to retrieve the BLOB data and key column + cursor.execute(f""" + SELECT DOCUMENT_ID, ALR_APPLICATION_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH + FROM OATS.OATS_DOCUMENTS + WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 + AND DOCUMENT_ID > {starting_document_id} + AND ALR_APPLICATION_ID IS NOT NULL + ORDER BY DOCUMENT_ID ASC + """) + + # Set the batch size + BATCH_SIZE = batch + + # Track progress + documents_processed = 0 + last_document_id = 0 + + try: + with tqdm(total=application_count, unit="file", desc="Uploading files to S3") as pbar: + while True: + # Fetch the next batch of BLOB data + data = cursor.fetchmany(BATCH_SIZE) + if not data: + break + # Upload the batch to S3 with a progress bar + for document_id, application_id, filename, file, code, description, source, created, updated, revision, length in data: + tqdm.write(f"{application_id}/{document_id}_{filename}") + + with tqdm(total=length, unit="B", unit_scale=True, desc=filename) as pbar2: + s3.upload_fileobj(file, ecs_bucket, f"migrate/application/{application_id}/{document_id}_{filename}", + Callback=lambda bytes_transferred: pbar2.update(bytes_transferred),) + pbar.update(1) + last_document_id = document_id + documents_processed += 1 + + except Exception as e: + print("Something went wrong:",e) + print("Processed", documents_processed, "files") + + # Set resume status in case of interuption + with open('last-file.pickle', 'wb') as file: + pickle.dump(last_document_id, file) + + cursor.close() + conn.close() + exit() + + # Display results + print("Process complete: Successfully migrated", documents_processed, "files.") + +def planning_docs(starting_planning_document_id,batch): + # Get total number of files + try: + cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND PLANNING_REVIEW_ID IS NOT NULL') + except cx_Oracle.Error as e: + error, = e.args + print(error.message) + + planning_review_count = cursor.fetchone()[0] + print('Count =', planning_review_count) + + # # Execute the SQL query to retrieve the BLOB data and key column + cursor.execute(f""" + SELECT DOCUMENT_ID, PLANNING_REVIEW_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH + FROM OATS.OATS_DOCUMENTS + WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 + AND DOCUMENT_ID > {starting_planning_document_id} + AND PLANNING_REVIEW_ID IS NOT NULL + ORDER BY DOCUMENT_ID ASC + """) + + # Set the batch size + BATCH_SIZE = batch + + # Track progress + documents_processed = 0 + last_planning_document_id = 0 + + try: + with tqdm(total=planning_review_count, unit="file", desc="Uploading files to S3") as pbar: + while True: + # Fetch the next batch of BLOB data + data = cursor.fetchmany(BATCH_SIZE) + if not data: + break + # Upload the batch to S3 with a progress bar + for document_id, planning_review_id, filename, file, code, description, source, created, updated, revision, length in data: + tqdm.write(f"{planning_review_id}/{document_id}_{filename}") + + with tqdm(total=length, unit="B", unit_scale=True, desc=filename) as pbar2: + s3.upload_fileobj(file, ecs_bucket, f"migrate/planning_review/{planning_review_id}/{document_id}_{filename}", + Callback=lambda bytes_transferred: pbar2.update(bytes_transferred),) + pbar.update(1) + last_planning_document_id = document_id + documents_processed += 1 + + except Exception as e: + print("Something went wrong:",e) + print("Processed", documents_processed, "files") + + # Set resume status in case of interuption + with open('last-planning-file.pickle', 'wb') as file: + pickle.dump(last_planning_document_id, file) + + cursor.close() + conn.close() + exit() + + # Display results + print("Process complete: Successfully migrated", documents_processed, "files.") + +def issue_docs(starting_issue_document_id,batch): + # Get total number of files + try: + cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND ISSUE_ID IS NOT NULL') + except cx_Oracle.Error as e: + error, = e.args + print(error.message) + + issue_count = cursor.fetchone()[0] + print('Count =', issue_count) + + # # Execute the SQL query to retrieve the BLOB data and key column + cursor.execute(f""" + SELECT DOCUMENT_ID, ISSUE_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH + FROM OATS.OATS_DOCUMENTS + WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 + AND DOCUMENT_ID > {starting_issue_document_id} + AND ISSUE_ID IS NOT NULL + ORDER BY DOCUMENT_ID ASC + """) + + # Set the batch size + BATCH_SIZE = batch + + # Track progress + documents_processed = 0 + last_issue_document_id = 0 + + try: + with tqdm(total=issue_count, unit="file", desc="Uploading files to S3") as pbar: + while True: + # Fetch the next batch of BLOB data + data = cursor.fetchmany(BATCH_SIZE) + if not data: + break + # Upload the batch to S3 with a progress bar + for document_id, issue_id, filename, file, code, description, source, created, updated, revision, length in data: + tqdm.write(f"{issue_id}/{document_id}_{filename}") + + with tqdm(total=length, unit="B", unit_scale=True, desc=filename) as pbar2: + s3.upload_fileobj(file, ecs_bucket, f"migrate/issue/{issue_id}/{document_id}_{filename}", + Callback=lambda bytes_transferred: pbar2.update(bytes_transferred),) + pbar.update(1) + last_issue_document_id = document_id + documents_processed += 1 + + except Exception as e: + print("Something went wrong:",e) + print("Processed", documents_processed, "files") + + # Set resume status in case of interuption + with open('last-issue-file.pickle', 'wb') as file: + pickle.dump(last_issue_document_id, file) + + cursor.close() + conn.close() + exit() + + # Display results + print("Process complete: Successfully migrated", documents_processed, "files.") # Load environment variables from .env file load_dotenv() @@ -41,208 +223,34 @@ with open('last-file.pickle', 'rb') as file: starting_document_id = pickle.load(file) -print('Starting applications from:', starting_document_id) - starting_planning_document_id = 0 # Determine job resume status if os.path.isfile('last-planning-file.pickle'): with open('last-planning-file.pickle', 'rb') as file: starting_planning_document_id = pickle.load(file) - #ignore imported files before failure - starting_document_id = 999999 - -print('Starting planning_review from:', starting_planning_document_id) + print('Starting planning_review from:', starting_planning_document_id) starting_issue_document_id = 0 # Determine job resume status if os.path.isfile('last-issue-file.pickle'): with open('last-issue-file.pickle', 'rb') as file: starting_issue_document_id = pickle.load(file) - #ignore imported files before failure - starting_document_id = 999999 - starting_planning_document_id = 999999 -print('Starting issues from:', starting_issue_document_id) - -# Get total number of files + print('Starting issues from:', starting_issue_document_id) cursor = conn.cursor() -try: - cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND ALR_APPLICATION_ID IS NOT NULL') -except cx_Oracle.Error as e: - error, = e.args - print(error.message) - -application_count = cursor.fetchone()[0] -print('Count =', application_count) - -# # Execute the SQL query to retrieve the BLOB data and key column -cursor.execute(f""" -SELECT DOCUMENT_ID, ALR_APPLICATION_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH -FROM OATS.OATS_DOCUMENTS -WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 -AND DOCUMENT_ID > {starting_document_id} -AND ALR_APPLICATION_ID IS NOT NULL -ORDER BY DOCUMENT_ID ASC -""") - -# Set the batch size -BATCH_SIZE = 10 - -# Track progress -documents_processed = 0 -last_document_id = 0 - -try: - with tqdm(total=application_count, unit="file", desc="Uploading files to S3") as pbar: - while True: - # Fetch the next batch of BLOB data - data = cursor.fetchmany(BATCH_SIZE) - if not data: - break - # Upload the batch to S3 with a progress bar - for document_id, application_id, filename, file, code, description, source, created, updated, revision, length in data: - tqdm.write(f"{application_id}/{document_id}_{filename}") - - with tqdm(total=length, unit="B", unit_scale=True, desc=filename) as pbar2: - s3.upload_fileobj(file, ecs_bucket, f"migrate/application/{application_id}/{document_id}_{filename}", - Callback=lambda bytes_transferred: pbar2.update(bytes_transferred),) - pbar.update(1) - last_document_id = document_id - documents_processed += 1 - -except Exception as e: - print("Something went wrong:",e) - print("Processed", documents_processed, "files") - - # Set resume status in case of interuption - with open('last-file.pickle', 'wb') as file: - pickle.dump(last_document_id, file) - - cursor.close() - conn.close() - exit() - -# Display results -print("Process complete: Successfully migrated", documents_processed, "files.") - -try: - cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND PLANNING_REVIEW_ID IS NOT NULL') -except cx_Oracle.Error as e: - error, = e.args - print(error.message) - -planning_review_count = cursor.fetchone()[0] -print('Count =', planning_review_count) - -# # Execute the SQL query to retrieve the BLOB data and key column -cursor.execute(f""" -SELECT DOCUMENT_ID, PLANNING_REVIEW_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH -FROM OATS.OATS_DOCUMENTS -WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 -AND DOCUMENT_ID > {starting_planning_document_id} -AND PLANNING_REVIEW_ID IS NOT NULL -ORDER BY DOCUMENT_ID ASC -""") - -# Set the batch size -BATCH_SIZE = 10 - -# Track progress -documents_processed = 0 -last_planning_document_id = 0 - -try: - with tqdm(total=planning_review_count, unit="file", desc="Uploading files to S3") as pbar: - while True: - # Fetch the next batch of BLOB data - data = cursor.fetchmany(BATCH_SIZE) - if not data: - break - # Upload the batch to S3 with a progress bar - for document_id, planning_review_id, filename, file, code, description, source, created, updated, revision, length in data: - tqdm.write(f"{planning_review_id}/{document_id}_{filename}") - - with tqdm(total=length, unit="B", unit_scale=True, desc=filename) as pbar2: - s3.upload_fileobj(file, ecs_bucket, f"migrate/planning_review/{planning_review_id}/{document_id}_{filename}", - Callback=lambda bytes_transferred: pbar2.update(bytes_transferred),) - pbar.update(1) - last_planning_document_id = document_id - documents_processed += 1 - -except Exception as e: - print("Something went wrong:",e) - print("Processed", documents_processed, "files") - - # Set resume status in case of interuption - with open('last-planning-file.pickle', 'wb') as file: - pickle.dump(last_planning_document_id, file) - - cursor.close() - conn.close() - exit() - -# Display results -print("Process complete: Successfully migrated", documents_processed, "files.") - -try: - cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND ISSUE_ID IS NOT NULL') -except cx_Oracle.Error as e: - error, = e.args - print(error.message) - -issue_count = cursor.fetchone()[0] -print('Count =', issue_count) - -# # Execute the SQL query to retrieve the BLOB data and key column -cursor.execute(f""" -SELECT DOCUMENT_ID, ISSUE_ID, FILE_NAME, DOCUMENT_BLOB, DOCUMENT_CODE, DESCRIPTION, DOCUMENT_SOURCE_CODE, UPLOADED_DATE, WHEN_UPDATED, REVISION_COUNT, dbms_lob.getLength(DOCUMENT_BLOB) as LENGTH -FROM OATS.OATS_DOCUMENTS -WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 -AND DOCUMENT_ID > {starting_issue_document_id} -AND ISSUE_ID IS NOT NULL -ORDER BY DOCUMENT_ID ASC -""") - -# Set the batch size -BATCH_SIZE = 10 - -# Track progress -documents_processed = 0 -last_issue_document_id = 0 - -try: - with tqdm(total=issue_count, unit="file", desc="Uploading files to S3") as pbar: - while True: - # Fetch the next batch of BLOB data - data = cursor.fetchmany(BATCH_SIZE) - if not data: - break - # Upload the batch to S3 with a progress bar - for document_id, issue_id, filename, file, code, description, source, created, updated, revision, length in data: - tqdm.write(f"{issue_id}/{document_id}_{filename}") - - with tqdm(total=length, unit="B", unit_scale=True, desc=filename) as pbar2: - s3.upload_fileobj(file, ecs_bucket, f"migrate/issue/{issue_id}/{document_id}_{filename}", - Callback=lambda bytes_transferred: pbar2.update(bytes_transferred),) - pbar.update(1) - last_issue_document_id = document_id - documents_processed += 1 - -except Exception as e: - print("Something went wrong:",e) - print("Processed", documents_processed, "files") - - # Set resume status in case of interuption - with open('last-issue-file.pickle', 'wb') as file: - pickle.dump(last_issue_document_id, file) - - cursor.close() - conn.close() - exit() - -# Display results -print("Process complete: Successfully migrated", documents_processed, "files.") +batch_size = 10 + +if starting_issue_document_id > 0: + issue_docs(starting_issue_document_id,batch_size) +elif starting_planning_document_id > 0: + planning_docs(starting_planning_document_id,batch_size) + issue_docs(0,batch_size) +else: + print('Starting applications from:', starting_document_id) + application_docs(starting_document_id,batch_size) + planning_docs(0,batch_size) + issue_docs(0,batch_size) # Close the database cursor and connection cursor.close() From 22a72d4452ade3ecab8e5101fd7318191c7af066 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 24 Jul 2023 16:35:09 -0700 Subject: [PATCH 121/954] Revert "Merge pull request #814 from bcgov/feature/ALCS-937" This reverts commit db38f88a6e23a6647a347a13d304b0f755defeef. --- .../edit-submission.component.html | 2 - .../edit-submission.component.ts | 25 ++++----- .../land-use/land-use.component.ts | 33 +++++------- .../other-parcels/other-parcels.component.ts | 52 +++++++++---------- .../parcel-details.component.ts | 40 +++++++------- .../primary-contact.component.ts | 49 +++++++---------- .../select-government.component.ts | 20 +++---- .../review-submission.component.ts | 2 +- .../application-submission.service.ts | 6 +-- 9 files changed, 96 insertions(+), 133 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html index 704d34ef5f..ce6c77d023 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html @@ -41,7 +41,6 @@
[showErrors]="showValidationErrors" (navigateToStep)="onBeforeSwitchStep($event)" (componentInitialized)="onParcelDetailsInitialized()" - (exit)="onExit()" >
@@ -52,7 +51,6 @@
[showErrors]="showValidationErrors" [$applicationSubmission]="$applicationSubmission" (navigateToStep)="onBeforeSwitchStep($event)" - (exit)="onExit()" >
diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index d77fe68f2b..5e42bb0b17 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -64,7 +64,7 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; - @ViewChild(OtherParcelsComponent) otherParcelsComponent!: OtherParcelsComponent; + @ViewChild(OtherParcelsComponent) otherParcelsComponent!: OtherAttachmentsComponent; @ViewChild(PrimaryContactComponent) primaryContactComponent!: PrimaryContactComponent; @ViewChild(SelectGovernmentComponent) selectGovernmentComponent!: SelectGovernmentComponent; @ViewChild(LandUseComponent) landUseComponent!: LandUseComponent; @@ -111,12 +111,10 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit } if (stepInd) { - const randomInt = Math.random() * 100; - console.debug(`Setting timeout for navigation ${randomInt}`); // setTimeout is required for stepper to be initialized setTimeout(() => { this.customStepper.navigateToStep(parseInt(stepInd), true); - console.debug(`Emitted step ${randomInt}`); + if (parcelUuid) { this.expandedParcelUuid = parcelUuid; } @@ -137,17 +135,15 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit } private async loadApplication(fileId: string) { - if (!this.applicationSubmission) { - this.overlayService.showSpinner(); - this.applicationSubmission = await this.applicationSubmissionService.getByFileId(fileId); - const documents = await this.applicationDocumentService.getByFileId(fileId); - if (documents) { - this.$applicationDocuments.next(documents); - } - this.fileId = fileId; - this.$applicationSubmission.next(this.applicationSubmission); - this.overlayService.hideSpinner(); + this.overlayService.showSpinner(); + this.applicationSubmission = await this.applicationSubmissionService.getByFileId(fileId); + const documents = await this.applicationDocumentService.getByFileId(fileId); + if (documents) { + this.$applicationDocuments.next(documents); } + this.fileId = fileId; + this.$applicationSubmission.next(this.applicationSubmission); + this.overlayService.hideSpinner(); } async onApplicationTypeChangeClicked() { @@ -173,7 +169,6 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit // this gets fired whenever applicant navigates away from edit page async canDeactivate(): Promise> { - console.debug('DEACTIVATING'); await this.saveApplication(this.customStepper.selectedIndex); return of(true); diff --git a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts index 6701fb7145..ffe5cea54e 100644 --- a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts +++ b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts @@ -92,25 +92,20 @@ export class LandUseComponent extends StepComponent implements OnInit, OnDestroy } async saveProgress() { - if (this.landUseForm.dirty) { - const formValues = this.landUseForm.getRawValue(); - const updatedSubmission = await this.applicationService.updatePending(this.submissionUuid, { - parcelsAgricultureDescription: formValues.parcelsAgricultureDescription, - parcelsAgricultureImprovementDescription: formValues.parcelsAgricultureImprovementDescription, - parcelsNonAgricultureUseDescription: formValues.parcelsNonAgricultureUseDescription, - northLandUseType: formValues.northLandUseType, - northLandUseTypeDescription: formValues.northLandUseTypeDescription, - eastLandUseType: formValues.eastLandUseType, - eastLandUseTypeDescription: formValues.eastLandUseTypeDescription, - southLandUseType: formValues.southLandUseType, - southLandUseTypeDescription: formValues.southLandUseTypeDescription, - westLandUseType: formValues.westLandUseType, - westLandUseTypeDescription: formValues.westLandUseTypeDescription, - }); - if (updatedSubmission) { - this.$applicationSubmission.next(updatedSubmission); - } - } + const formValues = this.landUseForm.getRawValue(); + await this.applicationService.updatePending(this.submissionUuid, { + parcelsAgricultureDescription: formValues.parcelsAgricultureDescription, + parcelsAgricultureImprovementDescription: formValues.parcelsAgricultureImprovementDescription, + parcelsNonAgricultureUseDescription: formValues.parcelsNonAgricultureUseDescription, + northLandUseType: formValues.northLandUseType, + northLandUseTypeDescription: formValues.northLandUseTypeDescription, + eastLandUseType: formValues.eastLandUseType, + eastLandUseTypeDescription: formValues.eastLandUseTypeDescription, + southLandUseType: formValues.southLandUseType, + southLandUseTypeDescription: formValues.southLandUseTypeDescription, + westLandUseType: formValues.westLandUseType, + westLandUseTypeDescription: formValues.westLandUseTypeDescription, + }); } async onSave() { diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts b/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts index 0af4fccb8a..b576c4b562 100644 --- a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts +++ b/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts @@ -129,37 +129,35 @@ export class OtherParcelsComponent extends StepComponent implements OnInit, OnDe // replace placeholder uuid with the real one before saving await this.replacePlaceholderParcel(); - if (this.parcelEntryChanged) { - // delete all OTHER parcels if user answered 'NO' on 'Is there other parcels in the community' - if (!parseStringToBoolean(this.hasOtherParcelsInCommunity.getRawValue())) { - if (this.otherParcels.some((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL)) { - await this.applicationParcelService.deleteMany( - this.otherParcels.filter((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL).map((e) => e.uuid) - ); - } - - return; + // delete all OTHER parcels if user answered 'NO' on 'Is there other parcels in the community' + if (!parseStringToBoolean(this.hasOtherParcelsInCommunity.getRawValue())) { + if (this.otherParcels.some((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL)) { + await this.applicationParcelService.deleteMany( + this.otherParcels.filter((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL).map((e) => e.uuid) + ); } - for (const parcel of this.otherParcels) { - parcelsToUpdate.push({ - uuid: parcel.uuid, - pid: parcel.pid?.toString() || null, - pin: parcel.pin?.toString() || null, - legalDescription: parcel.legalDescription, - civicAddress: parcel.civicAddress, - isFarm: parcel.isFarm, - purchasedDate: parcel.purchasedDate, - mapAreaHectares: parcel.mapAreaHectares, - ownershipTypeCode: parcel.ownershipTypeCode, - crownLandOwnerType: parcel.crownLandOwnerType, - isConfirmedByApplicant: false, - ownerUuids: parcel.owners.map((owner) => owner.uuid), - }); - } + return; + } - await this.applicationParcelService.update(parcelsToUpdate); + for (const parcel of this.otherParcels) { + parcelsToUpdate.push({ + uuid: parcel.uuid, + pid: parcel.pid?.toString() || null, + pin: parcel.pin?.toString() || null, + legalDescription: parcel.legalDescription, + civicAddress: parcel.civicAddress, + isFarm: parcel.isFarm, + purchasedDate: parcel.purchasedDate, + mapAreaHectares: parcel.mapAreaHectares, + ownershipTypeCode: parcel.ownershipTypeCode, + crownLandOwnerType: parcel.crownLandOwnerType, + isConfirmedByApplicant: false, + ownerUuids: parcel.owners.map((owner) => owner.uuid), + }); } + + await this.applicationParcelService.update(parcelsToUpdate); } async onSave() { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts index 52fb6bbc48..dcc2ee3388 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts +++ b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts @@ -31,7 +31,6 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft parcels: ApplicationParcelDto[] = []; $owners = new BehaviorSubject([]); newParcelAdded = false; - isDirty = false; constructor( private router: Router, @@ -94,7 +93,6 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft return; } - this.isDirty = true; parcel.pid = formData.pid !== undefined ? formData.pid : parcel.pid; parcel.pin = formData.pid !== undefined ? formData.pin : parcel.pin; parcel.civicAddress = formData.civicAddress !== undefined ? formData.civicAddress : parcel.civicAddress; @@ -115,27 +113,25 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft } private async saveProgress() { - if (this.isDirty || this.newParcelAdded) { - const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; - for (const parcel of this.parcels) { - parcelsToUpdate.push({ - uuid: parcel.uuid, - pid: parcel.pid?.toString() || null, - pin: parcel.pin?.toString() || null, - civicAddress: parcel.civicAddress ?? null, - legalDescription: parcel.legalDescription, - isFarm: parcel.isFarm, - purchasedDate: parcel.purchasedDate, - mapAreaHectares: parcel.mapAreaHectares, - ownershipTypeCode: parcel.ownershipTypeCode, - isConfirmedByApplicant: parcel.isConfirmedByApplicant, - crownLandOwnerType: parcel.crownLandOwnerType, - ownerUuids: parcel.owners.map((owner) => owner.uuid), - }); - } - await this.applicationParcelService.update(parcelsToUpdate); - //TODO: Do we need to reload submission? + const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; + for (const parcel of this.parcels) { + parcelsToUpdate.push({ + uuid: parcel.uuid, + pid: parcel.pid?.toString() || null, + pin: parcel.pin?.toString() || null, + civicAddress: parcel.civicAddress ?? null, + legalDescription: parcel.legalDescription, + isFarm: parcel.isFarm, + purchasedDate: parcel.purchasedDate, + mapAreaHectares: parcel.mapAreaHectares, + ownershipTypeCode: parcel.ownershipTypeCode, + isConfirmedByApplicant: parcel.isConfirmedByApplicant, + crownLandOwnerType: parcel.crownLandOwnerType, + ownerUuids: parcel.owners.map((owner) => owner.uuid), + }); } + + await this.applicationParcelService.update(parcelsToUpdate); } async onSave() { diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index 56f837e845..7efc0ae9f5 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -31,7 +31,6 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni isCrownOwner = false; isGovernmentUser = false; governmentName: string | undefined; - isDirty = false; firstName = new FormControl('', [Validators.required]); lastName = new FormControl('', [Validators.required]); @@ -95,7 +94,6 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } onSelectOwner(uuid: string) { - this.isDirty = true; this.selectedOwnerUuid = uuid; const selectedOwner = this.parcelOwners.find((owner) => owner.uuid === uuid); this.parcelOwners = this.parcelOwners.map((owner) => ({ @@ -166,37 +164,29 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } protected async save() { - if (this.isDirty || this.form.dirty) { - let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( - (owner) => owner.uuid === this.selectedOwnerUuid - ); + let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( + (owner) => owner.uuid === this.selectedOwnerUuid + ); - if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - organization: this.organizationName.getRawValue() ?? '', - firstName: this.firstName.getRawValue() ?? '', - lastName: this.lastName.getRawValue() ?? '', - email: this.email.getRawValue() ?? '', - phoneNumber: this.phoneNumber.getRawValue() ?? '', - ownerUuid: selectedOwner?.uuid, - type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, - }); - } else if (selectedOwner) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - ownerUuid: selectedOwner.uuid, - }); - } - await this.reloadApplication(); + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + organization: this.organizationName.getRawValue() ?? '', + firstName: this.firstName.getRawValue() ?? '', + lastName: this.lastName.getRawValue() ?? '', + email: this.email.getRawValue() ?? '', + phoneNumber: this.phoneNumber.getRawValue() ?? '', + ownerUuid: selectedOwner?.uuid, + type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, + }); + } else if (selectedOwner) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + ownerUuid: selectedOwner.uuid, + }); } } - private async reloadApplication() { - const application = await this.applicationService.getByUuid(this.submissionUuid); - this.$applicationSubmission.next(application); - } - private async loadOwners(submissionUuid: string, primaryContactOwnerUuid?: string) { const owners = await this.applicationOwnerService.fetchBySubmissionId(submissionUuid); if (owners) { @@ -245,7 +235,6 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni if (this.showErrors) { this.form.markAllAsTouched(); } - this.isDirty = false; } } diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts index 9acdf56fd4..830d2a8ae5 100644 --- a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts +++ b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts @@ -26,7 +26,6 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, selectGovernmentUuid = ''; localGovernments: LocalGovernmentDto[] = []; filteredLocalGovernments!: Observable; - isDirty = false; form = new FormGroup({ localGovernment: this.localGovernment, @@ -59,7 +58,6 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, } onChange($event: MatAutocompleteSelectedEvent) { - this.isDirty = true; const localGovernmentName = $event.option.value; if (localGovernmentName) { const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); @@ -97,19 +95,15 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, } private async save() { - if (this.isDirty) { - const localGovernmentName = this.localGovernment.getRawValue(); - if (localGovernmentName) { - const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + const localGovernmentName = this.localGovernment.getRawValue(); + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); - if (localGovernment) { - const res = await this.applicationSubmissionService.updatePending(this.submissionUuid, { - localGovernmentUuid: localGovernment.uuid, - }); - this.$applicationSubmission.next(res); - } + if (localGovernment) { + await this.applicationSubmissionService.updatePending(this.submissionUuid, { + localGovernmentUuid: localGovernment.uuid, + }); } - this.isDirty = false; } } diff --git a/portal-frontend/src/app/features/review-submission/review-submission.component.ts b/portal-frontend/src/app/features/review-submission/review-submission.component.ts index dc6a512380..f5ba423738 100644 --- a/portal-frontend/src/app/features/review-submission/review-submission.component.ts +++ b/portal-frontend/src/app/features/review-submission/review-submission.component.ts @@ -97,7 +97,7 @@ export class ReviewSubmissionComponent implements OnInit, OnDestroy { // setTimeout is required for stepper to be initialized setTimeout(() => { const stepInt = parseInt(stepInd); - //this.customStepper.navigateToStep(stepInt, true); + this.customStepper.navigateToStep(stepInt, true); this.showDownloadPdf = this.isFirstNationGovernment ? stepInt === ReviewApplicationFngSteps.ReviewAndSubmitFng diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index fff750c23b..764b880842 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -154,10 +154,8 @@ export class ApplicationSubmissionService { const applicationSubmission = await this.getOrFailByUuid(submissionUuid); applicationSubmission.applicant = updateDto.applicant; - applicationSubmission.purpose = filterUndefined( - updateDto.purpose, - applicationSubmission.purpose, - ); + applicationSubmission.purpose = + updateDto.purpose || applicationSubmission.purpose; applicationSubmission.typeCode = updateDto.typeCode || applicationSubmission.typeCode; applicationSubmission.localGovernmentUuid = updateDto.localGovernmentUuid; From 354f0c4309d4aed299de902bf99cebd77c880547 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 25 Jul 2023 10:34:28 -0700 Subject: [PATCH 122/954] add file before testing --- bin/migrate-files/migrate-files.py | 96 ++++++++++++++---------------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/bin/migrate-files/migrate-files.py b/bin/migrate-files/migrate-files.py index 92a04ff354..04883b2c9a 100644 --- a/bin/migrate-files/migrate-files.py +++ b/bin/migrate-files/migrate-files.py @@ -5,7 +5,37 @@ from tqdm import tqdm import pickle -def application_docs(starting_document_id,batch): +# Load environment variables from .env file +load_dotenv() + +# Load the database connection secrets from environment variables +db_username = os.getenv("DB_USERNAME") +db_password = os.getenv("DB_PASSWORD") +db_dsn = os.getenv("DB_DSN") +# db_path = os.getenv("DB_PATH") # only necessary if running on M1 + +# Connect to the Oracle database +# cx_Oracle.init_oracle_client(lib_dir=db_path) # only necessary if running on M1 +conn = cx_Oracle.connect( + user=db_username, password=db_password, dsn=db_dsn, encoding="UTF-8" +) + +# Load the ECS connection secrets from environment variables +ecs_host = os.getenv("ECS_HOSTNAME") +ecs_bucket = os.getenv("ECS_BUCKET") +ecs_access_key = os.getenv("ECS_ACCESS_KEY") +ecs_secret_key = os.getenv("ECS_SECRET_KEY") + +# Connect to S3 +s3 = boto3.client( + "s3", + aws_access_key_id=ecs_access_key, + aws_secret_access_key=ecs_secret_key, + use_ssl=True, + endpoint_url=ecs_host, +) + +def application_docs(starting_document_id,batch,cursor): # Get total number of files try: cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND ALR_APPLICATION_ID IS NOT NULL') @@ -26,9 +56,6 @@ def application_docs(starting_document_id,batch): ORDER BY DOCUMENT_ID ASC """) - # Set the batch size - BATCH_SIZE = batch - # Track progress documents_processed = 0 last_document_id = 0 @@ -37,7 +64,7 @@ def application_docs(starting_document_id,batch): with tqdm(total=application_count, unit="file", desc="Uploading files to S3") as pbar: while True: # Fetch the next batch of BLOB data - data = cursor.fetchmany(BATCH_SIZE) + data = cursor.fetchmany(batch) if not data: break # Upload the batch to S3 with a progress bar @@ -65,8 +92,9 @@ def application_docs(starting_document_id,batch): # Display results print("Process complete: Successfully migrated", documents_processed, "files.") + return -def planning_docs(starting_planning_document_id,batch): +def planning_docs(starting_planning_document_id,batch,cursor): # Get total number of files try: cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND PLANNING_REVIEW_ID IS NOT NULL') @@ -87,9 +115,6 @@ def planning_docs(starting_planning_document_id,batch): ORDER BY DOCUMENT_ID ASC """) - # Set the batch size - BATCH_SIZE = batch - # Track progress documents_processed = 0 last_planning_document_id = 0 @@ -98,7 +123,7 @@ def planning_docs(starting_planning_document_id,batch): with tqdm(total=planning_review_count, unit="file", desc="Uploading files to S3") as pbar: while True: # Fetch the next batch of BLOB data - data = cursor.fetchmany(BATCH_SIZE) + data = cursor.fetchmany(batch) if not data: break # Upload the batch to S3 with a progress bar @@ -126,8 +151,9 @@ def planning_docs(starting_planning_document_id,batch): # Display results print("Process complete: Successfully migrated", documents_processed, "files.") + return -def issue_docs(starting_issue_document_id,batch): +def issue_docs(starting_issue_document_id,batch,cursor): # Get total number of files try: cursor.execute ('SELECT COUNT(*) FROM OATS.OATS_DOCUMENTS WHERE dbms_lob.getLength(DOCUMENT_BLOB) > 0 AND ISSUE_ID IS NOT NULL') @@ -148,9 +174,6 @@ def issue_docs(starting_issue_document_id,batch): ORDER BY DOCUMENT_ID ASC """) - # Set the batch size - BATCH_SIZE = batch - # Track progress documents_processed = 0 last_issue_document_id = 0 @@ -159,7 +182,7 @@ def issue_docs(starting_issue_document_id,batch): with tqdm(total=issue_count, unit="file", desc="Uploading files to S3") as pbar: while True: # Fetch the next batch of BLOB data - data = cursor.fetchmany(BATCH_SIZE) + data = cursor.fetchmany(batch) if not data: break # Upload the batch to S3 with a progress bar @@ -187,35 +210,7 @@ def issue_docs(starting_issue_document_id,batch): # Display results print("Process complete: Successfully migrated", documents_processed, "files.") -# Load environment variables from .env file -load_dotenv() - -# Load the database connection secrets from environment variables -db_username = os.getenv("DB_USERNAME") -db_password = os.getenv("DB_PASSWORD") -db_dsn = os.getenv("DB_DSN") -# db_path = os.getenv("DB_PATH") # only necessary if running on local M1 - -# Connect to the Oracle database -# cx_Oracle.init_oracle_client(lib_dir=db_path) # only necessary if running on local M1 -conn = cx_Oracle.connect( - user=db_username, password=db_password, dsn=db_dsn, encoding="UTF-8" -) - -# Load the ECS connection secrets from environment variables -ecs_host = os.getenv("ECS_HOSTNAME") -ecs_bucket = os.getenv("ECS_BUCKET") -ecs_access_key = os.getenv("ECS_ACCESS_KEY") -ecs_secret_key = os.getenv("ECS_SECRET_KEY") - -# Connect to S3 -s3 = boto3.client( - "s3", - aws_access_key_id=ecs_access_key, - aws_secret_access_key=ecs_secret_key, - use_ssl=True, - endpoint_url=ecs_host, -) + return starting_document_id = 0 # Determine job resume status @@ -239,18 +234,19 @@ def issue_docs(starting_issue_document_id,batch): cursor = conn.cursor() +# Set batch size batch_size = 10 if starting_issue_document_id > 0: - issue_docs(starting_issue_document_id,batch_size) + issue_docs(starting_issue_document_id,batch_size,cursor) elif starting_planning_document_id > 0: - planning_docs(starting_planning_document_id,batch_size) - issue_docs(0,batch_size) + planning_docs(starting_planning_document_id,batch_size,cursor) + issue_docs(0,batch_size,cursor) else: print('Starting applications from:', starting_document_id) - application_docs(starting_document_id,batch_size) - planning_docs(0,batch_size) - issue_docs(0,batch_size) + application_docs(starting_document_id,batch_size,cursor) + planning_docs(0,batch_size,cursor) + issue_docs(0,batch_size,cursor) # Close the database cursor and connection cursor.close() From 0aa064a441d7962bbae68bbb188956dc3a8db1f1 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 25 Jul 2023 09:52:34 -0700 Subject: [PATCH 123/954] Improve Board Performance * Break get boards call to load less data * Add get board detail call to get full board details --- .../board-management-dialog.component.html | 3 ++ .../board-management-dialog.component.ts | 42 +++++++++++++------ .../board-management.component.ts | 4 +- .../features/board/board.component.spec.ts | 39 +++++++---------- .../src/app/features/board/board.component.ts | 33 +++++++++------ .../app-modification-dialog.component.html | 2 +- .../app-modification-dialog.component.spec.ts | 4 +- .../app-modification-dialog.component.ts | 2 +- .../application-dialog.component.html | 2 +- .../application-dialog.component.spec.ts | 14 +++++-- .../application-dialog.component.ts | 2 +- .../card-dialog/card-dialog.component.ts | 10 ++--- .../covenant/covenant-dialog.component.html | 2 +- .../covenant-dialog.component.spec.ts | 4 +- .../covenant/covenant-dialog.component.ts | 2 +- .../noi-modification-dialog.component.html | 2 +- .../noi-modification-dialog.component.spec.ts | 4 +- .../noi-modification-dialog.component.ts | 2 +- .../notice-of-intent-dialog.component.html | 2 +- .../notice-of-intent-dialog.component.spec.ts | 4 +- .../notice-of-intent-dialog.component.ts | 2 +- .../planning-review-dialog.component.html | 2 +- .../planning-review-dialog.component.spec.ts | 4 +- .../planning-review-dialog.component.ts | 2 +- .../reconsideration-dialog.component.html | 2 +- .../reconsideration-dialog.component.spec.ts | 4 +- .../reconsideration-dialog.component.ts | 2 +- .../assigned-table.component.ts | 2 +- .../subtask-table/subtask-table.component.ts | 2 +- .../src/app/services/board/board.dto.ts | 10 +++-- .../src/app/services/board/board.service.ts | 16 ++++--- .../src/app/services/card/card.dto.ts | 2 +- .../details-header.component.ts | 2 +- .../meeting-overview.component.spec.ts | 2 - .../application.controller.spec.ts | 3 +- .../src/alcs/board/board.controller.spec.ts | 8 ++-- .../alcs/src/alcs/board/board.controller.ts | 17 ++++++-- .../apps/alcs/src/alcs/board/board.dto.ts | 23 +++++----- .../apps/alcs/src/alcs/board/board.service.ts | 15 ++----- services/apps/alcs/src/alcs/card/card.dto.ts | 4 +- .../automapper/board.automapper.profile.ts | 12 +++++- .../automapper/card.automapper.profile.ts | 4 ++ 42 files changed, 175 insertions(+), 144 deletions(-) diff --git a/alcs-frontend/src/app/features/admin/board-management/board-management-dialog/board-management-dialog.component.html b/alcs-frontend/src/app/features/admin/board-management/board-management-dialog/board-management-dialog.component.html index 692aa9ead9..173ec575be 100644 --- a/alcs-frontend/src/app/features/admin/board-management/board-management-dialog/board-management-dialog.component.html +++ b/alcs-frontend/src/app/features/admin/board-management/board-management-dialog/board-management-dialog.component.html @@ -40,6 +40,9 @@

{{ isEdit ? 'Edit' : 'Create' }} Board

Click to add columns*
+
+ Disabled columns have cards and cannot be removed from the board +
  • diff --git a/alcs-frontend/src/app/features/admin/board-management/board-management-dialog/board-management-dialog.component.ts b/alcs-frontend/src/app/features/admin/board-management/board-management-dialog/board-management-dialog.component.ts index bf667a208f..bed89d78fb 100644 --- a/alcs-frontend/src/app/features/admin/board-management/board-management-dialog/board-management-dialog.component.ts +++ b/alcs-frontend/src/app/features/admin/board-management/board-management-dialog/board-management-dialog.component.ts @@ -5,7 +5,8 @@ import { MatCheckboxChange } from '@angular/material/checkbox'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { AdminBoardManagementService } from '../../../../services/admin-board-management/admin-board-management.service'; import { CardStatusDto } from '../../../../services/application/application-code.dto'; -import { BoardDto, BoardStatusDto } from '../../../../services/board/board.dto'; +import { BoardDto, BoardStatusDto, MinimalBoardDto } from '../../../../services/board/board.dto'; +import { BoardService } from '../../../../services/board/board.service'; import { CardStatusService } from '../../../../services/card/card-status/card-status.service'; import { CardType } from '../../../../shared/card/card.component'; import { BaseCodeDto } from '../../../../shared/dto/base.dto'; @@ -45,24 +46,18 @@ export class BoardManagementDialogComponent implements OnInit { constructor( @Inject(MAT_DIALOG_DATA) public data: { - board: BoardDto | undefined; + board: MinimalBoardDto | undefined; cardTypes: BaseCodeDto[]; }, private dialogRef: MatDialogRef, private cardStatusService: CardStatusService, private adminBoardManagementService: AdminBoardManagementService, - private formBuilder: FormBuilder + private formBuilder: FormBuilder, + private boardService: BoardService ) { if (data.board) { this.isEdit = true; this.form.controls.code.disable(); - this.form.patchValue({ - code: data.board.code, - title: data.board.title, - createCardTypes: data.board.createCardTypes, - permittedCardTypes: data.board.allowedCardTypes, - showOnSchedule: data.board.showOnSchedule ? 'true' : 'false', - }); } this.cardTypes = data.cardTypes; @@ -71,9 +66,31 @@ export class BoardManagementDialogComponent implements OnInit { } } + async loadBoard(code: string) { + return await this.boardService.fetchBoardDetail(code); + } + ngOnInit(): void { this.loadCanDelete(); - this.loadCardStatuses(); + this.prepareDialog(); + } + + private async prepareDialog() { + if (this.data.board) { + const board = await this.loadBoard(this.data.board.code); + + this.form.patchValue({ + code: board.code, + title: board.title, + createCardTypes: board.createCardTypes, + permittedCardTypes: board.allowedCardTypes, + showOnSchedule: board.showOnSchedule ? 'true' : 'false', + }); + + await this.loadCardStatuses(board); + } else { + await this.loadCardStatuses(); + } } onNextStep() { @@ -143,10 +160,9 @@ export class BoardManagementDialogComponent implements OnInit { } } - private async loadCardStatuses() { + private async loadCardStatuses(board?: BoardDto) { const cardStatuses = await this.cardStatusService.fetch(); - const board = this.data.board; const boardStatusCodes = board ? board.statuses .sort((a, b) => { diff --git a/alcs-frontend/src/app/features/admin/board-management/board-management.component.ts b/alcs-frontend/src/app/features/admin/board-management/board-management.component.ts index 36f016e6af..a5ad222f95 100644 --- a/alcs-frontend/src/app/features/admin/board-management/board-management.component.ts +++ b/alcs-frontend/src/app/features/admin/board-management/board-management.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Subject, takeUntil } from 'rxjs'; import { AdminBoardManagementService } from '../../../services/admin-board-management/admin-board-management.service'; -import { BoardDto } from '../../../services/board/board.dto'; +import { BoardDto, MinimalBoardDto } from '../../../services/board/board.dto'; import { BoardService } from '../../../services/board/board.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { BaseCodeDto } from '../../../shared/dto/base.dto'; @@ -16,7 +16,7 @@ import { BoardManagementDialogComponent } from './board-management-dialog/board- export class BoardManagementComponent implements OnInit, OnDestroy { $destroy = new Subject(); - boards: BoardDto[] = []; + boards: MinimalBoardDto[] = []; displayedColumns: string[] = ['boards', 'cardTypes', 'actions']; private cardTypes: BaseCodeDto[] = []; cardTypeMap: Record = {}; diff --git a/alcs-frontend/src/app/features/board/board.component.spec.ts b/alcs-frontend/src/app/features/board/board.component.spec.ts index 6a7744762b..89e328f90b 100644 --- a/alcs-frontend/src/app/features/board/board.component.spec.ts +++ b/alcs-frontend/src/app/features/board/board.component.spec.ts @@ -13,6 +13,7 @@ import { ApplicationReconsiderationDto } from '../../services/application/applic import { ApplicationReconsiderationService } from '../../services/application/application-reconsideration/application-reconsideration.service'; import { ApplicationDto } from '../../services/application/application.dto'; import { ApplicationService } from '../../services/application/application.service'; +import { BoardDto } from '../../services/board/board.dto'; import { BoardService, BoardWithFavourite } from '../../services/board/board.service'; import { CardDto } from '../../services/card/card.dto'; import { CardService } from '../../services/card/card.service'; @@ -73,17 +74,22 @@ describe('BoardComponent', () => { code: 'boardCode', title: 'boardTitle', isFavourite: false, - statuses: [], allowedCardTypes: [], - createCardTypes: [], showOnSchedule: true, }; + const mockDetailBoard: BoardDto = { + ...mockBoard, + statuses: [], + createCardTypes: [], + }; + beforeEach(async () => { applicationService = createMock(); boardService = createMock(); boardService.$boards = boardEmitter; - boardService.fetchCards.mockResolvedValue({ + boardService.fetchBoardWithCards.mockResolvedValue({ + board: mockDetailBoard, applications: [], covenants: [], modifications: [], @@ -202,26 +208,9 @@ describe('BoardComponent', () => { expect(component.currentBoardCode).toEqual('boardCode'); }); - it('should enable covenants when the board supports it', async () => { - boardEmitter.next([ - { - code: 'boardCode', - statuses: [], - createCardTypes: [CardType.COV], - title: '', - isFavourite: false, - allowedCardTypes: [CardType.COV], - showOnSchedule: true, - }, - ]); - - await fixture.whenStable(); - - expect(component.boardHasCreateCovenant).toBeTruthy(); - }); - it('should map an application into a card', async () => { - boardService.fetchCards.mockResolvedValue({ + boardService.fetchBoardWithCards.mockResolvedValue({ + board: mockDetailBoard, applications: [mockApplication], covenants: [], modifications: [], @@ -240,7 +229,8 @@ describe('BoardComponent', () => { }); it('should map a reconsideration into a card', async () => { - boardService.fetchCards.mockResolvedValue({ + boardService.fetchBoardWithCards.mockResolvedValue({ + board: mockDetailBoard, applications: [], covenants: [], modifications: [], @@ -281,7 +271,8 @@ describe('BoardComponent', () => { }, }, } as ApplicationDto; - boardService.fetchCards.mockResolvedValue({ + boardService.fetchBoardWithCards.mockResolvedValue({ + board: mockDetailBoard, applications: [mockApplication, highPriorityApplication, highActiveDays], covenants: [], modifications: [], diff --git a/alcs-frontend/src/app/features/board/board.component.ts b/alcs-frontend/src/app/features/board/board.component.ts index b7f6491ea9..526c511722 100644 --- a/alcs-frontend/src/app/features/board/board.component.ts +++ b/alcs-frontend/src/app/features/board/board.component.ts @@ -12,6 +12,7 @@ import { ApplicationReconsiderationDto } from '../../services/application/applic import { ApplicationReconsiderationService } from '../../services/application/application-reconsideration/application-reconsideration.service'; import { ApplicationDto } from '../../services/application/application.dto'; import { ApplicationService } from '../../services/application/application.service'; +import { CardsDto } from '../../services/board/board.dto'; import { BoardService, BoardWithFavourite } from '../../services/board/board.service'; import { CardService } from '../../services/card/card.service'; import { CovenantDto } from '../../services/covenant/covenant.dto'; @@ -208,14 +209,20 @@ export class BoardComponent implements OnInit, OnDestroy { } } - private setupBoard(board: BoardWithFavourite) { + private async setupBoard(board: BoardWithFavourite) { // clear cards to remove flickering this.cards = []; this.titleService.setTitle(`${environment.siteName} | ${board.title} Board`); + this.boardIsFavourite = board.isFavourite; + + this.loadBoard(board.code); + } + + private async loadBoard(boardCode: string) { + const response = await this.boardService.fetchBoardWithCards(boardCode); + const board = response.board; - this.loadCards(board.code); this.boardTitle = board.title; - this.boardIsFavourite = board.isFavourite; this.boardHasCreateApplication = board.createCardTypes.includes(CardType.APP); this.boardHasCreatePlanningReview = board.createCardTypes.includes(CardType.PLAN); this.boardHasCreateReconsideration = board.createCardTypes.includes(CardType.RECON); @@ -231,17 +238,17 @@ export class BoardComponent implements OnInit, OnDestroy { name: status.label, allowedTransitions: allStatuses, })); + this.mapAndSortCards(response, boardCode); } - private async loadCards(boardCode: string) { - const thingsWithCards = await this.boardService.fetchCards(boardCode); - const mappedApps = thingsWithCards.applications.map(this.mapApplicationDtoToCard.bind(this)); - const mappedRecons = thingsWithCards.reconsiderations.map(this.mapReconsiderationDtoToCard.bind(this)); - const mappedReviewMeetings = thingsWithCards.planningReviews.map(this.mapPlanningReviewToCard.bind(this)); - const mappedModifications = thingsWithCards.modifications.map(this.mapModificationToCard.bind(this)); - const mappedCovenants = thingsWithCards.covenants.map(this.mapCovenantToCard.bind(this)); - const mappedNoticeOfIntents = thingsWithCards.noticeOfIntents.map(this.mapNoticeOfIntentToCard.bind(this)); - const mappedNoticeOfIntentModifications = thingsWithCards.noiModifications.map( + private mapAndSortCards(response: CardsDto, boardCode: string) { + const mappedApps = response.applications.map(this.mapApplicationDtoToCard.bind(this)); + const mappedRecons = response.reconsiderations.map(this.mapReconsiderationDtoToCard.bind(this)); + const mappedReviewMeetings = response.planningReviews.map(this.mapPlanningReviewToCard.bind(this)); + const mappedModifications = response.modifications.map(this.mapModificationToCard.bind(this)); + const mappedCovenants = response.covenants.map(this.mapCovenantToCard.bind(this)); + const mappedNoticeOfIntents = response.noticeOfIntents.map(this.mapNoticeOfIntentToCard.bind(this)); + const mappedNoticeOfIntentModifications = response.noiModifications.map( this.mapNoticeOfIntentModificationToCard.bind(this) ); if (boardCode === BOARD_TYPE_CODES.VETT) { @@ -456,7 +463,7 @@ export class BoardComponent implements OnInit, OnDestroy { this.setUrl(); if (isDirty && this.selectedBoardCode) { - this.loadCards(this.selectedBoardCode); + this.loadBoard(this.selectedBoardCode); } }); } diff --git a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html index 2a26449772..c9fe6bef85 100644 --- a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.html @@ -44,7 +44,7 @@

    >star {{ board.title }} - check

diff --git a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.spec.ts index e351c8efa5..2a51ed79ee 100644 --- a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.spec.ts @@ -51,9 +51,7 @@ describe('ModificationDialogComponent', () => { status: { code: 'FAKE_STATUS', }, - board: { - code: 'FAKE_BOARD', - }, + boardCode: 'FAKE_BOARD', } as CardDto, }; diff --git a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.ts index 1e2f56834d..bbde078894 100644 --- a/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/app-modification/app-modification-dialog.component.ts @@ -58,7 +58,7 @@ export class AppModificationDialogComponent extends CardDialogComponent implemen this.selectedBoard = board.code; try { await this.boardService.changeBoard(this.data.card.uuid, board.code); - const loadedBoard = this.boards.find((board) => board.code === this.selectedBoard); + const loadedBoard = await this.boardService.fetchBoardDetail(board.code); if (loadedBoard) { this.boardStatuses = loadedBoard.statuses; } diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.html index 67298c3bb5..614288178e 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.html @@ -57,7 +57,7 @@

>star {{ board.title }} - check

diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.spec.ts index fe4df27895..cfb9d225fd 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.spec.ts @@ -7,6 +7,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ApplicationRegionDto, ApplicationTypeDto } from '../../../../services/application/application-code.dto'; import { APPLICATION_SYSTEM_SOURCE_TYPES, ApplicationDto } from '../../../../services/application/application.dto'; +import { BoardDto } from '../../../../services/board/board.dto'; import { BoardService } from '../../../../services/board/board.service'; import { CardDto } from '../../../../services/card/card.dto'; import { AssigneeDto } from '../../../../services/user/user.dto'; @@ -62,11 +63,9 @@ describe('ApplicationDialogComponent', () => { decisionMeetings: [], dateSubmittedToAlc: Date.now(), card: { + boardCode: 'a', assignee: mockAssignee, highPriority: false, - board: { - code: 'board-code', - }, status: { code: 'card-status', }, @@ -84,6 +83,14 @@ describe('ApplicationDialogComponent', () => { mockBoardService = createMock(); mockBoardService.$boards = new EventEmitter(); + mockBoardService.fetchBoardDetail.mockResolvedValue({ + allowedCardTypes: [], + code: '', + createCardTypes: [], + showOnSchedule: false, + statuses: [], + title: '', + }); await TestBed.configureTestingModule({ declarations: [ApplicationDialogComponent], @@ -106,6 +113,7 @@ describe('ApplicationDialogComponent', () => { fixture = TestBed.createComponent(ApplicationDialogComponent); component = fixture.componentInstance; component.data = mockApplication; + component.boardStatuses = []; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts index fb452c47e9..8963a37698 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts @@ -68,7 +68,7 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O this.selectedBoard = board.code; try { await this.boardService.changeBoard(this.application.card!.uuid, board.code); - const loadedBoard = this.boards.find((board) => board.code === this.selectedBoard); + const loadedBoard = await this.boardService.fetchBoardDetail(board.code); if (loadedBoard) { this.boardStatuses = loadedBoard.statuses; } diff --git a/alcs-frontend/src/app/features/board/dialogs/card-dialog/card-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/card-dialog/card-dialog.component.ts index afafa9862a..626f270139 100644 --- a/alcs-frontend/src/app/features/board/dialogs/card-dialog/card-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/card-dialog/card-dialog.component.ts @@ -44,12 +44,8 @@ export class CardDialogComponent implements OnInit, OnDestroy { ngOnInit(): void { this.boardService.$boards.pipe(takeUntil(this.$destroy)).subscribe((boards) => { - const loadedBoard = boards.find((board) => board.code === this.selectedBoard); this.boards = boards; this.allowedBoards = this.boards.filter((board) => this.card && board.allowedCardTypes.includes(this.card.type)); - if (loadedBoard) { - this.boardStatuses = loadedBoard.statuses; - } }); this.authService.$currentUser.pipe(takeUntil(this.$destroy)).subscribe((currentUser) => { @@ -64,18 +60,18 @@ export class CardDialogComponent implements OnInit, OnDestroy { }); } - populateCardData(card: CardDto) { + async populateCardData(card: CardDto) { this.card = card; this.selectedAssignee = card.assignee; this.selectedAssigneeName = this.selectedAssignee?.prettyName; this.selectedApplicationStatus = card.status.code; - this.selectedBoard = card.board.code; + this.selectedBoard = card.boardCode; this.allowedBoards = this.boards.filter((board) => this.card && board.allowedCardTypes.includes(this.card.type)); this.$users = this.userService.$assignableUsers; this.userService.fetchAssignableUsers(); - const loadedBoard = this.boards.find((board) => board.code === this.selectedBoard); + const loadedBoard = await this.boardService.fetchBoardDetail(this.selectedBoard); if (loadedBoard) { this.boardStatuses = loadedBoard.statuses; } diff --git a/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.html index 23e0edf7fe..388c1e5061 100644 --- a/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.html @@ -29,7 +29,7 @@

>star {{ board.title }} - check + check diff --git a/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.spec.ts index c190f65daa..57771dd0f8 100644 --- a/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.spec.ts @@ -44,9 +44,7 @@ describe('CovenantDialogComponent', () => { status: { code: 'FAKE_STATUS', }, - board: { - code: 'FAKE_BOARD', - }, + boardCode: 'FAKE_BOARD', } as CardDto, }; diff --git a/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.ts index dda29b228a..02bc3ba6dd 100644 --- a/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/covenant/covenant-dialog.component.ts @@ -66,7 +66,7 @@ export class CovenantDialogComponent extends CardDialogComponent implements OnIn this.selectedBoard = board.code; try { await this.boardService.changeBoard(this.covenant.card.uuid, board.code); - const loadedBoard = this.boards.find((board) => board.code === this.selectedBoard); + const loadedBoard = await this.boardService.fetchBoardDetail(board.code); if (loadedBoard) { this.boardStatuses = loadedBoard.statuses; } diff --git a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html index f9c38358bc..5767c928f4 100644 --- a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.html @@ -48,7 +48,7 @@

>star {{ board.title }} - check diff --git a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.spec.ts index 1de12a3416..60caf2d8dc 100644 --- a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.spec.ts @@ -50,9 +50,7 @@ describe('NoiModificationDialogComponent', () => { status: { code: 'FAKE_STATUS', }, - board: { - code: 'FAKE_BOARD', - }, + boardCode: 'FAKE_BOARD', } as CardDto, }; diff --git a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.ts index 989efddd63..7f11bdb0a8 100644 --- a/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/noi-modification/noi-modification-dialog.component.ts @@ -62,7 +62,7 @@ export class NoiModificationDialogComponent extends CardDialogComponent implemen this.selectedBoard = board.code; try { await this.boardService.changeBoard(this.data.card.uuid, board.code); - const loadedBoard = this.boards.find((board) => board.code === this.selectedBoard); + const loadedBoard = await this.boardService.fetchBoardDetail(board.code); if (loadedBoard) { this.boardStatuses = loadedBoard.statuses; } diff --git a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.html index 7c989eac30..56fb88abcc 100644 --- a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.html @@ -53,7 +53,7 @@

>star {{ board.title }} - check diff --git a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.spec.ts index 30ae14c4e2..5029260cb1 100644 --- a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.spec.ts @@ -45,9 +45,7 @@ describe('NoticeOfIntentDialogComponent', () => { status: { code: 'FAKE_STATUS', }, - board: { - code: 'FAKE_BOARD', - }, + boardCode: 'FAKE_BOARD', } as CardDto, activeDays: 0, paused: false, diff --git a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts index 8b00d1282c..db731c1ea6 100644 --- a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.ts @@ -66,7 +66,7 @@ export class NoticeOfIntentDialogComponent extends CardDialogComponent implement this.selectedBoard = board.code; try { await this.boardService.changeBoard(this.noticeOfIntent.card.uuid, board.code); - const loadedBoard = this.boards.find((board) => board.code === this.selectedBoard); + const loadedBoard = await this.boardService.fetchBoardDetail(board.code); if (loadedBoard) { this.boardStatuses = loadedBoard.statuses; } diff --git a/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.html index b39f343727..df9bac4e20 100644 --- a/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.html @@ -26,7 +26,7 @@

>star {{ board.title }} - check diff --git a/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.spec.ts index d0f2ccb5a8..f43ca33382 100644 --- a/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.spec.ts @@ -42,9 +42,7 @@ describe('PlanningReviewDialogComponent', () => { status: { code: 'FAKE_STATUS', }, - board: { - code: 'FAKE_BOARD', - }, + boardCode: 'FAKE_BOARD', } as CardDto, }; diff --git a/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.ts index e05c6d40db..9a45cdee10 100644 --- a/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/planning-review/planning-review-dialog.component.ts @@ -63,7 +63,7 @@ export class PlanningReviewDialogComponent extends CardDialogComponent implement this.selectedBoard = board.code; try { await this.boardService.changeBoard(this.planningReview.card.uuid, board.code); - const loadedBoard = this.boards.find((board) => board.code === this.selectedBoard); + const loadedBoard = await this.boardService.fetchBoardDetail(board.code); if (loadedBoard) { this.boardStatuses = loadedBoard.statuses; } diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html index 1db134e514..01047723a5 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.html @@ -42,7 +42,7 @@

>star {{ board.title }} - check diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.spec.ts index 31b0b2b2e4..e3222c716c 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.spec.ts @@ -58,9 +58,7 @@ describe('ReconsiderationDialogComponent', () => { status: { code: 'FAKE_STATUS', }, - board: { - code: 'FAKE_BOARD', - }, + boardCode: 'FAKE_BOARD', } as CardDto, resultingDecision: null, }; diff --git a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.ts index 23cf74a11d..086d778bc8 100644 --- a/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/reconsiderations/reconsideration-dialog.component.ts @@ -58,7 +58,7 @@ export class ReconsiderationDialogComponent extends CardDialogComponent implemen this.selectedBoard = board.code; try { await this.boardService.changeBoard(this.recon.card.uuid, board.code); - const loadedBoard = this.boards.find((board) => board.code === this.selectedBoard); + const loadedBoard = await this.boardService.fetchBoardDetail(board.code); if (loadedBoard) { this.boardStatuses = loadedBoard.statuses; } diff --git a/alcs-frontend/src/app/features/home/assigned/assigned-table/assigned-table.component.ts b/alcs-frontend/src/app/features/home/assigned/assigned-table/assigned-table.component.ts index 8ca8422441..4fa9491715 100644 --- a/alcs-frontend/src/app/features/home/assigned/assigned-table/assigned-table.component.ts +++ b/alcs-frontend/src/app/features/home/assigned/assigned-table/assigned-table.component.ts @@ -27,6 +27,6 @@ export class AssignedTableComponent { constructor(private router: Router) {} async onSelectCard(card: CardDto) { - await this.router.navigateByUrl(`/board/${card.board.code}?card=${card.uuid}&type=${card.type}`); + await this.router.navigateByUrl(`/board/${card.boardCode}?card=${card.uuid}&type=${card.type}`); } } diff --git a/alcs-frontend/src/app/features/home/subtask/subtask-table/subtask-table.component.ts b/alcs-frontend/src/app/features/home/subtask/subtask-table/subtask-table.component.ts index 89d6858454..bca19a4e76 100644 --- a/alcs-frontend/src/app/features/home/subtask/subtask-table/subtask-table.component.ts +++ b/alcs-frontend/src/app/features/home/subtask/subtask-table/subtask-table.component.ts @@ -38,7 +38,7 @@ export class SubtaskTableComponent { async openCard(subtask: HomepageSubtaskDto) { await this.router.navigateByUrl( - `/board/${subtask.card.board.code}?card=${subtask.card.uuid}&type=${subtask.card.type}` + `/board/${subtask.card.boardCode}?card=${subtask.card.uuid}&type=${subtask.card.type}` ); } diff --git a/alcs-frontend/src/app/services/board/board.dto.ts b/alcs-frontend/src/app/services/board/board.dto.ts index 4be8f2a724..6832f731b8 100644 --- a/alcs-frontend/src/app/services/board/board.dto.ts +++ b/alcs-frontend/src/app/services/board/board.dto.ts @@ -7,13 +7,16 @@ import { NoticeOfIntentModificationDto } from '../notice-of-intent/notice-of-int import { NoticeOfIntentDto } from '../notice-of-intent/notice-of-intent.dto'; import { PlanningReviewDto } from '../planning-review/planning-review.dto'; -export interface BoardDto { +export interface MinimalBoardDto { code: string; title: string; - statuses: BoardStatusDto[]; + showOnSchedule: boolean; allowedCardTypes: CardType[]; +} + +export interface BoardDto extends MinimalBoardDto { + statuses: BoardStatusDto[]; createCardTypes: CardType[]; - showOnSchedule: boolean; } export interface BoardStatusDto { @@ -23,6 +26,7 @@ export interface BoardStatusDto { } export interface CardsDto { + board: BoardDto; applications: ApplicationDto[]; reconsiderations: ApplicationReconsiderationDto[]; planningReviews: PlanningReviewDto[]; diff --git a/alcs-frontend/src/app/services/board/board.service.ts b/alcs-frontend/src/app/services/board/board.service.ts index 824bd30c71..1048d73913 100644 --- a/alcs-frontend/src/app/services/board/board.service.ts +++ b/alcs-frontend/src/app/services/board/board.service.ts @@ -5,9 +5,9 @@ import { environment } from '../../../environments/environment'; import { ApplicationDto } from '../application/application.dto'; import { UserDto } from '../user/user.dto'; import { UserService } from '../user/user.service'; -import { BoardDto, CardsDto } from './board.dto'; +import { BoardDto, CardsDto, MinimalBoardDto } from './board.dto'; -export interface BoardWithFavourite extends BoardDto { +export interface BoardWithFavourite extends MinimalBoardDto { isFavourite: boolean; } @@ -15,7 +15,7 @@ export interface BoardWithFavourite extends BoardDto { providedIn: 'root', }) export class BoardService { - private boards?: BoardDto[]; + private boards?: MinimalBoardDto[]; private userProfile?: UserDto; private boardsEmitter = new BehaviorSubject([]); $boards = this.boardsEmitter.asObservable(); @@ -35,7 +35,7 @@ export class BoardService { private async publishBoards(reload = false) { if (this.userProfile !== undefined) { if (!this.boards || reload) { - this.boards = await firstValueFrom(this.http.get(`${environment.apiUrl}/board`)); + this.boards = await firstValueFrom(this.http.get(`${environment.apiUrl}/board`)); } const mappedBoards = this.boards.map((board) => ({ ...board, @@ -46,8 +46,12 @@ export class BoardService { return; } - fetchCards(boardCode: string) { - return firstValueFrom(this.http.get(`${environment.apiUrl}/board/${boardCode}`)); + fetchBoardDetail(boardCode: string) { + return firstValueFrom(this.http.get(`${environment.apiUrl}/board/${boardCode}`)); + } + + fetchBoardWithCards(boardCode: string) { + return firstValueFrom(this.http.get(`${environment.apiUrl}/board/${boardCode}/cards`)); } changeBoard(cardUuid: string, boardCode: string) { diff --git a/alcs-frontend/src/app/services/card/card.dto.ts b/alcs-frontend/src/app/services/card/card.dto.ts index 5422242a60..da30dd66a0 100644 --- a/alcs-frontend/src/app/services/card/card.dto.ts +++ b/alcs-frontend/src/app/services/card/card.dto.ts @@ -27,6 +27,6 @@ export interface CardDto { highPriority: boolean; status: CardStatusDto; assignee?: AssigneeDto; - board: BoardDto; + boardCode: string; createdAt: number; } diff --git a/alcs-frontend/src/app/shared/details-header/details-header.component.ts b/alcs-frontend/src/app/shared/details-header/details-header.component.ts index 94a66557d4..302115e4c5 100644 --- a/alcs-frontend/src/app/shared/details-header/details-header.component.ts +++ b/alcs-frontend/src/app/shared/details-header/details-header.component.ts @@ -96,7 +96,7 @@ export class DetailsHeaderComponent { constructor(private router: Router, private submissionStatusService: ApplicationSubmissionStatusService) {} async onGoToCard(card: CardDto) { - const boardCode = card.board.code; + const boardCode = card.boardCode; const cardUuid = card.uuid; const cardTypeCode = card.type; await this.router.navigateByUrl(`/board/${boardCode}?card=${cardUuid}&type=${cardTypeCode}`); diff --git a/alcs-frontend/src/app/shared/meeting-overview/meeting-overview.component.spec.ts b/alcs-frontend/src/app/shared/meeting-overview/meeting-overview.component.spec.ts index 71f79a9692..ddf39f1ceb 100644 --- a/alcs-frontend/src/app/shared/meeting-overview/meeting-overview.component.spec.ts +++ b/alcs-frontend/src/app/shared/meeting-overview/meeting-overview.component.spec.ts @@ -83,12 +83,10 @@ describe('MeetingOverviewComponent', () => { boardEmitter.next([ { - statuses: [], isFavourite: true, title: '', code: 'boardCode', allowedCardTypes: [], - createCardTypes: [], showOnSchedule: true, }, ]); diff --git a/services/apps/alcs/src/alcs/application/application.controller.spec.ts b/services/apps/alcs/src/alcs/application/application.controller.spec.ts index b519566dbe..83173e98d7 100644 --- a/services/apps/alcs/src/alcs/application/application.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application/application.controller.spec.ts @@ -11,7 +11,6 @@ import { import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; import { ApplicationProfile } from '../../common/automapper/application.automapper.profile'; import { UserProfile } from '../../common/automapper/user.automapper.profile'; -import { BoardSmallDto } from '../board/board.dto'; import { CardStatusDto } from '../card/card-status/card-status.dto'; import { CardDto } from '../card/card.dto'; import { Card } from '../card/card.entity'; @@ -58,7 +57,7 @@ describe('ApplicationController', () => { type: 'fake', uuid: 'fake', highPriority: false, - board: {} as BoardSmallDto, + boardCode: 'boardCode', } as CardDto, source: 'ALCS', }; diff --git a/services/apps/alcs/src/alcs/board/board.controller.spec.ts b/services/apps/alcs/src/alcs/board/board.controller.spec.ts index 718f18b72f..978d3e7f42 100644 --- a/services/apps/alcs/src/alcs/board/board.controller.spec.ts +++ b/services/apps/alcs/src/alcs/board/board.controller.spec.ts @@ -46,6 +46,8 @@ describe('BoardController', () => { noiModificationService = createMock(); mockBoard = new Board({ allowedCardTypes: [], + statuses: [], + createCardTypes: [], uuid: 'fake-board', }); @@ -128,7 +130,7 @@ describe('BoardController', () => { }), ]; - await controller.getCards(mockBoard.uuid); + await controller.getBoardWithCards(mockBoard.uuid); expect(appService.getByBoard).toHaveBeenCalledTimes(1); expect(appService.getByBoard).toBeCalledWith(mockBoard.uuid); @@ -148,7 +150,7 @@ describe('BoardController', () => { }), ]; - await controller.getCards(boardCode); + await controller.getBoardWithCards(boardCode); expect(planningReviewService.getByBoard).toHaveBeenCalledTimes(1); expect(planningReviewService.mapToDtos).toHaveBeenCalledTimes(1); @@ -162,7 +164,7 @@ describe('BoardController', () => { }), ]; - await controller.getCards(boardCode); + await controller.getBoardWithCards(boardCode); expect(modificationService.getByBoard).toHaveBeenCalledTimes(1); expect(modificationService.mapToDtos).toHaveBeenCalledTimes(1); diff --git a/services/apps/alcs/src/alcs/board/board.controller.ts b/services/apps/alcs/src/alcs/board/board.controller.ts index 3849541813..8fa661e398 100644 --- a/services/apps/alcs/src/alcs/board/board.controller.ts +++ b/services/apps/alcs/src/alcs/board/board.controller.ts @@ -20,7 +20,7 @@ import { CovenantService } from '../covenant/covenant.service'; import { NoticeOfIntentModificationService } from '../notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.service'; import { NoticeOfIntentService } from '../notice-of-intent/notice-of-intent.service'; import { PlanningReviewService } from '../planning-review/planning-review.service'; -import { BoardDto } from './board.dto'; +import { BoardDto, MinimalBoardDto } from './board.dto'; import { Board } from './board.entity'; import { BoardService } from './board.service'; @@ -45,12 +45,22 @@ export class BoardController { @UserRoles(...ANY_AUTH_ROLE) async getBoards() { const boards = await this.boardService.list(); - return this.autoMapper.mapArray(boards, Board, BoardDto); + return this.autoMapper.mapArray(boards, Board, MinimalBoardDto); } @Get('/:boardCode') @UserRoles(...ROLES_ALLOWED_BOARDS) - async getCards(@Param('boardCode') boardCode: string) { + async getBoardDetail(@Param('boardCode') boardCode: string) { + const board = await this.boardService.getOneOrFail({ + code: boardCode, + }); + + return await this.autoMapper.mapAsync(board, Board, BoardDto); + } + + @Get('/:boardCode/cards') + @UserRoles(...ROLES_ALLOWED_BOARDS) + async getBoardWithCards(@Param('boardCode') boardCode: string) { const board = await this.boardService.getOneOrFail({ code: boardCode, }); @@ -86,6 +96,7 @@ export class BoardController { : []; return { + board: await this.autoMapper.mapAsync(board, Board, BoardDto), applications: await this.applicationService.mapToDtos(applications), reconsiderations: await this.reconsiderationService.mapToDtos(recons), planningReviews: await this.planningReviewService.mapToDtos( diff --git a/services/apps/alcs/src/alcs/board/board.dto.ts b/services/apps/alcs/src/alcs/board/board.dto.ts index b305a78e6b..1852f60048 100644 --- a/services/apps/alcs/src/alcs/board/board.dto.ts +++ b/services/apps/alcs/src/alcs/board/board.dto.ts @@ -7,7 +7,7 @@ export enum BOARD_CODES { EXECUTIVE_COMMITTEE = 'exec', } -export class BoardDto { +export class MinimalBoardDto { @AutoMap() @IsString() code: string; @@ -17,29 +17,28 @@ export class BoardDto { title: string; @AutoMap() - @IsArray() - statuses: BoardStatusDto[]; + @IsBoolean() + showOnSchedule: boolean; @IsArray() allowedCardTypes: string[]; - - @IsArray() - createCardTypes: string[]; - - @AutoMap() - @IsBoolean() - showOnSchedule: boolean; } -export class BoardSmallDto { +export class BoardDto extends MinimalBoardDto { @AutoMap() + @IsString() code: string; @AutoMap() + @IsString() title: string; @AutoMap() - decisionMaker: string; + @IsArray() + statuses: BoardStatusDto[]; + + @IsArray() + createCardTypes: string[]; } export class BoardStatusDto { diff --git a/services/apps/alcs/src/alcs/board/board.service.ts b/services/apps/alcs/src/alcs/board/board.service.ts index 33fe030ef1..1640907d6d 100644 --- a/services/apps/alcs/src/alcs/board/board.service.ts +++ b/services/apps/alcs/src/alcs/board/board.service.ts @@ -4,7 +4,6 @@ import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsWhere, Repository } from 'typeorm'; import { FindOptionsRelations } from 'typeorm/find-options/FindOptionsRelations'; import { CARD_STATUS } from '../card/card-status/card-status.entity'; -import { CardType } from '../card/card-type/card-type.entity'; import { CardService } from '../card/card.service'; import { BoardStatus } from './board-status.entity'; import { BoardDto } from './board.dto'; @@ -36,16 +35,10 @@ export class BoardService { } async list() { - const boards = await this.boardRepository.find({ - relations: this.DEFAULT_RELATIONS, - }); - - //Sort board statuses - return boards.map((board) => { - board.statuses.sort((statusA, statusB) => { - return statusA.order - statusB.order; - }); - return board; + return await this.boardRepository.find({ + relations: { + allowedCardTypes: true, + }, }); } diff --git a/services/apps/alcs/src/alcs/card/card.dto.ts b/services/apps/alcs/src/alcs/card/card.dto.ts index 0b7ea58a10..64460b7e21 100644 --- a/services/apps/alcs/src/alcs/card/card.dto.ts +++ b/services/apps/alcs/src/alcs/card/card.dto.ts @@ -1,7 +1,7 @@ import { AutoMap } from '@automapper/classes'; import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator'; -import { BoardSmallDto } from '../board/board.dto'; import { AssigneeDto } from '../../user/user.dto'; +import { MinimalBoardDto } from '../board/board.dto'; import { CardStatusDto, CardTypeDto } from './card-status/card-status.dto'; import { CARD_TYPE } from './card-type/card-type.entity'; @@ -80,7 +80,7 @@ export class CardDto { highPriority?: boolean; @AutoMap() - board: BoardSmallDto; + boardCode: string; @AutoMap() createdAt: number; diff --git a/services/apps/alcs/src/common/automapper/board.automapper.profile.ts b/services/apps/alcs/src/common/automapper/board.automapper.profile.ts index 001971e570..5e3037c840 100644 --- a/services/apps/alcs/src/common/automapper/board.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/board.automapper.profile.ts @@ -4,8 +4,8 @@ import { Injectable } from '@nestjs/common'; import { BoardStatus } from '../../alcs/board/board-status.entity'; import { BoardDto, - BoardSmallDto, BoardStatusDto, + MinimalBoardDto, } from '../../alcs/board/board.dto'; import { Board } from '../../alcs/board/board.entity'; import { CardStatusDto } from '../../alcs/card/card-status/card-status.dto'; @@ -21,7 +21,15 @@ export class BoardAutomapperProfile extends AutomapperProfile { override get profile() { return (mapper) => { - createMap(mapper, Board, BoardSmallDto); + createMap( + mapper, + Board, + MinimalBoardDto, + forMember( + (ad) => ad.allowedCardTypes, + mapFrom((a) => a.allowedCardTypes.map((cardType) => cardType.code)), + ), + ); createMap( mapper, diff --git a/services/apps/alcs/src/common/automapper/card.automapper.profile.ts b/services/apps/alcs/src/common/automapper/card.automapper.profile.ts index 48daa040db..fbcabf6d84 100644 --- a/services/apps/alcs/src/common/automapper/card.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/card.automapper.profile.ts @@ -37,6 +37,10 @@ export class CardProfile extends AutomapperProfile { (cd) => cd.createdAt, mapFrom((c) => c.createdAt.getTime()), ), + forMember( + (cd) => cd.boardCode, + mapFrom((c) => c.board?.code), + ), forMember( (ad) => ad.assignee, mapFrom((a) => this.mapper.map(a.assignee, User, AssigneeDto)), From 5f3bd4e7a65f1a85f062b8d2a605c9b94aaab568 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 25 Jul 2023 11:47:14 -0700 Subject: [PATCH 124/954] changed logic to upload last file each time + more descriptive console messages --- bin/migrate-files/migrate-files.py | 52 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/bin/migrate-files/migrate-files.py b/bin/migrate-files/migrate-files.py index 04883b2c9a..da82ea0632 100644 --- a/bin/migrate-files/migrate-files.py +++ b/bin/migrate-files/migrate-files.py @@ -44,7 +44,7 @@ def application_docs(starting_document_id,batch,cursor): print(error.message) application_count = cursor.fetchone()[0] - print('Count =', application_count) + print('Application count =', application_count) # # Execute the SQL query to retrieve the BLOB data and key column cursor.execute(f""" @@ -61,7 +61,7 @@ def application_docs(starting_document_id,batch,cursor): last_document_id = 0 try: - with tqdm(total=application_count, unit="file", desc="Uploading files to S3") as pbar: + with tqdm(total=application_count, unit="file", desc="Uploading application files to S3") as pbar: while True: # Fetch the next batch of BLOB data data = cursor.fetchmany(batch) @@ -80,7 +80,7 @@ def application_docs(starting_document_id,batch,cursor): except Exception as e: print("Something went wrong:",e) - print("Processed", documents_processed, "files") + print("Processed", documents_processed, "application files") # Set resume status in case of interuption with open('last-file.pickle', 'wb') as file: @@ -91,7 +91,11 @@ def application_docs(starting_document_id,batch,cursor): exit() # Display results - print("Process complete: Successfully migrated", documents_processed, "files.") + print("Process complete: Successfully migrated", documents_processed, "out of", application_count, "application files.") + + with open('last-file.pickle', 'wb') as file: + pickle.dump(last_document_id, file) + return def planning_docs(starting_planning_document_id,batch,cursor): @@ -103,7 +107,7 @@ def planning_docs(starting_planning_document_id,batch,cursor): print(error.message) planning_review_count = cursor.fetchone()[0] - print('Count =', planning_review_count) + print('Planning_review count =', planning_review_count) # # Execute the SQL query to retrieve the BLOB data and key column cursor.execute(f""" @@ -120,7 +124,7 @@ def planning_docs(starting_planning_document_id,batch,cursor): last_planning_document_id = 0 try: - with tqdm(total=planning_review_count, unit="file", desc="Uploading files to S3") as pbar: + with tqdm(total=planning_review_count, unit="file", desc="Uploading planning_revie files to S3") as pbar: while True: # Fetch the next batch of BLOB data data = cursor.fetchmany(batch) @@ -139,7 +143,7 @@ def planning_docs(starting_planning_document_id,batch,cursor): except Exception as e: print("Something went wrong:",e) - print("Processed", documents_processed, "files") + print("Processed", documents_processed, "planning_review files") # Set resume status in case of interuption with open('last-planning-file.pickle', 'wb') as file: @@ -150,7 +154,11 @@ def planning_docs(starting_planning_document_id,batch,cursor): exit() # Display results - print("Process complete: Successfully migrated", documents_processed, "files.") + print("Process complete: Successfully migrated", documents_processed, "out of", planning_review_count, "planning_review files.") + + with open('last-planning-file.pickle', 'wb') as file: + pickle.dump(last_planning_document_id, file) + return def issue_docs(starting_issue_document_id,batch,cursor): @@ -162,7 +170,7 @@ def issue_docs(starting_issue_document_id,batch,cursor): print(error.message) issue_count = cursor.fetchone()[0] - print('Count =', issue_count) + print('Issue count =', issue_count) # # Execute the SQL query to retrieve the BLOB data and key column cursor.execute(f""" @@ -198,7 +206,7 @@ def issue_docs(starting_issue_document_id,batch,cursor): except Exception as e: print("Something went wrong:",e) - print("Processed", documents_processed, "files") + print("Processed", documents_processed, "issue files") # Set resume status in case of interuption with open('last-issue-file.pickle', 'wb') as file: @@ -209,7 +217,11 @@ def issue_docs(starting_issue_document_id,batch,cursor): exit() # Display results - print("Process complete: Successfully migrated", documents_processed, "files.") + print("Process complete: Successfully migrated", documents_processed, "out of", issue_count, "issue files.") + + with open('last-issue-file.pickle', 'wb') as file: + pickle.dump(last_issue_document_id, file) + return starting_document_id = 0 @@ -217,13 +229,14 @@ def issue_docs(starting_issue_document_id,batch,cursor): if os.path.isfile('last-file.pickle'): with open('last-file.pickle', 'rb') as file: starting_document_id = pickle.load(file) + print('Starting applications from:', starting_document_id) starting_planning_document_id = 0 # Determine job resume status if os.path.isfile('last-planning-file.pickle'): with open('last-planning-file.pickle', 'rb') as file: starting_planning_document_id = pickle.load(file) - print('Starting planning_review from:', starting_planning_document_id) + print('Starting planning_reviews from:', starting_planning_document_id) starting_issue_document_id = 0 # Determine job resume status @@ -237,16 +250,11 @@ def issue_docs(starting_issue_document_id,batch,cursor): # Set batch size batch_size = 10 -if starting_issue_document_id > 0: - issue_docs(starting_issue_document_id,batch_size,cursor) -elif starting_planning_document_id > 0: - planning_docs(starting_planning_document_id,batch_size,cursor) - issue_docs(0,batch_size,cursor) -else: - print('Starting applications from:', starting_document_id) - application_docs(starting_document_id,batch_size,cursor) - planning_docs(0,batch_size,cursor) - issue_docs(0,batch_size,cursor) +application_docs(starting_document_id,batch_size,cursor) +planning_docs(starting_planning_document_id,batch_size,cursor) +issue_docs(starting_issue_document_id,batch_size,cursor) + +print('File upload complete, closing connection') # Close the database cursor and connection cursor.close() From 572bb000f11e72bcf8181fcf2e0c0ffecf29713d Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 24 Jul 2023 15:40:41 -0700 Subject: [PATCH 125/954] Reduce Network Calls / Loads for Edit Submission * Only save/load the application when components are dirty * Remove logic to auto-save on every step navigation * Add logging to help debug stepper issues --- .../alcs-edit-submission.component.ts | 22 ++++---- .../edit-submission.component.html | 2 + .../edit-submission.component.ts | 20 +++---- .../land-use/land-use.component.ts | 33 +++++++----- .../other-parcels/other-parcels.component.ts | 52 ++++++++++--------- .../parcel-details.component.ts | 40 +++++++------- .../primary-contact.component.ts | 49 ++++++++++------- .../excl-proposal/excl-proposal.component.ts | 2 +- .../nfu-proposal/nfu-proposal.component.ts | 2 +- .../pfrs-proposal.component.html | 20 ++++++- .../pfrs-proposal/pfrs-proposal.component.ts | 7 ++- .../pofo-proposal.component.html | 12 ++++- .../pofo-proposal/pofo-proposal.component.ts | 7 ++- .../roso-proposal.component.html | 12 ++++- .../roso-proposal/roso-proposal.component.ts | 7 ++- .../subd-proposal/subd-proposal.component.ts | 2 +- .../tur-proposal/tur-proposal.component.ts | 2 +- .../select-government.component.ts | 20 ++++--- .../application-submission.service.ts | 6 ++- 19 files changed, 200 insertions(+), 117 deletions(-) diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts index 4c02b0faaf..5dd7fb0286 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts +++ b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts @@ -54,7 +54,7 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; - @ViewChild(OtherParcelsComponent) otherParcelsComponent!: OtherAttachmentsComponent; + @ViewChild(OtherParcelsComponent) otherParcelsComponent!: OtherParcelsComponent; @ViewChild(PrimaryContactComponent) primaryContactComponent!: PrimaryContactComponent; @ViewChild(SelectGovernmentComponent) selectGovernmentComponent!: SelectGovernmentComponent; @ViewChild(LandUseComponent) landUseComponent!: LandUseComponent; @@ -135,16 +135,18 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView } private async loadDraftSubmission(fileId: string) { - this.overlayService.showSpinner(); - this.applicationSubmission = await this.applicationSubmissionDraftService.getByFileId(fileId); - this.loadOriginalSubmission(fileId); - const documents = await this.applicationDocumentService.getByFileId(fileId); - if (documents) { - this.$applicationDocuments.next(documents); + if (!this.applicationSubmission) { + this.overlayService.showSpinner(); + this.applicationSubmission = await this.applicationSubmissionDraftService.getByFileId(fileId); + this.loadOriginalSubmission(fileId); + const documents = await this.applicationDocumentService.getByFileId(fileId); + if (documents) { + this.$applicationDocuments.next(documents); + } + this.fileId = fileId; + this.$applicationSubmission.next(this.applicationSubmission); + this.overlayService.hideSpinner(); } - this.fileId = fileId; - this.$applicationSubmission.next(this.applicationSubmission); - this.overlayService.hideSpinner(); } // this gets fired whenever applicant navigates away from edit page diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html index f484aff4db..3e7250145e 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.html @@ -41,6 +41,7 @@
[showErrors]="showValidationErrors" (navigateToStep)="onBeforeSwitchStep($event)" (componentInitialized)="onParcelDetailsInitialized()" + (exit)="onExit()" > @@ -51,6 +52,7 @@
[showErrors]="showValidationErrors" [$applicationSubmission]="$applicationSubmission" (navigateToStep)="onBeforeSwitchStep($event)" + (exit)="onExit()" > diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index 60790c333b..9c60d9f655 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -64,7 +64,7 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; - @ViewChild(OtherParcelsComponent) otherParcelsComponent!: OtherAttachmentsComponent; + @ViewChild(OtherParcelsComponent) otherParcelsComponent!: OtherParcelsComponent; @ViewChild(PrimaryContactComponent) primaryContactComponent!: PrimaryContactComponent; @ViewChild(SelectGovernmentComponent) selectGovernmentComponent!: SelectGovernmentComponent; @ViewChild(LandUseComponent) landUseComponent!: LandUseComponent; @@ -133,15 +133,17 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit } private async loadApplication(fileId: string) { - this.overlayService.showSpinner(); - this.applicationSubmission = await this.applicationSubmissionService.getByFileId(fileId); - const documents = await this.applicationDocumentService.getByFileId(fileId); - if (documents) { - this.$applicationDocuments.next(documents); + if (!this.applicationSubmission) { + this.overlayService.showSpinner(); + this.applicationSubmission = await this.applicationSubmissionService.getByFileId(fileId); + const documents = await this.applicationDocumentService.getByFileId(fileId); + if (documents) { + this.$applicationDocuments.next(documents); + } + this.fileId = fileId; + this.$applicationSubmission.next(this.applicationSubmission); + this.overlayService.hideSpinner(); } - this.fileId = fileId; - this.$applicationSubmission.next(this.applicationSubmission); - this.overlayService.hideSpinner(); } async onApplicationTypeChangeClicked() { diff --git a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts index ffe5cea54e..6701fb7145 100644 --- a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts +++ b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts @@ -92,20 +92,25 @@ export class LandUseComponent extends StepComponent implements OnInit, OnDestroy } async saveProgress() { - const formValues = this.landUseForm.getRawValue(); - await this.applicationService.updatePending(this.submissionUuid, { - parcelsAgricultureDescription: formValues.parcelsAgricultureDescription, - parcelsAgricultureImprovementDescription: formValues.parcelsAgricultureImprovementDescription, - parcelsNonAgricultureUseDescription: formValues.parcelsNonAgricultureUseDescription, - northLandUseType: formValues.northLandUseType, - northLandUseTypeDescription: formValues.northLandUseTypeDescription, - eastLandUseType: formValues.eastLandUseType, - eastLandUseTypeDescription: formValues.eastLandUseTypeDescription, - southLandUseType: formValues.southLandUseType, - southLandUseTypeDescription: formValues.southLandUseTypeDescription, - westLandUseType: formValues.westLandUseType, - westLandUseTypeDescription: formValues.westLandUseTypeDescription, - }); + if (this.landUseForm.dirty) { + const formValues = this.landUseForm.getRawValue(); + const updatedSubmission = await this.applicationService.updatePending(this.submissionUuid, { + parcelsAgricultureDescription: formValues.parcelsAgricultureDescription, + parcelsAgricultureImprovementDescription: formValues.parcelsAgricultureImprovementDescription, + parcelsNonAgricultureUseDescription: formValues.parcelsNonAgricultureUseDescription, + northLandUseType: formValues.northLandUseType, + northLandUseTypeDescription: formValues.northLandUseTypeDescription, + eastLandUseType: formValues.eastLandUseType, + eastLandUseTypeDescription: formValues.eastLandUseTypeDescription, + southLandUseType: formValues.southLandUseType, + southLandUseTypeDescription: formValues.southLandUseTypeDescription, + westLandUseType: formValues.westLandUseType, + westLandUseTypeDescription: formValues.westLandUseTypeDescription, + }); + if (updatedSubmission) { + this.$applicationSubmission.next(updatedSubmission); + } + } } async onSave() { diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts b/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts index b576c4b562..0af4fccb8a 100644 --- a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts +++ b/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts @@ -129,35 +129,37 @@ export class OtherParcelsComponent extends StepComponent implements OnInit, OnDe // replace placeholder uuid with the real one before saving await this.replacePlaceholderParcel(); - // delete all OTHER parcels if user answered 'NO' on 'Is there other parcels in the community' - if (!parseStringToBoolean(this.hasOtherParcelsInCommunity.getRawValue())) { - if (this.otherParcels.some((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL)) { - await this.applicationParcelService.deleteMany( - this.otherParcels.filter((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL).map((e) => e.uuid) - ); + if (this.parcelEntryChanged) { + // delete all OTHER parcels if user answered 'NO' on 'Is there other parcels in the community' + if (!parseStringToBoolean(this.hasOtherParcelsInCommunity.getRawValue())) { + if (this.otherParcels.some((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL)) { + await this.applicationParcelService.deleteMany( + this.otherParcels.filter((e) => e.uuid !== PLACE_HOLDER_UUID_FOR_INITIAL_PARCEL).map((e) => e.uuid) + ); + } + + return; } - return; - } + for (const parcel of this.otherParcels) { + parcelsToUpdate.push({ + uuid: parcel.uuid, + pid: parcel.pid?.toString() || null, + pin: parcel.pin?.toString() || null, + legalDescription: parcel.legalDescription, + civicAddress: parcel.civicAddress, + isFarm: parcel.isFarm, + purchasedDate: parcel.purchasedDate, + mapAreaHectares: parcel.mapAreaHectares, + ownershipTypeCode: parcel.ownershipTypeCode, + crownLandOwnerType: parcel.crownLandOwnerType, + isConfirmedByApplicant: false, + ownerUuids: parcel.owners.map((owner) => owner.uuid), + }); + } - for (const parcel of this.otherParcels) { - parcelsToUpdate.push({ - uuid: parcel.uuid, - pid: parcel.pid?.toString() || null, - pin: parcel.pin?.toString() || null, - legalDescription: parcel.legalDescription, - civicAddress: parcel.civicAddress, - isFarm: parcel.isFarm, - purchasedDate: parcel.purchasedDate, - mapAreaHectares: parcel.mapAreaHectares, - ownershipTypeCode: parcel.ownershipTypeCode, - crownLandOwnerType: parcel.crownLandOwnerType, - isConfirmedByApplicant: false, - ownerUuids: parcel.owners.map((owner) => owner.uuid), - }); + await this.applicationParcelService.update(parcelsToUpdate); } - - await this.applicationParcelService.update(parcelsToUpdate); } async onSave() { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts index dcc2ee3388..52fb6bbc48 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts +++ b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts @@ -31,6 +31,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft parcels: ApplicationParcelDto[] = []; $owners = new BehaviorSubject([]); newParcelAdded = false; + isDirty = false; constructor( private router: Router, @@ -93,6 +94,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft return; } + this.isDirty = true; parcel.pid = formData.pid !== undefined ? formData.pid : parcel.pid; parcel.pin = formData.pid !== undefined ? formData.pin : parcel.pin; parcel.civicAddress = formData.civicAddress !== undefined ? formData.civicAddress : parcel.civicAddress; @@ -113,25 +115,27 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft } private async saveProgress() { - const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; - for (const parcel of this.parcels) { - parcelsToUpdate.push({ - uuid: parcel.uuid, - pid: parcel.pid?.toString() || null, - pin: parcel.pin?.toString() || null, - civicAddress: parcel.civicAddress ?? null, - legalDescription: parcel.legalDescription, - isFarm: parcel.isFarm, - purchasedDate: parcel.purchasedDate, - mapAreaHectares: parcel.mapAreaHectares, - ownershipTypeCode: parcel.ownershipTypeCode, - isConfirmedByApplicant: parcel.isConfirmedByApplicant, - crownLandOwnerType: parcel.crownLandOwnerType, - ownerUuids: parcel.owners.map((owner) => owner.uuid), - }); + if (this.isDirty || this.newParcelAdded) { + const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; + for (const parcel of this.parcels) { + parcelsToUpdate.push({ + uuid: parcel.uuid, + pid: parcel.pid?.toString() || null, + pin: parcel.pin?.toString() || null, + civicAddress: parcel.civicAddress ?? null, + legalDescription: parcel.legalDescription, + isFarm: parcel.isFarm, + purchasedDate: parcel.purchasedDate, + mapAreaHectares: parcel.mapAreaHectares, + ownershipTypeCode: parcel.ownershipTypeCode, + isConfirmedByApplicant: parcel.isConfirmedByApplicant, + crownLandOwnerType: parcel.crownLandOwnerType, + ownerUuids: parcel.owners.map((owner) => owner.uuid), + }); + } + await this.applicationParcelService.update(parcelsToUpdate); + //TODO: Do we need to reload submission? } - - await this.applicationParcelService.update(parcelsToUpdate); } async onSave() { diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index 71d0d984a6..5993e5c9c3 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -31,6 +31,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni isCrownOwner = false; isLocalGovernmentUser = false; governmentName: string | undefined; + isDirty = false; firstName = new FormControl('', [Validators.required]); lastName = new FormControl('', [Validators.required]); @@ -94,6 +95,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } onSelectOwner(uuid: string) { + this.isDirty = true; this.selectedOwnerUuid = uuid; const selectedOwner = this.parcelOwners.find((owner) => owner.uuid === uuid); this.parcelOwners = this.parcelOwners.map((owner) => ({ @@ -152,29 +154,37 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } protected async save() { - let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( - (owner) => owner.uuid === this.selectedOwnerUuid - ); + if (this.isDirty || this.form.dirty) { + let selectedOwner: ApplicationOwnerDto | undefined = this.owners.find( + (owner) => owner.uuid === this.selectedOwnerUuid + ); - if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - organization: this.organizationName.getRawValue() ?? '', - firstName: this.firstName.getRawValue() ?? '', - lastName: this.lastName.getRawValue() ?? '', - email: this.email.getRawValue() ?? '', - phoneNumber: this.phoneNumber.getRawValue() ?? '', - ownerUuid: selectedOwner?.uuid, - type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, - }); - } else if (selectedOwner) { - await this.applicationOwnerService.setPrimaryContact({ - applicationSubmissionUuid: this.submissionUuid, - ownerUuid: selectedOwner.uuid, - }); + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + organization: this.organizationName.getRawValue() ?? '', + firstName: this.firstName.getRawValue() ?? '', + lastName: this.lastName.getRawValue() ?? '', + email: this.email.getRawValue() ?? '', + phoneNumber: this.phoneNumber.getRawValue() ?? '', + ownerUuid: selectedOwner?.uuid, + type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, + }); + } else if (selectedOwner) { + await this.applicationOwnerService.setPrimaryContact({ + applicationSubmissionUuid: this.submissionUuid, + ownerUuid: selectedOwner.uuid, + }); + } + await this.reloadApplication(); } } + private async reloadApplication() { + const application = await this.applicationService.getByUuid(this.submissionUuid); + this.$applicationSubmission.next(application); + } + private async loadOwners(submissionUuid: string, primaryContactOwnerUuid?: string) { const owners = await this.applicationOwnerService.fetchBySubmissionId(submissionUuid); if (owners) { @@ -215,6 +225,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni if (this.showErrors) { this.form.markAllAsTouched(); } + this.isDirty = false; } } diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts index aa6e46f03a..9205eb6159 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts @@ -91,7 +91,7 @@ export class ExclProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId) { + if (this.fileId && this.form.dirty) { const inclExclHectares = this.hectares.value; const purpose = this.purpose.value; const exclWhyLand = this.whyExclude.value; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts index c2f9e5573a..b063aff119 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts @@ -87,7 +87,7 @@ export class NfuProposalComponent extends StepComponent implements OnInit, OnDes } private async save() { - if (this.fileId) { + if (this.fileId && this.form.dirty) { const nfuHectares = this.hectares.getRawValue(); const purpose = this.purpose.getRawValue(); const nfuOutsideLands = this.outsideLands.getRawValue(); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html index f589f11490..c011f7ad47 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html @@ -148,9 +148,21 @@

Proposal

tableHeader2="Fill to be Placed" [(data)]="removalTableData" [(data2)]="fillTableData" + (dataChange)="markDirty()" + (data2Change)="markDirty()" + > + + - -
tableHeader2="Fill already Placed" [(data)]="alreadyRemovedTableData" [(data2)]="alreadyFilledTableData" + (dataChange)="markDirty()" + (data2Change)="markDirty()" > diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts index 042cafd6b4..a61ca5a1e3 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts @@ -35,6 +35,7 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, crossSections: ApplicationDocumentDto[] = []; reclamationPlan: ApplicationDocumentDto[] = []; noticeOfWork: ApplicationDocumentDto[] = []; + areComponentsDirty = false; isNOIFollowUp = new FormControl(null, [Validators.required]); NOIIDs = new FormControl({ value: null, disabled: true }, [Validators.required]); @@ -186,7 +187,7 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId) { + if (this.fileId && (this.form.dirty || this.areComponentsDirty)) { const isNOIFollowUp = this.isNOIFollowUp.getRawValue(); const soilNOIIDs = this.NOIIDs.getRawValue(); const hasALCAuthorization = this.hasALCAuthorization.getRawValue(); @@ -266,4 +267,8 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, onChangeNoticeOfWork(selectedValue: string) { this.requiresNoticeOfWork = selectedValue === 'true'; } + + markDirty() { + this.areComponentsDirty = true; + } } diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html index 4c0b01cccc..417b8a580a 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html @@ -142,14 +142,22 @@

Proposal

Please refer to Contact Us on the ALC website for more detail. - +
If no proposed fill has already been placed, please leave 0 in the fields below
- +
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts index 5536e09cec..1fac888d36 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts @@ -53,6 +53,7 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, }); private submissionUuid = ''; + private areComponentsDirty = false; fillTableData: SoilTableData = {}; alreadyFilledTableData: SoilTableData = {}; @@ -131,7 +132,7 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId) { + if (this.fileId && this.form.dirty) { const isNOIFollowUp = this.isNOIFollowUp.getRawValue(); const soilNOIIDs = this.NOIIDs.getRawValue(); const hasALCAuthorization = this.hasALCAuthorization.getRawValue(); @@ -186,4 +187,8 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, this.applicationIDs.setValue(null); } } + + markDirty() { + this.areComponentsDirty = true; + } } diff --git a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html index 60488655d4..1719a9f3fd 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html @@ -142,7 +142,11 @@

Proposal

Please refer to Contact Us on the ALC website for more detail. - +

removed.
If no proposed soil has already been removed, please leave 0 in the fields below
- +
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts index 216e12b954..e3934f7b86 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts @@ -38,6 +38,7 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, reduceNegativeImpacts = new FormControl(null, [Validators.required]); projectDurationAmount = new FormControl(null, [Validators.required]); projectDurationUnit = new FormControl(null, [Validators.required]); + areComponentsDirty = false; form = new FormGroup({ isNOIFollowUp: this.isNOIFollowUp, @@ -129,7 +130,7 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId) { + if (this.fileId && this.form.dirty) { const isNOIFollowUp = this.isNOIFollowUp.getRawValue(); const soilNOIIDs = this.NOIIDs.getRawValue(); const hasALCAuthorization = this.hasALCAuthorization.getRawValue(); @@ -182,4 +183,8 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, this.applicationIDs.setValue(null); } } + + markDirty() { + this.areComponentsDirty = true; + } } diff --git a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts index 6e73edff80..86b663dcd1 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts @@ -104,7 +104,7 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId) { + if (this.fileId && this.form.dirty) { const purpose = this.purpose.getRawValue(); const subdSuitability = this.suitability.getRawValue(); const subdAgricultureSupport = this.agriculturalSupport.getRawValue(); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts index 30b4b08fa0..f0f9a2e831 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts @@ -84,7 +84,7 @@ export class TurProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId) { + if (this.fileId && this.form.dirty) { const purpose = this.purpose.getRawValue(); const turOutsideLands = this.outsideLands.getRawValue(); const turAgriculturalActivities = this.agriculturalActivities.getRawValue(); diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts index 830d2a8ae5..9acdf56fd4 100644 --- a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts +++ b/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts @@ -26,6 +26,7 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, selectGovernmentUuid = ''; localGovernments: LocalGovernmentDto[] = []; filteredLocalGovernments!: Observable; + isDirty = false; form = new FormGroup({ localGovernment: this.localGovernment, @@ -58,6 +59,7 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, } onChange($event: MatAutocompleteSelectedEvent) { + this.isDirty = true; const localGovernmentName = $event.option.value; if (localGovernmentName) { const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); @@ -95,15 +97,19 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, } private async save() { - const localGovernmentName = this.localGovernment.getRawValue(); - if (localGovernmentName) { - const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + if (this.isDirty) { + const localGovernmentName = this.localGovernment.getRawValue(); + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); - if (localGovernment) { - await this.applicationSubmissionService.updatePending(this.submissionUuid, { - localGovernmentUuid: localGovernment.uuid, - }); + if (localGovernment) { + const res = await this.applicationSubmissionService.updatePending(this.submissionUuid, { + localGovernmentUuid: localGovernment.uuid, + }); + this.$applicationSubmission.next(res); + } } + this.isDirty = false; } } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index b87504f608..db0be19151 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -154,8 +154,10 @@ export class ApplicationSubmissionService { const applicationSubmission = await this.getOrFailByUuid(submissionUuid); applicationSubmission.applicant = updateDto.applicant; - applicationSubmission.purpose = - updateDto.purpose || applicationSubmission.purpose; + applicationSubmission.purpose = filterUndefined( + updateDto.purpose, + applicationSubmission.purpose, + ); applicationSubmission.typeCode = updateDto.typeCode || applicationSubmission.typeCode; applicationSubmission.localGovernmentUuid = updateDto.localGovernmentUuid; From 97ada8f6205f6622d9cb5e929ebcee7155ba33c7 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 25 Jul 2023 13:06:59 -0700 Subject: [PATCH 126/954] added detail to 'something went wrong' --- bin/migrate-files/migrate-files.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/migrate-files/migrate-files.py b/bin/migrate-files/migrate-files.py index da82ea0632..40e8e29676 100644 --- a/bin/migrate-files/migrate-files.py +++ b/bin/migrate-files/migrate-files.py @@ -79,7 +79,7 @@ def application_docs(starting_document_id,batch,cursor): documents_processed += 1 except Exception as e: - print("Something went wrong:",e) + print("Something went wrong with application document upload:",e) print("Processed", documents_processed, "application files") # Set resume status in case of interuption @@ -124,7 +124,7 @@ def planning_docs(starting_planning_document_id,batch,cursor): last_planning_document_id = 0 try: - with tqdm(total=planning_review_count, unit="file", desc="Uploading planning_revie files to S3") as pbar: + with tqdm(total=planning_review_count, unit="file", desc="Uploading planning_review files to S3") as pbar: while True: # Fetch the next batch of BLOB data data = cursor.fetchmany(batch) @@ -142,7 +142,7 @@ def planning_docs(starting_planning_document_id,batch,cursor): documents_processed += 1 except Exception as e: - print("Something went wrong:",e) + print("Something went wrong with planning_review document upload:",e) print("Processed", documents_processed, "planning_review files") # Set resume status in case of interuption @@ -187,7 +187,7 @@ def issue_docs(starting_issue_document_id,batch,cursor): last_issue_document_id = 0 try: - with tqdm(total=issue_count, unit="file", desc="Uploading files to S3") as pbar: + with tqdm(total=issue_count, unit="file", desc="Uploading issue files to S3") as pbar: while True: # Fetch the next batch of BLOB data data = cursor.fetchmany(batch) @@ -205,7 +205,7 @@ def issue_docs(starting_issue_document_id,batch,cursor): documents_processed += 1 except Exception as e: - print("Something went wrong:",e) + print("Something went wrong with issue document upload:",e) print("Processed", documents_processed, "issue files") # Set resume status in case of interuption From 3858bf95c83b4d16e15f29be2dfaf3481fb85a60 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Tue, 25 Jul 2023 14:53:08 -0700 Subject: [PATCH 127/954] Add Exclusion Step 8 Review & View section (#819) * Fix empty purpose update * Add exclusion proposal review section * Add missing field descriptions and update styling to match design * Fix dialog action buttons layout to be on the right --- .../application-details.component.html | 9 ++ .../application-details.module.ts | 2 + .../excl-details/excl-details.component.html | 86 +++++++++++++++++++ .../excl-details/excl-details.component.scss | 0 .../excl-details.component.spec.ts | 37 ++++++++ .../excl-details/excl-details.component.ts | 58 +++++++++++++ .../create-application-dialog.component.html | 2 +- .../excl-proposal.component.html | 13 ++- .../excl-proposal.component.scss | 4 + 9 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 portal-frontend/src/app/features/application-details/excl-details/excl-details.component.html create mode 100644 portal-frontend/src/app/features/application-details/excl-details/excl-details.component.scss create mode 100644 portal-frontend/src/app/features/application-details/excl-details/excl-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/application-details/excl-details/excl-details.component.ts diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/application-details/application-details.component.html index 2f955132ec..65529b59be 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/application-details/application-details.component.html @@ -291,6 +291,15 @@

6. Proposal

[applicationDocuments]="appDocuments" [updatedFields]="updatedFields" > +

7. Optional Documents

diff --git a/portal-frontend/src/app/features/application-details/application-details.module.ts b/portal-frontend/src/app/features/application-details/application-details.module.ts index 18e913017f..c92b3d1b1b 100644 --- a/portal-frontend/src/app/features/application-details/application-details.module.ts +++ b/portal-frontend/src/app/features/application-details/application-details.module.ts @@ -11,6 +11,7 @@ import { PofoDetailsComponent } from './pofo-details/pofo-details.component'; import { RosoDetailsComponent } from './roso-details/roso-details.component'; import { SubdDetailsComponent } from './subd-details/subd-details.component'; import { TurDetailsComponent } from './tur-details/tur-details.component'; +import { ExclDetailsComponent } from './excl-details/excl-details.component'; @NgModule({ declarations: [ @@ -23,6 +24,7 @@ import { TurDetailsComponent } from './tur-details/tur-details.component'; PofoDetailsComponent, PfrsDetailsComponent, NaruDetailsComponent, + ExclDetailsComponent, ], imports: [CommonModule, SharedModule, NgxMaskPipe], exports: [ApplicationDetailsComponent], diff --git a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.html b/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.html new file mode 100644 index 0000000000..808b49d80d --- /dev/null +++ b/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.html @@ -0,0 +1,86 @@ +
+
+ The governmental or prescribed public body that is applying to exclude land + +
+
+ {{ _applicationSubmission.prescribedBody }} +
+ +
+ How many hectares are you proposing to exclude? (hectares) + +
+
+ {{ _applicationSubmission.inclExclHectares }} + +
+ +
+ Does any land under application share a common property line with land in another Local or First Nation Government? + +
+
+ + {{ _applicationSubmission.exclShareGovernmentBorders ? 'Yes' : 'No' }} + + +
+ +
+ What is the purpose of the proposal? + +
+
+ {{ _applicationSubmission.purpose }} + +
+ +
+ Explain why you believe that the parcel(s) should be excluded from the ALR + +
+
+ {{ _applicationSubmission.exclWhyLand }} + +
+ +
Proposal Map / Site Plan
+ + +
Notice of Public Hearing (Advertisement)
+ + +
Proof of Signage
+ + +
Report of Public Hearing
+ + +
+ +
+
diff --git a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.scss b/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.spec.ts b/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.spec.ts new file mode 100644 index 0000000000..e021e28723 --- /dev/null +++ b/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.spec.ts @@ -0,0 +1,37 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DeepMocked } from '@golevelup/ts-jest'; + +import { ExclDetailsComponent } from './excl-details.component'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; + +describe('ExclDetailsComponent', () => { + let component: ExclDetailsComponent; + let fixture: ComponentFixture; + let mockAppDocumentService: DeepMocked; + let mockAppParcelService: DeepMocked; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ExclDetailsComponent], + providers: [ + { + provide: ApplicationDocumentService, + useValue: mockAppDocumentService, + }, + { + provide: ApplicationParcelService, + useValue: mockAppParcelService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ExclDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.ts b/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.ts new file mode 100644 index 0000000000..d7ea9ecdf9 --- /dev/null +++ b/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.ts @@ -0,0 +1,58 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; + +@Component({ + selector: 'app-excl-details', + templateUrl: './excl-details.component.html', + styleUrls: ['./excl-details.component.scss'], +}) +export class ExclDetailsComponent { + @Input() showErrors = true; + @Input() showEdit = true; + @Input() draftMode = false; + @Input() updatedFields: string[] = []; + + _applicationSubmission: ApplicationSubmissionDetailedDto | undefined; + + @Input() set applicationSubmission(applicationSubmission: ApplicationSubmissionDetailedDto | undefined) { + if (applicationSubmission) { + this._applicationSubmission = applicationSubmission; + } + } + + @Input() set applicationDocuments(documents: ApplicationDocumentDto[]) { + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.noticeOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_ADVERTISING + ); + this.proofOfSignage = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_SIGNAGE); + this.reportOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING + ); + } + + proposalMap: ApplicationDocumentDto[] = []; + noticeOfPublicHearing: ApplicationDocumentDto[] = []; + proofOfSignage: ApplicationDocumentDto[] = []; + reportOfPublicHearing: ApplicationDocumentDto[] = []; + + constructor(private router: Router, private applicationDocumentService: ApplicationDocumentService) {} + + async onEditSection(step: number) { + if (this.draftMode) { + await this.router.navigateByUrl( + `/alcs/application/${this._applicationSubmission?.fileNumber}/edit/${step}?errors=t` + ); + } else { + await this.router.navigateByUrl(`application/${this._applicationSubmission?.fileNumber}/edit/${step}?errors=t`); + } + } + + async openFile(uuid: string) { + const res = await this.applicationDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } +} diff --git a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html index 8b840722d3..5cfbd26303 100644 --- a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html +++ b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html @@ -88,7 +88,7 @@

-
+
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html index 0949656217..16e5546590 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html @@ -30,6 +30,7 @@

Proposal

+
Note: 0.01 ha is 100m2
Proposal

Does any land under application share a common property line with land in another Local or First Nation Government? +
+ If any property under application shares a common boundary with a neighbouring Local or First Nation Government, + then that government must be notified of the application and given the opportunity to comment. +
Proposal
+
+ Be clear and concise in describing the proposal. Include why you are applying for exclusion and what the + proposal will achieve. +
Example: 40 ha of grazing land fenced in 2010.
- Characters left: {{ 4000 - outsideLandsText.textLength }} + Characters left: {{ 4000 - improvementsText.textLength }}
warning
This field is required
@@ -118,9 +118,119 @@

Proposal

[isRequired]="true" >
+
+ +
+ The ALR General Regulation does not require the {{ governmentName }} to complete a public hearing if all + inclusion application parcel(s) are owned by the {{ governmentName }}. +
+ Please refer to Inclusion page on the ALC website for more information. + + Yes + No + +
+ warning +
This field is required
+
+
+
+

Notification and Public Hearing Requirements

+

+ A printed copy of the application will need to be used for notification. Please ensure all prior fields are complete + and correct before downloading the PDF of the application (Step 8 of this application form will flag outstanding + fields). +

+ + + + You will not be able to complete the remaining portion of the application until the notification and public hearing + process is complete. + +
+
+
+ +
+ Proof that notice of the application was provided in a form and manner acceptable to the Commission +
+ +
+
+ +
+ Proof that a sign, in a form and manner acceptable to the Commission, was posted on the land that is the + subject of the application +
+ +
+
+ +
Public hearing report and any other public comments received
+ +
+
+ +
+
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss index e69de29bb2..b435af700d 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss @@ -0,0 +1,9 @@ +@use '../../../../../styles/functions' as *; + +section { + margin-top: rem(32); +} + +.requirement-description { + margin: rem(8) 0 !important; +} diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts index a0b183b23d..bda063fcdc 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts @@ -1,8 +1,10 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; -import { DeepMocked } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; +import { UserDto } from '../../../../services/authentication/authentication.dto'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; import { InclProposalComponent } from './incl-proposal.component'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; @@ -14,8 +16,14 @@ describe('InclProposalComponent', () => { let fixture: ComponentFixture; let mockApplicationService: DeepMocked; let mockAppDocumentService: DeepMocked; + let mockAuthService: DeepMocked; beforeEach(async () => { + mockApplicationService = createMock(); + mockAppDocumentService = createMock(); + mockAuthService = createMock(); + mockAuthService.$currentProfile = new BehaviorSubject(undefined); + await TestBed.configureTestingModule({ providers: [ { @@ -26,6 +34,10 @@ describe('InclProposalComponent', () => { provide: ApplicationDocumentService, useValue: mockAppDocumentService, }, + { + provide: AuthenticationService, + useValue: mockAuthService, + }, { provide: MatDialog, useValue: {}, diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts index 6f320455a1..1716091ead 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts @@ -1,4 +1,8 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; +import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; import { FilesStepComponent } from '../../files-step.partial'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; @@ -19,13 +23,17 @@ import { ApplicationSubmissionUpdateDto } from '../../../../services/application }) export class InclProposalComponent extends FilesStepComponent implements OnInit, OnDestroy { DOCUMENT = DOCUMENT_TYPE; - currentStep = EditApplicationSteps.Proposal; + private submissionUuid = ''; + isGovernmentUser = false; + governmentName? = ''; + disableNotificationFileUploads = false; hectares = new FormControl(null, [Validators.required]); purpose = new FormControl(null, [Validators.required]); agSupport = new FormControl(null, [Validators.required]); improvements = new FormControl(null, [Validators.required]); + governmentOwnsAllParcels = new FormControl(undefined, [Validators.required]); form = new FormGroup({ hectares: this.hectares, @@ -33,11 +41,15 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, agSupport: this.agSupport, improvements: this.improvements, }); - private submissionUuid = ''; + proposalMap: ApplicationDocumentDto[] = []; + noticeOfPublicHearing: ApplicationDocumentDto[] = []; + proofOfSignage: ApplicationDocumentDto[] = []; + reportOfPublicHearing: ApplicationDocumentDto[] = []; constructor( private applicationSubmissionService: ApplicationSubmissionService, + private authenticationService: AuthenticationService, applicationDocumentService: ApplicationDocumentService, dialog: MatDialog ) { @@ -57,6 +69,13 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, improvements: applicationSubmission.inclImprovements, }); + if (applicationSubmission.inclGovernmentOwnsAllParcels !== null) { + this.governmentOwnsAllParcels.setValue( + formatBooleanToString(applicationSubmission.inclGovernmentOwnsAllParcels) + ); + this.disableNotificationFileUploads = applicationSubmission.inclGovernmentOwnsAllParcels; + } + if (this.showErrors) { this.form.markAllAsTouched(); } @@ -65,6 +84,23 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, this.$applicationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.noticeOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_ADVERTISING + ); + this.proofOfSignage = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_SIGNAGE); + this.reportOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING + ); + }); + + this.authenticationService.$currentProfile.pipe(takeUntil(this.$destroy)).subscribe((userProfile) => { + if (userProfile) { + this.isGovernmentUser = userProfile?.isLocalGovernment || userProfile?.isFirstNationGovernment; + this.governmentName = userProfile.government; + + // @ts-ignore Angular / Typescript hate dynamic controls + this.form.addControl('isLFNGOwnerOfAllParcels', this.governmentOwnsAllParcels); + } }); } @@ -78,16 +114,22 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, const purpose = this.purpose.value; const inclAgricultureSupport = this.agSupport.value; const inclImprovements = this.improvements.value; + const inclGovernmentOwnsAllParcels = this.governmentOwnsAllParcels.value; const updateDto: ApplicationSubmissionUpdateDto = { inclExclHectares: inclExclHectares ? parseFloat(inclExclHectares) : null, purpose, inclAgricultureSupport, inclImprovements, + inclGovernmentOwnsAllParcels: parseStringToBoolean(inclGovernmentOwnsAllParcels), }; const updatedApp = await this.applicationSubmissionService.updatePending(this.submissionUuid, updateDto); this.$applicationSubmission.next(updatedApp); } } + + onSelectLocalGovernmentParcelOwner($event: MatButtonToggleChange) { + this.disableNotificationFileUploads = $event.value === 'true'; + } } diff --git a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts index 29056823d8..c831b683f0 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts @@ -154,6 +154,7 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD inclAgricultureSupport: string | null; inclImprovements: string | null; exclShareGovernmentBorders: boolean | null; + inclGovernmentOwnsAllParcels: boolean | null; } export interface ApplicationSubmissionUpdateDto { @@ -259,4 +260,5 @@ export interface ApplicationSubmissionUpdateDto { inclAgricultureSupport?: string | null; inclImprovements?: string | null; exclShareGovernmentBorders?: boolean | null; + inclGovernmentOwnsAllParcels?: boolean | null; } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts index 2959014317..3f37b8f3d8 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts @@ -308,6 +308,9 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { @AutoMap(() => Boolean) exclShareGovernmentBorders: boolean | null; + + @AutoMap(() => Boolean) + inclGovernmentOwnsAllParcels?: boolean | null; } export class ApplicationSubmissionCreateDto { @@ -694,4 +697,8 @@ export class ApplicationSubmissionUpdateDto { @IsBoolean() @IsOptional() exclShareGovernmentBorders?: boolean | null; + + @IsBoolean() + @IsOptional() + inclGovernmentOwnsAllParcels?: boolean | null; } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts index 41c10c042e..38a3c29e61 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts @@ -694,6 +694,10 @@ export class ApplicationSubmission extends Base { @Column({ type: 'boolean', nullable: true }) exclShareGovernmentBorders: boolean | null; + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + inclGovernmentOwnsAllParcels: boolean | null; + //END SUBMISSION FIELDS @AutoMap(() => Application) diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index fff750c23b..7406abff08 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -969,6 +969,10 @@ export class ApplicationSubmissionService { updateDto.exclShareGovernmentBorders, applicationSubmission.exclShareGovernmentBorders, ); + applicationSubmission.inclGovernmentOwnsAllParcels = filterUndefined( + updateDto.inclGovernmentOwnsAllParcels, + applicationSubmission.inclGovernmentOwnsAllParcels, + ); } async listNaruSubtypes() { diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690414066995-add_government_incl_fields_to_submission.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690414066995-add_government_incl_fields_to_submission.ts new file mode 100644 index 0000000000..f4264e8c35 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690414066995-add_government_incl_fields_to_submission.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addGovernmentInclFieldsToSubmission1690414066995 + implements MigrationInterface +{ + name = 'addGovernmentInclFieldsToSubmission1690414066995'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "incl_government_owns_all_parcels" boolean`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "incl_government_owns_all_parcels"`, + ); + } +} From 68fa41ac8e66cabef4f182f4d6fc54027a43f923 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 26 Jul 2023 16:55:51 -0700 Subject: [PATCH 136/954] Code review feedback * Remove debuggers * DRY up function --- .../decision-component/subd/subd.component.ts | 1 - .../decision-component.component.ts | 1 - .../application-submission.service.ts | 29 +++++++------------ 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts index f18314215f..634656fa3e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts @@ -16,7 +16,6 @@ export class SubdComponent { const lots = this.component.subdApprovedLots; if (lots && this.component.uuid) { lots[i].alrArea = alrArea ? parseFloat(alrArea) : null; - debugger; await this.componentService.update(this.component.uuid, { uuid: this.component.uuid, subdApprovedLots: lots, diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts index 5d9829cc72..968dfb1857 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts @@ -243,7 +243,6 @@ export class DecisionComponentComponent implements OnInit { private patchSubdFields() { this.form.addControl('subdApprovedLots', this.subdApprovedLots); - debugger; this.subdApprovedLots.setValue(this.data.subdApprovedLots ?? null); } diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts index 4b7eb988d0..255473cab4 100644 --- a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts @@ -71,15 +71,7 @@ export class ApplicationSubmissionService { } async updateStatus(fileNumber: string, statusCode: SUBMISSION_STATUS) { - //Load submission without relations to prevent save from crazy cascading - const submission = await this.applicationSubmissionRepository.findOneOrFail( - { - where: { - fileNumber: fileNumber, - }, - }, - ); - + const submission = await this.loadBarebonesSubmission(fileNumber); await this.applicationSubmissionStatusService.setStatusDate( submission.uuid, statusCode, @@ -90,15 +82,7 @@ export class ApplicationSubmissionService { fileNumber: string, updateDto: AlcsApplicationSubmissionUpdateDto, ) { - //Load submission without relations to prevent save from crazy cascading - const submission = await this.applicationSubmissionRepository.findOneOrFail( - { - where: { - fileNumber: fileNumber, - }, - }, - ); - + const submission = await this.loadBarebonesSubmission(fileNumber); submission.subdProposedLots = filterUndefined( updateDto.subProposedLots, submission.subdProposedLots, @@ -106,4 +90,13 @@ export class ApplicationSubmissionService { await this.applicationSubmissionRepository.save(submission); } + + private loadBarebonesSubmission(fileNumber: string) { + //Load submission without relations to prevent save from crazy cascading + return this.applicationSubmissionRepository.findOneOrFail({ + where: { + fileNumber, + }, + }); + } } From 79ce2562baa217aca82dcbc2a5d241b93950327a Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Wed, 26 Jul 2023 17:16:19 -0700 Subject: [PATCH 137/954] Feature/alcs 875 part 2 (#823) new nfu-subtypes app-prep mapping fixes app-prep validation improvements --- .../nfu-input/nfu-input.component.ts | 2 +- .../application/proposal/nfu/nfu.component.ts | 116 +------- .../application/proposal/nfu/nfu.constants.ts | 139 ++++++++++ bin/.gitignore | 3 +- .../applications/app_prep.py | 75 +++--- .../application_prep_basic_validation.sql | 30 ++- bin/migrate-oats-data/common/__init__.py | 2 +- .../common/oats_application_code_values.py | 251 ++++++++++++++++++ .../common/oats_application_enum.py | 29 -- bin/migrate-oats-data/migrate.py | 31 +++ 10 files changed, 487 insertions(+), 191 deletions(-) create mode 100644 alcs-frontend/src/app/features/application/proposal/nfu/nfu.constants.ts create mode 100644 bin/migrate-oats-data/common/oats_application_code_values.py delete mode 100644 bin/migrate-oats-data/common/oats_application_enum.py diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component.ts index 0c165ca2a4..c0bd834ab5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { NFU_SUBTYPES_OPTIONS, NFU_TYPES_OPTIONS } from '../../../../../../proposal/nfu/nfu.component'; +import { NFU_SUBTYPES_OPTIONS, NFU_TYPES_OPTIONS } from '../../../../../../proposal/nfu/nfu.constants'; @Component({ selector: 'app-nfu-input', diff --git a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.ts b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.ts index fa6ec1a6ab..50dcd08a74 100644 --- a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.ts +++ b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.ts @@ -3,121 +3,7 @@ import { Subject, takeUntil } from 'rxjs'; import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; import { ApplicationDto, UpdateApplicationDto } from '../../../../services/application/application.dto'; import { ToastService } from '../../../../services/toast/toast.service'; - -// TODO move to code tables? -export const NFU_TYPES_OPTIONS = [ - { - label: 'Agricultural / Farm', - value: 'Agricultural / Farm', - }, - { - label: 'Civic / Institutional', - value: 'Civic / Institutional', - }, - { - label: 'Commercial / Retail', - value: 'Commercial / Retail', - }, - { - label: 'Industrial', - value: 'Industrial', - }, - { - label: 'Other', - value: 'Other', - }, - { - label: 'Recreational', - value: 'Recreational', - }, - { - label: 'Residential', - value: 'Residential', - }, - { - label: 'Transportation / Utilities', - value: 'Transportation / Utilities', - }, - { - label: 'Unused', - value: 'Unused', - }, -]; - -export const NFU_SUBTYPES_OPTIONS = [ - { - label: 'Alcohol Processing', - value: 'Alcohol Processing', - }, - { - label: 'Cement / Asphalt / Concrete Plants', - value: 'Cement / Asphalt / Concrete Plants', - }, - { - label: 'Commercial / Retail', - value: 'Commercial / Retail', - }, - { - label: 'Deposition / Fill (All Types)', - value: 'Deposition / Fill (All Types)', - }, - { - label: 'Energy Production', - value: 'Energy Production', - }, - { - label: 'Recreational', - value: 'Recreational', - }, - { - label: 'Food Processing (Non-Meat)', - value: 'Food Processing (Non-Meat)', - }, - { - label: 'Industrial - Other', - value: 'Industrial - Other', - }, - { - label: 'Logging Operations', - value: 'Logging Operations', - }, - { - label: 'Lumber Manufacturing and Re-Manufacturing', - value: 'Lumber Manufacturing and Re-Manufacturing', - }, - { - label: 'Meat and Fish Processing (+ Abattoir)', - value: 'Meat and Fish Processing (+ Abattoir)', - }, - { - label: 'Mining', - value: 'Mining', - }, - { - label: 'Miscellaneous Processing', - value: 'Miscellaneous Processing', - }, - { - label: 'Oil and Gas Activities', - value: 'Oil and Gas Activities', - }, - { - label: 'Sand & Gravel', - value: 'Sand & Gravel', - }, - { - label: 'Sawmill', - value: 'Sawmill', - }, - { - label: 'Storage and Warehouse Facilities (Indoor/Outdoor - Large Scale Structures)', - value: 'Storage and Warehouse Facilities (Indoor/Outdoor - Large Scale Structures)', - }, - { - label: 'Work Camps or Associated Use', - value: 'Work Camps or Associated Use', - }, -]; +import { NFU_SUBTYPES_OPTIONS, NFU_TYPES_OPTIONS } from './nfu.constants'; @Component({ selector: 'app-proposal-nfu', diff --git a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.constants.ts b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.constants.ts new file mode 100644 index 0000000000..850f513867 --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.constants.ts @@ -0,0 +1,139 @@ +export const NFU_SUBTYPES_OPTIONS = [ + { label: 'Accessory Buildings', value: 'Accessory Buildings' }, + { label: 'Additional Dwelling(s)', value: 'Additional Dwelling(s)' }, + { label: 'Additional Structures for Farm Help', value: 'Additional Structures for Farm Help' }, + { label: 'Agricultural Land Use Remnant', value: 'Agricultural Land Use Remnant' }, + { label: 'Agricultural Lease', value: 'Agricultural Lease' }, + { label: 'Agricultural Subdivision Remnant', value: 'Agricultural Subdivision Remnant' }, + { label: 'Airports and Aviation related', value: 'Airports and Aviation related' }, + { label: 'Alcohol Processing', value: 'Alcohol Processing' }, + { label: 'Alcohol Production Associated Uses', value: 'Alcohol Production Associated Uses' }, + { label: 'Animal Boarding and Services', value: 'Animal Boarding and Services' }, + { label: 'Auto Services', value: 'Auto Services' }, + { label: 'Beef', value: 'Beef' }, + { label: 'Campground (Private) & RV Park', value: 'Campground (Private) & RV Park' }, + { label: 'Cannabis Related Uses', value: 'Cannabis Related Uses' }, + { label: 'Care Facilities', value: 'Care Facilities' }, + { label: 'Cement/ Asphalt/Concrete Plants', value: 'Cement/ Asphalt/Concrete Plants' }, + { label: 'Cemeteries', value: 'Cemeteries' }, + { label: 'Churches & Bible Schools', value: 'Churches & Bible Schools' }, + { label: 'Civic Facilities and Buildings', value: 'Civic Facilities and Buildings' }, + { label: 'Civic - other', value: 'Civic - other' }, + { label: 'Commercial - other', value: 'Commercial - other' }, + { label: 'Composting', value: 'Composting' }, + { label: 'Dairy', value: 'Dairy' }, + { label: 'Deposition/Fill (All Types)', value: 'Deposition/Fill (All Types)' }, + { label: 'Electrical Power Distribution Systems', value: 'Electrical Power Distribution Systems' }, + { label: 'Electrical Power Facilities', value: 'Electrical Power Facilities' }, + { label: 'Energy Production', value: 'Energy Production' }, + { label: 'Energy Production', value: 'Energy Production' }, + { label: 'Events', value: 'Events' }, + { label: 'Exhibitions and Festivals', value: 'Exhibitions and Festivals' }, + { label: 'Farm Help Accommodation', value: 'Farm Help Accommodation' }, + { label: 'Fire Hall and associated uses', value: 'Fire Hall and associated uses' }, + { label: 'Food and Beverage Services', value: 'Food and Beverage Services' }, + { label: 'Food Processing (non-meat)', value: 'Food Processing (non-meat)' }, + { label: 'Gas and Other Distribution Pipelines', value: 'Gas and Other Distribution Pipelines' }, + { label: 'Golf Course', value: 'Golf Course' }, + { label: 'Grain & Forage', value: 'Grain & Forage' }, + { label: 'Greenhouses', value: 'Greenhouses' }, + { label: 'Hall/Lodge (private)_', value: 'Hall/Lodge (private)_' }, + { label: 'Hospitals, Health Centres (Incl Private)', value: 'Hospitals, Health Centres (Incl Private)' }, + { label: 'Industrial - other', value: 'Industrial - other' }, + { label: 'Land Use Remnant', value: 'Land Use Remnant' }, + { label: 'Lease', value: 'Lease' }, + { label: 'Livestock-Unspecified', value: 'Livestock-Unspecified' }, + { label: 'Logging Operations', value: 'Logging Operations' }, + { label: 'Lumber Manufacturing and Re-manufacturing', value: 'Lumber Manufacturing and Re-manufacturing' }, + { label: 'Meat and Fish Processing (+abattoir)', value: 'Meat and Fish Processing (+abattoir)' }, + { label: 'Mining', value: 'Mining' }, + { label: 'Misc. Agricultural Use', value: 'Misc. Agricultural Use' }, + { label: 'Miscellaneous Processing', value: 'Miscellaneous Processing' }, + { label: 'Mixed Ag Uses', value: 'Mixed Ag Uses' }, + { label: 'Mixed Uses', value: 'Mixed Uses' }, + { label: 'Mobile Home Park', value: 'Mobile Home Park' }, + { label: 'Multi Family-Apartments/Condominiums', value: 'Multi Family-Apartments/Condominiums' }, + { label: 'Office Building (Primary Use)', value: 'Office Building (Primary Use)' }, + { label: 'Oil and Gas Activities', value: 'Oil and Gas Activities' }, + { label: 'Other-Undefined', value: 'Other-Undefined' }, + { label: 'Other Uses', value: 'Other Uses' }, + { label: "Parks-All Types operated by Local Gov't", value: "Parks-All Types operated by Local Gov't" }, + { label: 'Parks & Playing Fields', value: 'Parks & Playing Fields' }, + { label: 'Pigs/Hogs', value: 'Pigs/Hogs' }, + { label: 'Poultry', value: 'Poultry' }, + { label: 'Public Transportation Facilities', value: 'Public Transportation Facilities' }, + { label: 'Railway', value: 'Railway' }, + { label: 'Recreational - other', value: 'Recreational - other' }, + { label: 'Research Facilities', value: 'Research Facilities' }, + { label: 'Residential - other', value: 'Residential - other' }, + { label: 'Roads', value: 'Roads' }, + { label: 'Sand & Gravel', value: 'Sand & Gravel' }, + { label: 'Sanitary Land Fills', value: 'Sanitary Land Fills' }, + { label: 'Sawmill', value: 'Sawmill' }, + { label: 'Schools & Universities', value: 'Schools & Universities' }, + { label: 'Sewage Treatment Facilities', value: 'Sewage Treatment Facilities' }, + { label: 'Sewer Distribution Systems', value: 'Sewer Distribution Systems' }, + { label: 'Shopping Centre', value: 'Shopping Centre' }, + { label: 'Small Fruits-Berries', value: 'Small Fruits-Berries' }, + { label: 'Sports Facilities - commercial', value: 'Sports Facilities - commercial' }, + { label: 'Sports Facilities - municipal', value: 'Sports Facilities - municipal' }, + { + label: 'Storage and Warehouse Facilities (Indoor/Outdoor- Large Scale Structures)', + value: 'Storage and Warehouse Facilities (Indoor/Outdoor- Large Scale Structures)', + }, + { label: 'Storage & Warehouse', value: 'Storage & Warehouse' }, + { label: 'Store (Retail - All Types)', value: 'Store (Retail - All Types)' }, + { label: 'Subdivision Special Categories', value: 'Subdivision Special Categories' }, + { label: 'Subdivision Special Categories (Lease)', value: 'Subdivision Special Categories (Lease)' }, + { label: 'Telephone and Telecommunications', value: 'Telephone and Telecommunications' }, + { label: 'Tourist Accommodations', value: 'Tourist Accommodations' }, + { label: 'Trails', value: 'Trails' }, + { label: 'Transportation - other', value: 'Transportation - other' }, + { label: 'Tree Fruits', value: 'Tree Fruits' }, + { label: 'Turf Farm', value: 'Turf Farm' }, + { label: 'Vegetable & Truck', value: 'Vegetable & Truck' }, + { label: 'Vineyard and Associated Uses', value: 'Vineyard and Associated Uses' }, + { label: 'Water Distribution Systems', value: 'Water Distribution Systems' }, + { label: 'Water or Sewer Distribution Systems (inactive)', value: 'Water or Sewer Distribution Systems (inactive)' }, + { label: 'Water Treatment Facilities', value: 'Water Treatment Facilities' }, + { label: 'Work Camps or Associated Use', value: 'Work Camps or Associated Use' }, +]; + +export const NFU_TYPES_OPTIONS = [ + { + label: 'Agricultural / Farm', + value: 'Agricultural / Farm', + }, + { + label: 'Civic / Institutional', + value: 'Civic / Institutional', + }, + { + label: 'Commercial / Retail', + value: 'Commercial / Retail', + }, + { + label: 'Industrial', + value: 'Industrial', + }, + { + label: 'Other', + value: 'Other', + }, + { + label: 'Recreational', + value: 'Recreational', + }, + { + label: 'Residential', + value: 'Residential', + }, + { + label: 'Transportation / Utilities', + value: 'Transportation / Utilities', + }, + { + label: 'Unused', + value: 'Unused', + }, +]; diff --git a/bin/.gitignore b/bin/.gitignore index 6d655a1e07..12cddf3cc0 100644 --- a/bin/.gitignore +++ b/bin/.gitignore @@ -1,4 +1,5 @@ .env out tmp -*.pickle \ No newline at end of file +*.pickle +*etl_log.txt \ No newline at end of file diff --git a/bin/migrate-oats-data/applications/app_prep.py b/bin/migrate-oats-data/applications/app_prep.py index 06fe243535..88bacb10a2 100644 --- a/bin/migrate-oats-data/applications/app_prep.py +++ b/bin/migrate-oats-data/applications/app_prep.py @@ -4,6 +4,7 @@ AlcsNfuSubTypeCode, OatsCapabilitySourceCode, OatsAgriCapabilityCodes, + OATS_NFU_SUBTYPES, AlcsAgCap, AlcsAgCapSource, log_end, @@ -20,29 +21,29 @@ class OatsToAlcsNfuTypes(Enum): - AGR = AlcsNfuTypeCode.Agricultural_Farm - CIV = AlcsNfuTypeCode.Civic_Institutional - COM = AlcsNfuTypeCode.Commercial_Retail - IND = AlcsNfuTypeCode.Industrial - OTH = AlcsNfuTypeCode.Other - REC = AlcsNfuTypeCode.Recreational - RES = AlcsNfuTypeCode.Residential - TRA = AlcsNfuTypeCode.Transportation_Utilities - UNU = AlcsNfuTypeCode.Unused + AGR = AlcsNfuTypeCode.Agricultural_Farm.value + CIV = AlcsNfuTypeCode.Civic_Institutional.value + COM = AlcsNfuTypeCode.Commercial_Retail.value + IND = AlcsNfuTypeCode.Industrial.value + OTH = AlcsNfuTypeCode.Other.value + REC = AlcsNfuTypeCode.Recreational.value + RES = AlcsNfuTypeCode.Residential.value + TRA = AlcsNfuTypeCode.Transportation_Utilities.value + UNU = AlcsNfuTypeCode.Unused.value class OatsToAlcsAgCapSource(Enum): - BCLI = AlcsAgCapSource.BCLI - CLI = AlcsAgCapSource.CLI - ONSI = AlcsAgCapSource.On_site + BCLI = AlcsAgCapSource.BCLI.value + CLI = AlcsAgCapSource.CLI.value + ONSI = AlcsAgCapSource.On_site.value class OatsToAlcsAgCap(Enum): - P = AlcsAgCap.Prime - PD = AlcsAgCap.Prime_Dominant - MIX = AlcsAgCap.Mixed_Prime_Secondary - S = AlcsAgCap.Secondary - U = AlcsAgCap.Unclassified + P = AlcsAgCap.Prime.value + PD = AlcsAgCap.Prime_Dominant.value + MIX = AlcsAgCap.Mixed_Prime_Secondary.value + S = AlcsAgCap.Secondary.value + U = AlcsAgCap.Unclassified.value @inject_conn_pool @@ -54,6 +55,7 @@ def process_alcs_application_prep_fields(conn=None, batch_size=BATCH_UPLOAD_SIZE conn (psycopg2.extensions.connection): PostgreSQL database connection. Provided by the decorator. batch_size (int): The number of items to process at once. Defaults to BATCH_UPLOAD_SIZE. """ + log_start(etl_name) with conn.cursor(cursor_factory=RealDictCursor) as cursor: with open( @@ -201,14 +203,14 @@ def prepare_app_prep_data(app_prep_raw_data_list): data = dict(row) data = map_basic_field(data) - if data["alr_change_code"] == ALRChangeCode.NFU: + if data["alr_change_code"] == ALRChangeCode.NFU.value: data = mapOatsToAlcsAppPrep(data) nfu_data_list.append(data) - elif data["alr_change_code"] == ALRChangeCode.NAR: + elif data["alr_change_code"] == ALRChangeCode.NAR.value: nar_data_list.append(data) - elif data["alr_change_code"] == ALRChangeCode.EXC: + elif data["alr_change_code"] == ALRChangeCode.EXC.value: exc_data_list.append(data) - elif data["alr_change_code"] == ALRChangeCode.INC: + elif data["alr_change_code"] == ALRChangeCode.INC.value: inc_data_list.append(data) else: other_data_list.append(data) @@ -217,14 +219,18 @@ def prepare_app_prep_data(app_prep_raw_data_list): def mapOatsToAlcsAppPrep(data): + oats_type_code = data["nonfarm_use_type_code"] + oats_subtype_code = data["nonfarm_use_subtype_code"] + if data["nonfarm_use_type_code"]: data["nonfarm_use_type_code"] = str( OatsToAlcsNfuTypes[data["nonfarm_use_type_code"]].value ) if data["nonfarm_use_subtype_code"]: data["nonfarm_use_subtype_code"] = map_oats_to_alcs_nfu_subtypes( - data["nonfarm_use_subtype_code"] + oats_type_code, oats_subtype_code ) + return data @@ -314,22 +320,15 @@ def get_update_query_for_other(): return query -def map_oats_to_alcs_nfu_subtypes(oats_code): - # TODO this is work in progress and will be finished later - if oats_code == 1: - return "Accessory Buildings" - if oats_code == 2: - return "Additional Dwelling(s)" - if oats_code == 3: - return "Additional Structures for Farm Help" - if oats_code == 4: - return "Agricultural Land Use Remnant" - if oats_code == 5: - return "Agricultural Lease" - if oats_code == 6: - return "Agricultural Subdivision Remnant" - - return "" +def map_oats_to_alcs_nfu_subtypes(nfu_type_code, nfu_subtype_code): + for dict_obj in OATS_NFU_SUBTYPES: + if str(dict_obj["type_key"]) == str(nfu_type_code) and str( + dict_obj["subtype_key"] + ) == str(nfu_subtype_code): + return dict_obj["value"] + + # Return None when no matching key found + return None def map_basic_field(data): diff --git a/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql b/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql index d0d720f259..c2523a58f3 100644 --- a/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql +++ b/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql @@ -14,33 +14,51 @@ oats_app_prep_data AS ( oaac.agri_cap_consultant, oaac.component_area, oaac.capability_source_code, - oaac.nonfarm_use_type_code, - oaac.nonfarm_use_subtype_code, oaac.nonfarm_use_end_date, oaac.rsdntl_use_type_code, oaac.rsdntl_use_end_date, oaa.staff_comment_observations, oaac.alr_change_code, - oaac.exclsn_app_type_code + oaac.exclsn_app_type_code, + oaac.nonfarm_use_type_code, + oaac.nonfarm_use_subtype_code, + onutc.description AS nonfarm_use_type_description, + onusc.description AS nonfarm_use_subtype_description, + -- ALCS has typos fixed and this is required for proper validation + CASE + WHEN onusc.description = 'Water Distribtion Systems' THEN 'Water Distribution Systems' + WHEN onusc.description = 'Tourist Accomodations' THEN 'Tourist Accommodations' + WHEN onusc.description = 'Office Buiding (Primary Use)' THEN 'Office Building (Primary Use)' + ELSE onusc.description + END AS mapped_nonfarm_use_subtype_description FROM appl_components_grouped acg JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = acg.alr_application_id JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = acg.alr_application_id + JOIN oats.oats_nonfarm_use_subtype_codes onusc ON onusc.nonfarm_use_subtype_code = oaac.nonfarm_use_subtype_code + AND oaac.nonfarm_use_type_code = onusc.nonfarm_use_type_code + JOIN oats.oats_nonfarm_use_type_codes onutc ON onutc.nonfarm_use_type_code = oaac.nonfarm_use_type_code ) -SELECT a.alr_area, +SELECT oapd.alr_application_id, + a.alr_area, a.ag_cap, a.ag_cap_source, a.ag_cap_map, a.ag_cap_consultant, a.staff_observations, + a.nfu_use_type, + a.nfu_use_sub_type, oapd.component_area, oapd.agri_capability_code, oapd.capability_source_code, oapd.agri_cap_map, oapd.agri_cap_consultant, - oapd.staff_comment_observations + oapd.staff_comment_observations, + oapd.nonfarm_use_type_description, + oapd.mapped_nonfarm_use_subtype_description FROM alcs.application a LEFT JOIN oats_app_prep_data AS oapd ON a.file_number = oapd.alr_application_id::TEXT WHERE a.alr_area != oapd.component_area OR a.ag_cap_map != oapd.agri_cap_map OR a.ag_cap_consultant != oapd.agri_cap_consultant - OR a.staff_observations != oapd.staff_comment_observations \ No newline at end of file + OR a.staff_observations != oapd.staff_comment_observations + OR a.nfu_use_sub_type != oapd.mapped_nonfarm_use_subtype_description \ No newline at end of file diff --git a/bin/migrate-oats-data/common/__init__.py b/bin/migrate-oats-data/common/__init__.py index 77c58f32c9..26c07e6d74 100644 --- a/bin/migrate-oats-data/common/__init__.py +++ b/bin/migrate-oats-data/common/__init__.py @@ -1,3 +1,3 @@ -from .oats_application_enum import * +from .oats_application_code_values import * from .alcs_application_enum import * from .etl_logger import log_start, log_end diff --git a/bin/migrate-oats-data/common/oats_application_code_values.py b/bin/migrate-oats-data/common/oats_application_code_values.py new file mode 100644 index 0000000000..da8b8139bc --- /dev/null +++ b/bin/migrate-oats-data/common/oats_application_code_values.py @@ -0,0 +1,251 @@ +from enum import Enum + + +class ALRChangeCode(Enum): + TUR = "TUR" # Transport, Utility, and Recreation + INC = "INC" # Inclusion + EXC = "EXC" # Exclusion + SDV = "SDV" # Subdivision + NFU = "NFU" # Non Farm Use + SCH = "SCH" # Extraction and Fill + EXT = "EXT" # Extraction + FILL = "FILL" # Fill + SRW = "SRW" # Notification of Statutory Right of Way + CSC = "CSC" # Conservation Covenant + NAR = "NAR" # Non-Adhering Residential Use + + +class OatsCapabilitySourceCode(Enum): + BCLI = "BCLI" + CLI = "CLI" + ONSI = "ONSI" + + +class OatsAgriCapabilityCodes(Enum): + P = "P" + PD = "PD" + MIX = "MIX" + S = "S" + U = "U" + +OATS_NFU_SUBTYPES = [ + {"type_key": "AGR", "subtype_key": "1", "value": "Accessory Buildings"}, + {"type_key": "RES", "subtype_key": "2", "value": "Additional Dwelling(s)"}, + { + "type_key": "AGR", + "subtype_key": "3", + "value": "Additional Structures for Farm Help", + }, + { + "type_key": "AGR", + "subtype_key": "4", + "value": "Agricultural Land Use Remnant", + }, + {"type_key": "AGR", "subtype_key": "5", "value": "Agricultural Lease"}, + { + "type_key": "AGR", + "subtype_key": "6", + "value": "Agricultural Subdivision Remnant", + }, + { + "type_key": "TRA", + "subtype_key": "7", + "value": "Airports and Aviation related", + }, + {"type_key": "IND", "subtype_key": "8", "value": "Alcohol Processing"}, + { + "type_key": "COM", + "subtype_key": "9", + "value": "Animal Boarding and Services", + }, + {"type_key": "COM", "subtype_key": "11", "value": "Auto Services"}, + {"type_key": "AGR", "subtype_key": "12", "value": "Beef"}, + { + "type_key": "COM", + "subtype_key": "13", + "value": "Campground (Private) & RV Park", + }, + {"type_key": "COM", "subtype_key": "14", "value": "Care Facilities"}, + { + "type_key": "IND", + "subtype_key": "15", + "value": "Cement/ Asphalt/Concrete Plants", + }, + {"type_key": "CIV", "subtype_key": "16", "value": "Cemeteries"}, + {"type_key": "CIV", "subtype_key": "17", "value": "Churches & Bible Schools"}, + {"type_key": "CIV", "subtype_key": "18", "value": "Civic - other"}, + { + "type_key": "CIV", + "subtype_key": "19", + "value": "Civic Facilities and Buildings", + }, + {"type_key": "COM", "subtype_key": "20", "value": "Commercial - other"}, + {"type_key": "IND", "subtype_key": "21", "value": "Composting"}, + {"type_key": "AGR", "subtype_key": "22", "value": "Dairy"}, + { + "type_key": "IND", + "subtype_key": "23", + "value": "Deposition/Fill (All Types)", + }, + {"type_key": "COM", "subtype_key": "24", "value": "Golf Course"}, + {"type_key": "AGR", "subtype_key": "25", "value": "Grain & Forage"}, + { + "type_key": "TRA", + "subtype_key": "25", + "value": "Electrical Power Distribution Systems", + }, + {"type_key": "AGR", "subtype_key": "26", "value": "Greenhouses"}, + { + "type_key": "TRA", + "subtype_key": "26", + "value": "Electrical Power Facilities", + }, + {"type_key": "COM", "subtype_key": "28", "value": "Exhibitions and Festivals"}, + { + "type_key": "RES", + "subtype_key": "29", + "value": "Subdivision Special Categories", + }, + {"type_key": "COM", "subtype_key": "30", "value": "Food and Beverage Services"}, + { + "type_key": "RES", + "subtype_key": "30", + "value": "Subdivision Special Categories (Lease)", + }, + {"type_key": "IND", "subtype_key": "31", "value": "Food Processing (non-meat)"}, + {"type_key": "IND", "subtype_key": "32", "value": "Industrial - other"}, + {"type_key": "AGR", "subtype_key": "33", "value": "Land Use Remnant"}, + {"type_key": "AGR", "subtype_key": "34", "value": "Livestock-Unspecified"}, + {"type_key": "IND", "subtype_key": "35", "value": "Logging Operations"}, + { + "type_key": "IND", + "subtype_key": "36", + "value": "Lumber Manufacturing and Re-manufacturing", + }, + { + "type_key": "IND", + "subtype_key": "37", + "value": "Meat and Fish Processing (+abattoir)", + }, + {"type_key": "IND", "subtype_key": "38", "value": "Mining"}, + {"type_key": "AGR", "subtype_key": "39", "value": "Misc. Agricultural Use"}, + {"type_key": "AGR", "subtype_key": "40", "value": "Mixed Ag Uses"}, + {"type_key": "OTH", "subtype_key": "41", "value": "Mixed Uses"}, + {"type_key": "RES", "subtype_key": "42", "value": "Mobile Home Park"}, + { + "type_key": "RES", + "subtype_key": "43", + "value": "Multi Family-Apartments/Condominiums", + }, + { + "type_key": "COM", + "subtype_key": "44", + "value": "Office Building (Primary Use)", + }, + {"type_key": "IND", "subtype_key": "45", "value": "Oil and Gas Activities"}, + {"type_key": "OTH", "subtype_key": "46", "value": "Other Uses"}, + {"type_key": "AGR", "subtype_key": "47", "value": "Other-Undefined"}, + {"type_key": "REC", "subtype_key": "48", "value": "Parks & Playing Fields"}, + { + "type_key": "CIV", + "subtype_key": "49", + "value": "Parks-All Types operated by Local Gov't", + }, + {"type_key": "AGR", "subtype_key": "50", "value": "Pigs/Hogs"}, + {"type_key": "AGR", "subtype_key": "51", "value": "Poultry"}, + { + "type_key": "TRA", + "subtype_key": "52", + "value": "Public Transportation Facilities", + }, + {"type_key": "TRA", "subtype_key": "53", "value": "Railway"}, + {"type_key": "REC", "subtype_key": "54", "value": "Recreational - other"}, + {"type_key": "CIV", "subtype_key": "55", "value": "Research Facilities"}, + {"type_key": "RES", "subtype_key": "56", "value": "Residential - other"}, + {"type_key": "TRA", "subtype_key": "57", "value": "Roads"}, + {"type_key": "IND", "subtype_key": "58", "value": "Sand & Gravel"}, + {"type_key": "CIV", "subtype_key": "59", "value": "Sanitary Land Fills"}, + {"type_key": "CIV", "subtype_key": "60", "value": "Schools & Universities"}, + { + "type_key": "TRA", + "subtype_key": "61", + "value": "Sewage Treatment Facilities", + }, + {"type_key": "TRA", "subtype_key": "62", "value": "Sewer Distribution Systems"}, + {"type_key": "COM", "subtype_key": "63", "value": "Shopping Centre"}, + {"type_key": "AGR", "subtype_key": "64", "value": "Small Fruits-Berries"}, + { + "type_key": "COM", + "subtype_key": "65", + "value": "Sports Facilities - commercial", + }, + { + "type_key": "REC", + "subtype_key": "66", + "value": "Sports Facilities - municipal", + }, + {"type_key": "COM", "subtype_key": "67", "value": "Storage & Warehouse"}, + {"type_key": "COM", "subtype_key": "68", "value": "Store (Retail - All Types)"}, + { + "type_key": "TRA", + "subtype_key": "69", + "value": "Telephone and Telecommunications", + }, + {"type_key": "COM", "subtype_key": "70", "value": "Tourist Accommodations"}, + {"type_key": "REC", "subtype_key": "71", "value": "Trails"}, + {"type_key": "TRA", "subtype_key": "72", "value": "Transportation - other"}, + {"type_key": "AGR", "subtype_key": "73", "value": "Tree Fruits"}, + {"type_key": "AGR", "subtype_key": "74", "value": "Turf Farm"}, + {"type_key": "AGR", "subtype_key": "75", "value": "Vegetable & Truck"}, + { + "type_key": "AGR", + "subtype_key": "76", + "value": "Vineyard and Associated Uses", + }, + {"type_key": "TRA", "subtype_key": "77", "value": "Water Distribution Systems"}, + { + "type_key": "TRA", + "subtype_key": "78", + "value": "Water or Sewer Distribution Systems (inactive)", + }, + {"type_key": "TRA", "subtype_key": "79", "value": "Water Treatment Facilities"}, + {"type_key": "COM", "subtype_key": "80", "value": "Hall/Lodge (private)_"}, + { + "type_key": "CIV", + "subtype_key": "81", + "value": "Hospitals, Health Centres (Incl Private)", + }, + {"type_key": "RES", "subtype_key": "82", "value": "Farm Help Accommodation"}, + { + "type_key": "TRA", + "subtype_key": "27", + "value": "Gas and Other Distribution Pipelines", + }, + {"type_key": "AGR", "subtype_key": "23", "value": "Energy Production"}, + {"type_key": "IND", "subtype_key": "25", "value": "Energy Production"}, + {"type_key": "RES", "subtype_key": "83", "value": "Lease"}, + { + "type_key": "IND", + "subtype_key": "86", + "value": "Storage and Warehouse Facilities (Indoor/Outdoor- Large Scale Structures)", + }, + { + "type_key": "IND", + "subtype_key": "84", + "value": "Work Camps or Associated Use", + }, + {"type_key": "IND", "subtype_key": "85", "value": "Miscellaneous Processing"}, + {"type_key": "COM", "subtype_key": "87", "value": "Events"}, + {"type_key": "IND", "subtype_key": "88", "value": "Sawmill"}, + { + "type_key": "CIV", + "subtype_key": "89", + "value": "Fire Hall and associated uses", + }, + { + "type_key": "AGR", + "subtype_key": "90", + "value": "Alcohol Production Associated Uses", + }, + {"type_key": "AGR", "subtype_key": "91", "value": "Cannabis Related Uses"}, + ] \ No newline at end of file diff --git a/bin/migrate-oats-data/common/oats_application_enum.py b/bin/migrate-oats-data/common/oats_application_enum.py deleted file mode 100644 index 1a88319a57..0000000000 --- a/bin/migrate-oats-data/common/oats_application_enum.py +++ /dev/null @@ -1,29 +0,0 @@ -from enum import Enum - - -class ALRChangeCode(Enum): - TUR = "TUR" # Transport, Utility, and Recreation - INC = "INC" # Inclusion - EXC = "EXC" # Exclusion - SDV = "SDV" # Subdivision - NFU = "NFU" # Non Farm Use - SCH = "SCH" # Extraction and Fill - EXT = "EXT" # Extraction - FILL = "FILL" # Fill - SRW = "SRW" # Notification of Statutory Right of Way - CSC = "CSC" # Conservation Covenant - NAR = "NAR" # Non-Adhering Residential Use - - -class OatsCapabilitySourceCode(Enum): - BCLI = "BCLI" - CLI = "CLI" - ONSI = "ONSI" - - -class OatsAgriCapabilityCodes(Enum): - P = "P" - PD = "PD" - MIX = "MIX" - S = "S" - U = "U" diff --git a/bin/migrate-oats-data/migrate.py b/bin/migrate-oats-data/migrate.py index 2c2d6748ee..86e7e4fa8b 100644 --- a/bin/migrate-oats-data/migrate.py +++ b/bin/migrate-oats-data/migrate.py @@ -17,6 +17,8 @@ import_batch_size = BATCH_UPLOAD_SIZE +# TODO tidy import menu setup + def application_import_command_parser(import_batch_size, subparsers): application_import_command = subparsers.add_parser( @@ -63,6 +65,21 @@ def application_document_import_command_parser(import_batch_size, subparsers): application_document_import_command.set_defaults(func=import_batch_size) +def app_prep_import_command_parser(import_batch_size, subparsers): + app_prep_import_command = subparsers.add_parser( + "app-prep-import", + help=f"Import App prep into ALCS (update applications table) in specified batch size: (default: {import_batch_size})", + ) + app_prep_import_command.add_argument( + "--batch-size", + type=int, + default=import_batch_size, + metavar="", + help=f"batch size (default: {import_batch_size})", + ) + app_prep_import_command.set_defaults(func=import_batch_size) + + def import_command_parser(subparsers): import_command = subparsers.add_parser( "import", @@ -86,6 +103,7 @@ def setup_menu_args_parser(import_batch_size): application_import_command_parser(import_batch_size, subparsers) document_import_command_parser(import_batch_size, subparsers) application_document_import_command_parser(import_batch_size, subparsers) + app_prep_import_command_parser(import_batch_size, subparsers) import_command_parser(subparsers) subparsers.add_parser("clean", help="Clean all imported data") @@ -167,6 +185,19 @@ def setup_menu_args_parser(import_batch_size): ) process_application_documents(batch_size=import_batch_size) + case "app-prep-import": + console.log("Beginning OATS -> ALCS app-prep import process") + with console.status( + "[bold green]App prep import (applications table update in ALCS)..." + ) as status: + if args.batch_size: + import_batch_size = args.batch_size + + console.log( + f"Processing app-prep import in batch size = {import_batch_size}" + ) + + process_alcs_application_prep_fields(batch_size=import_batch_size) finally: if connection_pool: From ba01273bb2b612cc7c87d4720db0d29104b5384d Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 26 Jul 2023 17:24:35 -0700 Subject: [PATCH 138/954] LFNG As applicant bug fixes * Check if submission is to own users government to control copying * Add error to step 8 when missing Department * Fix logic in primary contact to show errors at the right time --- .../application-details/application-details.component.html | 5 ++++- .../primary-contact/primary-contact.component.html | 2 +- .../primary-contact/primary-contact.component.ts | 7 ++----- .../application-submission-review.controller.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/application-details/application-details.component.html index 65529b59be..fc85a50f39 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/application-details/application-details.component.html @@ -48,7 +48,10 @@

3. Primary Contact

{{ primaryContact?.organizationName }} - +
Phone
diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html index dd83477975..8a32cf1e4b 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html @@ -160,7 +160,7 @@
Authorization Letters (if applicable)
(uploadFiles)="attachFile($event, DOCUMENT_TYPE.AUTHORIZATION_LETTER)" (deleteFile)="onDeleteFile($event)" (openFile)="openFile($event)" - [showErrors]="showErrors || selectedLocalGovernment" + [showErrors]="showErrors" [isRequired]="needsAuthorizationLetter" >
diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index 56f837e845..bd05e3c1a7 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -143,9 +143,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } private calculateLetterRequired() { - const isSelfApplicant = - this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || - this.owners[0].type.code === APPLICATION_OWNER.GOVERNMENT; + const isSelfApplicant = this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || this.selectedLocalGovernment; this.needsAuthorizationLetter = this.selectedThirdPartyAgent || @@ -226,8 +224,6 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni phoneNumber: selectedOwner.phoneNumber, email: selectedOwner.email, }); - - this.calculateLetterRequired(); } else if (selectedOwner) { this.onSelectOwner(selectedOwner.uuid); } else { @@ -246,6 +242,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.form.markAllAsTouched(); } this.isDirty = false; + this.calculateLetterRequired(); } } diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index 3448114556..16fa1c1ae8 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -205,8 +205,8 @@ export class ApplicationSubmissionReviewController { } if ( - primaryContact && - primaryContact.type.code === APPLICATION_OWNER.GOVERNMENT + userLocalGovernment.uuid === applicationSubmission.localGovernmentUuid && + primaryContact ) { //Copy contact details over to government form await this.applicationSubmissionReviewService.update( From 10bf32802afee0e509d9d2e1ff24baaf005d4241 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 27 Jul 2023 09:22:14 -0700 Subject: [PATCH 139/954] adding wip changes --- bin/migrate-oats-data/noi/noi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index 2d3d291e94..8084020e21 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -46,7 +46,7 @@ def process_nois(conn=None, batch_size=10000): try: applications_to_be_inserted_count = len(rows) - insert_query = compile_application_insert_query( + insert_query = noi_insert_query( applications_to_be_inserted_count ) cursor.execute(insert_query, rows) From 0dd5b0278b3e38f086f7a0f3dc41dc4767072767 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 27 Jul 2023 10:43:15 -0700 Subject: [PATCH 140/954] Bug fixes for L/FNG again --- .../edit-submission.component.ts | 4 +++ .../primary-contact.component.ts | 25 +++++++++++-------- .../application-submission.service.ts | 4 +-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index 17cc530fdd..527a09d860 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -94,6 +94,10 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit ngOnInit(): void { this.expandedParcelUuid = undefined; + + this.$applicationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + this.applicationSubmission = submission; + }); } ngAfterViewInit(): void { diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index bd05e3c1a7..fedf84f5fa 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -143,17 +143,20 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } private calculateLetterRequired() { - const isSelfApplicant = this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || this.selectedLocalGovernment; - - this.needsAuthorizationLetter = - this.selectedThirdPartyAgent || - !( - isSelfApplicant && - (this.owners.length === 1 || - (this.owners.length === 2 && - this.owners[1].type.code === APPLICATION_OWNER.AGENT && - !this.selectedThirdPartyAgent)) - ); + if (this.selectedLocalGovernment) { + this.needsAuthorizationLetter = false; + } else { + const isSelfApplicant = this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL; + this.needsAuthorizationLetter = + this.selectedThirdPartyAgent || + !( + isSelfApplicant && + (this.owners.length === 1 || + (this.owners.length === 2 && + this.owners[1].type.code === APPLICATION_OWNER.AGENT && + !this.selectedThirdPartyAgent)) + ); + } this.files = this.files.map((file) => ({ ...file, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index fff750c23b..c14b769569 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -77,9 +77,7 @@ export class ApplicationSubmissionService { fileNumber, isDraft: false, }, - relations: { - naruSubtype: true, - }, + relations: this.DEFAULT_RELATIONS, }); if (!application) { throw new Error('Failed to find document'); From 75abcde2126a268f266d8693b20bf20a36aaf136 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:48:54 -0700 Subject: [PATCH 141/954] nfu subtype with inline ng select (#826) --- .../proposal/nfu/nfu.component.html | 6 +- .../proposal/nfu/nfu.component.scss | 4 ++ .../inline-ng-select.component.html | 36 ++++++++++++ .../inline-ng-select.component.scss | 58 +++++++++++++++++++ .../inline-ng-select.component.spec.ts | 24 ++++++++ .../inline-ng-select.component.ts | 54 +++++++++++++++++ alcs-frontend/src/app/shared/shared.module.ts | 4 ++ 7 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.html create mode 100644 alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.scss create mode 100644 alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.ts diff --git a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html index 6b81c0bf65..5d270e97f9 100644 --- a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html +++ b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html @@ -6,13 +6,13 @@ (save)="updateApplicationValue('nfuUseType', $event)" > -
+
Non-Farm Use Sub-Type
- + >
Use End Date
diff --git a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.scss b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.scss index 229ba6c084..2f69c7774f 100644 --- a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.scss +++ b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.scss @@ -1,3 +1,7 @@ div { margin: 32px 0; } + +.nfu-subtype-wrapper { + width: 25%; +} diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.html new file mode 100644 index 0000000000..d49670474e --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.html @@ -0,0 +1,36 @@ +
+ + Select an option + Select option(s) + + {{ value }} + + + {{ coerceArray(value)?.join(', ') }} + + + +
+
+ +
+
+ + +
+
+
diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.scss new file mode 100644 index 0000000000..1eef4be4cd --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.scss @@ -0,0 +1,58 @@ +@use '../../../../styles/colors'; + +.inline-number-wrapper { + padding-top: 4px; +} + +.editing { + display: inline-block; + width: 100%; +} + +.editing.hidden { + display: none; +} + +.edit-button { + height: 24px; + width: 24px; + display: flex; + align-items: center; +} + +.edit-icon { + font-size: inherit; + line-height: 22px; +} + +.button-container { + display: flex; + justify-content: flex-end; + + button:not(:last-child) { + margin-right: 2px !important; + } +} + +.add { + cursor: pointer; +} + +.save { + color: colors.$primary-color; +} + +:host::ng-deep { + .mat-form-field-wrapper { + padding: 0 !important; + margin: 0 !important; + } + + button mat-icon { + overflow: visible; + } + + .mat-mdc-icon-button.mat-mdc-button-base { + padding: 0 !important; + } +} diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.spec.ts new file mode 100644 index 0000000000..7984b8d927 --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InlineNgSelectComponent } from './inline-ng-select.component'; + +describe('InlineDropdownComponent', () => { + let component: InlineNgSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [InlineNgSelectComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(InlineNgSelectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.ts new file mode 100644 index 0000000000..1d6e2c709b --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.ts @@ -0,0 +1,54 @@ +import { AfterContentChecked, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; + +@Component({ + selector: 'app-inline-ng-select[options][value]', + templateUrl: './inline-ng-select.component.html', + styleUrls: ['./inline-ng-select.component.scss'], +}) +export class InlineNgSelectComponent implements AfterContentChecked { + @Input() value?: string | string[] | undefined; + @Input() placeholder: string = 'Enter a value'; + @Input() options: { label: string; value: string }[] = []; + @Input() multiple = false; + + @Output() save = new EventEmitter(); + + @ViewChild('editInput') textInput!: ElementRef; + + isEditing = false; + pendingValue: undefined | string | string[]; + + constructor() {} + + startEdit() { + this.isEditing = true; + this.pendingValue = this.value; + } + + ngAfterContentChecked(): void { + if (this.textInput) { + this.textInput.nativeElement.focus(); + } + } + + confirmEdit() { + if (this.pendingValue !== this.value) { + this.save.emit(this.pendingValue ?? null); + this.value = this.pendingValue; + } + + this.isEditing = false; + } + + cancelEdit() { + this.isEditing = false; + this.pendingValue = this.value; + } + + coerceArray(value: string | string[] | undefined) { + if (value instanceof Array) { + return value; + } + return undefined; + } +} diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index d60257ce20..756471ca40 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -47,6 +47,7 @@ import { InlineNumberComponent } from './inline-editors/inline-number/inline-num import { InlineReviewOutcomeComponent } from './inline-editors/inline-review-outcome/inline-review-outcome.component'; import { InlineTextComponent } from './inline-editors/inline-text/inline-text.component'; import { InlineTextareaComponent } from './inline-editors/inline-textarea/inline-textarea.component'; +import { InlineNgSelectComponent } from './inline-editors/inline-ng-select/inline-ng-select.component'; import { LotsTableFormComponent } from './lots-table/lots-table-form.component'; import { MeetingOverviewComponent } from './meeting-overview/meeting-overview.component'; import { NoDataComponent } from './no-data/no-data.component'; @@ -94,6 +95,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen WarningBannerComponent, ErrorMessageComponent, LotsTableFormComponent, + InlineNgSelectComponent, ], imports: [ CommonModule, @@ -117,6 +119,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen RouterModule, MatDatepickerModule, MatDialogModule, + NgSelectModule, ], exports: [ CommonModule, @@ -150,6 +153,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen InlineNumberComponent, InlineTextComponent, InlineDropdownComponent, + InlineNgSelectComponent, MatAutocompleteModule, MatButtonToggleModule, DetailsHeaderComponent, From 40f0d48adcae6fa88cb5dab53fa6f12490f92f09 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 27 Jul 2023 11:46:55 -0700 Subject: [PATCH 142/954] Fix Styling for SUBD, Fix Decisions with no components or conditions * Bug fix for header change after checked --- .../decision-input-v2.component.ts | 11 ++++--- .../lots-table/lots-table-form.component.scss | 4 +++ .../lots-table/lots-table-form.component.ts | 30 +++++++++++-------- .../app/shared/header/header.component.html | 10 ++----- .../src/app/shared/header/header.component.ts | 13 ++++---- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index f2e92007fa..2bc38b496b 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -528,8 +528,8 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { private runValidation() { this.form.markAllAsTouched(); - this.componentsValid = this.componentsValid && this.components.length > 0; - this.conditionsValid = this.conditionsValid && this.conditionUpdates.length > 0; + const requiresConditions = this.showConditions; + const requiresComponents = this.showComponents; if (this.decisionComponentsComponent) { this.decisionComponentsComponent.onValidate(); @@ -543,8 +543,8 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { !this.form.valid || !this.conditionsValid || !this.componentsValid || - (this.components.length === 0 && this.showComponents) || - (this.conditionUpdates.length === 0 && this.showConditions) + (this.components.length === 0 && requiresComponents) || + (this.conditionUpdates.length === 0 && requiresConditions) ) { this.form.controls.decisionMaker.markAsDirty(); this.toastService.showErrorToast('Please correct all errors before submitting the form'); @@ -594,9 +594,8 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { } onConditionsChange($event: { conditions: UpdateApplicationDecisionConditionDto[]; isValid: boolean }) { - this.conditionUpdates = $event.conditions; - this.conditionsValid = $event.isValid; this.conditionUpdates = Array.from($event.conditions); + this.conditionsValid = $event.isValid; } onChangeDecisionOutcome(selectedOutcome: DecisionOutcomeCodeDto) { diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss index e69de29bb2..5aa07a5e2e 100644 --- a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss @@ -0,0 +1,4 @@ +fieldset { + border: none; + padding: 0; +} diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts index 9979b53925..c9a65c177e 100644 --- a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts @@ -33,7 +33,7 @@ type ProposedLot = { type: 'Lot' | 'Road Dedication' | null; size: string | null export class LotsTableFormComponent implements ControlValueAccessor, Validator { displayedColumns = ['index', 'type', 'size', 'alrArea']; lotsSource = new MatTableDataSource([]); - lotCount = 0; + lotCount: number | null = null; touched = false; disabled = false; @@ -99,12 +99,14 @@ export class LotsTableFormComponent implements ControlValueAccessor, Validator { this.disabled = disabled; } - writeValue(proposedLots: ProposedLot[]) { + writeValue(proposedLots: ProposedLot[] | null) { this.resetForm(proposedLots ?? []); - this.lotCount = proposedLots.length; + this.lotCount = proposedLots?.length ?? null; let lots = this.mapFormToLots(); this.lotsSource = new MatTableDataSource(lots); - this.count.setValue(this.lotCount.toString()); + if (this.lotCount !== null) { + this.count.setValue(this.lotCount.toString()); + } } validate(control: AbstractControl) { @@ -135,15 +137,17 @@ export class LotsTableFormComponent implements ControlValueAccessor, Validator { private mapFormToLots() { const proposedLots: ProposedLot[] = []; - for (let index = 0; index < this.lotCount; index++) { - const lotType = this.form.controls[`${index}-lotType`].value; - const lotSize = this.form.controls[`${index}-lotSize`].value; - const lotAlrArea = this.form.controls[`${index}-lotAlrArea`].value; - proposedLots.push({ - size: lotSize, - type: lotType, - alrArea: lotAlrArea, - }); + if (this.lotCount !== null) { + for (let index = 0; index < this.lotCount; index++) { + const lotType = this.form.controls[`${index}-lotType`].value; + const lotSize = this.form.controls[`${index}-lotSize`].value; + const lotAlrArea = this.form.controls[`${index}-lotAlrArea`].value; + proposedLots.push({ + size: lotSize, + type: lotType, + alrArea: lotAlrArea, + }); + } } return proposedLots; } diff --git a/portal-frontend/src/app/shared/header/header.component.html b/portal-frontend/src/app/shared/header/header.component.html index a6c5058f6c..acbc8394dc 100644 --- a/portal-frontend/src/app/shared/header/header.component.html +++ b/portal-frontend/src/app/shared/header/header.component.html @@ -9,15 +9,11 @@
Provincial Agricultural Land Commission Portal
- - + +
-
+

Portal Inbox

Log Out

diff --git a/portal-frontend/src/app/shared/header/header.component.ts b/portal-frontend/src/app/shared/header/header.component.ts index 79817826b0..8b86d4843d 100644 --- a/portal-frontend/src/app/shared/header/header.component.ts +++ b/portal-frontend/src/app/shared/header/header.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; import { AuthenticationService } from '../../services/authentication/authentication.service'; @@ -13,13 +13,16 @@ export class HeaderComponent implements OnInit, OnDestroy { isAuthenticated = false; isMenuOpen = false; - constructor(private authenticationService: AuthenticationService, private router: Router) {} + constructor( + private authenticationService: AuthenticationService, + private router: Router, + private changeDetectorRef: ChangeDetectorRef + ) {} ngOnInit(): void { this.authenticationService.$currentTokenUser.pipe(takeUntil(this.$destroy)).subscribe((user) => { - if (user) { - this.isAuthenticated = true; - } + this.isAuthenticated = !!user; + this.changeDetectorRef.detectChanges(); }); } From 9d1baccc086e91d2a4a561f14268ab8eeb64a311 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 27 Jul 2023 15:55:44 -0700 Subject: [PATCH 143/954] Load the full type and set it instead of just code * Setting just the code will not trump the actual value so saving a changed code doe not work --- .../application-decision-condition.service.spec.ts | 9 +++++++++ .../application-decision-condition.service.ts | 12 +++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts index 63b309e1d4..e1df986106 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; import { UpdateApplicationDecisionConditionDto } from './application-decision-condition.dto'; import { ApplicationDecisionCondition } from './application-decision-condition.entity'; import { ApplicationDecisionConditionService } from './application-decision-condition.service'; @@ -11,9 +12,13 @@ describe('ApplicationDecisionConditionService', () => { let mockApplicationDecisionConditionRepository: DeepMocked< Repository >; + let mockAppDecCondTypeRepository: DeepMocked< + Repository + >; beforeEach(async () => { mockApplicationDecisionConditionRepository = createMock(); + mockAppDecCondTypeRepository = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -22,6 +27,10 @@ describe('ApplicationDecisionConditionService', () => { provide: getRepositoryToken(ApplicationDecisionCondition), useValue: mockApplicationDecisionConditionRepository, }, + { + provide: getRepositoryToken(ApplicationDecisionConditionType), + useValue: mockAppDecCondTypeRepository, + }, ], }).compile(); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts index 7c71d98fed..ad9698e8a8 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; import { ApplicationDecisionComponent } from '../application-decision-v2/application-decision/component/application-decision-component.entity'; +import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; import { UpdateApplicationDecisionConditionDto, UpdateApplicationDecisionConditionServiceDto, @@ -14,6 +15,8 @@ export class ApplicationDecisionConditionService { constructor( @InjectRepository(ApplicationDecisionCondition) private repository: Repository, + @InjectRepository(ApplicationDecisionConditionType) + private typeRepository: Repository, ) {} async getOneOrFail(uuid: string) { @@ -41,7 +44,14 @@ export class ApplicationDecisionConditionService { } else { condition = new ApplicationDecisionCondition(); } - condition.typeCode = updateDto.type?.code ?? null; + if (updateDto.type?.code) { + condition.type = await this.typeRepository.findOneOrFail({ + where: { + code: updateDto.type.code, + }, + }); + } + condition.administrativeFee = updateDto.administrativeFee ?? null; condition.description = updateDto.description ?? null; condition.securityAmount = updateDto.securityAmount ?? null; From 34f9305e9b3309be11d17231a2931fc4b8977be7 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:00:56 -0700 Subject: [PATCH 144/954] Add Exclusion applications in ALCS and fix LFNG info (#829) * Add exclusion detail in alcs * Fix LFNG sections in ALCS and portal * Add excl and incl proposal to alcs edit submission * Init excl proposal component and add to app prep * Select and save inclExclApplicantType with inline selector * Update frontend tests * Clean up conditional view to boolean variable --- .../application-details.component.html | 6 ++ .../application-details.module.ts | 2 + .../excl-details/excl-details.component.html | 56 +++++++++++ .../excl-details/excl-details.component.scss | 0 .../excl-details.component.spec.ts | 31 ++++++ .../excl-details/excl-details.component.ts | 45 +++++++++ .../application/application.module.ts | 2 + .../lfng-info/lfng-info.component.html | 99 +++++++++++++++---- .../lfng-info/lfng-info.component.ts | 20 ++++ .../proposal/excl/excl.component.html | 7 ++ .../proposal/excl/excl.component.scss | 0 .../proposal/excl/excl.component.spec.ts | 42 ++++++++ .../proposal/excl/excl.component.ts | 44 +++++++++ .../proposal/proposal.component.html | 1 + .../application-dialog.component.spec.ts | 1 + .../covenant-dialog.component.spec.ts | 1 + .../notice-of-intent-dialog.component.spec.ts | 1 + .../planning-review-dialog.component.spec.ts | 1 + .../application-document.service.ts | 3 + .../application-local-government.dto.ts | 1 + .../services/application/application.dto.ts | 2 + .../application/application.service.spec.ts | 1 + .../inline-applicant-type.component.html | 23 +++++ .../inline-applicant-type.component.scss | 45 +++++++++ .../inline-applicant-type.component.spec.ts | 28 ++++++ .../inline-applicant-type.component.ts | 37 +++++++ alcs-frontend/src/app/shared/shared.module.ts | 3 + .../alcs-edit-submission.component.html | 16 +++ .../alcs-edit-submission.component.ts | 10 ++ .../edit-submission-base.module.ts | 2 + .../review-submit.component.html | 16 ++- .../lfng-review/lfng-review.component.html | 8 +- .../lfng-review/lfng-review.component.ts | 8 ++ .../application/application.controller.ts | 1 + .../src/alcs/application/application.dto.ts | 8 ++ .../alcs/application/application.entity.ts | 8 ++ ...incl_excl_applicant_type_to_application.ts | 23 +++++ 37 files changed, 573 insertions(+), 29 deletions(-) create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/excl-details/excl-details.component.html create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/excl-details/excl-details.component.scss create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/excl-details/excl-details.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/excl-details/excl-details.component.ts create mode 100644 alcs-frontend/src/app/features/application/proposal/excl/excl.component.html create mode 100644 alcs-frontend/src/app/features/application/proposal/excl/excl.component.scss create mode 100644 alcs-frontend/src/app/features/application/proposal/excl/excl.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/proposal/excl/excl.component.ts create mode 100644 alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.html create mode 100644 alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.scss create mode 100644 alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1690485189579-add_incl_excl_applicant_type_to_application.ts diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html index cda1a72718..0a5a4d1b62 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html @@ -172,6 +172,12 @@

Proposal

[applicationSubmission]="submission" [files]="files" > + +
+ +
+
+ + Land Owner + L/FNG Initiated + + +
+ + +
+
+
diff --git a/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.scss b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.scss new file mode 100644 index 0000000000..9b074fdc29 --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.scss @@ -0,0 +1,45 @@ +@use '../../../styles/colors'; + +.inline-applicant-type-wrapper { + padding-top: 4px; +} + +.editing.hidden { + display: none; +} + +.button-container { + button:not(:last-child) { + margin-right: 2px !important; + } + .save { + color: colors.$primary-color; + } +} + +.edit-button { + height: 24px; + width: 24px; + display: flex; + align-items: center; +} + +.edit-icon { + font-size: inherit; + line-height: 22px; +} + +:host::ng-deep { + .mat-form-field-wrapper { + padding: 0 !important; + margin: 0 !important; + } + + button mat-icon { + overflow: visible; + } + + .mat-mdc-icon-button.mat-mdc-button-base { + padding: 0 !important; + } +} diff --git a/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.spec.ts b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.spec.ts new file mode 100644 index 0000000000..b692b19be4 --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.spec.ts @@ -0,0 +1,28 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { SharedModule } from '../shared.module'; + +import { InlineApplicantTypeComponent } from './inline-applicant-type.component'; + +describe('InlineApplicantTypeComponent', () => { + let component: InlineApplicantTypeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SharedModule, FormsModule, ReactiveFormsModule, MatButtonToggleModule], + declarations: [InlineApplicantTypeComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(InlineApplicantTypeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.ts b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.ts new file mode 100644 index 0000000000..1c4e6e238f --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.ts @@ -0,0 +1,37 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-inline-applicant-type', + templateUrl: './inline-applicant-type.component.html', + styleUrls: ['./inline-applicant-type.component.scss'], +}) +export class InlineApplicantTypeComponent implements OnInit { + @Input() selectedValue?: string | null; + @Input() value?: string | null; + + @Output() save = new EventEmitter(); + + form!: FormGroup; + isEditing = false; + pendingApplicantType?: string | null; + + constructor(private fb: FormBuilder) { + this.pendingApplicantType = this.selectedValue; + } + + ngOnInit(): void { + this.form = this.fb.group({ + applicantType: this.value || this.selectedValue, + }); + } + + toggleEdit() { + this.isEditing = !this.isEditing; + } + + onSave() { + this.save.emit(this.form.get('applicantType')!.value); + this.isEditing = false; + } +} diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 756471ca40..36ea036d66 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -40,6 +40,7 @@ import { AvatarCircleComponent } from './avatar-circle/avatar-circle.component'; import { DetailsHeaderComponent } from './details-header/details-header.component'; import { ErrorMessageComponent } from './error-message/error-message.component'; import { FavoriteButtonComponent } from './favorite-button/favorite-button.component'; +import { InlineApplicantTypeComponent } from './inline-applicant-type/inline-applicant-type.component'; import { InlineBooleanComponent } from './inline-editors/inline-boolean/inline-boolean.component'; import { InlineDatepickerComponent } from './inline-editors/inline-datepicker/inline-datepicker.component'; import { InlineDropdownComponent } from './inline-editors/inline-dropdown/inline-dropdown.component'; @@ -72,6 +73,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen MomentPipe, StartOfDayPipe, MeetingOverviewComponent, + InlineApplicantTypeComponent, InlineTextareaComponent, InlineBooleanComponent, InlineNumberComponent, @@ -148,6 +150,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen MtxButtonModule, StartOfDayPipe, MatTooltipModule, + InlineApplicantTypeComponent, InlineTextareaComponent, InlineBooleanComponent, InlineNumberComponent, diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html index 406803dac5..74ccaa5382 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html +++ b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html @@ -137,6 +137,22 @@
(navigateToStep)="switchStep($event)" (exit)="onExit()" > + +
diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts index 5dd7fb0286..32486f33ba 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts +++ b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts @@ -29,6 +29,8 @@ import { TurProposalComponent } from '../edit-submission/proposal/tur-proposal/t import { SelectGovernmentComponent } from '../edit-submission/select-government/select-government.component'; import { ConfirmPublishDialogComponent } from './confirm-publish-dialog/confirm-publish-dialog.component'; import { scrollToElement } from '../../shared/utils/scroll-helper'; +import { ExclProposalComponent } from '../edit-submission/proposal/excl-proposal/excl-proposal.component'; +import { InclProposalComponent } from '../edit-submission/proposal/incl-proposal/incl-proposal.component'; @Component({ selector: 'app-alcs-edit-submission', @@ -64,6 +66,8 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView @ViewChild(RosoProposalComponent) rosoProposalComponent?: RosoProposalComponent; @ViewChild(PofoProposalComponent) profoProposalComponent?: PofoProposalComponent; @ViewChild(PfrsProposalComponent) pfrsProposalComponent?: PfrsProposalComponent; + @ViewChild(ExclProposalComponent) exclProposalComponent?: ExclProposalComponent; + @ViewChild(InclProposalComponent) inclProposalComponent?: InclProposalComponent; @ViewChild(OtherAttachmentsComponent) otherAttachmentsComponent!: OtherAttachmentsComponent; constructor( @@ -209,6 +213,12 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView if (this.pfrsProposalComponent) { await this.pfrsProposalComponent.onSave(); } + if (this.exclProposalComponent) { + await this.exclProposalComponent.onSave(); + } + if (this.inclProposalComponent) { + await this.inclProposalComponent.onSave(); + } } async switchStep(index: number) { diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts index acea5917c7..71801160f1 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts @@ -110,6 +110,8 @@ import { SelectGovernmentComponent } from './select-government/select-government PfrsProposalComponent, NaruProposalComponent, SoilTableComponent, + ExclProposalComponent, + InclProposalComponent, ], }) export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html b/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html index cb01882b58..70f9d717a5 100644 --- a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html +++ b/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html @@ -179,7 +179,11 @@

4. Resolution

The only option available to you is to forward this application on to the ALC.
Resolution for Application to Proceed to the ALC
@@ -191,7 +195,7 @@

4. Resolution

@@ -423,7 +427,11 @@

4. Resolution

The only option available to you is to forward this application on to the ALC.
Resolution for Application to Proceed to the ALC
@@ -435,7 +443,7 @@

4. Resolution

diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html index 5afec266c1..4a8902487a 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html +++ b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html @@ -224,13 +224,7 @@

Resolution

no authorizing resolution is required as per S. 25 (3) or S. 29 (4) of the ALC Act. The only option available is to forward this application on to the ALC.
-
+
Resolution for Application to Proceed to the ALC
{{ diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts index 8305093ae2..d893645219 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts +++ b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts @@ -34,6 +34,7 @@ export class LfngReviewComponent implements OnInit, OnDestroy { staffReport: ApplicationDocumentDto[] = []; resolutionDocument: ApplicationDocumentDto[] = []; governmentOtherAttachments: ApplicationDocumentDto[] = []; + hasCompletedStepsBeforeResolution = false; hasCompletedStepsBeforeDocuments = false; submittedToAlcStatus = false; @@ -49,6 +50,13 @@ export class LfngReviewComponent implements OnInit, OnDestroy { if (appReview) { this.applicationReview = appReview; + this.hasCompletedStepsBeforeResolution = + appReview.isFirstNationGovernment || + (!appReview.isFirstNationGovernment && + appReview.isOCPDesignation !== null && + appReview.isSubjectToZoning !== null && + (appReview.isOCPDesignation === true || appReview.isSubjectToZoning === true)); + this.hasCompletedStepsBeforeDocuments = (appReview.isAuthorized !== null && appReview.isFirstNationGovernment) || (appReview.isAuthorized !== null && diff --git a/services/apps/alcs/src/alcs/application/application.controller.ts b/services/apps/alcs/src/alcs/application/application.controller.ts index 0dc5422c0e..19532b1390 100644 --- a/services/apps/alcs/src/alcs/application/application.controller.ts +++ b/services/apps/alcs/src/alcs/application/application.controller.ts @@ -102,6 +102,7 @@ export class ApplicationController { proposalExpiryDate: formatIncomingDate(updates.proposalExpiryDate), nfuUseSubType: updates.nfuUseSubType, nfuUseType: updates.nfuUseType, + inclExclApplicantType: updates.inclExclApplicantType, staffObservations: updates.staffObservations, }, ); diff --git a/services/apps/alcs/src/alcs/application/application.dto.ts b/services/apps/alcs/src/alcs/application/application.dto.ts index e836e2a8fc..c756dda3c2 100644 --- a/services/apps/alcs/src/alcs/application/application.dto.ts +++ b/services/apps/alcs/src/alcs/application/application.dto.ts @@ -147,6 +147,10 @@ export class UpdateApplicationDto { @IsString() nfuUseSubType?: string; + @IsOptional() + @IsString() + inclExclApplicantType?: string; + @IsOptional() @IsNumber() proposalEndDate?: number; @@ -245,6 +249,9 @@ export class ApplicationDto { @AutoMap(() => String) nfuUseSubType?: string; + @AutoMap(() => String) + inclExclApplicantType?: string; + proposalEndDate?: number; proposalExpiryDate?: number; } @@ -271,6 +278,7 @@ export class ApplicationUpdateServiceDto { agCapConsultant?: string; nfuUseType?: string; nfuUseSubType?: string; + inclExclApplicantType?: string; proposalEndDate?: Date | null; proposalExpiryDate?: Date | null; staffObservations?: string | null; diff --git a/services/apps/alcs/src/alcs/application/application.entity.ts b/services/apps/alcs/src/alcs/application/application.entity.ts index ffe1c314c2..9955871366 100644 --- a/services/apps/alcs/src/alcs/application/application.entity.ts +++ b/services/apps/alcs/src/alcs/application/application.entity.ts @@ -226,6 +226,14 @@ export class Application extends Base { }) nfuUseSubType?: string | null; + @AutoMap(() => String) + @Column({ + type: 'text', + comment: 'Inclusion Exclusion Applicant Type', + nullable: true, + }) + inclExclApplicantType?: string | null; + @Column({ type: 'timestamptz', comment: 'The date at which the proposal use ends', diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690485189579-add_incl_excl_applicant_type_to_application.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690485189579-add_incl_excl_applicant_type_to_application.ts new file mode 100644 index 0000000000..7fce8cd0ea --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690485189579-add_incl_excl_applicant_type_to_application.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addInclExclApplicantTypeToApplication1690485189579 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application" ADD "incl_excl_applicant_type" text`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application"."incl_excl_applicant_type" IS 'Inclusion Exclusion Applicant Type'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application"."incl_excl_applicant_type" IS 'Inclusion Exclusion Applicant Type'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application" DROP COLUMN "incl_excl_applicant_type"`, + ); + } +} From e46318682148017d11c9746133a4c26c804338fe Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:25:29 -0700 Subject: [PATCH 145/954] no data for tables with units (#821) * no data for tables with units --- .../pfrs/pfrs.component.html | 88 +++++++++++++++---- .../pofo/pofo.component.html | 44 ++++++++-- .../roso/roso.component.html | 40 +++++++-- 3 files changed, 138 insertions(+), 34 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html index 076a4c03bf..6c9016cb7b 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html @@ -20,7 +20,9 @@
Type, origin and quality of fill proposed to be placed.
{{ component.soilFillTypeToPlace }}
- +
@@ -32,34 +34,86 @@ - - - - + + + - - - - + + + - - - - + + + - - - - + + +
Volume{{ component.soilToRemoveVolume }} m3{{ component.soilToPlaceVolume }} m3 + {{ component.soilToRemoveVolume }} m3 + + + {{ component.soilToPlaceVolume }} m3 + +
Area
Note: 0.01 ha is 100m2
{{ component.soilToRemoveArea }} ha{{ component.soilToPlaceArea }} ha + + {{ component.soilToRemoveArea }} ha + + + {{ component.soilToPlaceArea }} ha + +
Maximum Depth{{ component.soilToRemoveMaximumDepth }} m{{ component.soilToPlaceMaximumDepth }} m + {{ component.soilToRemoveMaximumDepth }} m + + + {{ component.soilToPlaceMaximumDepth }} m + +
Average Depth{{ component.soilToRemoveAverageDepth }} m{{ component.soilToPlaceAverageDepth }} m + {{ component.soilToRemoveAverageDepth }} m + + + + {{ component.soilToPlaceAverageDepth }} m + +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html index 9cda6ebd15..658d1947cf 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html @@ -15,7 +15,9 @@
Type, origin and quality of fill proposed to be placed.
{{ component.soilFillTypeToPlace }}
- +
@@ -26,26 +28,50 @@ - - + - - + - - + - - +
Volume{{ component.soilToPlaceVolume }} m3 + + {{ component.soilToPlaceVolume }} m3 + + +
Area
Note: 0.01 ha is 100m2
{{ component.soilToPlaceArea }} ha + + {{ component.soilToPlaceArea }} ha + + +
Maximum Depth{{ component.soilToPlaceMaximumDepth }} m + + {{ component.soilToPlaceMaximumDepth }} m + + +
Average Depth{{ component.soilToPlaceAverageDepth }} m + {{ component.soilToPlaceAverageDepth }} m + +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html index 14ae67a69d..732fd4cd46 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html @@ -26,23 +26,47 @@ Volume - {{ component.soilToRemoveVolume }} m3 - + + {{ component.soilToRemoveVolume }} m3 + + Area - {{ component.soilToRemoveArea }} ha - + + + {{ component.soilToRemoveArea }} ha + + + Maximum Depth - {{ component.soilToRemoveMaximumDepth }} m - + + {{ component.soilToRemoveMaximumDepth }} m + + Average Depth - {{ component.soilToRemoveAverageDepth }} m - + + {{ component.soilToRemoveAverageDepth }} m + + From 1ae87a12c224bc2d923c15063338fcf9c10ccfb6 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 26 Jul 2023 14:57:52 -0700 Subject: [PATCH 146/954] Add Subdivision Decision Components * Add Subdivision decision component with new lot-input-form component * Update Subdivision proposal component with new labels / fields * Add new DB field for storing decision component approved lots * Move inline component to their own folder to better organize shared --- .../subd/subd.component.html | 32 ++++ .../subd/subd.component.scss | 0 .../subd/subd.component.spec.ts | 24 +++ .../decision-component/subd/subd.component.ts | 11 ++ .../decision-component.component.html | 6 + .../decision-component.component.ts | 32 +++- .../subd-input/subd-input.component.html | 3 + .../subd-input/subd-input.component.scss | 10 ++ .../subd-input/subd-input.component.spec.ts | 24 +++ .../subd-input/subd-input.component.ts | 17 ++ .../decision-components.component.ts | 9 +- .../decision-v2/decision-v2.component.html | 6 + .../application/decision/decision.module.ts | 28 +++- .../proposal/subd/sub.dcomponent.scss | 2 +- .../proposal/subd/subd.component.html | 24 +-- .../proposal/subd/subd.component.ts | 18 +- .../application-submission.service.ts | 11 +- .../services/application/application.dto.ts | 5 + .../application-decision-v2.dto.ts | 9 +- .../inline-boolean.component.html | 0 .../inline-boolean.component.scss | 2 +- .../inline-boolean.component.spec.ts | 0 .../inline-boolean.component.ts | 0 .../inline-datepicker.component.html | 0 .../inline-datepicker.component.scss | 2 +- .../inline-datepicker.component.spec.ts | 2 +- .../inline-datepicker.component.ts | 4 +- .../inline-dropdown.component.html | 0 .../inline-dropdown.component.scss | 2 +- .../inline-dropdown.component.spec.ts | 0 .../inline-dropdown.component.ts | 0 .../inline-number.component.html | 0 .../inline-number.component.scss | 2 +- .../inline-number.component.spec.ts | 0 .../inline-number/inline-number.component.ts | 0 .../inline-review-outcome.component.html | 0 .../inline-review-outcome.component.scss | 2 +- .../inline-review-outcome.component.spec.ts | 2 +- .../inline-review-outcome.component.ts | 0 .../inline-text/inline-text.component.html | 0 .../inline-text/inline-text.component.scss | 2 +- .../inline-text/inline-text.component.spec.ts | 0 .../inline-text/inline-text.component.ts | 0 .../inline-textarea.component.html | 0 .../inline-textarea.component.scss | 2 +- .../inline-textarea.component.spec.ts | 0 .../inline-textarea.component.ts | 0 .../lots-table/lots-table-form.component.html | 85 ++++++++++ .../lots-table/lots-table-form.component.scss | 0 .../lots-table-form.component.spec.ts | 22 +++ .../lots-table/lots-table-form.component.ts | 157 ++++++++++++++++++ alcs-frontend/src/app/shared/shared.module.ts | 17 +- .../application-decision.dto.ts | 4 +- .../application-decision-component.dto.ts | 10 +- .../application-decision-component.entity.ts | 10 ++ .../application-decision-component.service.ts | 5 + .../application-submission.controller.ts | 14 ++ .../application-submission.dto.ts | 8 + .../application-submission.service.ts | 24 +++ .../application-submission.dto.ts | 1 + .../application-submission.entity.ts | 5 +- .../1690323153558-add_subd_dec_components.ts | 16 ++ .../1690406098396-add_subd_decision_lots.ts | 23 +++ 63 files changed, 652 insertions(+), 42 deletions(-) create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.scss create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.html create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.scss create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.ts rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-boolean/inline-boolean.component.html (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-boolean/inline-boolean.component.scss (95%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-boolean/inline-boolean.component.spec.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-boolean/inline-boolean.component.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-datepicker/inline-datepicker.component.html (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-datepicker/inline-datepicker.component.scss (95%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-datepicker/inline-datepicker.component.spec.ts (93%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-datepicker/inline-datepicker.component.ts (92%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-dropdown/inline-dropdown.component.html (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-dropdown/inline-dropdown.component.scss (95%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-dropdown/inline-dropdown.component.spec.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-dropdown/inline-dropdown.component.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-number/inline-number.component.html (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-number/inline-number.component.scss (95%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-number/inline-number.component.spec.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-number/inline-number.component.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-review-outcome/inline-review-outcome.component.html (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-review-outcome/inline-review-outcome.component.scss (83%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-review-outcome/inline-review-outcome.component.spec.ts (94%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-review-outcome/inline-review-outcome.component.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-text/inline-text.component.html (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-text/inline-text.component.scss (95%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-text/inline-text.component.spec.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-text/inline-text.component.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-textarea/inline-textarea.component.html (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-textarea/inline-textarea.component.scss (96%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-textarea/inline-textarea.component.spec.ts (100%) rename alcs-frontend/src/app/shared/{ => inline-editors}/inline-textarea/inline-textarea.component.ts (100%) create mode 100644 alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html create mode 100644 alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss create mode 100644 alcs-frontend/src/app/shared/lots-table/lots-table-form.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts create mode 100644 services/apps/alcs/src/alcs/application/application-submission/application-submission.dto.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1690323153558-add_subd_dec_components.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1690406098396-add_subd_decision_lots.ts diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html new file mode 100644 index 0000000000..f731dfa356 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html @@ -0,0 +1,32 @@ +
+ +
+
Total Number of Lots Approved
+ {{ component.subdApprovedLots?.length }} + +
+
+
+ + + + + + + + + + + + + + + +
#TypeSize (ha)ALR Area (ha)
{{ i + 1 }}{{ lot.type }}{{ lot.size }}{{ lot.alrArea }}
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts new file mode 100644 index 0000000000..5634294fcf --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; + +import { SubdComponent } from './subd.component'; + +describe('PfrsComponent', () => { + let component: SubdComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SubdComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SubdComponent); + component = fixture.componentInstance; + component.component = {} as DecisionComponentDto; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts new file mode 100644 index 0000000000..c197412739 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; + +@Component({ + selector: 'app-subd[component]', + templateUrl: './subd.component.html', + styleUrls: ['./subd.component.scss'], +}) +export class SubdComponent { + @Input() component!: DecisionComponentDto; +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html index 4cdc8a3f17..004bd7bfd4 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html @@ -96,4 +96,10 @@ [codes]="codes" >
+
+ +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts index ea748febec..5d9829cc72 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts @@ -1,5 +1,6 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { ProposedLot } from '../../../../../../../services/application/application.dto'; import { APPLICATION_DECISION_COMPONENT_TYPE, DecisionCodesDto, @@ -9,11 +10,13 @@ import { NfuDecisionComponentDto, PofoDecisionComponentDto, RosoDecisionComponentDto, + SubdDecisionComponentDto, TurpDecisionComponentDto, } from '../../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ToastService } from '../../../../../../../services/toast/toast.service'; import { formatDateForApi } from '../../../../../../../shared/utils/api-date-formatter'; import { AG_CAP_OPTIONS, AG_CAP_SOURCE_OPTIONS } from '../../../../../proposal/proposal.component'; +import { SubdInputComponent } from './subd-input/subd-input.component'; @Component({ selector: 'app-decision-component', @@ -25,6 +28,8 @@ export class DecisionComponentComponent implements OnInit { @Input() codes!: DecisionCodesDto; @Output() dataChange = new EventEmitter(); + @ViewChild(SubdInputComponent) subdInputComponent?: SubdInputComponent; + COMPONENT_TYPE = APPLICATION_DECISION_COMPONENT_TYPE; agCapOptions = AG_CAP_OPTIONS; @@ -56,6 +61,9 @@ export class DecisionComponentComponent implements OnInit { naruSubtypeCode = new FormControl(null, [Validators.required]); naruEndDate = new FormControl(null); + //subd + subdApprovedLots = new FormControl([], [Validators.required]); + // general alrArea = new FormControl(null, [Validators.required]); agCap = new FormControl(null, [Validators.required]); @@ -101,6 +109,9 @@ export class DecisionComponentComponent implements OnInit { case APPLICATION_DECISION_COMPONENT_TYPE.NARU: this.patchNaruFields(); break; + case APPLICATION_DECISION_COMPONENT_TYPE.SUBD: + this.patchSubdFields(); + break; default: this.toastService.showErrorToast('Wrong decision component type'); break; @@ -162,6 +173,9 @@ export class DecisionComponentComponent implements OnInit { case APPLICATION_DECISION_COMPONENT_TYPE.NARU: dataChange = { ...dataChange, ...this.getNaruDataChange() }; break; + case APPLICATION_DECISION_COMPONENT_TYPE.SUBD: + dataChange = { ...dataChange, ...this.getSubdDataChange() }; + break; default: this.toastService.showErrorToast('Wrong decision component type'); break; @@ -227,6 +241,12 @@ export class DecisionComponentComponent implements OnInit { this.naruSubtypeCode.setValue(this.data.naruSubtypeCode ?? null); } + private patchSubdFields() { + this.form.addControl('subdApprovedLots', this.subdApprovedLots); + debugger; + this.subdApprovedLots.setValue(this.data.subdApprovedLots ?? null); + } + private getNfuDataChange(): NfuDecisionComponentDto { return { nfuType: this.nfuType.value ? this.nfuType.value : null, @@ -286,4 +306,14 @@ export class DecisionComponentComponent implements OnInit { naruSubtypeCode: this.naruSubtypeCode.value ?? null, }; } + + private getSubdDataChange(): SubdDecisionComponentDto { + return { + subdApprovedLots: this.subdApprovedLots.value ?? undefined, + }; + } + + markTouched() { + this.subdInputComponent?.markAllAsTouched(); + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.html new file mode 100644 index 0000000000..84d52339b1 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.scss new file mode 100644 index 0000000000..4b9601d65d --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.scss @@ -0,0 +1,10 @@ + +:host::ng-deep { + .mat-mdc-form-field { + width: 100%; + } + + .mat-mdc-table .mdc-data-table__row { + height: 78px; + } +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.spec.ts new file mode 100644 index 0000000000..d8f14022eb --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SubdInputComponent } from './subd-input.component'; + +describe('RosoInputComponent', () => { + let component: SubdInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SubdInputComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SubdInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.ts new file mode 100644 index 0000000000..63bd2c5206 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, ViewChild } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { LotsTableFormComponent } from '../../../../../../../../shared/lots-table/lots-table-form.component'; + +@Component({ + selector: 'app-subd-input', + templateUrl: './subd-input.component.html', + styleUrls: ['./subd-input.component.scss'], +}) +export class SubdInputComponent { + @Input() form!: FormGroup; + @ViewChild(LotsTableFormComponent) lotsTableFormComponent?: LotsTableFormComponent; + + markAllAsTouched() { + this.lotsTableFormComponent?.markTouched(); + } +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index 8fa1564740..19b14d17fc 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -116,6 +116,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView applicationDecisionComponentType: this.decisionComponentTypes.find( (e) => e.code === typeCode && e.uiCode !== 'COPY' ), + subdApprovedLots: this.application.submittedApplication?.subdProposedLots, }; if (typeCode === APPLICATION_DECISION_COMPONENT_TYPE.NFUP) { @@ -151,6 +152,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView case APPLICATION_DECISION_COMPONENT_TYPE.ROSO: case APPLICATION_DECISION_COMPONENT_TYPE.PFRS: case APPLICATION_DECISION_COMPONENT_TYPE.NARU: + case APPLICATION_DECISION_COMPONENT_TYPE.SUBD: this.components.push({ applicationDecisionComponentTypeCode: typeCode, applicationDecisionComponentType: this.decisionComponentTypes.find( @@ -228,6 +230,11 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView } onValidate() { - this.childComponents.forEach((component) => component.form.markAllAsTouched()); + this.childComponents.forEach((component) => { + component.form.markAllAsTouched(); + if ('markTouched' in component) { + component.markTouched(); + } + }); } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html index 1e62004cb3..1cc2816ba7 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html @@ -255,6 +255,12 @@
Decision Components and Conditions
*ngIf="component.applicationDecisionComponentTypeCode === COMPONENT_TYPE.NARU" [component]="component" > + +
diff --git a/alcs-frontend/src/app/features/application/decision/decision.module.ts b/alcs-frontend/src/app/features/application/decision/decision.module.ts index ad64698c9b..adf3641992 100644 --- a/alcs-frontend/src/app/features/application/decision/decision.module.ts +++ b/alcs-frontend/src/app/features/application/decision/decision.module.ts @@ -1,6 +1,14 @@ +import { NgIf } from '@angular/common'; import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatOptionModule } from '@angular/material/core'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { RouterModule } from '@angular/router'; +import { NgxMaskDirective } from 'ngx-mask'; import { SharedModule } from '../../../shared/shared.module'; import { ConditionComponent } from './conditions/condition/condition.component'; import { ConditionsComponent } from './conditions/conditions.component'; @@ -11,6 +19,7 @@ import { NfupComponent } from './decision-v2/decision-component/nfup/nfup.compon import { PfrsComponent } from './decision-v2/decision-component/pfrs/pfrs.component'; import { PofoComponent } from './decision-v2/decision-component/pofo/pofo.component'; import { RosoComponent } from './decision-v2/decision-component/roso/roso.component'; +import { SubdComponent } from './decision-v2/decision-component/subd/subd.component'; import { TurpComponent } from './decision-v2/decision-component/turp/turp.component'; import { DecisionDocumentsComponent } from './decision-v2/decision-documents/decision-documents.component'; import { DecisionComponentComponent } from './decision-v2/decision-input/decision-components/decision-component/decision-component.component'; @@ -19,6 +28,7 @@ import { NfuInputComponent } from './decision-v2/decision-input/decision-compone import { PfrsInputComponent } from './decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component'; import { PofoInputComponent } from './decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component'; import { RosoInputComponent } from './decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component'; +import { SubdInputComponent } from './decision-v2/decision-input/decision-components/decision-component/subd-input/subd-input.component'; import { TurpInputComponent } from './decision-v2/decision-input/decision-components/decision-component/turp-input/turp-input.component'; import { DecisionComponentsComponent } from './decision-v2/decision-input/decision-components/decision-components.component'; import { DecisionConditionComponent } from './decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component'; @@ -82,13 +92,29 @@ export const decisionChildRoutes = [ RosoComponent, RosoInputComponent, PfrsInputComponent, + SubdInputComponent, PfrsComponent, NaruComponent, + SubdComponent, NaruInputComponent, ConditionsComponent, ConditionComponent, BasicComponent, ], - imports: [SharedModule.forRoot(), RouterModule.forChild(decisionChildRoutes), MatTabsModule], + imports: [ + SharedModule.forRoot(), + RouterModule.forChild(decisionChildRoutes), + MatTabsModule, + MatFormFieldModule, + MatInputModule, + MatOptionModule, + MatSelectModule, + MatTableModule, + NgIf, + NgxMaskDirective, + ReactiveFormsModule, + MatInputModule, + MatFormFieldModule, + ], }) export class DecisionModule {} diff --git a/alcs-frontend/src/app/features/application/proposal/subd/sub.dcomponent.scss b/alcs-frontend/src/app/features/application/proposal/subd/sub.dcomponent.scss index deb66eaa6f..9fe6dba7ef 100644 --- a/alcs-frontend/src/app/features/application/proposal/subd/sub.dcomponent.scss +++ b/alcs-frontend/src/app/features/application/proposal/subd/sub.dcomponent.scss @@ -1,6 +1,6 @@ .parcel-table { display: grid; - grid-template-columns: max-content max-content max-content; + grid-template-columns: max-content max-content max-content max-content; grid-column-gap: 36px; grid-row-gap: 12px; } diff --git a/alcs-frontend/src/app/features/application/proposal/subd/subd.component.html b/alcs-frontend/src/app/features/application/proposal/subd/subd.component.html index 2a73f6f527..cdf92af9a2 100644 --- a/alcs-frontend/src/app/features/application/proposal/subd/subd.component.html +++ b/alcs-frontend/src/app/features/application/proposal/subd/subd.component.html @@ -1,19 +1,23 @@ -
-
Proposed Lot Areas
-
-
#
-
Type
-
Size
- -
+
+
Proposed Lot Areas
+
+
#
+
Type
+
Size (ha)
+
ALR Area (ha)
+ +
{{ i + 1 }}
-
+
{{ lot.type }}
-
+
{{ lot.size }}
+
+ +
diff --git a/alcs-frontend/src/app/features/application/proposal/subd/subd.component.ts b/alcs-frontend/src/app/features/application/proposal/subd/subd.component.ts index f1a1e3a7f8..981b75da43 100644 --- a/alcs-frontend/src/app/features/application/proposal/subd/subd.component.ts +++ b/alcs-frontend/src/app/features/application/proposal/subd/subd.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subject, takeUntil } from 'rxjs'; import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; import { ApplicationSubmissionService } from '../../../../services/application/application-submission/application-submission.service'; -import { ApplicationSubmissionDto } from '../../../../services/application/application.dto'; +import { ApplicationSubmissionDto, ProposedLot } from '../../../../services/application/application.dto'; @Component({ selector: 'app-proposal-subd', @@ -11,7 +11,8 @@ import { ApplicationSubmissionDto } from '../../../../services/application/appli }) export class SubdProposalComponent implements OnInit, OnDestroy { $destroy = new Subject(); - applicationSubmission: ApplicationSubmissionDto | undefined; + proposedLots: ProposedLot[] = []; + private fileNumber: string | undefined; constructor( private applicationDetailService: ApplicationDetailService, @@ -21,17 +22,28 @@ export class SubdProposalComponent implements OnInit, OnDestroy { ngOnInit(): void { this.applicationDetailService.$application.pipe(takeUntil(this.$destroy)).subscribe((application) => { if (application) { + this.fileNumber = application.fileNumber; this.loadSubmission(application.fileNumber); } }); } async loadSubmission(fileNumber: string) { - this.applicationSubmission = await this.applicationSubmissionService.fetchSubmission(fileNumber); + const submission = await this.applicationSubmissionService.fetchSubmission(fileNumber); + this.proposedLots = submission.subdProposedLots; } ngOnDestroy(): void { this.$destroy.next(); this.$destroy.complete(); } + + async saveALRArea(i: number, $event: string | null) { + if (this.fileNumber) { + this.proposedLots[i].alrArea = $event ? parseFloat($event) : null; + await this.applicationSubmissionService.update(this.fileNumber, { + subProposedLots: this.proposedLots, + }); + } + } } diff --git a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.ts b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.ts index 8630599896..9ec1e88d94 100644 --- a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.ts +++ b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.ts @@ -4,7 +4,7 @@ import { firstValueFrom } from 'rxjs'; import { environment } from '../../../../environments/environment'; import { openFileInline } from '../../../shared/utils/file'; import { ToastService } from '../../toast/toast.service'; -import { ApplicationSubmissionDto } from '../application.dto'; +import { ApplicationSubmissionDto, UpdateApplicationSubmissionDto } from '../application.dto'; @Injectable({ providedIn: 'root', @@ -35,4 +35,13 @@ export class ApplicationSubmissionService { throw e; } } + + update(fileNumber: string, update: UpdateApplicationSubmissionDto) { + try { + return firstValueFrom(this.http.patch(`${this.baseUrl}/${fileNumber}`, update)); + } catch (e) { + this.toastService.showErrorToast('Failed to update Application Submission'); + throw e; + } + } } diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 90421efad0..41f4445cb3 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -50,6 +50,7 @@ export interface ApplicationDecisionMeetingDto { export interface ProposedLot { type: 'Lot' | 'Road Dedication' | null; size: number | null; + alrArea?: number | null; } export interface ApplicationReviewDto { @@ -100,6 +101,10 @@ export interface ApplicationParcelDto { alrArea: number; } +export interface UpdateApplicationSubmissionDto { + subProposedLots?: ProposedLot[]; +} + export interface ApplicationSubmissionDto { uuid: string; fileNumber: string; diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts index 40183d8d27..838db7e479 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts @@ -1,4 +1,5 @@ import { BaseCodeDto } from '../../../../shared/dto/base.dto'; +import { ProposedLot } from '../../application.dto'; export enum DecisionMaker { CEO = 'CEOP', @@ -146,12 +147,17 @@ export interface RosoDecisionComponentDto { soilToRemoveAverageDepth?: number | null; } +export interface SubdDecisionComponentDto { + subdApprovedLots?: ProposedLot[]; +} + export interface DecisionComponentDto extends NfuDecisionComponentDto, TurpDecisionComponentDto, PofoDecisionComponentDto, RosoDecisionComponentDto, - NaruDecisionComponentDto { + NaruDecisionComponentDto, + SubdDecisionComponentDto { uuid?: string; alrArea?: number | null; agCap?: string | null; @@ -181,6 +187,7 @@ export enum APPLICATION_DECISION_COMPONENT_TYPE { ROSO = 'ROSO', PFRS = 'PFRS', NARU = 'NARU', + SUBD = 'SUBD', } export interface ApplicationDecisionConditionTypeDto extends BaseCodeDto {} diff --git a/alcs-frontend/src/app/shared/inline-boolean/inline-boolean.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-boolean/inline-boolean.component.html similarity index 100% rename from alcs-frontend/src/app/shared/inline-boolean/inline-boolean.component.html rename to alcs-frontend/src/app/shared/inline-editors/inline-boolean/inline-boolean.component.html diff --git a/alcs-frontend/src/app/shared/inline-boolean/inline-boolean.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-boolean/inline-boolean.component.scss similarity index 95% rename from alcs-frontend/src/app/shared/inline-boolean/inline-boolean.component.scss rename to alcs-frontend/src/app/shared/inline-editors/inline-boolean/inline-boolean.component.scss index 2ed9b06eab..82f5256a6c 100644 --- a/alcs-frontend/src/app/shared/inline-boolean/inline-boolean.component.scss +++ b/alcs-frontend/src/app/shared/inline-editors/inline-boolean/inline-boolean.component.scss @@ -1,4 +1,4 @@ -@use '../../../styles/colors'; +@use '../../../../styles/colors'; .inline-date-picker-wrapper { padding-top: 4px; diff --git a/alcs-frontend/src/app/shared/inline-boolean/inline-boolean.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-boolean/inline-boolean.component.spec.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-boolean/inline-boolean.component.spec.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-boolean/inline-boolean.component.spec.ts diff --git a/alcs-frontend/src/app/shared/inline-boolean/inline-boolean.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-boolean/inline-boolean.component.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-boolean/inline-boolean.component.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-boolean/inline-boolean.component.ts diff --git a/alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.html similarity index 100% rename from alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.html rename to alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.html diff --git a/alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.scss similarity index 95% rename from alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.scss rename to alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.scss index 7839a3f8cb..5ec04b385b 100644 --- a/alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.scss +++ b/alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.scss @@ -1,4 +1,4 @@ -@use '../../../styles/colors'; +@use '../../../../styles/colors'; .inline-date-picker-wrapper { padding-top: 4px; diff --git a/alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.spec.ts similarity index 93% rename from alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.spec.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.spec.ts index 1e30f6a434..678b774f4e 100644 --- a/alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.spec.ts +++ b/alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.spec.ts @@ -1,7 +1,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { StartOfDayPipe } from '../pipes/startOfDay.pipe'; +import { StartOfDayPipe } from '../../pipes/startOfDay.pipe'; import { InlineDatepickerComponent } from './inline-datepicker.component'; diff --git a/alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.ts similarity index 92% rename from alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.ts index 4a046f0df4..719d0ca627 100644 --- a/alcs-frontend/src/app/shared/inline-datepicker/inline-datepicker.component.ts +++ b/alcs-frontend/src/app/shared/inline-editors/inline-datepicker/inline-datepicker.component.ts @@ -2,8 +2,8 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild } import { FormControl } from '@angular/forms'; import { MatDatepicker } from '@angular/material/datepicker'; import moment from 'moment'; -import { environment } from '../../../environments/environment'; -import { formatDateForApi } from '../utils/api-date-formatter'; +import { environment } from '../../../../environments/environment'; +import { formatDateForApi } from '../../utils/api-date-formatter'; @Component({ selector: 'app-inline-datepicker', diff --git a/alcs-frontend/src/app/shared/inline-dropdown/inline-dropdown.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-dropdown/inline-dropdown.component.html similarity index 100% rename from alcs-frontend/src/app/shared/inline-dropdown/inline-dropdown.component.html rename to alcs-frontend/src/app/shared/inline-editors/inline-dropdown/inline-dropdown.component.html diff --git a/alcs-frontend/src/app/shared/inline-dropdown/inline-dropdown.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-dropdown/inline-dropdown.component.scss similarity index 95% rename from alcs-frontend/src/app/shared/inline-dropdown/inline-dropdown.component.scss rename to alcs-frontend/src/app/shared/inline-editors/inline-dropdown/inline-dropdown.component.scss index f1779f748d..6487e5e781 100644 --- a/alcs-frontend/src/app/shared/inline-dropdown/inline-dropdown.component.scss +++ b/alcs-frontend/src/app/shared/inline-editors/inline-dropdown/inline-dropdown.component.scss @@ -1,4 +1,4 @@ -@use '../../../styles/colors'; +@use '../../../../styles/colors'; .inline-number-wrapper { padding-top: 4px; diff --git a/alcs-frontend/src/app/shared/inline-dropdown/inline-dropdown.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-dropdown/inline-dropdown.component.spec.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-dropdown/inline-dropdown.component.spec.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-dropdown/inline-dropdown.component.spec.ts diff --git a/alcs-frontend/src/app/shared/inline-dropdown/inline-dropdown.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-dropdown/inline-dropdown.component.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-dropdown/inline-dropdown.component.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-dropdown/inline-dropdown.component.ts diff --git a/alcs-frontend/src/app/shared/inline-number/inline-number.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-number/inline-number.component.html similarity index 100% rename from alcs-frontend/src/app/shared/inline-number/inline-number.component.html rename to alcs-frontend/src/app/shared/inline-editors/inline-number/inline-number.component.html diff --git a/alcs-frontend/src/app/shared/inline-number/inline-number.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-number/inline-number.component.scss similarity index 95% rename from alcs-frontend/src/app/shared/inline-number/inline-number.component.scss rename to alcs-frontend/src/app/shared/inline-editors/inline-number/inline-number.component.scss index f1779f748d..6487e5e781 100644 --- a/alcs-frontend/src/app/shared/inline-number/inline-number.component.scss +++ b/alcs-frontend/src/app/shared/inline-editors/inline-number/inline-number.component.scss @@ -1,4 +1,4 @@ -@use '../../../styles/colors'; +@use '../../../../styles/colors'; .inline-number-wrapper { padding-top: 4px; diff --git a/alcs-frontend/src/app/shared/inline-number/inline-number.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-number/inline-number.component.spec.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-number/inline-number.component.spec.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-number/inline-number.component.spec.ts diff --git a/alcs-frontend/src/app/shared/inline-number/inline-number.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-number/inline-number.component.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-number/inline-number.component.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-number/inline-number.component.ts diff --git a/alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.html similarity index 100% rename from alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.html rename to alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.html diff --git a/alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.scss similarity index 83% rename from alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.scss rename to alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.scss index 6643b30b9d..820f56a50d 100644 --- a/alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.scss +++ b/alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.scss @@ -1,4 +1,4 @@ -@use '../../../styles/colors'; +@use '../../../../styles/colors'; .editing.hidden { display: none; diff --git a/alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.spec.ts similarity index 94% rename from alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.spec.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.spec.ts index e2a194606c..a3783275a9 100644 --- a/alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.spec.ts +++ b/alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; -import { SharedModule } from '../shared.module'; +import { SharedModule } from '../../shared.module'; import { InlineReviewOutcomeComponent } from './inline-review-outcome.component'; diff --git a/alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-review-outcome/inline-review-outcome.component.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-review-outcome/inline-review-outcome.component.ts diff --git a/alcs-frontend/src/app/shared/inline-text/inline-text.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-text/inline-text.component.html similarity index 100% rename from alcs-frontend/src/app/shared/inline-text/inline-text.component.html rename to alcs-frontend/src/app/shared/inline-editors/inline-text/inline-text.component.html diff --git a/alcs-frontend/src/app/shared/inline-text/inline-text.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-text/inline-text.component.scss similarity index 95% rename from alcs-frontend/src/app/shared/inline-text/inline-text.component.scss rename to alcs-frontend/src/app/shared/inline-editors/inline-text/inline-text.component.scss index f1779f748d..6487e5e781 100644 --- a/alcs-frontend/src/app/shared/inline-text/inline-text.component.scss +++ b/alcs-frontend/src/app/shared/inline-editors/inline-text/inline-text.component.scss @@ -1,4 +1,4 @@ -@use '../../../styles/colors'; +@use '../../../../styles/colors'; .inline-number-wrapper { padding-top: 4px; diff --git a/alcs-frontend/src/app/shared/inline-text/inline-text.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-text/inline-text.component.spec.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-text/inline-text.component.spec.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-text/inline-text.component.spec.ts diff --git a/alcs-frontend/src/app/shared/inline-text/inline-text.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-text/inline-text.component.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-text/inline-text.component.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-text/inline-text.component.ts diff --git a/alcs-frontend/src/app/shared/inline-textarea/inline-textarea.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.html similarity index 100% rename from alcs-frontend/src/app/shared/inline-textarea/inline-textarea.component.html rename to alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.html diff --git a/alcs-frontend/src/app/shared/inline-textarea/inline-textarea.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.scss similarity index 96% rename from alcs-frontend/src/app/shared/inline-textarea/inline-textarea.component.scss rename to alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.scss index a3e30967b1..cc09826d94 100644 --- a/alcs-frontend/src/app/shared/inline-textarea/inline-textarea.component.scss +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.scss @@ -1,4 +1,4 @@ -@use '../../../styles/colors'; +@use '../../../../styles/colors'; .editable { display: inline-block; diff --git a/alcs-frontend/src/app/shared/inline-textarea/inline-textarea.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.spec.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-textarea/inline-textarea.component.spec.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.spec.ts diff --git a/alcs-frontend/src/app/shared/inline-textarea/inline-textarea.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.ts similarity index 100% rename from alcs-frontend/src/app/shared/inline-textarea/inline-textarea.component.ts rename to alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.ts diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html new file mode 100644 index 0000000000..4506f99631 --- /dev/null +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html @@ -0,0 +1,85 @@ +
+
+ + Total Number of Proposed Lots + + lots + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#{{ i + 1 }}Type + + + Lot + Road Dedication + + + Size (ha)* + + + ha + + Alr Area (ha)* + + + ha + +
No Proposed Lots Entered
+
+
diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.spec.ts b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.spec.ts new file mode 100644 index 0000000000..ffde6b07e5 --- /dev/null +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LotsTableFormComponent } from './lots-table-form.component'; + +describe('LotsTableComponent', () => { + let component: LotsTableFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [LotsTableFormComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(LotsTableFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts new file mode 100644 index 0000000000..9979b53925 --- /dev/null +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts @@ -0,0 +1,157 @@ +import { Component, OnInit } from '@angular/core'; +import { + AbstractControl, + ControlValueAccessor, + FormControl, + FormGroup, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + Validator, + Validators, +} from '@angular/forms'; +import { MatTableDataSource } from '@angular/material/table'; + +type ProposedLot = { type: 'Lot' | 'Road Dedication' | null; size: string | null; alrArea: string | null }; + +@Component({ + selector: 'app-lots-table', + templateUrl: './lots-table-form.component.html', + styleUrls: ['./lots-table-form.component.scss'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + multi: true, + useExisting: LotsTableFormComponent, + }, + { + provide: NG_VALIDATORS, + multi: true, + useExisting: LotsTableFormComponent, + }, + ], +}) +export class LotsTableFormComponent implements ControlValueAccessor, Validator { + displayedColumns = ['index', 'type', 'size', 'alrArea']; + lotsSource = new MatTableDataSource([]); + lotCount = 0; + + touched = false; + disabled = false; + + count = new FormControl(null, [Validators.required]); + form = new FormGroup({ + count: this.count, + } as any); + + private onTouched: Function | undefined; + private onChange: Function | undefined; + + markTouched() { + this.touched = true; + this.form.markAllAsTouched(); + } + + onChangeLotCount(event: Event) { + this.markAsTouched(); + const targetString = (event.target as HTMLInputElement).value; + const targetCount = parseInt(targetString); + + let lots = this.mapFormToLots(); + lots = lots.slice(0, targetCount); + while (lots.length < targetCount) { + lots.push({ + size: null, + alrArea: null, + type: null, + }); + } + + this.lotCount = targetCount; + this.resetForm(lots); + + this.lotsSource = new MatTableDataSource(lots); + this.fireChanged(); + } + + resetForm(lots: ProposedLot[]) { + for (const controlName of Object.keys(this.form.controls)) { + if (controlName.includes('lot')) { + this.form.removeControl(controlName); + } + } + + lots.forEach((lot, index) => { + this.form.addControl(`${index}-lotType`, new FormControl(lot.type, [Validators.required])); + this.form.addControl(`${index}-lotSize`, new FormControl(lot.size, [Validators.required])); + this.form.addControl(`${index}-lotAlrArea`, new FormControl(lot.alrArea, [Validators.required])); + }); + } + + registerOnChange(onChange: any) { + this.onChange = onChange; + } + + registerOnTouched(onTouched: Function) { + this.onTouched = onTouched; + } + + setDisabledState(disabled: boolean) { + this.disabled = disabled; + } + + writeValue(proposedLots: ProposedLot[]) { + this.resetForm(proposedLots ?? []); + this.lotCount = proposedLots.length; + let lots = this.mapFormToLots(); + this.lotsSource = new MatTableDataSource(lots); + this.count.setValue(this.lotCount.toString()); + } + + validate(control: AbstractControl) { + if (this.form.valid) { + return null; + } + + let errors: any = {}; + for (const controlName of Object.keys(this.form.controls)) { + const controlErrors = this.form.controls[controlName].errors; + + if (controlErrors) { + errors[controlName] = controlErrors; + } + } + + return errors; + } + + private markAsTouched() { + if (!this.touched) { + if (this.onTouched) { + this.onTouched(); + } + this.touched = true; + } + } + + private mapFormToLots() { + const proposedLots: ProposedLot[] = []; + for (let index = 0; index < this.lotCount; index++) { + const lotType = this.form.controls[`${index}-lotType`].value; + const lotSize = this.form.controls[`${index}-lotSize`].value; + const lotAlrArea = this.form.controls[`${index}-lotAlrArea`].value; + proposedLots.push({ + size: lotSize, + type: lotType, + alrArea: lotAlrArea, + }); + } + return proposedLots; + } + + fireChanged() { + if (this.onChange) { + const mappedLots = this.mapFormToLots(); + this.onChange(mappedLots); + } + } +} diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 53d08a1cb0..d60257ce20 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -40,13 +40,14 @@ import { AvatarCircleComponent } from './avatar-circle/avatar-circle.component'; import { DetailsHeaderComponent } from './details-header/details-header.component'; import { ErrorMessageComponent } from './error-message/error-message.component'; import { FavoriteButtonComponent } from './favorite-button/favorite-button.component'; -import { InlineBooleanComponent } from './inline-boolean/inline-boolean.component'; -import { InlineDatepickerComponent } from './inline-datepicker/inline-datepicker.component'; -import { InlineDropdownComponent } from './inline-dropdown/inline-dropdown.component'; -import { InlineNumberComponent } from './inline-number/inline-number.component'; -import { InlineReviewOutcomeComponent } from './inline-review-outcome/inline-review-outcome.component'; -import { InlineTextComponent } from './inline-text/inline-text.component'; -import { InlineTextareaComponent } from './inline-textarea/inline-textarea.component'; +import { InlineBooleanComponent } from './inline-editors/inline-boolean/inline-boolean.component'; +import { InlineDatepickerComponent } from './inline-editors/inline-datepicker/inline-datepicker.component'; +import { InlineDropdownComponent } from './inline-editors/inline-dropdown/inline-dropdown.component'; +import { InlineNumberComponent } from './inline-editors/inline-number/inline-number.component'; +import { InlineReviewOutcomeComponent } from './inline-editors/inline-review-outcome/inline-review-outcome.component'; +import { InlineTextComponent } from './inline-editors/inline-text/inline-text.component'; +import { InlineTextareaComponent } from './inline-editors/inline-textarea/inline-textarea.component'; +import { LotsTableFormComponent } from './lots-table/lots-table-form.component'; import { MeetingOverviewComponent } from './meeting-overview/meeting-overview.component'; import { NoDataComponent } from './no-data/no-data.component'; import { BooleanToStringPipe } from './pipes/boolean-to-string.pipe'; @@ -92,6 +93,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen ApplicationSubmissionStatusTypePillComponent, WarningBannerComponent, ErrorMessageComponent, + LotsTableFormComponent, ], imports: [ CommonModule, @@ -177,6 +179,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen ApplicationSubmissionStatusTypePillComponent, WarningBannerComponent, ErrorMessageComponent, + LotsTableFormComponent, ], }) export class SharedModule { diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts index b0b5f3a706..c0e8132722 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts @@ -16,7 +16,7 @@ import { ApplicationDecisionMakerCodeDto } from '../../application-decision-make import { CeoCriterionCodeDto } from './ceo-criterion/ceo-criterion.dto'; import { ApplicationDecisionComponentDto, - CreateApplicationDecisionComponentDto, + UpdateApplicationDecisionComponentDto, } from './component/application-decision-component.dto'; export class UpdateApplicationDecisionDto { @@ -108,7 +108,7 @@ export class UpdateApplicationDecisionDto { isDraft: boolean; @IsOptional() - decisionComponents?: CreateApplicationDecisionComponentDto[]; + decisionComponents?: UpdateApplicationDecisionComponentDto[]; @IsOptional() @IsArray() diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts index 5b06536b9d..1fa5cd93b5 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts @@ -1,7 +1,8 @@ import { AutoMap } from '@automapper/classes'; -import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; import { BaseCodeDto } from '../../../../../common/dtos/base.dto'; import { NaruSubtypeDto } from '../../../../../portal/application-submission/application-submission.dto'; +import { ProposedLot } from '../../../../../portal/application-submission/application-submission.entity'; export class ApplicationDecisionComponentTypeDto extends BaseCodeDto {} @@ -91,6 +92,10 @@ export class UpdateApplicationDecisionComponentDto { @IsString() @IsOptional() naruSubtypeCode: string; + + @IsArray() + @IsOptional() + subdApprovedLots?: ProposedLot[]; } export class CreateApplicationDecisionComponentDto extends UpdateApplicationDecisionComponentDto { @@ -174,6 +179,9 @@ export class ApplicationDecisionComponentDto { @AutoMap(() => NaruSubtypeDto) naruSubtype: NaruSubtypeDto; + + @AutoMap(() => [ProposedLot]) + subdApprovedLots: ProposedLot[]; } export enum APPLICATION_DECISION_COMPONENT_TYPE { diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts index 2a4ec38d7f..51a31a9206 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts @@ -1,6 +1,7 @@ import { AutoMap } from '@automapper/classes'; import { Column, Entity, Index, ManyToMany, ManyToOne } from 'typeorm'; import { Base } from '../../../../../common/entities/base.entity'; +import { ProposedLot } from '../../../../../portal/application-submission/application-submission.entity'; import { NaruSubtype } from '../../../../../portal/application-submission/naru-subtype/naru-subtype.entity'; import { ColumnNumericTransformer } from '../../../../../utils/column-numeric-transform'; import { ApplicationDecisionCondition } from '../../../application-decision-condition/application-decision-condition.entity'; @@ -189,6 +190,15 @@ export class ApplicationDecisionComponent extends Base { @ManyToOne(() => NaruSubtype) naruSubtype: NaruSubtype; + @AutoMap(() => [ProposedLot]) + @Column({ + comment: 'JSONB Column containing the approved subdivision lots', + type: 'jsonb', + array: false, + default: () => `'[]'`, + }) + subdApprovedLots: ProposedLot[]; + @AutoMap() @Column({ nullable: false }) applicationDecisionComponentTypeCode: string; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts index 46052fd7ef..096b502cd0 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts @@ -50,6 +50,11 @@ export class ApplicationDecisionComponentService { this.patchRosoFields(component, updateDto); this.patchNaruFields(component, updateDto); + //SUBD + if (updateDto.subdApprovedLots) { + component.subdApprovedLots = updateDto.subdApprovedLots; + } + updatedComponents.push(component); } diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.controller.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.controller.ts index 978837b2d6..9f37588d3a 100644 --- a/services/apps/alcs/src/alcs/application/application-submission/application-submission.controller.ts +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.controller.ts @@ -7,6 +7,7 @@ import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; import { RolesGuard } from '../../../common/authorization/roles-guard.service'; import { UserRoles } from '../../../common/authorization/roles.decorator'; import { DocumentService } from '../../../document/document.service'; +import { AlcsApplicationSubmissionUpdateDto } from './application-submission.dto'; import { ApplicationSubmissionService } from './application-submission.service'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @@ -49,4 +50,17 @@ export class ApplicationSubmissionController { ); return this.get(fileNumber); } + + @UserRoles(...ANY_AUTH_ROLE) + @Patch('/:fileNumber') + async updateSubmission( + @Param('fileNumber') fileNumber: string, + @Body() updateDto: AlcsApplicationSubmissionUpdateDto, + ) { + if (!fileNumber) { + throw new ServiceValidationException('File number is required'); + } + await this.applicationSubmissionService.update(fileNumber, updateDto); + return this.get(fileNumber); + } } diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.dto.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.dto.ts new file mode 100644 index 0000000000..8473d4c30f --- /dev/null +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.dto.ts @@ -0,0 +1,8 @@ +import { IsArray, IsOptional } from 'class-validator'; +import { ProposedLot } from '../../../portal/application-submission/application-submission.entity'; + +export class AlcsApplicationSubmissionUpdateDto { + @IsArray() + @IsOptional() + subProposedLots?: ProposedLot[]; +} diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts index 933388c497..4b7eb988d0 100644 --- a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts @@ -8,8 +8,11 @@ import { ApplicationSubmissionStatusType } from '../../../application-submission import { SUBMISSION_STATUS } from '../../../application-submission-status/submission-status.dto'; import { ApplicationOwnerDto } from '../../../portal/application-submission/application-owner/application-owner.dto'; import { ApplicationOwner } from '../../../portal/application-submission/application-owner/application-owner.entity'; +import { ApplicationSubmissionUpdateDto } from '../../../portal/application-submission/application-submission.dto'; import { ApplicationSubmission } from '../../../portal/application-submission/application-submission.entity'; +import { filterUndefined } from '../../../utils/undefined'; import { AlcsApplicationSubmissionDto } from '../application.dto'; +import { AlcsApplicationSubmissionUpdateDto } from './application-submission.dto'; @Injectable() export class ApplicationSubmissionService { @@ -82,4 +85,25 @@ export class ApplicationSubmissionService { statusCode, ); } + + async update( + fileNumber: string, + updateDto: AlcsApplicationSubmissionUpdateDto, + ) { + //Load submission without relations to prevent save from crazy cascading + const submission = await this.applicationSubmissionRepository.findOneOrFail( + { + where: { + fileNumber: fileNumber, + }, + }, + ); + + submission.subdProposedLots = filterUndefined( + updateDto.subProposedLots, + submission.subdProposedLots, + ); + + await this.applicationSubmissionRepository.save(submission); + } } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts index 2959014317..e8e271ddf0 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts @@ -148,6 +148,7 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { @AutoMap(() => Boolean) subdIsHomeSiteSeverance?: boolean | null; + @AutoMap(() => [ProposedLot]) subdProposedLots?: ProposedLot[]; //Soil Fields diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts index 41c10c042e..8c53d774e7 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts @@ -18,8 +18,9 @@ import { ApplicationParcel } from './application-parcel/application-parcel.entit import { NaruSubtype } from './naru-subtype/naru-subtype.entity'; export class ProposedLot { - type: 'Lot' | 'Road Dedication'; - size: number; + type: 'Lot' | 'Road Dedication' | null; + alrArea?: number | null; + size: number | null; } @Entity() diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690323153558-add_subd_dec_components.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690323153558-add_subd_dec_components.ts new file mode 100644 index 0000000000..87af5b6cff --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690323153558-add_subd_dec_components.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addSubdDecComponents1690323153558 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + INSERT INTO alcs.application_decision_component_type + (audit_deleted_date_at, audit_created_at, audit_updated_at, audit_created_by, audit_updated_by, "label", code, description) + values + (null , now(), now(), 'seed-migration','seed-migration' , 'Subdivision', 'SUBD', 'Subdivision'); + `, + ); + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690406098396-add_subd_decision_lots.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690406098396-add_subd_decision_lots.ts new file mode 100644 index 0000000000..ccbef26744 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690406098396-add_subd_decision_lots.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addSubdDecisionLots1690406098396 implements MigrationInterface { + name = 'addSubdDecisionLots1690406098396'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_component" ADD "subd_approved_lots" jsonb NOT NULL DEFAULT '[]'`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_decision_component"."subd_approved_lots" IS 'JSONB Column containing the approved subdivision lots'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_decision_component"."subd_approved_lots" IS 'JSONB Column containing the approved subdivision lots'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_component" DROP COLUMN "subd_approved_lots"`, + ); + } +} From 4dbdacce3ae65fd7014687514f4fede2efc8b283 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 26 Jul 2023 15:27:22 -0700 Subject: [PATCH 147/954] Add inline ALR Area editing --- .../decision-component/subd/subd.component.html | 4 +++- .../subd/subd.component.spec.ts | 13 +++++++++++++ .../decision-component/subd/subd.component.ts | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html index f731dfa356..295fe38210 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html @@ -25,7 +25,9 @@ {{ i + 1 }} {{ lot.type }} {{ lot.size }} - {{ lot.alrArea }} + + + diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts index 5634294fcf..42e60e27ef 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts @@ -1,4 +1,7 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ApplicationDecisionComponentService } from '../../../../../../services/application/decision/application-decision-v2/application-decision-component/application-decision-component.service'; import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { SubdComponent } from './subd.component'; @@ -6,10 +9,20 @@ import { SubdComponent } from './subd.component'; describe('PfrsComponent', () => { let component: SubdComponent; let fixture: ComponentFixture; + let mockAppDecService: DeepMocked; beforeEach(async () => { + mockAppDecService = createMock(); + await TestBed.configureTestingModule({ declarations: [SubdComponent], + providers: [ + { + provide: ApplicationDecisionComponentService, + useValue: mockAppDecService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(SubdComponent); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts index c197412739..f18314215f 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts @@ -1,4 +1,5 @@ import { Component, Input } from '@angular/core'; +import { ApplicationDecisionComponentService } from '../../../../../../services/application/decision/application-decision-v2/application-decision-component/application-decision-component.service'; import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ @@ -7,5 +8,20 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec styleUrls: ['./subd.component.scss'], }) export class SubdComponent { + constructor(private componentService: ApplicationDecisionComponentService) {} + @Input() component!: DecisionComponentDto; + + async onSaveAlrArea(i: number, alrArea: string | null) { + const lots = this.component.subdApprovedLots; + if (lots && this.component.uuid) { + lots[i].alrArea = alrArea ? parseFloat(alrArea) : null; + debugger; + await this.componentService.update(this.component.uuid, { + uuid: this.component.uuid, + subdApprovedLots: lots, + applicationDecisionComponentTypeCode: this.component.applicationDecisionComponentTypeCode, + }); + } + } } From b7aace20fcd0a4d0f29929c3b011e76c77164c78 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 26 Jul 2023 16:55:51 -0700 Subject: [PATCH 148/954] Code review feedback * Remove debuggers * DRY up function --- .../decision-component/subd/subd.component.ts | 1 - .../decision-component.component.ts | 1 - .../application-submission.service.ts | 29 +++++++------------ 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts index f18314215f..634656fa3e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts @@ -16,7 +16,6 @@ export class SubdComponent { const lots = this.component.subdApprovedLots; if (lots && this.component.uuid) { lots[i].alrArea = alrArea ? parseFloat(alrArea) : null; - debugger; await this.componentService.update(this.component.uuid, { uuid: this.component.uuid, subdApprovedLots: lots, diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts index 5d9829cc72..968dfb1857 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts @@ -243,7 +243,6 @@ export class DecisionComponentComponent implements OnInit { private patchSubdFields() { this.form.addControl('subdApprovedLots', this.subdApprovedLots); - debugger; this.subdApprovedLots.setValue(this.data.subdApprovedLots ?? null); } diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts index 4b7eb988d0..255473cab4 100644 --- a/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.service.ts @@ -71,15 +71,7 @@ export class ApplicationSubmissionService { } async updateStatus(fileNumber: string, statusCode: SUBMISSION_STATUS) { - //Load submission without relations to prevent save from crazy cascading - const submission = await this.applicationSubmissionRepository.findOneOrFail( - { - where: { - fileNumber: fileNumber, - }, - }, - ); - + const submission = await this.loadBarebonesSubmission(fileNumber); await this.applicationSubmissionStatusService.setStatusDate( submission.uuid, statusCode, @@ -90,15 +82,7 @@ export class ApplicationSubmissionService { fileNumber: string, updateDto: AlcsApplicationSubmissionUpdateDto, ) { - //Load submission without relations to prevent save from crazy cascading - const submission = await this.applicationSubmissionRepository.findOneOrFail( - { - where: { - fileNumber: fileNumber, - }, - }, - ); - + const submission = await this.loadBarebonesSubmission(fileNumber); submission.subdProposedLots = filterUndefined( updateDto.subProposedLots, submission.subdProposedLots, @@ -106,4 +90,13 @@ export class ApplicationSubmissionService { await this.applicationSubmissionRepository.save(submission); } + + private loadBarebonesSubmission(fileNumber: string) { + //Load submission without relations to prevent save from crazy cascading + return this.applicationSubmissionRepository.findOneOrFail({ + where: { + fileNumber, + }, + }); + } } From 37ab19f7e147997fb639e52f2e27361e22fd92f4 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Wed, 26 Jul 2023 17:16:19 -0700 Subject: [PATCH 149/954] Feature/alcs 875 part 2 (#823) new nfu-subtypes app-prep mapping fixes app-prep validation improvements --- .../nfu-input/nfu-input.component.ts | 2 +- .../application/proposal/nfu/nfu.component.ts | 116 +------- .../application/proposal/nfu/nfu.constants.ts | 139 ++++++++++ bin/.gitignore | 3 +- .../applications/app_prep.py | 75 +++--- .../application_prep_basic_validation.sql | 30 ++- bin/migrate-oats-data/common/__init__.py | 2 +- .../common/oats_application_code_values.py | 251 ++++++++++++++++++ .../common/oats_application_enum.py | 29 -- bin/migrate-oats-data/migrate.py | 31 +++ 10 files changed, 487 insertions(+), 191 deletions(-) create mode 100644 alcs-frontend/src/app/features/application/proposal/nfu/nfu.constants.ts create mode 100644 bin/migrate-oats-data/common/oats_application_code_values.py delete mode 100644 bin/migrate-oats-data/common/oats_application_enum.py diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component.ts index 0c165ca2a4..c0bd834ab5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { NFU_SUBTYPES_OPTIONS, NFU_TYPES_OPTIONS } from '../../../../../../proposal/nfu/nfu.component'; +import { NFU_SUBTYPES_OPTIONS, NFU_TYPES_OPTIONS } from '../../../../../../proposal/nfu/nfu.constants'; @Component({ selector: 'app-nfu-input', diff --git a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.ts b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.ts index fa6ec1a6ab..50dcd08a74 100644 --- a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.ts +++ b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.ts @@ -3,121 +3,7 @@ import { Subject, takeUntil } from 'rxjs'; import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; import { ApplicationDto, UpdateApplicationDto } from '../../../../services/application/application.dto'; import { ToastService } from '../../../../services/toast/toast.service'; - -// TODO move to code tables? -export const NFU_TYPES_OPTIONS = [ - { - label: 'Agricultural / Farm', - value: 'Agricultural / Farm', - }, - { - label: 'Civic / Institutional', - value: 'Civic / Institutional', - }, - { - label: 'Commercial / Retail', - value: 'Commercial / Retail', - }, - { - label: 'Industrial', - value: 'Industrial', - }, - { - label: 'Other', - value: 'Other', - }, - { - label: 'Recreational', - value: 'Recreational', - }, - { - label: 'Residential', - value: 'Residential', - }, - { - label: 'Transportation / Utilities', - value: 'Transportation / Utilities', - }, - { - label: 'Unused', - value: 'Unused', - }, -]; - -export const NFU_SUBTYPES_OPTIONS = [ - { - label: 'Alcohol Processing', - value: 'Alcohol Processing', - }, - { - label: 'Cement / Asphalt / Concrete Plants', - value: 'Cement / Asphalt / Concrete Plants', - }, - { - label: 'Commercial / Retail', - value: 'Commercial / Retail', - }, - { - label: 'Deposition / Fill (All Types)', - value: 'Deposition / Fill (All Types)', - }, - { - label: 'Energy Production', - value: 'Energy Production', - }, - { - label: 'Recreational', - value: 'Recreational', - }, - { - label: 'Food Processing (Non-Meat)', - value: 'Food Processing (Non-Meat)', - }, - { - label: 'Industrial - Other', - value: 'Industrial - Other', - }, - { - label: 'Logging Operations', - value: 'Logging Operations', - }, - { - label: 'Lumber Manufacturing and Re-Manufacturing', - value: 'Lumber Manufacturing and Re-Manufacturing', - }, - { - label: 'Meat and Fish Processing (+ Abattoir)', - value: 'Meat and Fish Processing (+ Abattoir)', - }, - { - label: 'Mining', - value: 'Mining', - }, - { - label: 'Miscellaneous Processing', - value: 'Miscellaneous Processing', - }, - { - label: 'Oil and Gas Activities', - value: 'Oil and Gas Activities', - }, - { - label: 'Sand & Gravel', - value: 'Sand & Gravel', - }, - { - label: 'Sawmill', - value: 'Sawmill', - }, - { - label: 'Storage and Warehouse Facilities (Indoor/Outdoor - Large Scale Structures)', - value: 'Storage and Warehouse Facilities (Indoor/Outdoor - Large Scale Structures)', - }, - { - label: 'Work Camps or Associated Use', - value: 'Work Camps or Associated Use', - }, -]; +import { NFU_SUBTYPES_OPTIONS, NFU_TYPES_OPTIONS } from './nfu.constants'; @Component({ selector: 'app-proposal-nfu', diff --git a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.constants.ts b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.constants.ts new file mode 100644 index 0000000000..850f513867 --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.constants.ts @@ -0,0 +1,139 @@ +export const NFU_SUBTYPES_OPTIONS = [ + { label: 'Accessory Buildings', value: 'Accessory Buildings' }, + { label: 'Additional Dwelling(s)', value: 'Additional Dwelling(s)' }, + { label: 'Additional Structures for Farm Help', value: 'Additional Structures for Farm Help' }, + { label: 'Agricultural Land Use Remnant', value: 'Agricultural Land Use Remnant' }, + { label: 'Agricultural Lease', value: 'Agricultural Lease' }, + { label: 'Agricultural Subdivision Remnant', value: 'Agricultural Subdivision Remnant' }, + { label: 'Airports and Aviation related', value: 'Airports and Aviation related' }, + { label: 'Alcohol Processing', value: 'Alcohol Processing' }, + { label: 'Alcohol Production Associated Uses', value: 'Alcohol Production Associated Uses' }, + { label: 'Animal Boarding and Services', value: 'Animal Boarding and Services' }, + { label: 'Auto Services', value: 'Auto Services' }, + { label: 'Beef', value: 'Beef' }, + { label: 'Campground (Private) & RV Park', value: 'Campground (Private) & RV Park' }, + { label: 'Cannabis Related Uses', value: 'Cannabis Related Uses' }, + { label: 'Care Facilities', value: 'Care Facilities' }, + { label: 'Cement/ Asphalt/Concrete Plants', value: 'Cement/ Asphalt/Concrete Plants' }, + { label: 'Cemeteries', value: 'Cemeteries' }, + { label: 'Churches & Bible Schools', value: 'Churches & Bible Schools' }, + { label: 'Civic Facilities and Buildings', value: 'Civic Facilities and Buildings' }, + { label: 'Civic - other', value: 'Civic - other' }, + { label: 'Commercial - other', value: 'Commercial - other' }, + { label: 'Composting', value: 'Composting' }, + { label: 'Dairy', value: 'Dairy' }, + { label: 'Deposition/Fill (All Types)', value: 'Deposition/Fill (All Types)' }, + { label: 'Electrical Power Distribution Systems', value: 'Electrical Power Distribution Systems' }, + { label: 'Electrical Power Facilities', value: 'Electrical Power Facilities' }, + { label: 'Energy Production', value: 'Energy Production' }, + { label: 'Energy Production', value: 'Energy Production' }, + { label: 'Events', value: 'Events' }, + { label: 'Exhibitions and Festivals', value: 'Exhibitions and Festivals' }, + { label: 'Farm Help Accommodation', value: 'Farm Help Accommodation' }, + { label: 'Fire Hall and associated uses', value: 'Fire Hall and associated uses' }, + { label: 'Food and Beverage Services', value: 'Food and Beverage Services' }, + { label: 'Food Processing (non-meat)', value: 'Food Processing (non-meat)' }, + { label: 'Gas and Other Distribution Pipelines', value: 'Gas and Other Distribution Pipelines' }, + { label: 'Golf Course', value: 'Golf Course' }, + { label: 'Grain & Forage', value: 'Grain & Forage' }, + { label: 'Greenhouses', value: 'Greenhouses' }, + { label: 'Hall/Lodge (private)_', value: 'Hall/Lodge (private)_' }, + { label: 'Hospitals, Health Centres (Incl Private)', value: 'Hospitals, Health Centres (Incl Private)' }, + { label: 'Industrial - other', value: 'Industrial - other' }, + { label: 'Land Use Remnant', value: 'Land Use Remnant' }, + { label: 'Lease', value: 'Lease' }, + { label: 'Livestock-Unspecified', value: 'Livestock-Unspecified' }, + { label: 'Logging Operations', value: 'Logging Operations' }, + { label: 'Lumber Manufacturing and Re-manufacturing', value: 'Lumber Manufacturing and Re-manufacturing' }, + { label: 'Meat and Fish Processing (+abattoir)', value: 'Meat and Fish Processing (+abattoir)' }, + { label: 'Mining', value: 'Mining' }, + { label: 'Misc. Agricultural Use', value: 'Misc. Agricultural Use' }, + { label: 'Miscellaneous Processing', value: 'Miscellaneous Processing' }, + { label: 'Mixed Ag Uses', value: 'Mixed Ag Uses' }, + { label: 'Mixed Uses', value: 'Mixed Uses' }, + { label: 'Mobile Home Park', value: 'Mobile Home Park' }, + { label: 'Multi Family-Apartments/Condominiums', value: 'Multi Family-Apartments/Condominiums' }, + { label: 'Office Building (Primary Use)', value: 'Office Building (Primary Use)' }, + { label: 'Oil and Gas Activities', value: 'Oil and Gas Activities' }, + { label: 'Other-Undefined', value: 'Other-Undefined' }, + { label: 'Other Uses', value: 'Other Uses' }, + { label: "Parks-All Types operated by Local Gov't", value: "Parks-All Types operated by Local Gov't" }, + { label: 'Parks & Playing Fields', value: 'Parks & Playing Fields' }, + { label: 'Pigs/Hogs', value: 'Pigs/Hogs' }, + { label: 'Poultry', value: 'Poultry' }, + { label: 'Public Transportation Facilities', value: 'Public Transportation Facilities' }, + { label: 'Railway', value: 'Railway' }, + { label: 'Recreational - other', value: 'Recreational - other' }, + { label: 'Research Facilities', value: 'Research Facilities' }, + { label: 'Residential - other', value: 'Residential - other' }, + { label: 'Roads', value: 'Roads' }, + { label: 'Sand & Gravel', value: 'Sand & Gravel' }, + { label: 'Sanitary Land Fills', value: 'Sanitary Land Fills' }, + { label: 'Sawmill', value: 'Sawmill' }, + { label: 'Schools & Universities', value: 'Schools & Universities' }, + { label: 'Sewage Treatment Facilities', value: 'Sewage Treatment Facilities' }, + { label: 'Sewer Distribution Systems', value: 'Sewer Distribution Systems' }, + { label: 'Shopping Centre', value: 'Shopping Centre' }, + { label: 'Small Fruits-Berries', value: 'Small Fruits-Berries' }, + { label: 'Sports Facilities - commercial', value: 'Sports Facilities - commercial' }, + { label: 'Sports Facilities - municipal', value: 'Sports Facilities - municipal' }, + { + label: 'Storage and Warehouse Facilities (Indoor/Outdoor- Large Scale Structures)', + value: 'Storage and Warehouse Facilities (Indoor/Outdoor- Large Scale Structures)', + }, + { label: 'Storage & Warehouse', value: 'Storage & Warehouse' }, + { label: 'Store (Retail - All Types)', value: 'Store (Retail - All Types)' }, + { label: 'Subdivision Special Categories', value: 'Subdivision Special Categories' }, + { label: 'Subdivision Special Categories (Lease)', value: 'Subdivision Special Categories (Lease)' }, + { label: 'Telephone and Telecommunications', value: 'Telephone and Telecommunications' }, + { label: 'Tourist Accommodations', value: 'Tourist Accommodations' }, + { label: 'Trails', value: 'Trails' }, + { label: 'Transportation - other', value: 'Transportation - other' }, + { label: 'Tree Fruits', value: 'Tree Fruits' }, + { label: 'Turf Farm', value: 'Turf Farm' }, + { label: 'Vegetable & Truck', value: 'Vegetable & Truck' }, + { label: 'Vineyard and Associated Uses', value: 'Vineyard and Associated Uses' }, + { label: 'Water Distribution Systems', value: 'Water Distribution Systems' }, + { label: 'Water or Sewer Distribution Systems (inactive)', value: 'Water or Sewer Distribution Systems (inactive)' }, + { label: 'Water Treatment Facilities', value: 'Water Treatment Facilities' }, + { label: 'Work Camps or Associated Use', value: 'Work Camps or Associated Use' }, +]; + +export const NFU_TYPES_OPTIONS = [ + { + label: 'Agricultural / Farm', + value: 'Agricultural / Farm', + }, + { + label: 'Civic / Institutional', + value: 'Civic / Institutional', + }, + { + label: 'Commercial / Retail', + value: 'Commercial / Retail', + }, + { + label: 'Industrial', + value: 'Industrial', + }, + { + label: 'Other', + value: 'Other', + }, + { + label: 'Recreational', + value: 'Recreational', + }, + { + label: 'Residential', + value: 'Residential', + }, + { + label: 'Transportation / Utilities', + value: 'Transportation / Utilities', + }, + { + label: 'Unused', + value: 'Unused', + }, +]; diff --git a/bin/.gitignore b/bin/.gitignore index 6d655a1e07..12cddf3cc0 100644 --- a/bin/.gitignore +++ b/bin/.gitignore @@ -1,4 +1,5 @@ .env out tmp -*.pickle \ No newline at end of file +*.pickle +*etl_log.txt \ No newline at end of file diff --git a/bin/migrate-oats-data/applications/app_prep.py b/bin/migrate-oats-data/applications/app_prep.py index 06fe243535..88bacb10a2 100644 --- a/bin/migrate-oats-data/applications/app_prep.py +++ b/bin/migrate-oats-data/applications/app_prep.py @@ -4,6 +4,7 @@ AlcsNfuSubTypeCode, OatsCapabilitySourceCode, OatsAgriCapabilityCodes, + OATS_NFU_SUBTYPES, AlcsAgCap, AlcsAgCapSource, log_end, @@ -20,29 +21,29 @@ class OatsToAlcsNfuTypes(Enum): - AGR = AlcsNfuTypeCode.Agricultural_Farm - CIV = AlcsNfuTypeCode.Civic_Institutional - COM = AlcsNfuTypeCode.Commercial_Retail - IND = AlcsNfuTypeCode.Industrial - OTH = AlcsNfuTypeCode.Other - REC = AlcsNfuTypeCode.Recreational - RES = AlcsNfuTypeCode.Residential - TRA = AlcsNfuTypeCode.Transportation_Utilities - UNU = AlcsNfuTypeCode.Unused + AGR = AlcsNfuTypeCode.Agricultural_Farm.value + CIV = AlcsNfuTypeCode.Civic_Institutional.value + COM = AlcsNfuTypeCode.Commercial_Retail.value + IND = AlcsNfuTypeCode.Industrial.value + OTH = AlcsNfuTypeCode.Other.value + REC = AlcsNfuTypeCode.Recreational.value + RES = AlcsNfuTypeCode.Residential.value + TRA = AlcsNfuTypeCode.Transportation_Utilities.value + UNU = AlcsNfuTypeCode.Unused.value class OatsToAlcsAgCapSource(Enum): - BCLI = AlcsAgCapSource.BCLI - CLI = AlcsAgCapSource.CLI - ONSI = AlcsAgCapSource.On_site + BCLI = AlcsAgCapSource.BCLI.value + CLI = AlcsAgCapSource.CLI.value + ONSI = AlcsAgCapSource.On_site.value class OatsToAlcsAgCap(Enum): - P = AlcsAgCap.Prime - PD = AlcsAgCap.Prime_Dominant - MIX = AlcsAgCap.Mixed_Prime_Secondary - S = AlcsAgCap.Secondary - U = AlcsAgCap.Unclassified + P = AlcsAgCap.Prime.value + PD = AlcsAgCap.Prime_Dominant.value + MIX = AlcsAgCap.Mixed_Prime_Secondary.value + S = AlcsAgCap.Secondary.value + U = AlcsAgCap.Unclassified.value @inject_conn_pool @@ -54,6 +55,7 @@ def process_alcs_application_prep_fields(conn=None, batch_size=BATCH_UPLOAD_SIZE conn (psycopg2.extensions.connection): PostgreSQL database connection. Provided by the decorator. batch_size (int): The number of items to process at once. Defaults to BATCH_UPLOAD_SIZE. """ + log_start(etl_name) with conn.cursor(cursor_factory=RealDictCursor) as cursor: with open( @@ -201,14 +203,14 @@ def prepare_app_prep_data(app_prep_raw_data_list): data = dict(row) data = map_basic_field(data) - if data["alr_change_code"] == ALRChangeCode.NFU: + if data["alr_change_code"] == ALRChangeCode.NFU.value: data = mapOatsToAlcsAppPrep(data) nfu_data_list.append(data) - elif data["alr_change_code"] == ALRChangeCode.NAR: + elif data["alr_change_code"] == ALRChangeCode.NAR.value: nar_data_list.append(data) - elif data["alr_change_code"] == ALRChangeCode.EXC: + elif data["alr_change_code"] == ALRChangeCode.EXC.value: exc_data_list.append(data) - elif data["alr_change_code"] == ALRChangeCode.INC: + elif data["alr_change_code"] == ALRChangeCode.INC.value: inc_data_list.append(data) else: other_data_list.append(data) @@ -217,14 +219,18 @@ def prepare_app_prep_data(app_prep_raw_data_list): def mapOatsToAlcsAppPrep(data): + oats_type_code = data["nonfarm_use_type_code"] + oats_subtype_code = data["nonfarm_use_subtype_code"] + if data["nonfarm_use_type_code"]: data["nonfarm_use_type_code"] = str( OatsToAlcsNfuTypes[data["nonfarm_use_type_code"]].value ) if data["nonfarm_use_subtype_code"]: data["nonfarm_use_subtype_code"] = map_oats_to_alcs_nfu_subtypes( - data["nonfarm_use_subtype_code"] + oats_type_code, oats_subtype_code ) + return data @@ -314,22 +320,15 @@ def get_update_query_for_other(): return query -def map_oats_to_alcs_nfu_subtypes(oats_code): - # TODO this is work in progress and will be finished later - if oats_code == 1: - return "Accessory Buildings" - if oats_code == 2: - return "Additional Dwelling(s)" - if oats_code == 3: - return "Additional Structures for Farm Help" - if oats_code == 4: - return "Agricultural Land Use Remnant" - if oats_code == 5: - return "Agricultural Lease" - if oats_code == 6: - return "Agricultural Subdivision Remnant" - - return "" +def map_oats_to_alcs_nfu_subtypes(nfu_type_code, nfu_subtype_code): + for dict_obj in OATS_NFU_SUBTYPES: + if str(dict_obj["type_key"]) == str(nfu_type_code) and str( + dict_obj["subtype_key"] + ) == str(nfu_subtype_code): + return dict_obj["value"] + + # Return None when no matching key found + return None def map_basic_field(data): diff --git a/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql b/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql index d0d720f259..c2523a58f3 100644 --- a/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql +++ b/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql @@ -14,33 +14,51 @@ oats_app_prep_data AS ( oaac.agri_cap_consultant, oaac.component_area, oaac.capability_source_code, - oaac.nonfarm_use_type_code, - oaac.nonfarm_use_subtype_code, oaac.nonfarm_use_end_date, oaac.rsdntl_use_type_code, oaac.rsdntl_use_end_date, oaa.staff_comment_observations, oaac.alr_change_code, - oaac.exclsn_app_type_code + oaac.exclsn_app_type_code, + oaac.nonfarm_use_type_code, + oaac.nonfarm_use_subtype_code, + onutc.description AS nonfarm_use_type_description, + onusc.description AS nonfarm_use_subtype_description, + -- ALCS has typos fixed and this is required for proper validation + CASE + WHEN onusc.description = 'Water Distribtion Systems' THEN 'Water Distribution Systems' + WHEN onusc.description = 'Tourist Accomodations' THEN 'Tourist Accommodations' + WHEN onusc.description = 'Office Buiding (Primary Use)' THEN 'Office Building (Primary Use)' + ELSE onusc.description + END AS mapped_nonfarm_use_subtype_description FROM appl_components_grouped acg JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = acg.alr_application_id JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = acg.alr_application_id + JOIN oats.oats_nonfarm_use_subtype_codes onusc ON onusc.nonfarm_use_subtype_code = oaac.nonfarm_use_subtype_code + AND oaac.nonfarm_use_type_code = onusc.nonfarm_use_type_code + JOIN oats.oats_nonfarm_use_type_codes onutc ON onutc.nonfarm_use_type_code = oaac.nonfarm_use_type_code ) -SELECT a.alr_area, +SELECT oapd.alr_application_id, + a.alr_area, a.ag_cap, a.ag_cap_source, a.ag_cap_map, a.ag_cap_consultant, a.staff_observations, + a.nfu_use_type, + a.nfu_use_sub_type, oapd.component_area, oapd.agri_capability_code, oapd.capability_source_code, oapd.agri_cap_map, oapd.agri_cap_consultant, - oapd.staff_comment_observations + oapd.staff_comment_observations, + oapd.nonfarm_use_type_description, + oapd.mapped_nonfarm_use_subtype_description FROM alcs.application a LEFT JOIN oats_app_prep_data AS oapd ON a.file_number = oapd.alr_application_id::TEXT WHERE a.alr_area != oapd.component_area OR a.ag_cap_map != oapd.agri_cap_map OR a.ag_cap_consultant != oapd.agri_cap_consultant - OR a.staff_observations != oapd.staff_comment_observations \ No newline at end of file + OR a.staff_observations != oapd.staff_comment_observations + OR a.nfu_use_sub_type != oapd.mapped_nonfarm_use_subtype_description \ No newline at end of file diff --git a/bin/migrate-oats-data/common/__init__.py b/bin/migrate-oats-data/common/__init__.py index 77c58f32c9..26c07e6d74 100644 --- a/bin/migrate-oats-data/common/__init__.py +++ b/bin/migrate-oats-data/common/__init__.py @@ -1,3 +1,3 @@ -from .oats_application_enum import * +from .oats_application_code_values import * from .alcs_application_enum import * from .etl_logger import log_start, log_end diff --git a/bin/migrate-oats-data/common/oats_application_code_values.py b/bin/migrate-oats-data/common/oats_application_code_values.py new file mode 100644 index 0000000000..da8b8139bc --- /dev/null +++ b/bin/migrate-oats-data/common/oats_application_code_values.py @@ -0,0 +1,251 @@ +from enum import Enum + + +class ALRChangeCode(Enum): + TUR = "TUR" # Transport, Utility, and Recreation + INC = "INC" # Inclusion + EXC = "EXC" # Exclusion + SDV = "SDV" # Subdivision + NFU = "NFU" # Non Farm Use + SCH = "SCH" # Extraction and Fill + EXT = "EXT" # Extraction + FILL = "FILL" # Fill + SRW = "SRW" # Notification of Statutory Right of Way + CSC = "CSC" # Conservation Covenant + NAR = "NAR" # Non-Adhering Residential Use + + +class OatsCapabilitySourceCode(Enum): + BCLI = "BCLI" + CLI = "CLI" + ONSI = "ONSI" + + +class OatsAgriCapabilityCodes(Enum): + P = "P" + PD = "PD" + MIX = "MIX" + S = "S" + U = "U" + +OATS_NFU_SUBTYPES = [ + {"type_key": "AGR", "subtype_key": "1", "value": "Accessory Buildings"}, + {"type_key": "RES", "subtype_key": "2", "value": "Additional Dwelling(s)"}, + { + "type_key": "AGR", + "subtype_key": "3", + "value": "Additional Structures for Farm Help", + }, + { + "type_key": "AGR", + "subtype_key": "4", + "value": "Agricultural Land Use Remnant", + }, + {"type_key": "AGR", "subtype_key": "5", "value": "Agricultural Lease"}, + { + "type_key": "AGR", + "subtype_key": "6", + "value": "Agricultural Subdivision Remnant", + }, + { + "type_key": "TRA", + "subtype_key": "7", + "value": "Airports and Aviation related", + }, + {"type_key": "IND", "subtype_key": "8", "value": "Alcohol Processing"}, + { + "type_key": "COM", + "subtype_key": "9", + "value": "Animal Boarding and Services", + }, + {"type_key": "COM", "subtype_key": "11", "value": "Auto Services"}, + {"type_key": "AGR", "subtype_key": "12", "value": "Beef"}, + { + "type_key": "COM", + "subtype_key": "13", + "value": "Campground (Private) & RV Park", + }, + {"type_key": "COM", "subtype_key": "14", "value": "Care Facilities"}, + { + "type_key": "IND", + "subtype_key": "15", + "value": "Cement/ Asphalt/Concrete Plants", + }, + {"type_key": "CIV", "subtype_key": "16", "value": "Cemeteries"}, + {"type_key": "CIV", "subtype_key": "17", "value": "Churches & Bible Schools"}, + {"type_key": "CIV", "subtype_key": "18", "value": "Civic - other"}, + { + "type_key": "CIV", + "subtype_key": "19", + "value": "Civic Facilities and Buildings", + }, + {"type_key": "COM", "subtype_key": "20", "value": "Commercial - other"}, + {"type_key": "IND", "subtype_key": "21", "value": "Composting"}, + {"type_key": "AGR", "subtype_key": "22", "value": "Dairy"}, + { + "type_key": "IND", + "subtype_key": "23", + "value": "Deposition/Fill (All Types)", + }, + {"type_key": "COM", "subtype_key": "24", "value": "Golf Course"}, + {"type_key": "AGR", "subtype_key": "25", "value": "Grain & Forage"}, + { + "type_key": "TRA", + "subtype_key": "25", + "value": "Electrical Power Distribution Systems", + }, + {"type_key": "AGR", "subtype_key": "26", "value": "Greenhouses"}, + { + "type_key": "TRA", + "subtype_key": "26", + "value": "Electrical Power Facilities", + }, + {"type_key": "COM", "subtype_key": "28", "value": "Exhibitions and Festivals"}, + { + "type_key": "RES", + "subtype_key": "29", + "value": "Subdivision Special Categories", + }, + {"type_key": "COM", "subtype_key": "30", "value": "Food and Beverage Services"}, + { + "type_key": "RES", + "subtype_key": "30", + "value": "Subdivision Special Categories (Lease)", + }, + {"type_key": "IND", "subtype_key": "31", "value": "Food Processing (non-meat)"}, + {"type_key": "IND", "subtype_key": "32", "value": "Industrial - other"}, + {"type_key": "AGR", "subtype_key": "33", "value": "Land Use Remnant"}, + {"type_key": "AGR", "subtype_key": "34", "value": "Livestock-Unspecified"}, + {"type_key": "IND", "subtype_key": "35", "value": "Logging Operations"}, + { + "type_key": "IND", + "subtype_key": "36", + "value": "Lumber Manufacturing and Re-manufacturing", + }, + { + "type_key": "IND", + "subtype_key": "37", + "value": "Meat and Fish Processing (+abattoir)", + }, + {"type_key": "IND", "subtype_key": "38", "value": "Mining"}, + {"type_key": "AGR", "subtype_key": "39", "value": "Misc. Agricultural Use"}, + {"type_key": "AGR", "subtype_key": "40", "value": "Mixed Ag Uses"}, + {"type_key": "OTH", "subtype_key": "41", "value": "Mixed Uses"}, + {"type_key": "RES", "subtype_key": "42", "value": "Mobile Home Park"}, + { + "type_key": "RES", + "subtype_key": "43", + "value": "Multi Family-Apartments/Condominiums", + }, + { + "type_key": "COM", + "subtype_key": "44", + "value": "Office Building (Primary Use)", + }, + {"type_key": "IND", "subtype_key": "45", "value": "Oil and Gas Activities"}, + {"type_key": "OTH", "subtype_key": "46", "value": "Other Uses"}, + {"type_key": "AGR", "subtype_key": "47", "value": "Other-Undefined"}, + {"type_key": "REC", "subtype_key": "48", "value": "Parks & Playing Fields"}, + { + "type_key": "CIV", + "subtype_key": "49", + "value": "Parks-All Types operated by Local Gov't", + }, + {"type_key": "AGR", "subtype_key": "50", "value": "Pigs/Hogs"}, + {"type_key": "AGR", "subtype_key": "51", "value": "Poultry"}, + { + "type_key": "TRA", + "subtype_key": "52", + "value": "Public Transportation Facilities", + }, + {"type_key": "TRA", "subtype_key": "53", "value": "Railway"}, + {"type_key": "REC", "subtype_key": "54", "value": "Recreational - other"}, + {"type_key": "CIV", "subtype_key": "55", "value": "Research Facilities"}, + {"type_key": "RES", "subtype_key": "56", "value": "Residential - other"}, + {"type_key": "TRA", "subtype_key": "57", "value": "Roads"}, + {"type_key": "IND", "subtype_key": "58", "value": "Sand & Gravel"}, + {"type_key": "CIV", "subtype_key": "59", "value": "Sanitary Land Fills"}, + {"type_key": "CIV", "subtype_key": "60", "value": "Schools & Universities"}, + { + "type_key": "TRA", + "subtype_key": "61", + "value": "Sewage Treatment Facilities", + }, + {"type_key": "TRA", "subtype_key": "62", "value": "Sewer Distribution Systems"}, + {"type_key": "COM", "subtype_key": "63", "value": "Shopping Centre"}, + {"type_key": "AGR", "subtype_key": "64", "value": "Small Fruits-Berries"}, + { + "type_key": "COM", + "subtype_key": "65", + "value": "Sports Facilities - commercial", + }, + { + "type_key": "REC", + "subtype_key": "66", + "value": "Sports Facilities - municipal", + }, + {"type_key": "COM", "subtype_key": "67", "value": "Storage & Warehouse"}, + {"type_key": "COM", "subtype_key": "68", "value": "Store (Retail - All Types)"}, + { + "type_key": "TRA", + "subtype_key": "69", + "value": "Telephone and Telecommunications", + }, + {"type_key": "COM", "subtype_key": "70", "value": "Tourist Accommodations"}, + {"type_key": "REC", "subtype_key": "71", "value": "Trails"}, + {"type_key": "TRA", "subtype_key": "72", "value": "Transportation - other"}, + {"type_key": "AGR", "subtype_key": "73", "value": "Tree Fruits"}, + {"type_key": "AGR", "subtype_key": "74", "value": "Turf Farm"}, + {"type_key": "AGR", "subtype_key": "75", "value": "Vegetable & Truck"}, + { + "type_key": "AGR", + "subtype_key": "76", + "value": "Vineyard and Associated Uses", + }, + {"type_key": "TRA", "subtype_key": "77", "value": "Water Distribution Systems"}, + { + "type_key": "TRA", + "subtype_key": "78", + "value": "Water or Sewer Distribution Systems (inactive)", + }, + {"type_key": "TRA", "subtype_key": "79", "value": "Water Treatment Facilities"}, + {"type_key": "COM", "subtype_key": "80", "value": "Hall/Lodge (private)_"}, + { + "type_key": "CIV", + "subtype_key": "81", + "value": "Hospitals, Health Centres (Incl Private)", + }, + {"type_key": "RES", "subtype_key": "82", "value": "Farm Help Accommodation"}, + { + "type_key": "TRA", + "subtype_key": "27", + "value": "Gas and Other Distribution Pipelines", + }, + {"type_key": "AGR", "subtype_key": "23", "value": "Energy Production"}, + {"type_key": "IND", "subtype_key": "25", "value": "Energy Production"}, + {"type_key": "RES", "subtype_key": "83", "value": "Lease"}, + { + "type_key": "IND", + "subtype_key": "86", + "value": "Storage and Warehouse Facilities (Indoor/Outdoor- Large Scale Structures)", + }, + { + "type_key": "IND", + "subtype_key": "84", + "value": "Work Camps or Associated Use", + }, + {"type_key": "IND", "subtype_key": "85", "value": "Miscellaneous Processing"}, + {"type_key": "COM", "subtype_key": "87", "value": "Events"}, + {"type_key": "IND", "subtype_key": "88", "value": "Sawmill"}, + { + "type_key": "CIV", + "subtype_key": "89", + "value": "Fire Hall and associated uses", + }, + { + "type_key": "AGR", + "subtype_key": "90", + "value": "Alcohol Production Associated Uses", + }, + {"type_key": "AGR", "subtype_key": "91", "value": "Cannabis Related Uses"}, + ] \ No newline at end of file diff --git a/bin/migrate-oats-data/common/oats_application_enum.py b/bin/migrate-oats-data/common/oats_application_enum.py deleted file mode 100644 index 1a88319a57..0000000000 --- a/bin/migrate-oats-data/common/oats_application_enum.py +++ /dev/null @@ -1,29 +0,0 @@ -from enum import Enum - - -class ALRChangeCode(Enum): - TUR = "TUR" # Transport, Utility, and Recreation - INC = "INC" # Inclusion - EXC = "EXC" # Exclusion - SDV = "SDV" # Subdivision - NFU = "NFU" # Non Farm Use - SCH = "SCH" # Extraction and Fill - EXT = "EXT" # Extraction - FILL = "FILL" # Fill - SRW = "SRW" # Notification of Statutory Right of Way - CSC = "CSC" # Conservation Covenant - NAR = "NAR" # Non-Adhering Residential Use - - -class OatsCapabilitySourceCode(Enum): - BCLI = "BCLI" - CLI = "CLI" - ONSI = "ONSI" - - -class OatsAgriCapabilityCodes(Enum): - P = "P" - PD = "PD" - MIX = "MIX" - S = "S" - U = "U" diff --git a/bin/migrate-oats-data/migrate.py b/bin/migrate-oats-data/migrate.py index 2c2d6748ee..86e7e4fa8b 100644 --- a/bin/migrate-oats-data/migrate.py +++ b/bin/migrate-oats-data/migrate.py @@ -17,6 +17,8 @@ import_batch_size = BATCH_UPLOAD_SIZE +# TODO tidy import menu setup + def application_import_command_parser(import_batch_size, subparsers): application_import_command = subparsers.add_parser( @@ -63,6 +65,21 @@ def application_document_import_command_parser(import_batch_size, subparsers): application_document_import_command.set_defaults(func=import_batch_size) +def app_prep_import_command_parser(import_batch_size, subparsers): + app_prep_import_command = subparsers.add_parser( + "app-prep-import", + help=f"Import App prep into ALCS (update applications table) in specified batch size: (default: {import_batch_size})", + ) + app_prep_import_command.add_argument( + "--batch-size", + type=int, + default=import_batch_size, + metavar="", + help=f"batch size (default: {import_batch_size})", + ) + app_prep_import_command.set_defaults(func=import_batch_size) + + def import_command_parser(subparsers): import_command = subparsers.add_parser( "import", @@ -86,6 +103,7 @@ def setup_menu_args_parser(import_batch_size): application_import_command_parser(import_batch_size, subparsers) document_import_command_parser(import_batch_size, subparsers) application_document_import_command_parser(import_batch_size, subparsers) + app_prep_import_command_parser(import_batch_size, subparsers) import_command_parser(subparsers) subparsers.add_parser("clean", help="Clean all imported data") @@ -167,6 +185,19 @@ def setup_menu_args_parser(import_batch_size): ) process_application_documents(batch_size=import_batch_size) + case "app-prep-import": + console.log("Beginning OATS -> ALCS app-prep import process") + with console.status( + "[bold green]App prep import (applications table update in ALCS)..." + ) as status: + if args.batch_size: + import_batch_size = args.batch_size + + console.log( + f"Processing app-prep import in batch size = {import_batch_size}" + ) + + process_alcs_application_prep_fields(batch_size=import_batch_size) finally: if connection_pool: From 4b76e81a7a51c57f56664959be838fa91bd654e8 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 09:21:53 -0700 Subject: [PATCH 150/954] commit python changes --- .../applications/noi_temp.py | 4 ++ bin/migrate-oats-data/noi/noi.py | 54 ++++++++++++++++++- bin/migrate-oats-data/noi/sql/insert_noi.sql | 39 ++++++-------- 3 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 bin/migrate-oats-data/applications/noi_temp.py diff --git a/bin/migrate-oats-data/applications/noi_temp.py b/bin/migrate-oats-data/applications/noi_temp.py new file mode 100644 index 0000000000..d114d3333b --- /dev/null +++ b/bin/migrate-oats-data/applications/noi_temp.py @@ -0,0 +1,4 @@ + +LG_DICT = [ {"key" : 'Islands Trust Gabriola Island', "value" : 'Islands Trust Gabriola Island (Historical)'}, + {"key" : 'Islands Trust Galiano Island', "value" : 'Islands Trust Galiano Island (Historical)'}, + {"key" : 'Islands Trust Gambier Island', "value" : 'Islands Trust Gambier Island (Historical)'}] \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index 8084020e21..16170c3623 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -69,4 +69,56 @@ def process_nois(conn=None, batch_size=10000): print("Total amount of successful inserts:", successful_inserts_count) - print("Total failed inserts:", failed_inserts) \ No newline at end of file + print("Total failed inserts:", failed_inserts) + +def applicant_lookup(): + query = """ + SELECT DISTINCT + oaap.alr_application_id AS application_id, + string_agg (DISTINCT oo.organization_name, ', ') FILTER ( + WHERE + oo.organization_name IS NOT NULL + ) AS orgs, + string_agg ( + DISTINCT concat (op.first_name || ' ' || op.last_name), + ', ' + ) FILTER ( + WHERE + op.last_name IS NOT NULL + OR op.first_name IS NOT NULL + ) AS persons + FROM + oats.oats_alr_application_parties oaap + LEFT JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + LEFT JOIN oats.oats_persons op ON op.person_id = opo.person_id + LEFT JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oaap.alr_appl_role_code = 'APPL' + GROUP BY + oaap.alr_application_id + """ + return query + +def oats_gov(): + query = """ + SELECT + oaap.alr_application_id AS application_id, + oo.organization_name AS oats_gov_name + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oo.organization_type_cd IN ('MUNI','FN','RD') + """ + return query + +def alcs_gov(): + query = """ + SELECT + oats_gov.application_id AS application_id, + alg.uuid AS gov_uuid + FROM + oats_gov + JOIN alcs.application_local_government alg on oats_gov.oats_gov_name = alg."name" + """ \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index 5022345d80..7644a2347d 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -1,26 +1,21 @@ WITH - applicant_lookup AS ( - SELECT DISTINCT - oaap.alr_application_id AS application_id, - string_agg (DISTINCT oo.organization_name, ', ') FILTER ( - WHERE - oo.organization_name IS NOT NULL - ) AS orgs, - string_agg ( - DISTINCT concat (op.first_name || ' ' || op.last_name), - ', ' - ) FILTER ( - WHERE - op.last_name IS NOT NULL - OR op.first_name IS NOT NULL - ) AS persons + noi_grouped AS ( + SELECT + oaac.alr_application_id FROM - oats.oats_alr_application_parties oaap - LEFT JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - LEFT JOIN oats.oats_persons op ON op.person_id = opo.person_id - LEFT JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + oats.oats_alr_appl_components oaac + JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = oaac.alr_application_id WHERE - oaap.alr_appl_role_code = 'APPL' + oaa.application_class_code IN ('NOI') GROUP BY - oaap.alr_application_id - ), \ No newline at end of file + oaac.alr_application_id + HAVING + count(oaac.alr_application_id) < 2 -- ignore all applications wit multiple components + ) +SELECT + oaa.alr_application_id, + oaac.alr_change_code +FROM + noi_grouped noi + JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = noi.alr_application_id + JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = noi.alr_application_id \ No newline at end of file From 1e2c000706743e9700c2d54dc42b190131ec2e2c Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 09:43:07 -0700 Subject: [PATCH 151/954] starting sql implementation --- bin/migrate-oats-data/noi/sql/insert_noi.sql | 131 ++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index 7644a2347d..6074746059 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -1,7 +1,7 @@ WITH noi_grouped AS ( SELECT - oaac.alr_application_id + oaac.alr_application_id as noi_application_id FROM oats.oats_alr_appl_components oaac JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = oaac.alr_application_id @@ -11,6 +11,135 @@ WITH oaac.alr_application_id HAVING count(oaac.alr_application_id) < 2 -- ignore all applications wit multiple components + ), + applicant_lookup AS ( + SELECT DISTINCT + oaap.alr_application_id AS application_id, + string_agg (DISTINCT oo.organization_name, ', ') FILTER ( + WHERE + oo.organization_name IS NOT NULL + ) AS orgs, + string_agg ( + DISTINCT concat (op.first_name || ' ' || op.last_name), + ', ' + ) FILTER ( + WHERE + op.last_name IS NOT NULL + OR op.first_name IS NOT NULL + ) AS persons + FROM + oats.oats_alr_application_parties oaap + LEFT JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + LEFT JOIN oats.oats_persons op ON op.person_id = opo.person_id + LEFT JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oaap.alr_appl_role_code = 'APPL' + GROUP BY + oaap.alr_application_id + ), + -- Step 2: get local gov application name & match to uuid + oats_gov AS ( + SELECT + oaap.alr_application_id AS application_id, + oo.organization_name AS oats_gov_name + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oo.organization_type_cd IN ('MUNI', 'FN', 'RD') + ), + alcs_gov AS ( + SELECT + oats_gov.application_id AS application_id, + alg.uuid AS gov_uuid + FROM + oats_gov + JOIN alcs.application_local_government alg on ( + CASE + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gabriola Island' THEN 'Islands Trust Gabriola Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Galiano Island' THEN 'Islands Trust Galiano Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gambier Island' THEN 'Islands Trust Gambier Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Hornby Island' THEN 'Islands Trust Hornby Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Lasqueti Island' THEN 'Islands Trust Lasqueti Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Mayne Island' THEN 'Islands Trust Mayne Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Pender Island' THEN 'Islands Trust Pender Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Quadra Island' THEN 'Islands Trust Quadra Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Salt Spring Island' THEN 'Islands Trust Salt Spring Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Sunshine Coast' THEN 'Islands Trust Sunshine Coast (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Bowen Island' THEN 'Bowen Island (Island Municipality)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Denman Island' THEN 'Islands Trust Denman Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' + WHEN oats_gov.oats_gov_name LIKE 'Thompson Nicola%' THEN 'Thompson Nicola Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Cariboo%' THEN 'Cariboo Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Fraser Valley%' THEN 'Fraser Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Columbia Shuswap%' THEN 'Columbia Shuswap Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Okanagan%' THEN 'Central Okanagan Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Squamish Lillooet%' THEN 'Squamish Lillooet Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Alberni-Clayoquot%' THEN 'Alberni-Clayoquot Regional District' + WHEN oats_gov.oats_gov_name LIKE 'qathet%' THEN 'qathet Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Peace River%' THEN 'Peace River Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Okanagan Similkameen%' THEN 'Okanagan Similkameen Regional District' + WHEN oats_gov.oats_gov_name LIKE 'East Kootenay%' THEN 'East Kootenay Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Bulkley-Nechako%' THEN 'Bulkley-Nechako Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Sunshine Coast%' THEN 'Sunshine Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Nanaimo%' THEN 'Nanaimo Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Kitimat Stikine%' THEN 'Kitimat Stikine Regional District' + WHEN oats_gov.oats_gov_name LIKE 'North Okanagan%' THEN 'North Okanagan Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Fraser Fort George%' THEN 'Fraser Fort George Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Cowichan Valley%' THEN 'Cowichan Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Kootenay Boundary%' THEN 'Kootenay Boundary Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Comox Valley%' THEN 'Comox Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Kootenay%' THEN 'Central Kootenay Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Capital%' THEN 'Capital Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Metro Vancouver%' THEN 'Metro Vancouver Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Coast%' THEN 'Central Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'North Coast%' THEN 'North Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Strathcona%' THEN 'Strathcona Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Mount Waddington%' THEN 'Mount Waddington Regional District' + ELSE oats_gov.oats_gov_name + END + ) = alg."name" + ), + -- Step 3: Perform a lookup to retrieve the region code for each application ID + panel_lookup AS ( + SELECT DISTINCT + oaap.alr_application_id AS application_id, + CASE + WHEN oo2.parent_organization_id IS NULL THEN oo2.organization_name + WHEN oo3.parent_organization_id IS NULL THEN oo3.organization_name + ELSE 'NONE' + END AS panel_region + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + LEFT JOIN oats.oats_organizations oo2 ON oo.parent_organization_id = oo2.organization_id + LEFT JOIN oats.oats_organizations oo3 ON oo2.parent_organization_id = oo3.organization_id + WHERE + oo2.organization_type_cd = 'PANEL' + OR oo3.organization_type_cd = 'PANEL' + ), + -- Step 4: Perform lookup to retrieve type code + application_type_lookup AS ( + SELECT + oaac.alr_application_id AS application_id, + oacc."description" AS "description", + oaac.alr_change_code AS code + FROM + oats.oats_alr_appl_components AS oaac + JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code + LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id + WHERE + aee.component_id IS NULL ) SELECT oaa.alr_application_id, From a1cfae2b73e3d78c3c7cd478df516bdec51cf1d4 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 26 Jul 2023 17:24:35 -0700 Subject: [PATCH 152/954] LFNG As applicant bug fixes * Check if submission is to own users government to control copying * Add error to step 8 when missing Department * Fix logic in primary contact to show errors at the right time --- .../application-details/application-details.component.html | 5 ++++- .../primary-contact/primary-contact.component.html | 2 +- .../primary-contact/primary-contact.component.ts | 7 ++----- .../application-submission-review.controller.ts | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/application-details/application-details.component.html index 65529b59be..fc85a50f39 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/application-details/application-details.component.html @@ -48,7 +48,10 @@

3. Primary Contact

{{ primaryContact?.organizationName }} - +
Phone
diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html index dd83477975..8a32cf1e4b 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html @@ -160,7 +160,7 @@
Authorization Letters (if applicable)
(uploadFiles)="attachFile($event, DOCUMENT_TYPE.AUTHORIZATION_LETTER)" (deleteFile)="onDeleteFile($event)" (openFile)="openFile($event)" - [showErrors]="showErrors || selectedLocalGovernment" + [showErrors]="showErrors" [isRequired]="needsAuthorizationLetter" >
diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index 56f837e845..bd05e3c1a7 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -143,9 +143,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } private calculateLetterRequired() { - const isSelfApplicant = - this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || - this.owners[0].type.code === APPLICATION_OWNER.GOVERNMENT; + const isSelfApplicant = this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || this.selectedLocalGovernment; this.needsAuthorizationLetter = this.selectedThirdPartyAgent || @@ -226,8 +224,6 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni phoneNumber: selectedOwner.phoneNumber, email: selectedOwner.email, }); - - this.calculateLetterRequired(); } else if (selectedOwner) { this.onSelectOwner(selectedOwner.uuid); } else { @@ -246,6 +242,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni this.form.markAllAsTouched(); } this.isDirty = false; + this.calculateLetterRequired(); } } diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index 3448114556..16fa1c1ae8 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -205,8 +205,8 @@ export class ApplicationSubmissionReviewController { } if ( - primaryContact && - primaryContact.type.code === APPLICATION_OWNER.GOVERNMENT + userLocalGovernment.uuid === applicationSubmission.localGovernmentUuid && + primaryContact ) { //Copy contact details over to government form await this.applicationSubmissionReviewService.update( From 6f65cd536d7919d04d70de9de93a91c5131717a9 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 26 Jul 2023 16:40:21 -0700 Subject: [PATCH 153/954] Add inclusion fields when user is LFNG * Add question to indicate parcel ownership and store in DB * Add more file uploads if the answer is no --- .../application-details.component.spec.ts | 1 + .../application-submission.service.spec.ts | 1 + .../services/application/application.dto.ts | 1 + .../incl-proposal.component.html | 114 +++++++++++++++++- .../incl-proposal.component.scss | 9 ++ .../incl-proposal.component.spec.ts | 14 ++- .../incl-proposal/incl-proposal.component.ts | 46 ++++++- .../application-submission.dto.ts | 2 + .../application-submission.dto.ts | 7 ++ .../application-submission.entity.ts | 4 + .../application-submission.service.ts | 4 + ...dd_government_incl_fields_to_submission.ts | 19 +++ 12 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1690414066995-add_government_incl_fields_to_submission.ts diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts index 33e28b6222..39de635f82 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts @@ -29,6 +29,7 @@ describe('ApplicationDetailsComponent', () => { component = fixture.componentInstance; component.submission = { exclShareGovernmentBorders: null, + inclGovernmentOwnsAllParcels: null, exclWhyLand: null, inclAgricultureSupport: null, inclExclHectares: null, diff --git a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts index 2db85d84bd..63630b4421 100644 --- a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts @@ -105,6 +105,7 @@ describe('ApplicationSubmissionService', () => { inclAgricultureSupport: null, inclImprovements: null, exclShareGovernmentBorders: null, + inclGovernmentOwnsAllParcels: null, exclWhyLand: null, inclExclHectares: null, prescribedBody: null, diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 41f4445cb3..91199eea36 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -220,6 +220,7 @@ export interface ApplicationSubmissionDto { inclAgricultureSupport: string | null; inclImprovements: string | null; exclShareGovernmentBorders: boolean | null; + inclGovernmentOwnsAllParcels: boolean | null; } export interface ApplicationDto { diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html index 17d3efc0de..c7463b9985 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html @@ -91,7 +91,7 @@

Proposal

Example: 40 ha of grazing land fenced in 2010.
- Characters left: {{ 4000 - outsideLandsText.textLength }} + Characters left: {{ 4000 - improvementsText.textLength }}
warning
This field is required
@@ -118,9 +118,119 @@

Proposal

[isRequired]="true" >
+
+ +
+ The ALR General Regulation does not require the {{ governmentName }} to complete a public hearing if all + inclusion application parcel(s) are owned by the {{ governmentName }}. +
+ Please refer to Inclusion page on the ALC website for more information. + + Yes + No + +
+ warning +
This field is required
+
+
+
+

Notification and Public Hearing Requirements

+

+ A printed copy of the application will need to be used for notification. Please ensure all prior fields are complete + and correct before downloading the PDF of the application (Step 8 of this application form will flag outstanding + fields). +

+ + + + You will not be able to complete the remaining portion of the application until the notification and public hearing + process is complete. + +
+
+
+ +
+ Proof that notice of the application was provided in a form and manner acceptable to the Commission +
+ +
+
+ +
+ Proof that a sign, in a form and manner acceptable to the Commission, was posted on the land that is the + subject of the application +
+ +
+
+ +
Public hearing report and any other public comments received
+ +
+
+
+
+
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss index e69de29bb2..b435af700d 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss @@ -0,0 +1,9 @@ +@use '../../../../../styles/functions' as *; + +section { + margin-top: rem(32); +} + +.requirement-description { + margin: rem(8) 0 !important; +} diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts index a0b183b23d..bda063fcdc 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts @@ -1,8 +1,10 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; -import { DeepMocked } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; +import { UserDto } from '../../../../services/authentication/authentication.dto'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; import { InclProposalComponent } from './incl-proposal.component'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; @@ -14,8 +16,14 @@ describe('InclProposalComponent', () => { let fixture: ComponentFixture; let mockApplicationService: DeepMocked; let mockAppDocumentService: DeepMocked; + let mockAuthService: DeepMocked; beforeEach(async () => { + mockApplicationService = createMock(); + mockAppDocumentService = createMock(); + mockAuthService = createMock(); + mockAuthService.$currentProfile = new BehaviorSubject(undefined); + await TestBed.configureTestingModule({ providers: [ { @@ -26,6 +34,10 @@ describe('InclProposalComponent', () => { provide: ApplicationDocumentService, useValue: mockAppDocumentService, }, + { + provide: AuthenticationService, + useValue: mockAuthService, + }, { provide: MatDialog, useValue: {}, diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts index 6f320455a1..1716091ead 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts @@ -1,4 +1,8 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; +import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; import { FilesStepComponent } from '../../files-step.partial'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; @@ -19,13 +23,17 @@ import { ApplicationSubmissionUpdateDto } from '../../../../services/application }) export class InclProposalComponent extends FilesStepComponent implements OnInit, OnDestroy { DOCUMENT = DOCUMENT_TYPE; - currentStep = EditApplicationSteps.Proposal; + private submissionUuid = ''; + isGovernmentUser = false; + governmentName? = ''; + disableNotificationFileUploads = false; hectares = new FormControl(null, [Validators.required]); purpose = new FormControl(null, [Validators.required]); agSupport = new FormControl(null, [Validators.required]); improvements = new FormControl(null, [Validators.required]); + governmentOwnsAllParcels = new FormControl(undefined, [Validators.required]); form = new FormGroup({ hectares: this.hectares, @@ -33,11 +41,15 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, agSupport: this.agSupport, improvements: this.improvements, }); - private submissionUuid = ''; + proposalMap: ApplicationDocumentDto[] = []; + noticeOfPublicHearing: ApplicationDocumentDto[] = []; + proofOfSignage: ApplicationDocumentDto[] = []; + reportOfPublicHearing: ApplicationDocumentDto[] = []; constructor( private applicationSubmissionService: ApplicationSubmissionService, + private authenticationService: AuthenticationService, applicationDocumentService: ApplicationDocumentService, dialog: MatDialog ) { @@ -57,6 +69,13 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, improvements: applicationSubmission.inclImprovements, }); + if (applicationSubmission.inclGovernmentOwnsAllParcels !== null) { + this.governmentOwnsAllParcels.setValue( + formatBooleanToString(applicationSubmission.inclGovernmentOwnsAllParcels) + ); + this.disableNotificationFileUploads = applicationSubmission.inclGovernmentOwnsAllParcels; + } + if (this.showErrors) { this.form.markAllAsTouched(); } @@ -65,6 +84,23 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, this.$applicationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.noticeOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_ADVERTISING + ); + this.proofOfSignage = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_SIGNAGE); + this.reportOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING + ); + }); + + this.authenticationService.$currentProfile.pipe(takeUntil(this.$destroy)).subscribe((userProfile) => { + if (userProfile) { + this.isGovernmentUser = userProfile?.isLocalGovernment || userProfile?.isFirstNationGovernment; + this.governmentName = userProfile.government; + + // @ts-ignore Angular / Typescript hate dynamic controls + this.form.addControl('isLFNGOwnerOfAllParcels', this.governmentOwnsAllParcels); + } }); } @@ -78,16 +114,22 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, const purpose = this.purpose.value; const inclAgricultureSupport = this.agSupport.value; const inclImprovements = this.improvements.value; + const inclGovernmentOwnsAllParcels = this.governmentOwnsAllParcels.value; const updateDto: ApplicationSubmissionUpdateDto = { inclExclHectares: inclExclHectares ? parseFloat(inclExclHectares) : null, purpose, inclAgricultureSupport, inclImprovements, + inclGovernmentOwnsAllParcels: parseStringToBoolean(inclGovernmentOwnsAllParcels), }; const updatedApp = await this.applicationSubmissionService.updatePending(this.submissionUuid, updateDto); this.$applicationSubmission.next(updatedApp); } } + + onSelectLocalGovernmentParcelOwner($event: MatButtonToggleChange) { + this.disableNotificationFileUploads = $event.value === 'true'; + } } diff --git a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts index 29056823d8..c831b683f0 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts @@ -154,6 +154,7 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD inclAgricultureSupport: string | null; inclImprovements: string | null; exclShareGovernmentBorders: boolean | null; + inclGovernmentOwnsAllParcels: boolean | null; } export interface ApplicationSubmissionUpdateDto { @@ -259,4 +260,5 @@ export interface ApplicationSubmissionUpdateDto { inclAgricultureSupport?: string | null; inclImprovements?: string | null; exclShareGovernmentBorders?: boolean | null; + inclGovernmentOwnsAllParcels?: boolean | null; } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts index e8e271ddf0..249009cbc4 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts @@ -309,6 +309,9 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { @AutoMap(() => Boolean) exclShareGovernmentBorders: boolean | null; + + @AutoMap(() => Boolean) + inclGovernmentOwnsAllParcels?: boolean | null; } export class ApplicationSubmissionCreateDto { @@ -695,4 +698,8 @@ export class ApplicationSubmissionUpdateDto { @IsBoolean() @IsOptional() exclShareGovernmentBorders?: boolean | null; + + @IsBoolean() + @IsOptional() + inclGovernmentOwnsAllParcels?: boolean | null; } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts index 8c53d774e7..9e67aabbc1 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts @@ -695,6 +695,10 @@ export class ApplicationSubmission extends Base { @Column({ type: 'boolean', nullable: true }) exclShareGovernmentBorders: boolean | null; + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + inclGovernmentOwnsAllParcels: boolean | null; + //END SUBMISSION FIELDS @AutoMap(() => Application) diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index fff750c23b..7406abff08 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -969,6 +969,10 @@ export class ApplicationSubmissionService { updateDto.exclShareGovernmentBorders, applicationSubmission.exclShareGovernmentBorders, ); + applicationSubmission.inclGovernmentOwnsAllParcels = filterUndefined( + updateDto.inclGovernmentOwnsAllParcels, + applicationSubmission.inclGovernmentOwnsAllParcels, + ); } async listNaruSubtypes() { diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690414066995-add_government_incl_fields_to_submission.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690414066995-add_government_incl_fields_to_submission.ts new file mode 100644 index 0000000000..f4264e8c35 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690414066995-add_government_incl_fields_to_submission.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addGovernmentInclFieldsToSubmission1690414066995 + implements MigrationInterface +{ + name = 'addGovernmentInclFieldsToSubmission1690414066995'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "incl_government_owns_all_parcels" boolean`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "incl_government_owns_all_parcels"`, + ); + } +} From d6b6465a74386cee2af628c11158071060d07709 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:48:54 -0700 Subject: [PATCH 154/954] nfu subtype with inline ng select (#826) --- .../proposal/nfu/nfu.component.html | 6 +- .../proposal/nfu/nfu.component.scss | 4 ++ .../inline-ng-select.component.html | 36 ++++++++++++ .../inline-ng-select.component.scss | 58 +++++++++++++++++++ .../inline-ng-select.component.spec.ts | 24 ++++++++ .../inline-ng-select.component.ts | 54 +++++++++++++++++ alcs-frontend/src/app/shared/shared.module.ts | 4 ++ 7 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.html create mode 100644 alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.scss create mode 100644 alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.ts diff --git a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html index 6b81c0bf65..5d270e97f9 100644 --- a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html +++ b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.html @@ -6,13 +6,13 @@ (save)="updateApplicationValue('nfuUseType', $event)" >
-
+
Non-Farm Use Sub-Type
- + >
Use End Date
diff --git a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.scss b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.scss index 229ba6c084..2f69c7774f 100644 --- a/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.scss +++ b/alcs-frontend/src/app/features/application/proposal/nfu/nfu.component.scss @@ -1,3 +1,7 @@ div { margin: 32px 0; } + +.nfu-subtype-wrapper { + width: 25%; +} diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.html new file mode 100644 index 0000000000..d49670474e --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.html @@ -0,0 +1,36 @@ +
+ + Select an option + Select option(s) + + {{ value }} + + + {{ coerceArray(value)?.join(', ') }} + + + +
+
+ +
+
+ + +
+
+
diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.scss new file mode 100644 index 0000000000..1eef4be4cd --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.scss @@ -0,0 +1,58 @@ +@use '../../../../styles/colors'; + +.inline-number-wrapper { + padding-top: 4px; +} + +.editing { + display: inline-block; + width: 100%; +} + +.editing.hidden { + display: none; +} + +.edit-button { + height: 24px; + width: 24px; + display: flex; + align-items: center; +} + +.edit-icon { + font-size: inherit; + line-height: 22px; +} + +.button-container { + display: flex; + justify-content: flex-end; + + button:not(:last-child) { + margin-right: 2px !important; + } +} + +.add { + cursor: pointer; +} + +.save { + color: colors.$primary-color; +} + +:host::ng-deep { + .mat-form-field-wrapper { + padding: 0 !important; + margin: 0 !important; + } + + button mat-icon { + overflow: visible; + } + + .mat-mdc-icon-button.mat-mdc-button-base { + padding: 0 !important; + } +} diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.spec.ts new file mode 100644 index 0000000000..7984b8d927 --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InlineNgSelectComponent } from './inline-ng-select.component'; + +describe('InlineDropdownComponent', () => { + let component: InlineNgSelectComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [InlineNgSelectComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(InlineNgSelectComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.ts new file mode 100644 index 0000000000..1d6e2c709b --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-ng-select/inline-ng-select.component.ts @@ -0,0 +1,54 @@ +import { AfterContentChecked, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; + +@Component({ + selector: 'app-inline-ng-select[options][value]', + templateUrl: './inline-ng-select.component.html', + styleUrls: ['./inline-ng-select.component.scss'], +}) +export class InlineNgSelectComponent implements AfterContentChecked { + @Input() value?: string | string[] | undefined; + @Input() placeholder: string = 'Enter a value'; + @Input() options: { label: string; value: string }[] = []; + @Input() multiple = false; + + @Output() save = new EventEmitter(); + + @ViewChild('editInput') textInput!: ElementRef; + + isEditing = false; + pendingValue: undefined | string | string[]; + + constructor() {} + + startEdit() { + this.isEditing = true; + this.pendingValue = this.value; + } + + ngAfterContentChecked(): void { + if (this.textInput) { + this.textInput.nativeElement.focus(); + } + } + + confirmEdit() { + if (this.pendingValue !== this.value) { + this.save.emit(this.pendingValue ?? null); + this.value = this.pendingValue; + } + + this.isEditing = false; + } + + cancelEdit() { + this.isEditing = false; + this.pendingValue = this.value; + } + + coerceArray(value: string | string[] | undefined) { + if (value instanceof Array) { + return value; + } + return undefined; + } +} diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index d60257ce20..756471ca40 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -47,6 +47,7 @@ import { InlineNumberComponent } from './inline-editors/inline-number/inline-num import { InlineReviewOutcomeComponent } from './inline-editors/inline-review-outcome/inline-review-outcome.component'; import { InlineTextComponent } from './inline-editors/inline-text/inline-text.component'; import { InlineTextareaComponent } from './inline-editors/inline-textarea/inline-textarea.component'; +import { InlineNgSelectComponent } from './inline-editors/inline-ng-select/inline-ng-select.component'; import { LotsTableFormComponent } from './lots-table/lots-table-form.component'; import { MeetingOverviewComponent } from './meeting-overview/meeting-overview.component'; import { NoDataComponent } from './no-data/no-data.component'; @@ -94,6 +95,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen WarningBannerComponent, ErrorMessageComponent, LotsTableFormComponent, + InlineNgSelectComponent, ], imports: [ CommonModule, @@ -117,6 +119,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen RouterModule, MatDatepickerModule, MatDialogModule, + NgSelectModule, ], exports: [ CommonModule, @@ -150,6 +153,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen InlineNumberComponent, InlineTextComponent, InlineDropdownComponent, + InlineNgSelectComponent, MatAutocompleteModule, MatButtonToggleModule, DetailsHeaderComponent, From 9e8af39990a000a46fe4c674e5bde7178daaf05c Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 27 Jul 2023 10:43:15 -0700 Subject: [PATCH 155/954] Bug fixes for L/FNG again --- .../edit-submission.component.ts | 4 +++ .../primary-contact.component.ts | 25 +++++++++++-------- .../application-submission.service.ts | 4 +-- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts index 17cc530fdd..527a09d860 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts @@ -94,6 +94,10 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit ngOnInit(): void { this.expandedParcelUuid = undefined; + + this.$applicationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + this.applicationSubmission = submission; + }); } ngAfterViewInit(): void { diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts index bd05e3c1a7..fedf84f5fa 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts @@ -143,17 +143,20 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } private calculateLetterRequired() { - const isSelfApplicant = this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || this.selectedLocalGovernment; - - this.needsAuthorizationLetter = - this.selectedThirdPartyAgent || - !( - isSelfApplicant && - (this.owners.length === 1 || - (this.owners.length === 2 && - this.owners[1].type.code === APPLICATION_OWNER.AGENT && - !this.selectedThirdPartyAgent)) - ); + if (this.selectedLocalGovernment) { + this.needsAuthorizationLetter = false; + } else { + const isSelfApplicant = this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL; + this.needsAuthorizationLetter = + this.selectedThirdPartyAgent || + !( + isSelfApplicant && + (this.owners.length === 1 || + (this.owners.length === 2 && + this.owners[1].type.code === APPLICATION_OWNER.AGENT && + !this.selectedThirdPartyAgent)) + ); + } this.files = this.files.map((file) => ({ ...file, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index 7406abff08..95a463ec5b 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -77,9 +77,7 @@ export class ApplicationSubmissionService { fileNumber, isDraft: false, }, - relations: { - naruSubtype: true, - }, + relations: this.DEFAULT_RELATIONS, }); if (!application) { throw new Error('Failed to find document'); From c642e1f6eec4bc405e22a123b2a487bfcc02c453 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 27 Jul 2023 11:46:55 -0700 Subject: [PATCH 156/954] Fix Styling for SUBD, Fix Decisions with no components or conditions * Bug fix for header change after checked --- .../decision-input-v2.component.ts | 11 ++++--- .../lots-table/lots-table-form.component.scss | 4 +++ .../lots-table/lots-table-form.component.ts | 30 +++++++++++-------- .../app/shared/header/header.component.html | 10 ++----- .../src/app/shared/header/header.component.ts | 13 ++++---- 5 files changed, 37 insertions(+), 31 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index f2e92007fa..2bc38b496b 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -528,8 +528,8 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { private runValidation() { this.form.markAllAsTouched(); - this.componentsValid = this.componentsValid && this.components.length > 0; - this.conditionsValid = this.conditionsValid && this.conditionUpdates.length > 0; + const requiresConditions = this.showConditions; + const requiresComponents = this.showComponents; if (this.decisionComponentsComponent) { this.decisionComponentsComponent.onValidate(); @@ -543,8 +543,8 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { !this.form.valid || !this.conditionsValid || !this.componentsValid || - (this.components.length === 0 && this.showComponents) || - (this.conditionUpdates.length === 0 && this.showConditions) + (this.components.length === 0 && requiresComponents) || + (this.conditionUpdates.length === 0 && requiresConditions) ) { this.form.controls.decisionMaker.markAsDirty(); this.toastService.showErrorToast('Please correct all errors before submitting the form'); @@ -594,9 +594,8 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { } onConditionsChange($event: { conditions: UpdateApplicationDecisionConditionDto[]; isValid: boolean }) { - this.conditionUpdates = $event.conditions; - this.conditionsValid = $event.isValid; this.conditionUpdates = Array.from($event.conditions); + this.conditionsValid = $event.isValid; } onChangeDecisionOutcome(selectedOutcome: DecisionOutcomeCodeDto) { diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss index e69de29bb2..5aa07a5e2e 100644 --- a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.scss @@ -0,0 +1,4 @@ +fieldset { + border: none; + padding: 0; +} diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts index 9979b53925..c9a65c177e 100644 --- a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts @@ -33,7 +33,7 @@ type ProposedLot = { type: 'Lot' | 'Road Dedication' | null; size: string | null export class LotsTableFormComponent implements ControlValueAccessor, Validator { displayedColumns = ['index', 'type', 'size', 'alrArea']; lotsSource = new MatTableDataSource([]); - lotCount = 0; + lotCount: number | null = null; touched = false; disabled = false; @@ -99,12 +99,14 @@ export class LotsTableFormComponent implements ControlValueAccessor, Validator { this.disabled = disabled; } - writeValue(proposedLots: ProposedLot[]) { + writeValue(proposedLots: ProposedLot[] | null) { this.resetForm(proposedLots ?? []); - this.lotCount = proposedLots.length; + this.lotCount = proposedLots?.length ?? null; let lots = this.mapFormToLots(); this.lotsSource = new MatTableDataSource(lots); - this.count.setValue(this.lotCount.toString()); + if (this.lotCount !== null) { + this.count.setValue(this.lotCount.toString()); + } } validate(control: AbstractControl) { @@ -135,15 +137,17 @@ export class LotsTableFormComponent implements ControlValueAccessor, Validator { private mapFormToLots() { const proposedLots: ProposedLot[] = []; - for (let index = 0; index < this.lotCount; index++) { - const lotType = this.form.controls[`${index}-lotType`].value; - const lotSize = this.form.controls[`${index}-lotSize`].value; - const lotAlrArea = this.form.controls[`${index}-lotAlrArea`].value; - proposedLots.push({ - size: lotSize, - type: lotType, - alrArea: lotAlrArea, - }); + if (this.lotCount !== null) { + for (let index = 0; index < this.lotCount; index++) { + const lotType = this.form.controls[`${index}-lotType`].value; + const lotSize = this.form.controls[`${index}-lotSize`].value; + const lotAlrArea = this.form.controls[`${index}-lotAlrArea`].value; + proposedLots.push({ + size: lotSize, + type: lotType, + alrArea: lotAlrArea, + }); + } } return proposedLots; } diff --git a/portal-frontend/src/app/shared/header/header.component.html b/portal-frontend/src/app/shared/header/header.component.html index a6c5058f6c..acbc8394dc 100644 --- a/portal-frontend/src/app/shared/header/header.component.html +++ b/portal-frontend/src/app/shared/header/header.component.html @@ -9,15 +9,11 @@
Provincial Agricultural Land Commission Portal
- - + +
-
+

Portal Inbox

Log Out

diff --git a/portal-frontend/src/app/shared/header/header.component.ts b/portal-frontend/src/app/shared/header/header.component.ts index 79817826b0..8b86d4843d 100644 --- a/portal-frontend/src/app/shared/header/header.component.ts +++ b/portal-frontend/src/app/shared/header/header.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; import { AuthenticationService } from '../../services/authentication/authentication.service'; @@ -13,13 +13,16 @@ export class HeaderComponent implements OnInit, OnDestroy { isAuthenticated = false; isMenuOpen = false; - constructor(private authenticationService: AuthenticationService, private router: Router) {} + constructor( + private authenticationService: AuthenticationService, + private router: Router, + private changeDetectorRef: ChangeDetectorRef + ) {} ngOnInit(): void { this.authenticationService.$currentTokenUser.pipe(takeUntil(this.$destroy)).subscribe((user) => { - if (user) { - this.isAuthenticated = true; - } + this.isAuthenticated = !!user; + this.changeDetectorRef.detectChanges(); }); } From 5163e35a8e2db56331dc4b1ce1c549c85079a7bf Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Thu, 27 Jul 2023 16:00:56 -0700 Subject: [PATCH 157/954] Add Exclusion applications in ALCS and fix LFNG info (#829) * Add exclusion detail in alcs * Fix LFNG sections in ALCS and portal * Add excl and incl proposal to alcs edit submission * Init excl proposal component and add to app prep * Select and save inclExclApplicantType with inline selector * Update frontend tests * Clean up conditional view to boolean variable --- .../application-details.component.html | 6 ++ .../application-details.module.ts | 2 + .../excl-details/excl-details.component.html | 56 +++++++++++ .../excl-details/excl-details.component.scss | 0 .../excl-details.component.spec.ts | 31 ++++++ .../excl-details/excl-details.component.ts | 45 +++++++++ .../application/application.module.ts | 2 + .../lfng-info/lfng-info.component.html | 99 +++++++++++++++---- .../lfng-info/lfng-info.component.ts | 20 ++++ .../proposal/excl/excl.component.html | 7 ++ .../proposal/excl/excl.component.scss | 0 .../proposal/excl/excl.component.spec.ts | 42 ++++++++ .../proposal/excl/excl.component.ts | 44 +++++++++ .../proposal/proposal.component.html | 1 + .../application-dialog.component.spec.ts | 1 + .../covenant-dialog.component.spec.ts | 1 + .../notice-of-intent-dialog.component.spec.ts | 1 + .../planning-review-dialog.component.spec.ts | 1 + .../application-document.service.ts | 3 + .../application-local-government.dto.ts | 1 + .../services/application/application.dto.ts | 2 + .../application/application.service.spec.ts | 1 + .../inline-applicant-type.component.html | 23 +++++ .../inline-applicant-type.component.scss | 45 +++++++++ .../inline-applicant-type.component.spec.ts | 28 ++++++ .../inline-applicant-type.component.ts | 37 +++++++ alcs-frontend/src/app/shared/shared.module.ts | 3 + .../alcs-edit-submission.component.html | 16 +++ .../alcs-edit-submission.component.ts | 10 ++ .../edit-submission-base.module.ts | 2 + .../review-submit.component.html | 16 ++- .../lfng-review/lfng-review.component.html | 8 +- .../lfng-review/lfng-review.component.ts | 8 ++ .../application/application.controller.ts | 1 + .../src/alcs/application/application.dto.ts | 8 ++ .../alcs/application/application.entity.ts | 8 ++ ...incl_excl_applicant_type_to_application.ts | 23 +++++ 37 files changed, 573 insertions(+), 29 deletions(-) create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/excl-details/excl-details.component.html create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/excl-details/excl-details.component.scss create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/excl-details/excl-details.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/excl-details/excl-details.component.ts create mode 100644 alcs-frontend/src/app/features/application/proposal/excl/excl.component.html create mode 100644 alcs-frontend/src/app/features/application/proposal/excl/excl.component.scss create mode 100644 alcs-frontend/src/app/features/application/proposal/excl/excl.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/proposal/excl/excl.component.ts create mode 100644 alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.html create mode 100644 alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.scss create mode 100644 alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1690485189579-add_incl_excl_applicant_type_to_application.ts diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html index cda1a72718..0a5a4d1b62 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html @@ -172,6 +172,12 @@

Proposal

[applicationSubmission]="submission" [files]="files" > + +
+ +
+
+ + Land Owner + L/FNG Initiated + +
+
+ + +
+
+
diff --git a/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.scss b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.scss new file mode 100644 index 0000000000..9b074fdc29 --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.scss @@ -0,0 +1,45 @@ +@use '../../../styles/colors'; + +.inline-applicant-type-wrapper { + padding-top: 4px; +} + +.editing.hidden { + display: none; +} + +.button-container { + button:not(:last-child) { + margin-right: 2px !important; + } + .save { + color: colors.$primary-color; + } +} + +.edit-button { + height: 24px; + width: 24px; + display: flex; + align-items: center; +} + +.edit-icon { + font-size: inherit; + line-height: 22px; +} + +:host::ng-deep { + .mat-form-field-wrapper { + padding: 0 !important; + margin: 0 !important; + } + + button mat-icon { + overflow: visible; + } + + .mat-mdc-icon-button.mat-mdc-button-base { + padding: 0 !important; + } +} diff --git a/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.spec.ts b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.spec.ts new file mode 100644 index 0000000000..b692b19be4 --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.spec.ts @@ -0,0 +1,28 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { SharedModule } from '../shared.module'; + +import { InlineApplicantTypeComponent } from './inline-applicant-type.component'; + +describe('InlineApplicantTypeComponent', () => { + let component: InlineApplicantTypeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SharedModule, FormsModule, ReactiveFormsModule, MatButtonToggleModule], + declarations: [InlineApplicantTypeComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(InlineApplicantTypeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.ts b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.ts new file mode 100644 index 0000000000..1c4e6e238f --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-applicant-type/inline-applicant-type.component.ts @@ -0,0 +1,37 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-inline-applicant-type', + templateUrl: './inline-applicant-type.component.html', + styleUrls: ['./inline-applicant-type.component.scss'], +}) +export class InlineApplicantTypeComponent implements OnInit { + @Input() selectedValue?: string | null; + @Input() value?: string | null; + + @Output() save = new EventEmitter(); + + form!: FormGroup; + isEditing = false; + pendingApplicantType?: string | null; + + constructor(private fb: FormBuilder) { + this.pendingApplicantType = this.selectedValue; + } + + ngOnInit(): void { + this.form = this.fb.group({ + applicantType: this.value || this.selectedValue, + }); + } + + toggleEdit() { + this.isEditing = !this.isEditing; + } + + onSave() { + this.save.emit(this.form.get('applicantType')!.value); + this.isEditing = false; + } +} diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 756471ca40..36ea036d66 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -40,6 +40,7 @@ import { AvatarCircleComponent } from './avatar-circle/avatar-circle.component'; import { DetailsHeaderComponent } from './details-header/details-header.component'; import { ErrorMessageComponent } from './error-message/error-message.component'; import { FavoriteButtonComponent } from './favorite-button/favorite-button.component'; +import { InlineApplicantTypeComponent } from './inline-applicant-type/inline-applicant-type.component'; import { InlineBooleanComponent } from './inline-editors/inline-boolean/inline-boolean.component'; import { InlineDatepickerComponent } from './inline-editors/inline-datepicker/inline-datepicker.component'; import { InlineDropdownComponent } from './inline-editors/inline-dropdown/inline-dropdown.component'; @@ -72,6 +73,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen MomentPipe, StartOfDayPipe, MeetingOverviewComponent, + InlineApplicantTypeComponent, InlineTextareaComponent, InlineBooleanComponent, InlineNumberComponent, @@ -148,6 +150,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen MtxButtonModule, StartOfDayPipe, MatTooltipModule, + InlineApplicantTypeComponent, InlineTextareaComponent, InlineBooleanComponent, InlineNumberComponent, diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html index 406803dac5..74ccaa5382 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html +++ b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html @@ -137,6 +137,22 @@
(navigateToStep)="switchStep($event)" (exit)="onExit()" > + +
diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts index 5dd7fb0286..32486f33ba 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts +++ b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts @@ -29,6 +29,8 @@ import { TurProposalComponent } from '../edit-submission/proposal/tur-proposal/t import { SelectGovernmentComponent } from '../edit-submission/select-government/select-government.component'; import { ConfirmPublishDialogComponent } from './confirm-publish-dialog/confirm-publish-dialog.component'; import { scrollToElement } from '../../shared/utils/scroll-helper'; +import { ExclProposalComponent } from '../edit-submission/proposal/excl-proposal/excl-proposal.component'; +import { InclProposalComponent } from '../edit-submission/proposal/incl-proposal/incl-proposal.component'; @Component({ selector: 'app-alcs-edit-submission', @@ -64,6 +66,8 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView @ViewChild(RosoProposalComponent) rosoProposalComponent?: RosoProposalComponent; @ViewChild(PofoProposalComponent) profoProposalComponent?: PofoProposalComponent; @ViewChild(PfrsProposalComponent) pfrsProposalComponent?: PfrsProposalComponent; + @ViewChild(ExclProposalComponent) exclProposalComponent?: ExclProposalComponent; + @ViewChild(InclProposalComponent) inclProposalComponent?: InclProposalComponent; @ViewChild(OtherAttachmentsComponent) otherAttachmentsComponent!: OtherAttachmentsComponent; constructor( @@ -209,6 +213,12 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView if (this.pfrsProposalComponent) { await this.pfrsProposalComponent.onSave(); } + if (this.exclProposalComponent) { + await this.exclProposalComponent.onSave(); + } + if (this.inclProposalComponent) { + await this.inclProposalComponent.onSave(); + } } async switchStep(index: number) { diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts index acea5917c7..71801160f1 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts @@ -110,6 +110,8 @@ import { SelectGovernmentComponent } from './select-government/select-government PfrsProposalComponent, NaruProposalComponent, SoilTableComponent, + ExclProposalComponent, + InclProposalComponent, ], }) export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html b/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html index cb01882b58..70f9d717a5 100644 --- a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html +++ b/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html @@ -179,7 +179,11 @@

4. Resolution

The only option available to you is to forward this application on to the ALC.
Resolution for Application to Proceed to the ALC
@@ -191,7 +195,7 @@

4. Resolution

@@ -423,7 +427,11 @@

4. Resolution

The only option available to you is to forward this application on to the ALC.
Resolution for Application to Proceed to the ALC
@@ -435,7 +443,7 @@

4. Resolution

diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html index 5afec266c1..4a8902487a 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html +++ b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html @@ -224,13 +224,7 @@

Resolution

no authorizing resolution is required as per S. 25 (3) or S. 29 (4) of the ALC Act. The only option available is to forward this application on to the ALC.
-
+
Resolution for Application to Proceed to the ALC
{{ diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts index 8305093ae2..d893645219 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts +++ b/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts @@ -34,6 +34,7 @@ export class LfngReviewComponent implements OnInit, OnDestroy { staffReport: ApplicationDocumentDto[] = []; resolutionDocument: ApplicationDocumentDto[] = []; governmentOtherAttachments: ApplicationDocumentDto[] = []; + hasCompletedStepsBeforeResolution = false; hasCompletedStepsBeforeDocuments = false; submittedToAlcStatus = false; @@ -49,6 +50,13 @@ export class LfngReviewComponent implements OnInit, OnDestroy { if (appReview) { this.applicationReview = appReview; + this.hasCompletedStepsBeforeResolution = + appReview.isFirstNationGovernment || + (!appReview.isFirstNationGovernment && + appReview.isOCPDesignation !== null && + appReview.isSubjectToZoning !== null && + (appReview.isOCPDesignation === true || appReview.isSubjectToZoning === true)); + this.hasCompletedStepsBeforeDocuments = (appReview.isAuthorized !== null && appReview.isFirstNationGovernment) || (appReview.isAuthorized !== null && diff --git a/services/apps/alcs/src/alcs/application/application.controller.ts b/services/apps/alcs/src/alcs/application/application.controller.ts index 0dc5422c0e..19532b1390 100644 --- a/services/apps/alcs/src/alcs/application/application.controller.ts +++ b/services/apps/alcs/src/alcs/application/application.controller.ts @@ -102,6 +102,7 @@ export class ApplicationController { proposalExpiryDate: formatIncomingDate(updates.proposalExpiryDate), nfuUseSubType: updates.nfuUseSubType, nfuUseType: updates.nfuUseType, + inclExclApplicantType: updates.inclExclApplicantType, staffObservations: updates.staffObservations, }, ); diff --git a/services/apps/alcs/src/alcs/application/application.dto.ts b/services/apps/alcs/src/alcs/application/application.dto.ts index e836e2a8fc..c756dda3c2 100644 --- a/services/apps/alcs/src/alcs/application/application.dto.ts +++ b/services/apps/alcs/src/alcs/application/application.dto.ts @@ -147,6 +147,10 @@ export class UpdateApplicationDto { @IsString() nfuUseSubType?: string; + @IsOptional() + @IsString() + inclExclApplicantType?: string; + @IsOptional() @IsNumber() proposalEndDate?: number; @@ -245,6 +249,9 @@ export class ApplicationDto { @AutoMap(() => String) nfuUseSubType?: string; + @AutoMap(() => String) + inclExclApplicantType?: string; + proposalEndDate?: number; proposalExpiryDate?: number; } @@ -271,6 +278,7 @@ export class ApplicationUpdateServiceDto { agCapConsultant?: string; nfuUseType?: string; nfuUseSubType?: string; + inclExclApplicantType?: string; proposalEndDate?: Date | null; proposalExpiryDate?: Date | null; staffObservations?: string | null; diff --git a/services/apps/alcs/src/alcs/application/application.entity.ts b/services/apps/alcs/src/alcs/application/application.entity.ts index ffe1c314c2..9955871366 100644 --- a/services/apps/alcs/src/alcs/application/application.entity.ts +++ b/services/apps/alcs/src/alcs/application/application.entity.ts @@ -226,6 +226,14 @@ export class Application extends Base { }) nfuUseSubType?: string | null; + @AutoMap(() => String) + @Column({ + type: 'text', + comment: 'Inclusion Exclusion Applicant Type', + nullable: true, + }) + inclExclApplicantType?: string | null; + @Column({ type: 'timestamptz', comment: 'The date at which the proposal use ends', diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690485189579-add_incl_excl_applicant_type_to_application.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690485189579-add_incl_excl_applicant_type_to_application.ts new file mode 100644 index 0000000000..7fce8cd0ea --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690485189579-add_incl_excl_applicant_type_to_application.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addInclExclApplicantTypeToApplication1690485189579 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application" ADD "incl_excl_applicant_type" text`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application"."incl_excl_applicant_type" IS 'Inclusion Exclusion Applicant Type'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application"."incl_excl_applicant_type" IS 'Inclusion Exclusion Applicant Type'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application" DROP COLUMN "incl_excl_applicant_type"`, + ); + } +} From e4eba8eb067c0d55863fa8b2086011448c9360df Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 27 Jul 2023 15:55:44 -0700 Subject: [PATCH 158/954] Load the full type and set it instead of just code * Setting just the code will not trump the actual value so saving a changed code doe not work --- .../application-decision-condition.service.spec.ts | 9 +++++++++ .../application-decision-condition.service.ts | 12 +++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts index 63b309e1d4..e1df986106 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; import { UpdateApplicationDecisionConditionDto } from './application-decision-condition.dto'; import { ApplicationDecisionCondition } from './application-decision-condition.entity'; import { ApplicationDecisionConditionService } from './application-decision-condition.service'; @@ -11,9 +12,13 @@ describe('ApplicationDecisionConditionService', () => { let mockApplicationDecisionConditionRepository: DeepMocked< Repository >; + let mockAppDecCondTypeRepository: DeepMocked< + Repository + >; beforeEach(async () => { mockApplicationDecisionConditionRepository = createMock(); + mockAppDecCondTypeRepository = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -22,6 +27,10 @@ describe('ApplicationDecisionConditionService', () => { provide: getRepositoryToken(ApplicationDecisionCondition), useValue: mockApplicationDecisionConditionRepository, }, + { + provide: getRepositoryToken(ApplicationDecisionConditionType), + useValue: mockAppDecCondTypeRepository, + }, ], }).compile(); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts index 7c71d98fed..ad9698e8a8 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; import { ApplicationDecisionComponent } from '../application-decision-v2/application-decision/component/application-decision-component.entity'; +import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; import { UpdateApplicationDecisionConditionDto, UpdateApplicationDecisionConditionServiceDto, @@ -14,6 +15,8 @@ export class ApplicationDecisionConditionService { constructor( @InjectRepository(ApplicationDecisionCondition) private repository: Repository, + @InjectRepository(ApplicationDecisionConditionType) + private typeRepository: Repository, ) {} async getOneOrFail(uuid: string) { @@ -41,7 +44,14 @@ export class ApplicationDecisionConditionService { } else { condition = new ApplicationDecisionCondition(); } - condition.typeCode = updateDto.type?.code ?? null; + if (updateDto.type?.code) { + condition.type = await this.typeRepository.findOneOrFail({ + where: { + code: updateDto.type.code, + }, + }); + } + condition.administrativeFee = updateDto.administrativeFee ?? null; condition.description = updateDto.description ?? null; condition.securityAmount = updateDto.securityAmount ?? null; From 73eb6835a312bd5245247bc7f390ca6363c4dd02 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 09:48:03 -0700 Subject: [PATCH 159/954] trying to fix git issues --- bin/migrate-oats-data/noi/noi.py | 54 +------------------------------- 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index 16170c3623..8084020e21 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -69,56 +69,4 @@ def process_nois(conn=None, batch_size=10000): print("Total amount of successful inserts:", successful_inserts_count) - print("Total failed inserts:", failed_inserts) - -def applicant_lookup(): - query = """ - SELECT DISTINCT - oaap.alr_application_id AS application_id, - string_agg (DISTINCT oo.organization_name, ', ') FILTER ( - WHERE - oo.organization_name IS NOT NULL - ) AS orgs, - string_agg ( - DISTINCT concat (op.first_name || ' ' || op.last_name), - ', ' - ) FILTER ( - WHERE - op.last_name IS NOT NULL - OR op.first_name IS NOT NULL - ) AS persons - FROM - oats.oats_alr_application_parties oaap - LEFT JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - LEFT JOIN oats.oats_persons op ON op.person_id = opo.person_id - LEFT JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - WHERE - oaap.alr_appl_role_code = 'APPL' - GROUP BY - oaap.alr_application_id - """ - return query - -def oats_gov(): - query = """ - SELECT - oaap.alr_application_id AS application_id, - oo.organization_name AS oats_gov_name - FROM - oats.oats_alr_application_parties oaap - JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - WHERE - oo.organization_type_cd IN ('MUNI','FN','RD') - """ - return query - -def alcs_gov(): - query = """ - SELECT - oats_gov.application_id AS application_id, - alg.uuid AS gov_uuid - FROM - oats_gov - JOIN alcs.application_local_government alg on oats_gov.oats_gov_name = alg."name" - """ \ No newline at end of file + print("Total failed inserts:", failed_inserts) \ No newline at end of file From 83d77aeb8e13084185203ff26c3d7b87d64e5ade Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 09:48:03 -0700 Subject: [PATCH 160/954] sql complete --- bin/migrate-oats-data/noi/noi.py | 6 +-- bin/migrate-oats-data/noi/sql/insert_noi.sql | 41 +++++++++++-------- .../noi/sql/insert_noi_count.sql | 21 +++++++--- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index 8084020e21..da82b8aa86 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -2,16 +2,14 @@ def noi_insert_query(number_of_rows_to_insert): nois_to_insert = ",".join(["%s"] * number_of_rows_to_insert) - # need to update for noi's return f""" - INSERT INTO alcs.notice_of_intent (file_number, summary, + INSERT INTO alcs.notice_of_intent (file_number, applicant, region_code, local_government_uuid, audit_created_by) VALUES{nois_to_insert} ON CONFLICT (file_number) DO UPDATE SET file_number = EXCLUDED.file_number, - type_code = EXCLUDED.type_code, - summary = EXCLUDED.summary, + applicant = EXCLUDED.applicant, region_code = EXCLUDED.region_code, local_government_uuid = EXCLUDED.local_government_uuid, audit_created_by = EXCLUDED.audit_created_by diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index 6074746059..a53ecca430 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -127,24 +127,29 @@ WITH WHERE oo2.organization_type_cd = 'PANEL' OR oo3.organization_type_cd = 'PANEL' - ), - -- Step 4: Perform lookup to retrieve type code - application_type_lookup AS ( - SELECT - oaac.alr_application_id AS application_id, - oacc."description" AS "description", - oaac.alr_change_code AS code - FROM - oats.oats_alr_appl_components AS oaac - JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code - LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id - WHERE - aee.component_id IS NULL ) SELECT - oaa.alr_application_id, - oaac.alr_change_code + ng.noi_application_id :: text AS file_number, + CASE + WHEN applicant_lookup.orgs IS NOT NULL THEN applicant_lookup.orgs + WHEN applicant_lookup.persons IS NOT NULL THEN applicant_lookup.persons + ELSE 'Unknown' + END AS applicant, + ar.code AS region_code, + alcs_gov.gov_uuid AS local_government_uuid, + 'oats_etl' AS audit_created_by FROM - noi_grouped noi - JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = noi.alr_application_id - JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = noi.alr_application_id \ No newline at end of file + noi_grouped AS ng + -- JOIN oats.alcs_etl_application_duplicate AS ae ON oa.alr_application_id = ae.application_id + -- AND ae.duplicated IS false + LEFT JOIN applicant_lookup ON ng.noi_application_id = applicant_lookup.application_id + LEFT JOIN panel_lookup ON ng.noi_application_id = panel_lookup.application_id + LEFT JOIN alcs.application_region ar ON panel_lookup.panel_region = ar."label" + LEFT JOIN alcs_gov ON ng.noi_application_id = alcs_gov.application_id + -- SELECT + -- oaa.alr_application_id, + -- oaac.alr_change_code + -- FROM + -- noi_grouped noi + -- JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = noi.alr_application_id + -- JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = noi.alr_application_id \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/sql/insert_noi_count.sql b/bin/migrate-oats-data/noi/sql/insert_noi_count.sql index 3d98e1766b..0081cc65ca 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi_count.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi_count.sql @@ -1,7 +1,18 @@ +WITH + noi_grouped AS ( + SELECT + oaac.alr_application_id as noi_application_id + FROM + oats.oats_alr_appl_components oaac + JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = oaac.alr_application_id + WHERE + oaa.application_class_code IN ('NOI') + GROUP BY + oaac.alr_application_id + HAVING + count(oaac.alr_application_id) < 2 -- ignore all applications wit multiple components + ) SELECT count(*) -From - oats.oats_alr_applications AS oa - -- JOIN oats.alcs_etl_application_duplicate AS ae ON oa.alr_application_id = ae.application_id AND ae.duplicated IS false -WHERE - oa.application_class_code \ No newline at end of file +FROM + noi_grouped ng \ No newline at end of file From bb7eed85791678129ef7c8ad90405e9b0d5dce3e Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 26 Jul 2023 14:10:24 -0700 Subject: [PATCH 161/954] noi files --- bin/migrate-oats-data/noi/__init__.py | 0 bin/migrate-oats-data/noi/noi.py | 72 +++++++++++++++++++ bin/migrate-oats-data/noi/sql/insert_noi.sql | 26 +++++++ .../noi/sql/insert_noi_count.sql | 7 ++ 4 files changed, 105 insertions(+) create mode 100644 bin/migrate-oats-data/noi/__init__.py create mode 100644 bin/migrate-oats-data/noi/noi.py create mode 100644 bin/migrate-oats-data/noi/sql/insert_noi.sql create mode 100644 bin/migrate-oats-data/noi/sql/insert_noi_count.sql diff --git a/bin/migrate-oats-data/noi/__init__.py b/bin/migrate-oats-data/noi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py new file mode 100644 index 0000000000..2d3d291e94 --- /dev/null +++ b/bin/migrate-oats-data/noi/noi.py @@ -0,0 +1,72 @@ +from db import inject_conn_pool + +def noi_insert_query(number_of_rows_to_insert): + nois_to_insert = ",".join(["%s"] * number_of_rows_to_insert) + # need to update for noi's + return f""" + INSERT INTO alcs.notice_of_intent (file_number, summary, + applicant, region_code, local_government_uuid, audit_created_by) + + VALUES{nois_to_insert} + ON CONFLICT (file_number) DO UPDATE SET + file_number = EXCLUDED.file_number, + type_code = EXCLUDED.type_code, + summary = EXCLUDED.summary, + region_code = EXCLUDED.region_code, + local_government_uuid = EXCLUDED.local_government_uuid, + audit_created_by = EXCLUDED.audit_created_by + """ + +@inject_conn_pool +def process_nois(conn=None, batch_size=10000): + with conn.cursor() as cursor: + + with open( + "sql/insert_noi_count.sql", "r", encoding="utf-8" + ) as sql_file: + count_query = sql_file.read() + cursor.execute(count_query) + count_total = cursor.fetchone()[0] + print("- Applications to insert: ", count_total) + + failed_inserts = 0 + successful_inserts_count = 0 + last_application_id = 0 + + with open("sql/insert-batch-application.sql", "r", encoding="utf-8") as sql_file: + application_sql = sql_file.read() + while True: + cursor.execute( + f"{application_sql} WHERE ae.application_id > {last_application_id} ORDER by ae.application_id;" + ) + + rows = cursor.fetchmany(batch_size) + if not rows: + break + try: + applications_to_be_inserted_count = len(rows) + + insert_query = compile_application_insert_query( + applications_to_be_inserted_count + ) + cursor.execute(insert_query, rows) + conn.commit() + + last_application_id = rows[-1][0] + successful_inserts_count = ( + successful_inserts_count + applications_to_be_inserted_count + ) + + print( + f"retrieved/inserted items count: {applications_to_be_inserted_count}; total successfully inserted/updated applications so far {successful_inserts_count}; last inserted applidation_id: {last_application_id}" + + ) + except Exception as e: + conn.rollback() + print("Error", e) + failed_inserts = count_total - successful_inserts_count + last_application_id = last_application_id +1 + + + print("Total amount of successful inserts:", successful_inserts_count) + print("Total failed inserts:", failed_inserts) \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql new file mode 100644 index 0000000000..5022345d80 --- /dev/null +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -0,0 +1,26 @@ +WITH + applicant_lookup AS ( + SELECT DISTINCT + oaap.alr_application_id AS application_id, + string_agg (DISTINCT oo.organization_name, ', ') FILTER ( + WHERE + oo.organization_name IS NOT NULL + ) AS orgs, + string_agg ( + DISTINCT concat (op.first_name || ' ' || op.last_name), + ', ' + ) FILTER ( + WHERE + op.last_name IS NOT NULL + OR op.first_name IS NOT NULL + ) AS persons + FROM + oats.oats_alr_application_parties oaap + LEFT JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + LEFT JOIN oats.oats_persons op ON op.person_id = opo.person_id + LEFT JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oaap.alr_appl_role_code = 'APPL' + GROUP BY + oaap.alr_application_id + ), \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/sql/insert_noi_count.sql b/bin/migrate-oats-data/noi/sql/insert_noi_count.sql new file mode 100644 index 0000000000..3d98e1766b --- /dev/null +++ b/bin/migrate-oats-data/noi/sql/insert_noi_count.sql @@ -0,0 +1,7 @@ +SELECT + count(*) +From + oats.oats_alr_applications AS oa + -- JOIN oats.alcs_etl_application_duplicate AS ae ON oa.alr_application_id = ae.application_id AND ae.duplicated IS false +WHERE + oa.application_class_code \ No newline at end of file From bee19699e74e7c9a4717106258f323fa1101e78b Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 26 Jul 2023 14:22:25 -0700 Subject: [PATCH 162/954] ignoring NOIs --- bin/migrate-oats-data/applications/base_applications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/applications/base_applications.py b/bin/migrate-oats-data/applications/base_applications.py index f0ca30ec7c..a8b93f4222 100644 --- a/bin/migrate-oats-data/applications/base_applications.py +++ b/bin/migrate-oats-data/applications/base_applications.py @@ -73,7 +73,7 @@ def process_applications(conn=None, batch_size=10000): application_sql = sql_file.read() while True: cursor.execute( - f"{application_sql} WHERE ae.application_id > {last_application_id} ORDER by ae.application_id;" + f"{application_sql} WHERE ae.application_id > {last_application_id} AND oa.application_class_code <> 'NOI' ORDER by ae.application_id;" ) rows = cursor.fetchmany(batch_size) From b8a960e1c917358062199b5d8fba9abd9e6adc9c Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 26 Jul 2023 15:44:55 -0700 Subject: [PATCH 163/954] updated to only catch LOA and BLK --- bin/migrate-oats-data/applications/base_applications.py | 2 +- .../applications/sql/insert_batch_application_count.sql | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/bin/migrate-oats-data/applications/base_applications.py b/bin/migrate-oats-data/applications/base_applications.py index a8b93f4222..4d5be73c85 100644 --- a/bin/migrate-oats-data/applications/base_applications.py +++ b/bin/migrate-oats-data/applications/base_applications.py @@ -73,7 +73,7 @@ def process_applications(conn=None, batch_size=10000): application_sql = sql_file.read() while True: cursor.execute( - f"{application_sql} WHERE ae.application_id > {last_application_id} AND oa.application_class_code <> 'NOI' ORDER by ae.application_id;" + f"{application_sql} WHERE ae.application_id > {last_application_id} AND (oa.application_class_code = 'LOA' OR oa.application_class_code = 'BLK') ORDER by ae.application_id;" ) rows = cursor.fetchmany(batch_size) diff --git a/bin/migrate-oats-data/applications/sql/insert_batch_application_count.sql b/bin/migrate-oats-data/applications/sql/insert_batch_application_count.sql index 8b03c80863..a6e4a047f4 100644 --- a/bin/migrate-oats-data/applications/sql/insert_batch_application_count.sql +++ b/bin/migrate-oats-data/applications/sql/insert_batch_application_count.sql @@ -1,7 +1,9 @@ - SELECT count(*) From oats.oats_alr_applications AS oa - JOIN oats.alcs_etl_application_duplicate AS ae ON oa.alr_application_id = ae.application_id AND ae.duplicated IS false - \ No newline at end of file + JOIN oats.alcs_etl_application_duplicate AS ae ON oa.alr_application_id = ae.application_id + AND ae.duplicated IS false +WHERE + oa.application_class_code = 'LOA' + OR oa.application_class_code = 'BLK' \ No newline at end of file From 5c17171bd1d07bac4084dd160031e73c6ba66e42 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 27 Jul 2023 09:22:14 -0700 Subject: [PATCH 164/954] adding wip changes --- bin/migrate-oats-data/noi/noi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index 2d3d291e94..8084020e21 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -46,7 +46,7 @@ def process_nois(conn=None, batch_size=10000): try: applications_to_be_inserted_count = len(rows) - insert_query = compile_application_insert_query( + insert_query = noi_insert_query( applications_to_be_inserted_count ) cursor.execute(insert_query, rows) From 84b14302a052bf95e930020715587fce16f62e89 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 09:21:53 -0700 Subject: [PATCH 165/954] commit python changes --- .../applications/noi_temp.py | 4 ++ bin/migrate-oats-data/noi/noi.py | 54 ++++++++++++++++++- bin/migrate-oats-data/noi/sql/insert_noi.sql | 39 ++++++-------- 3 files changed, 74 insertions(+), 23 deletions(-) create mode 100644 bin/migrate-oats-data/applications/noi_temp.py diff --git a/bin/migrate-oats-data/applications/noi_temp.py b/bin/migrate-oats-data/applications/noi_temp.py new file mode 100644 index 0000000000..d114d3333b --- /dev/null +++ b/bin/migrate-oats-data/applications/noi_temp.py @@ -0,0 +1,4 @@ + +LG_DICT = [ {"key" : 'Islands Trust Gabriola Island', "value" : 'Islands Trust Gabriola Island (Historical)'}, + {"key" : 'Islands Trust Galiano Island', "value" : 'Islands Trust Galiano Island (Historical)'}, + {"key" : 'Islands Trust Gambier Island', "value" : 'Islands Trust Gambier Island (Historical)'}] \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index 8084020e21..16170c3623 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -69,4 +69,56 @@ def process_nois(conn=None, batch_size=10000): print("Total amount of successful inserts:", successful_inserts_count) - print("Total failed inserts:", failed_inserts) \ No newline at end of file + print("Total failed inserts:", failed_inserts) + +def applicant_lookup(): + query = """ + SELECT DISTINCT + oaap.alr_application_id AS application_id, + string_agg (DISTINCT oo.organization_name, ', ') FILTER ( + WHERE + oo.organization_name IS NOT NULL + ) AS orgs, + string_agg ( + DISTINCT concat (op.first_name || ' ' || op.last_name), + ', ' + ) FILTER ( + WHERE + op.last_name IS NOT NULL + OR op.first_name IS NOT NULL + ) AS persons + FROM + oats.oats_alr_application_parties oaap + LEFT JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + LEFT JOIN oats.oats_persons op ON op.person_id = opo.person_id + LEFT JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oaap.alr_appl_role_code = 'APPL' + GROUP BY + oaap.alr_application_id + """ + return query + +def oats_gov(): + query = """ + SELECT + oaap.alr_application_id AS application_id, + oo.organization_name AS oats_gov_name + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oo.organization_type_cd IN ('MUNI','FN','RD') + """ + return query + +def alcs_gov(): + query = """ + SELECT + oats_gov.application_id AS application_id, + alg.uuid AS gov_uuid + FROM + oats_gov + JOIN alcs.application_local_government alg on oats_gov.oats_gov_name = alg."name" + """ \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index 5022345d80..7644a2347d 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -1,26 +1,21 @@ WITH - applicant_lookup AS ( - SELECT DISTINCT - oaap.alr_application_id AS application_id, - string_agg (DISTINCT oo.organization_name, ', ') FILTER ( - WHERE - oo.organization_name IS NOT NULL - ) AS orgs, - string_agg ( - DISTINCT concat (op.first_name || ' ' || op.last_name), - ', ' - ) FILTER ( - WHERE - op.last_name IS NOT NULL - OR op.first_name IS NOT NULL - ) AS persons + noi_grouped AS ( + SELECT + oaac.alr_application_id FROM - oats.oats_alr_application_parties oaap - LEFT JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - LEFT JOIN oats.oats_persons op ON op.person_id = opo.person_id - LEFT JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + oats.oats_alr_appl_components oaac + JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = oaac.alr_application_id WHERE - oaap.alr_appl_role_code = 'APPL' + oaa.application_class_code IN ('NOI') GROUP BY - oaap.alr_application_id - ), \ No newline at end of file + oaac.alr_application_id + HAVING + count(oaac.alr_application_id) < 2 -- ignore all applications wit multiple components + ) +SELECT + oaa.alr_application_id, + oaac.alr_change_code +FROM + noi_grouped noi + JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = noi.alr_application_id + JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = noi.alr_application_id \ No newline at end of file From a419215112825debfb818323506f7d6484317056 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 09:43:07 -0700 Subject: [PATCH 166/954] starting sql implementation --- bin/migrate-oats-data/noi/sql/insert_noi.sql | 131 ++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index 7644a2347d..6074746059 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -1,7 +1,7 @@ WITH noi_grouped AS ( SELECT - oaac.alr_application_id + oaac.alr_application_id as noi_application_id FROM oats.oats_alr_appl_components oaac JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = oaac.alr_application_id @@ -11,6 +11,135 @@ WITH oaac.alr_application_id HAVING count(oaac.alr_application_id) < 2 -- ignore all applications wit multiple components + ), + applicant_lookup AS ( + SELECT DISTINCT + oaap.alr_application_id AS application_id, + string_agg (DISTINCT oo.organization_name, ', ') FILTER ( + WHERE + oo.organization_name IS NOT NULL + ) AS orgs, + string_agg ( + DISTINCT concat (op.first_name || ' ' || op.last_name), + ', ' + ) FILTER ( + WHERE + op.last_name IS NOT NULL + OR op.first_name IS NOT NULL + ) AS persons + FROM + oats.oats_alr_application_parties oaap + LEFT JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + LEFT JOIN oats.oats_persons op ON op.person_id = opo.person_id + LEFT JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oaap.alr_appl_role_code = 'APPL' + GROUP BY + oaap.alr_application_id + ), + -- Step 2: get local gov application name & match to uuid + oats_gov AS ( + SELECT + oaap.alr_application_id AS application_id, + oo.organization_name AS oats_gov_name + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + WHERE + oo.organization_type_cd IN ('MUNI', 'FN', 'RD') + ), + alcs_gov AS ( + SELECT + oats_gov.application_id AS application_id, + alg.uuid AS gov_uuid + FROM + oats_gov + JOIN alcs.application_local_government alg on ( + CASE + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gabriola Island' THEN 'Islands Trust Gabriola Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Galiano Island' THEN 'Islands Trust Galiano Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Gambier Island' THEN 'Islands Trust Gambier Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Hornby Island' THEN 'Islands Trust Hornby Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Lasqueti Island' THEN 'Islands Trust Lasqueti Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Mayne Island' THEN 'Islands Trust Mayne Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Pender Island' THEN 'Islands Trust Pender Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Quadra Island' THEN 'Islands Trust Quadra Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Salt Spring Island' THEN 'Islands Trust Salt Spring Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Saturna Island' THEN 'Islands Trust Saturna Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Sidney Island' THEN 'Islands Trust Sidney Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Comox Strathcona' THEN 'Islands Trust Comox Strathcona (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust- Nanaimo' THEN 'Islands Trust Nanaimo (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Capital' THEN 'Islands Trust Capital (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Powell River' THEN 'Islands Trust Powell River (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust-Sunshine Coast' THEN 'Islands Trust Sunshine Coast (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Bowen Island' THEN 'Bowen Island (Island Municipality)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust Denman Island' THEN 'Islands Trust Denman Island (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Islands Trust - Cowichan Valley' THEN 'Islands Trust Cowichan Valley (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Northern Rockies' THEN 'Northern Rockies (Historical)' + WHEN oats_gov.oats_gov_name LIKE 'Sliammon%' THEN 'Tla''amin Nation' + WHEN oats_gov.oats_gov_name LIKE 'Thompson Nicola%' THEN 'Thompson Nicola Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Cariboo%' THEN 'Cariboo Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Fraser Valley%' THEN 'Fraser Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Columbia Shuswap%' THEN 'Columbia Shuswap Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Okanagan%' THEN 'Central Okanagan Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Squamish Lillooet%' THEN 'Squamish Lillooet Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Alberni-Clayoquot%' THEN 'Alberni-Clayoquot Regional District' + WHEN oats_gov.oats_gov_name LIKE 'qathet%' THEN 'qathet Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Peace River%' THEN 'Peace River Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Okanagan Similkameen%' THEN 'Okanagan Similkameen Regional District' + WHEN oats_gov.oats_gov_name LIKE 'East Kootenay%' THEN 'East Kootenay Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Bulkley-Nechako%' THEN 'Bulkley-Nechako Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Sunshine Coast%' THEN 'Sunshine Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Nanaimo%' THEN 'Nanaimo Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Kitimat Stikine%' THEN 'Kitimat Stikine Regional District' + WHEN oats_gov.oats_gov_name LIKE 'North Okanagan%' THEN 'North Okanagan Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Fraser Fort George%' THEN 'Fraser Fort George Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Cowichan Valley%' THEN 'Cowichan Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Kootenay Boundary%' THEN 'Kootenay Boundary Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Comox Valley%' THEN 'Comox Valley Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Kootenay%' THEN 'Central Kootenay Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Capital%' THEN 'Capital Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Metro Vancouver%' THEN 'Metro Vancouver Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Central Coast%' THEN 'Central Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'North Coast%' THEN 'North Coast Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Strathcona%' THEN 'Strathcona Regional District' + WHEN oats_gov.oats_gov_name LIKE 'Mount Waddington%' THEN 'Mount Waddington Regional District' + ELSE oats_gov.oats_gov_name + END + ) = alg."name" + ), + -- Step 3: Perform a lookup to retrieve the region code for each application ID + panel_lookup AS ( + SELECT DISTINCT + oaap.alr_application_id AS application_id, + CASE + WHEN oo2.parent_organization_id IS NULL THEN oo2.organization_name + WHEN oo3.parent_organization_id IS NULL THEN oo3.organization_name + ELSE 'NONE' + END AS panel_region + FROM + oats.oats_alr_application_parties oaap + JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id + JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id + LEFT JOIN oats.oats_organizations oo2 ON oo.parent_organization_id = oo2.organization_id + LEFT JOIN oats.oats_organizations oo3 ON oo2.parent_organization_id = oo3.organization_id + WHERE + oo2.organization_type_cd = 'PANEL' + OR oo3.organization_type_cd = 'PANEL' + ), + -- Step 4: Perform lookup to retrieve type code + application_type_lookup AS ( + SELECT + oaac.alr_application_id AS application_id, + oacc."description" AS "description", + oaac.alr_change_code AS code + FROM + oats.oats_alr_appl_components AS oaac + JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code + LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id + WHERE + aee.component_id IS NULL ) SELECT oaa.alr_application_id, From 44f1266fd732854b24784f76bf073c25e002c151 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 09:48:03 -0700 Subject: [PATCH 167/954] trying to fix git issues --- bin/migrate-oats-data/noi/noi.py | 54 +------------------------------- 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index 16170c3623..8084020e21 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -69,56 +69,4 @@ def process_nois(conn=None, batch_size=10000): print("Total amount of successful inserts:", successful_inserts_count) - print("Total failed inserts:", failed_inserts) - -def applicant_lookup(): - query = """ - SELECT DISTINCT - oaap.alr_application_id AS application_id, - string_agg (DISTINCT oo.organization_name, ', ') FILTER ( - WHERE - oo.organization_name IS NOT NULL - ) AS orgs, - string_agg ( - DISTINCT concat (op.first_name || ' ' || op.last_name), - ', ' - ) FILTER ( - WHERE - op.last_name IS NOT NULL - OR op.first_name IS NOT NULL - ) AS persons - FROM - oats.oats_alr_application_parties oaap - LEFT JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - LEFT JOIN oats.oats_persons op ON op.person_id = opo.person_id - LEFT JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - WHERE - oaap.alr_appl_role_code = 'APPL' - GROUP BY - oaap.alr_application_id - """ - return query - -def oats_gov(): - query = """ - SELECT - oaap.alr_application_id AS application_id, - oo.organization_name AS oats_gov_name - FROM - oats.oats_alr_application_parties oaap - JOIN oats.oats_person_organizations opo ON oaap.person_organization_id = opo.person_organization_id - JOIN oats.oats_organizations oo ON opo.organization_id = oo.organization_id - WHERE - oo.organization_type_cd IN ('MUNI','FN','RD') - """ - return query - -def alcs_gov(): - query = """ - SELECT - oats_gov.application_id AS application_id, - alg.uuid AS gov_uuid - FROM - oats_gov - JOIN alcs.application_local_government alg on oats_gov.oats_gov_name = alg."name" - """ \ No newline at end of file + print("Total failed inserts:", failed_inserts) \ No newline at end of file From b06ddbc797bef47dd9d046068ca9833afc3d7e3c Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 09:48:03 -0700 Subject: [PATCH 168/954] sql complete --- bin/migrate-oats-data/noi/noi.py | 6 +-- bin/migrate-oats-data/noi/sql/insert_noi.sql | 41 +++++++++++-------- .../noi/sql/insert_noi_count.sql | 21 +++++++--- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index 8084020e21..da82b8aa86 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -2,16 +2,14 @@ def noi_insert_query(number_of_rows_to_insert): nois_to_insert = ",".join(["%s"] * number_of_rows_to_insert) - # need to update for noi's return f""" - INSERT INTO alcs.notice_of_intent (file_number, summary, + INSERT INTO alcs.notice_of_intent (file_number, applicant, region_code, local_government_uuid, audit_created_by) VALUES{nois_to_insert} ON CONFLICT (file_number) DO UPDATE SET file_number = EXCLUDED.file_number, - type_code = EXCLUDED.type_code, - summary = EXCLUDED.summary, + applicant = EXCLUDED.applicant, region_code = EXCLUDED.region_code, local_government_uuid = EXCLUDED.local_government_uuid, audit_created_by = EXCLUDED.audit_created_by diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index 6074746059..a53ecca430 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -127,24 +127,29 @@ WITH WHERE oo2.organization_type_cd = 'PANEL' OR oo3.organization_type_cd = 'PANEL' - ), - -- Step 4: Perform lookup to retrieve type code - application_type_lookup AS ( - SELECT - oaac.alr_application_id AS application_id, - oacc."description" AS "description", - oaac.alr_change_code AS code - FROM - oats.oats_alr_appl_components AS oaac - JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code - LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id - WHERE - aee.component_id IS NULL ) SELECT - oaa.alr_application_id, - oaac.alr_change_code + ng.noi_application_id :: text AS file_number, + CASE + WHEN applicant_lookup.orgs IS NOT NULL THEN applicant_lookup.orgs + WHEN applicant_lookup.persons IS NOT NULL THEN applicant_lookup.persons + ELSE 'Unknown' + END AS applicant, + ar.code AS region_code, + alcs_gov.gov_uuid AS local_government_uuid, + 'oats_etl' AS audit_created_by FROM - noi_grouped noi - JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = noi.alr_application_id - JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = noi.alr_application_id \ No newline at end of file + noi_grouped AS ng + -- JOIN oats.alcs_etl_application_duplicate AS ae ON oa.alr_application_id = ae.application_id + -- AND ae.duplicated IS false + LEFT JOIN applicant_lookup ON ng.noi_application_id = applicant_lookup.application_id + LEFT JOIN panel_lookup ON ng.noi_application_id = panel_lookup.application_id + LEFT JOIN alcs.application_region ar ON panel_lookup.panel_region = ar."label" + LEFT JOIN alcs_gov ON ng.noi_application_id = alcs_gov.application_id + -- SELECT + -- oaa.alr_application_id, + -- oaac.alr_change_code + -- FROM + -- noi_grouped noi + -- JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = noi.alr_application_id + -- JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = noi.alr_application_id \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/sql/insert_noi_count.sql b/bin/migrate-oats-data/noi/sql/insert_noi_count.sql index 3d98e1766b..0081cc65ca 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi_count.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi_count.sql @@ -1,7 +1,18 @@ +WITH + noi_grouped AS ( + SELECT + oaac.alr_application_id as noi_application_id + FROM + oats.oats_alr_appl_components oaac + JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = oaac.alr_application_id + WHERE + oaa.application_class_code IN ('NOI') + GROUP BY + oaac.alr_application_id + HAVING + count(oaac.alr_application_id) < 2 -- ignore all applications wit multiple components + ) SELECT count(*) -From - oats.oats_alr_applications AS oa - -- JOIN oats.alcs_etl_application_duplicate AS ae ON oa.alr_application_id = ae.application_id AND ae.duplicated IS false -WHERE - oa.application_class_code \ No newline at end of file +FROM + noi_grouped ng \ No newline at end of file From 8ffb37eef2b26f20029b71b19dcdfeaf791a5b5f Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 10:47:48 -0700 Subject: [PATCH 169/954] noi part implemented --- bin/migrate-oats-data/noi/noi.py | 19 ++++++++++++++----- bin/migrate-oats-data/noi/sql/insert_noi.sql | 9 +-------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index da82b8aa86..b03a19a03c 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -25,17 +25,17 @@ def process_nois(conn=None, batch_size=10000): count_query = sql_file.read() cursor.execute(count_query) count_total = cursor.fetchone()[0] - print("- Applications to insert: ", count_total) + print("- NOIs to insert: ", count_total) failed_inserts = 0 successful_inserts_count = 0 last_application_id = 0 - with open("sql/insert-batch-application.sql", "r", encoding="utf-8") as sql_file: + with open("sql/insert_noi.sql", "r", encoding="utf-8") as sql_file: application_sql = sql_file.read() while True: cursor.execute( - f"{application_sql} WHERE ae.application_id > {last_application_id} ORDER by ae.application_id;" + f"{application_sql} WHERE ng._noi_application_id > {last_application_id} ORDER by ng.noi_application_id;" ) rows = cursor.fetchmany(batch_size) @@ -56,7 +56,7 @@ def process_nois(conn=None, batch_size=10000): ) print( - f"retrieved/inserted items count: {applications_to_be_inserted_count}; total successfully inserted/updated applications so far {successful_inserts_count}; last inserted applidation_id: {last_application_id}" + f"retrieved/inserted items count: {applications_to_be_inserted_count}; total successfully inserted/updated NOIs so far {successful_inserts_count}; last inserted noi_applidation_id: {last_application_id}" ) except Exception as e: @@ -67,4 +67,13 @@ def process_nois(conn=None, batch_size=10000): print("Total amount of successful inserts:", successful_inserts_count) - print("Total failed inserts:", failed_inserts) \ No newline at end of file + print("Total failed inserts:", failed_inserts) + +@inject_conn_pool +def clean_nois(conn=None): + print("Start NOI cleaning") + with conn.cursor() as cursor: + cursor.execute( + "DELETE FROM alcs.notice_of_intent a WHERE a.audit_created_by = 'oats_etl'" + ) + print(f"Deleted items count = {cursor.rowcount}") \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index a53ecca430..ce3dab4c03 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -145,11 +145,4 @@ FROM LEFT JOIN applicant_lookup ON ng.noi_application_id = applicant_lookup.application_id LEFT JOIN panel_lookup ON ng.noi_application_id = panel_lookup.application_id LEFT JOIN alcs.application_region ar ON panel_lookup.panel_region = ar."label" - LEFT JOIN alcs_gov ON ng.noi_application_id = alcs_gov.application_id - -- SELECT - -- oaa.alr_application_id, - -- oaac.alr_change_code - -- FROM - -- noi_grouped noi - -- JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = noi.alr_application_id - -- JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = noi.alr_application_id \ No newline at end of file + LEFT JOIN alcs_gov ON ng.noi_application_id = alcs_gov.application_id \ No newline at end of file From d3a1168572a672e9548aabe77e74979fb65ce125 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 11:00:10 -0700 Subject: [PATCH 170/954] ready to test --- bin/migrate-oats-data/migrate.py | 36 +++++++++++++++++++++++++++ bin/migrate-oats-data/noi/__init__.py | 1 + 2 files changed, 37 insertions(+) diff --git a/bin/migrate-oats-data/migrate.py b/bin/migrate-oats-data/migrate.py index 86e7e4fa8b..79fcd3ab7f 100644 --- a/bin/migrate-oats-data/migrate.py +++ b/bin/migrate-oats-data/migrate.py @@ -13,6 +13,10 @@ clean_applications, process_alcs_application_prep_fields, ) +from noi import ( + process_nois, + clean_nois, +) from constants import BATCH_UPLOAD_SIZE import_batch_size = BATCH_UPLOAD_SIZE @@ -79,6 +83,20 @@ def app_prep_import_command_parser(import_batch_size, subparsers): ) app_prep_import_command.set_defaults(func=import_batch_size) +def noi_import_command_parser(import_batch_size, subparsers): + application_import_command = subparsers.add_parser( + "noi-import", + help=f"Import NOI with specified batch size: (default: {import_batch_size})", + ) + application_import_command.add_argument( + "--batch-size", + type=int, + default=import_batch_size, + metavar="", + help=f"batch size (default: {import_batch_size})", + ) + application_import_command.set_defaults(func=process_applications) + def import_command_parser(subparsers): import_command = subparsers.add_parser( @@ -104,6 +122,7 @@ def setup_menu_args_parser(import_batch_size): document_import_command_parser(import_batch_size, subparsers) application_document_import_command_parser(import_batch_size, subparsers) app_prep_import_command_parser(import_batch_size, subparsers) + noi_import_command_parser(import_batch_size, subparsers) import_command_parser(subparsers) subparsers.add_parser("clean", help="Clean all imported data") @@ -150,6 +169,9 @@ def setup_menu_args_parser(import_batch_size): console.log("Processing application prep:") process_alcs_application_prep_fields(batch_size=import_batch_size) + console.log("Processing NOIs:") + process_nois(batch_size=import_batch_size) + console.log("Done") case "clean": with console.status("[bold green]Cleaning previous ETL...") as status: @@ -159,6 +181,7 @@ def setup_menu_args_parser(import_batch_size): clean_application_documents() clean_documents() clean_applications() + clean_nois() console.log("Done") case "document-import": @@ -198,6 +221,19 @@ def setup_menu_args_parser(import_batch_size): ) process_alcs_application_prep_fields(batch_size=import_batch_size) + case "noi-import": + console.log("Beginning OATS -> ALCS NOI import process") + with console.status( + "[bold green]NOI import (notice_of_intent table update in ALCS)..." + ) as status: + if args.batch_size: + import_batch_size = args.batch_size + + console.log( + f"Processing NOI import in batch size = {import_batch_size}" + ) + + process_nois(batch_size=import_batch_size) finally: if connection_pool: diff --git a/bin/migrate-oats-data/noi/__init__.py b/bin/migrate-oats-data/noi/__init__.py index e69de29bb2..57d7c89a17 100644 --- a/bin/migrate-oats-data/noi/__init__.py +++ b/bin/migrate-oats-data/noi/__init__.py @@ -0,0 +1 @@ +from .noi import * \ No newline at end of file From 6bbb7357770fefcfb0ac49de5059439a61c0fe93 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 11:32:20 -0700 Subject: [PATCH 171/954] nois importing --- bin/migrate-oats-data/noi/noi.py | 6 +++--- bin/migrate-oats-data/noi/sql/insert_noi.sql | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index b03a19a03c..c7ce704e89 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -20,7 +20,7 @@ def process_nois(conn=None, batch_size=10000): with conn.cursor() as cursor: with open( - "sql/insert_noi_count.sql", "r", encoding="utf-8" + "noi/sql/insert_noi_count.sql", "r", encoding="utf-8" ) as sql_file: count_query = sql_file.read() cursor.execute(count_query) @@ -31,11 +31,11 @@ def process_nois(conn=None, batch_size=10000): successful_inserts_count = 0 last_application_id = 0 - with open("sql/insert_noi.sql", "r", encoding="utf-8") as sql_file: + with open("noi/sql/insert_noi.sql", "r", encoding="utf-8") as sql_file: application_sql = sql_file.read() while True: cursor.execute( - f"{application_sql} WHERE ng._noi_application_id > {last_application_id} ORDER by ng.noi_application_id;" + f"{application_sql} WHERE ng.noi_application_id > {last_application_id} ORDER by ng.noi_application_id;" ) rows = cursor.fetchmany(batch_size) diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index ce3dab4c03..e44cbc17dc 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -136,7 +136,10 @@ SELECT ELSE 'Unknown' END AS applicant, ar.code AS region_code, - alcs_gov.gov_uuid AS local_government_uuid, + CASE + WHEN alcs_gov.gov_uuid IS NOT NULL THEN alcs_gov.gov_uuid + ELSE '001cfdad-bc6e-4d25-9294-1550603da980' --Peace River + END AS local_government_uuid, 'oats_etl' AS audit_created_by FROM noi_grouped AS ng From b56fc23ff741a744c7fd6cec97b6d070d235c420 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 28 Jul 2023 11:58:10 -0700 Subject: [PATCH 172/954] cleaned up code and added application to migrate.py --- bin/migrate-oats-data/migrate.py | 13 +++++++++++++ bin/migrate-oats-data/noi/sql/insert_noi.sql | 12 ++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/bin/migrate-oats-data/migrate.py b/bin/migrate-oats-data/migrate.py index 79fcd3ab7f..cf23629726 100644 --- a/bin/migrate-oats-data/migrate.py +++ b/bin/migrate-oats-data/migrate.py @@ -234,6 +234,19 @@ def setup_menu_args_parser(import_batch_size): ) process_nois(batch_size=import_batch_size) + case "application-import": + console.log("Beginning OATS -> ALCS application import process") + with console.status( + "[bold green]application import (application table update in ALCS)..." + ) as status: + if args.batch_size: + import_batch_size = args.batch_size + + console.log( + f"Processing applications import in batch size = {import_batch_size}" + ) + + process_applications(batch_size=import_batch_size) finally: if connection_pool: diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index e44cbc17dc..1cb4f5c966 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -1,4 +1,5 @@ WITH + -- Step 1: filter out multiple component NOIs noi_grouped AS ( SELECT oaac.alr_application_id as noi_application_id @@ -10,8 +11,9 @@ WITH GROUP BY oaac.alr_application_id HAVING - count(oaac.alr_application_id) < 2 -- ignore all applications wit multiple components + count(oaac.alr_application_id) < 2 ), + -- Step 2: get applicant applicant_lookup AS ( SELECT DISTINCT oaap.alr_application_id AS application_id, @@ -37,7 +39,7 @@ WITH GROUP BY oaap.alr_application_id ), - -- Step 2: get local gov application name & match to uuid + -- Step 3: get local gov application name & match to uuid oats_gov AS ( SELECT oaap.alr_application_id AS application_id, @@ -109,7 +111,7 @@ WITH END ) = alg."name" ), - -- Step 3: Perform a lookup to retrieve the region code for each application ID + -- Step 4: Perform a lookup to retrieve the region code for each application ID panel_lookup AS ( SELECT DISTINCT oaap.alr_application_id AS application_id, @@ -138,13 +140,11 @@ SELECT ar.code AS region_code, CASE WHEN alcs_gov.gov_uuid IS NOT NULL THEN alcs_gov.gov_uuid - ELSE '001cfdad-bc6e-4d25-9294-1550603da980' --Peace River + ELSE '001cfdad-bc6e-4d25-9294-1550603da980' --Peace River if unable to find uuid END AS local_government_uuid, 'oats_etl' AS audit_created_by FROM noi_grouped AS ng - -- JOIN oats.alcs_etl_application_duplicate AS ae ON oa.alr_application_id = ae.application_id - -- AND ae.duplicated IS false LEFT JOIN applicant_lookup ON ng.noi_application_id = applicant_lookup.application_id LEFT JOIN panel_lookup ON ng.noi_application_id = panel_lookup.application_id LEFT JOIN alcs.application_region ar ON panel_lookup.panel_region = ar."label" From 22f52c30d43afc0d41de589d883ad2b95dfe8300 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 28 Jul 2023 11:51:45 -0700 Subject: [PATCH 173/954] Incl/Excl Features * Add Incl/Excl to Submission Validator * Add Incl to Portal and ALCS App Details * Update Incl form with interface to remove ts-ignore * Change it so Incl does not have the prescribed body select * --- .../application-details.component.html | 6 + .../application-details.module.ts | 4 +- .../incl-details/incl-details.component.html | 62 +++++ .../incl-details/incl-details.component.scss | 0 .../incl-details.component.spec.ts | 31 +++ .../incl-details/incl-details.component.ts | 45 +++ .../application-details.component.html | 9 + .../application-details.module.ts | 2 + .../incl-details/incl-details.component.html | 89 ++++++ .../incl-details/incl-details.component.scss | 0 .../incl-details.component.spec.ts | 41 +++ .../incl-details/incl-details.component.ts | 81 ++++++ .../create-application-dialog.component.html | 4 +- .../create-application-dialog.component.ts | 2 +- .../incl-proposal/incl-proposal.component.ts | 14 +- ...ation-submission-validator.service.spec.ts | 261 ++++++++++++++++++ ...pplication-submission-validator.service.ts | 145 ++++++++++ 17 files changed, 789 insertions(+), 7 deletions(-) create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/incl-details/incl-details.component.html create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/incl-details/incl-details.component.scss create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/incl-details/incl-details.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/applicant-info/application-details/incl-details/incl-details.component.ts create mode 100644 portal-frontend/src/app/features/application-details/incl-details/incl-details.component.html create mode 100644 portal-frontend/src/app/features/application-details/incl-details/incl-details.component.scss create mode 100644 portal-frontend/src/app/features/application-details/incl-details/incl-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/application-details/incl-details/incl-details.component.ts diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html index 0a5a4d1b62..0562fc8de0 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html @@ -178,6 +178,12 @@

Proposal

[files]="files" > + +
+
+
diff --git a/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.scss b/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.spec.ts b/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.spec.ts new file mode 100644 index 0000000000..beb030ce61 --- /dev/null +++ b/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.spec.ts @@ -0,0 +1,41 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { UserDto } from '../../../services/authentication/authentication.dto'; +import { AuthenticationService } from '../../../services/authentication/authentication.service'; +import { InclDetailsComponent } from './incl-details.component'; + +describe('InclDetailsComponent', () => { + let component: InclDetailsComponent; + let fixture: ComponentFixture; + let mockAppDocumentService: DeepMocked; + let mockAuthService: DeepMocked; + + beforeEach(async () => { + mockAuthService = createMock(); + mockAuthService.$currentProfile = new BehaviorSubject(undefined); + + await TestBed.configureTestingModule({ + declarations: [InclDetailsComponent], + providers: [ + { + provide: ApplicationDocumentService, + useValue: mockAppDocumentService, + }, + { + provide: AuthenticationService, + useValue: mockAuthService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(InclDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.ts b/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.ts new file mode 100644 index 0000000000..0e66fb9817 --- /dev/null +++ b/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.ts @@ -0,0 +1,81 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; +import { AuthenticationService } from '../../../services/authentication/authentication.service'; + +@Component({ + selector: 'app-incl-details', + templateUrl: './incl-details.component.html', + styleUrls: ['./incl-details.component.scss'], +}) +export class InclDetailsComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + + @Input() showErrors = true; + @Input() showEdit = true; + @Input() draftMode = false; + @Input() updatedFields: string[] = []; + + governmentName = 'applying government'; + + _applicationSubmission: ApplicationSubmissionDetailedDto | undefined; + + @Input() set applicationSubmission(applicationSubmission: ApplicationSubmissionDetailedDto | undefined) { + if (applicationSubmission) { + this._applicationSubmission = applicationSubmission; + } + } + + @Input() set applicationDocuments(documents: ApplicationDocumentDto[]) { + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.noticeOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_ADVERTISING + ); + this.proofOfSignage = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_SIGNAGE); + this.reportOfPublicHearing = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING + ); + } + + proposalMap: ApplicationDocumentDto[] = []; + noticeOfPublicHearing: ApplicationDocumentDto[] = []; + proofOfSignage: ApplicationDocumentDto[] = []; + reportOfPublicHearing: ApplicationDocumentDto[] = []; + + constructor( + private router: Router, + private applicationDocumentService: ApplicationDocumentService, + private authenticationService: AuthenticationService + ) {} + + ngOnInit(): void { + this.authenticationService.$currentProfile.pipe(takeUntil(this.$destroy)).subscribe((userProfile) => { + if (userProfile && userProfile.government && this.showEdit) { + this.governmentName = userProfile.government; + } + }); + } + + async onEditSection(step: number) { + if (this.draftMode) { + await this.router.navigateByUrl( + `/alcs/application/${this._applicationSubmission?.fileNumber}/edit/${step}?errors=t` + ); + } else { + await this.router.navigateByUrl(`application/${this._applicationSubmission?.fileNumber}/edit/${step}?errors=t`); + } + } + + async openFile(uuid: string) { + const res = await this.applicationDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html index 5cfbd26303..ed0c6a90db 100644 --- a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html +++ b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html @@ -101,8 +101,8 @@

(click)="onSubmit()" [disabled]="!selectedAppType || !selectedSubmissionType" > - next - create + next + create

diff --git a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts index 6a59ee5472..126f5c57a9 100644 --- a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts +++ b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts @@ -64,7 +64,7 @@ export class CreateApplicationDialogComponent implements OnInit, AfterViewChecke } async onSubmit() { - if (this.selectedAppType! && ['INCL', 'EXCL'].includes(this.selectedAppType!.code)) { + if (this.selectedAppType && this.selectedAppType.code === 'EXCL') { this.currentStepIndex++; } else { await this.createApplication(); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts index 1716091ead..5284e66a3c 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts @@ -16,6 +16,14 @@ import { EditApplicationSteps } from '../../edit-submission.component'; import { takeUntil } from 'rxjs'; import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; +interface InclForm { + hectares: FormControl; + purpose: FormControl; + agSupport: FormControl; + improvements: FormControl; + isLFNGOwnerOfAllParcels?: FormControl; +} + @Component({ selector: 'app-incl-proposal', templateUrl: './incl-proposal.component.html', @@ -35,7 +43,7 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, improvements = new FormControl(null, [Validators.required]); governmentOwnsAllParcels = new FormControl(undefined, [Validators.required]); - form = new FormGroup({ + form = new FormGroup({ hectares: this.hectares, purpose: this.purpose, agSupport: this.agSupport, @@ -74,6 +82,7 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, formatBooleanToString(applicationSubmission.inclGovernmentOwnsAllParcels) ); this.disableNotificationFileUploads = applicationSubmission.inclGovernmentOwnsAllParcels; + this.form.setControl('isLFNGOwnerOfAllParcels', this.governmentOwnsAllParcels); } if (this.showErrors) { @@ -98,8 +107,7 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, this.isGovernmentUser = userProfile?.isLocalGovernment || userProfile?.isFirstNationGovernment; this.governmentName = userProfile.government; - // @ts-ignore Angular / Typescript hate dynamic controls - this.form.addControl('isLFNGOwnerOfAllParcels', this.governmentOwnsAllParcels); + this.form.setControl('isLFNGOwnerOfAllParcels', this.governmentOwnsAllParcels); } }); } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts index d8ed4a4351..4b162e1a1c 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts @@ -967,4 +967,265 @@ describe('ApplicationSubmissionValidatorService', () => { ).toBe(true); }); }); + + describe('INCL Applications', () => { + it('should require basic fields to be complete', async () => { + const application = new ApplicationSubmission({ + owners: [], + typeCode: 'INCL', + inclImprovements: null, + }); + + const res = await service.validateSubmission(application); + + expect( + includesError( + res.errors, + new Error(`INCL proposal missing inclusion fields`), + ), + ).toBe(true); + }); + + it('should be happy if submission is complete', async () => { + const application = new ApplicationSubmission({ + owners: [], + applicant: 'applicant', + purpose: 'purpose', + typeCode: 'INCL', + inclImprovements: 'inclImprovements', + inclAgricultureSupport: 'inclAgricultureSupport', + inclExclHectares: 2, + inclGovernmentOwnsAllParcels: true, + }); + + const documents = [ + new ApplicationDocument({ + typeCode: DOCUMENT_TYPE.PROPOSAL_MAP, + type: new ApplicationDocumentCode({ + code: DOCUMENT_TYPE.PROPOSAL_MAP, + }), + }), + ]; + mockAppDocumentService.getApplicantDocuments.mockResolvedValue(documents); + + const res = await service.validateSubmission(application); + + expect( + includesError( + res.errors, + new Error(`INCL proposal missing inclusion fields`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`INCL proposal is missing proposal map / site plan`), + ), + ).toBe(false); + }); + + it('should require documents when government does not own all parcels', async () => { + const application = new ApplicationSubmission({ + owners: [], + inclGovernmentOwnsAllParcels: false, + typeCode: 'INCL', + }); + + const res = await service.validateSubmission(application); + + expect( + includesError( + res.errors, + new Error(`INCL proposal is missing proof of advertising`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`INCL proposal is missing proof of signage`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`INCL proposal is missing report of public hearing`), + ), + ).toBe(true); + }); + + it('should note require documents when government owns all parcels', async () => { + const application = new ApplicationSubmission({ + owners: [], + inclGovernmentOwnsAllParcels: true, + typeCode: 'INCL', + }); + + const res = await service.validateSubmission(application); + + expect( + includesError( + res.errors, + new Error(`INCL proposal is missing proof of advertising`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`INCL proposal is missing proof of signage`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`INCL proposal is missing report of public hearing`), + ), + ).toBe(false); + }); + }); + + describe('EXCL Applications', () => { + it('should require basic fields to be complete', async () => { + const application = new ApplicationSubmission({ + owners: [], + typeCode: 'EXCL', + prescribedBody: null, + }); + + const res = await service.validateSubmission(application); + + expect( + includesError( + res.errors, + new Error(`EXCL proposal missing exclusion fields`), + ), + ).toBe(true); + }); + + it('should be happy if basic fields are complete', async () => { + const application = new ApplicationSubmission({ + owners: [], + applicant: 'applicant', + purpose: 'purpose', + typeCode: 'EXCL', + prescribedBody: 'inclImprovements', + exclShareGovernmentBorders: false, + inclExclHectares: 2, + exclWhyLand: 'exclWhyLand', + }); + + const documents = [ + new ApplicationDocument({ + typeCode: DOCUMENT_TYPE.PROPOSAL_MAP, + type: new ApplicationDocumentCode({ + code: DOCUMENT_TYPE.PROPOSAL_MAP, + }), + }), + ]; + mockAppDocumentService.getApplicantDocuments.mockResolvedValue(documents); + + const res = await service.validateSubmission(application); + + expect( + includesError( + res.errors, + new Error(`EXCL proposal missing inclusion fields`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`EXCL proposal is missing proposal map / site plan`), + ), + ).toBe(false); + }); + + it('should require all documents', async () => { + const application = new ApplicationSubmission({ + owners: [], + typeCode: 'EXCL', + }); + + const res = await service.validateSubmission(application); + + expect( + includesError( + res.errors, + new Error(`EXCL proposal is missing proof of advertising`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`EXCL proposal is missing proof of signage`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`EXCL proposal is missing report of public hearing`), + ), + ).toBe(true); + }); + + it('should be happy if all documents are provided', async () => { + const application = new ApplicationSubmission({ + owners: [], + inclGovernmentOwnsAllParcels: true, + typeCode: 'EXCL', + }); + + const documents = [ + new ApplicationDocument({ + typeCode: DOCUMENT_TYPE.PROOF_OF_ADVERTISING, + type: new ApplicationDocumentCode({ + code: DOCUMENT_TYPE.PROOF_OF_ADVERTISING, + }), + }), + new ApplicationDocument({ + typeCode: DOCUMENT_TYPE.PROOF_OF_SIGNAGE, + type: new ApplicationDocumentCode({ + code: DOCUMENT_TYPE.PROOF_OF_SIGNAGE, + }), + }), + new ApplicationDocument({ + typeCode: DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING, + type: new ApplicationDocumentCode({ + code: DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING, + }), + }), + ]; + mockAppDocumentService.getApplicantDocuments.mockResolvedValue(documents); + + const res = await service.validateSubmission(application); + + expect( + includesError( + res.errors, + new Error(`EXCL proposal is missing proof of advertising`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`EXCL proposal is missing proof of signage`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`EXCL proposal is missing report of public hearing`), + ), + ).toBe(false); + }); + }); }); diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts index b902f52f8d..70a7013a65 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts @@ -117,6 +117,22 @@ export class ApplicationSubmissionValidatorService { ); } + if (applicationSubmission.typeCode === 'EXCL') { + await this.validateExclProposal( + applicationSubmission, + applicantDocuments, + errors, + ); + } + + if (applicationSubmission.typeCode === 'INCL') { + await this.validateInclProposal( + applicationSubmission, + applicantDocuments, + errors, + ); + } + const validatedApplication = validatedParcels && validatedPrimaryContact ? ({ @@ -658,4 +674,133 @@ export class ApplicationSubmissionValidatorService { ); } } + + private async validateExclProposal( + applicationSubmission: ApplicationSubmission, + applicantDocuments: ApplicationDocument[], + errors: Error[], + ) { + if ( + applicationSubmission.prescribedBody === null || + applicationSubmission.exclShareGovernmentBorders === null || + applicationSubmission.exclWhyLand === null || + applicationSubmission.inclExclHectares === null + ) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal missing exclusion fields`, + ), + ); + } + + const proposalMap = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.PROPOSAL_MAP, + ); + if (proposalMap.length === 0) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal is missing proposal map / site plan`, + ), + ); + } + + const proofOfAdvertising = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.PROOF_OF_ADVERTISING, + ); + if (proofOfAdvertising.length === 0) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal is missing proof of advertising`, + ), + ); + } + + const proofOfSignage = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.PROOF_OF_SIGNAGE, + ); + if (proofOfSignage.length === 0) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal is missing proof of signage`, + ), + ); + } + + const reportOfHearing = applicantDocuments.filter( + (document) => + document.typeCode === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING, + ); + if (reportOfHearing.length === 0) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal is missing report of public hearing`, + ), + ); + } + } + + private async validateInclProposal( + applicationSubmission: ApplicationSubmission, + applicantDocuments: ApplicationDocument[], + errors: Error[], + ) { + if ( + applicationSubmission.inclImprovements === null || + applicationSubmission.inclAgricultureSupport === null || + applicationSubmission.inclExclHectares === null + ) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal missing inclusion fields`, + ), + ); + } + + const proposalMap = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.PROPOSAL_MAP, + ); + if (proposalMap.length === 0) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal is missing proposal map / site plan`, + ), + ); + } + + if (applicationSubmission.inclGovernmentOwnsAllParcels === false) { + const proofOfAdvertising = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.PROOF_OF_ADVERTISING, + ); + if (proofOfAdvertising.length === 0) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal is missing proof of advertising`, + ), + ); + } + + const proofOfSignage = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.PROOF_OF_SIGNAGE, + ); + if (proofOfSignage.length === 0) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal is missing proof of signage`, + ), + ); + } + + const reportOfHearing = applicantDocuments.filter( + (document) => + document.typeCode === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING, + ); + if (reportOfHearing.length === 0) { + errors.push( + new ServiceValidationException( + `${applicationSubmission.typeCode} proposal is missing report of public hearing`, + ), + ); + } + } + } } From 0606c5d957c60fefa873db1faa92c8a008622c54 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 28 Jul 2023 14:31:35 -0700 Subject: [PATCH 174/954] Add INCL Proposal Component --- .../application/application.module.ts | 2 + .../proposal/incl/incl.component.html | 4 ++ .../proposal/incl/incl.component.scss | 3 ++ .../proposal/incl/incl.component.spec.ts | 49 +++++++++++++++++++ .../proposal/incl/incl.component.ts | 41 ++++++++++++++++ .../proposal/proposal.component.html | 1 + 6 files changed, 100 insertions(+) create mode 100644 alcs-frontend/src/app/features/application/proposal/incl/incl.component.html create mode 100644 alcs-frontend/src/app/features/application/proposal/incl/incl.component.scss create mode 100644 alcs-frontend/src/app/features/application/proposal/incl/incl.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/proposal/incl/incl.component.ts diff --git a/alcs-frontend/src/app/features/application/application.module.ts b/alcs-frontend/src/app/features/application/application.module.ts index c60c905f04..a26b8f313c 100644 --- a/alcs-frontend/src/app/features/application/application.module.ts +++ b/alcs-frontend/src/app/features/application/application.module.ts @@ -22,6 +22,7 @@ import { UncancelApplicationDialogComponent } from './overview/uncancel-applicat import { EditModificationDialogComponent } from './post-decision/edit-modification-dialog/edit-modification-dialog.component'; import { EditReconsiderationDialogComponent } from './post-decision/edit-reconsideration-dialog/edit-reconsideration-dialog.component'; import { PostDecisionComponent } from './post-decision/post-decision.component'; +import { InclProposalComponent } from './proposal/incl/incl.component'; import { NaruProposalComponent } from './proposal/naru/naru.component'; import { NfuProposalComponent } from './proposal/nfu/nfu.component'; import { ParcelPrepComponent } from './proposal/parcel-prep/parcel-prep.component'; @@ -76,6 +77,7 @@ const routes: Routes = [ TurProposalComponent, ExclProposalComponent, NaruProposalComponent, + InclProposalComponent, ParcelPrepComponent, UncancelApplicationDialogComponent, ], diff --git a/alcs-frontend/src/app/features/application/proposal/incl/incl.component.html b/alcs-frontend/src/app/features/application/proposal/incl/incl.component.html new file mode 100644 index 0000000000..3aab1c4713 --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/incl/incl.component.html @@ -0,0 +1,4 @@ +
+
Applicant Type
+ {{ applicantType }} +
diff --git a/alcs-frontend/src/app/features/application/proposal/incl/incl.component.scss b/alcs-frontend/src/app/features/application/proposal/incl/incl.component.scss new file mode 100644 index 0000000000..bdb73c1e94 --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/incl/incl.component.scss @@ -0,0 +1,3 @@ +span { + margin-top: 6px; +} diff --git a/alcs-frontend/src/app/features/application/proposal/incl/incl.component.spec.ts b/alcs-frontend/src/app/features/application/proposal/incl/incl.component.spec.ts new file mode 100644 index 0000000000..93d08cd915 --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/incl/incl.component.spec.ts @@ -0,0 +1,49 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ApplicationSubmissionService } from '../../../../services/application/application-submission/application-submission.service'; +import { InclProposalComponent } from './incl.component'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationDto } from '../../../../services/application/application.dto'; + +describe('InclProposalComponent', () => { + let component: InclProposalComponent; + let fixture: ComponentFixture; + let mockApplicationDetailService: DeepMocked; + let mockAppSubmissionService: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(async () => { + mockApplicationDetailService = createMock(); + mockToastService = createMock(); + mockAppSubmissionService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [InclProposalComponent], + providers: [ + { provide: ToastService, useValue: mockToastService }, + { + provide: ApplicationDetailService, + useValue: mockApplicationDetailService, + }, + { + provide: ApplicationSubmissionService, + useValue: mockAppSubmissionService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + mockApplicationDetailService.$application = new BehaviorSubject(undefined); + + fixture = TestBed.createComponent(InclProposalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/proposal/incl/incl.component.ts b/alcs-frontend/src/app/features/application/proposal/incl/incl.component.ts new file mode 100644 index 0000000000..1aae10eb8a --- /dev/null +++ b/alcs-frontend/src/app/features/application/proposal/incl/incl.component.ts @@ -0,0 +1,41 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; +import { ApplicationSubmissionService } from '../../../../services/application/application-submission/application-submission.service'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { Subject, takeUntil } from 'rxjs'; +import { ApplicationDto, UpdateApplicationDto } from '../../../../services/application/application.dto'; + +@Component({ + selector: 'app-proposal-incl', + templateUrl: './incl.component.html', + styleUrls: ['./incl.component.scss'], +}) +export class InclProposalComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + application: ApplicationDto | undefined; + applicantType: string | undefined; + + constructor( + private applicationDetailService: ApplicationDetailService, + private applicationSubmissionService: ApplicationSubmissionService + ) {} + + ngOnInit(): void { + this.applicationDetailService.$application.pipe(takeUntil(this.$destroy)).subscribe((application) => { + if (application) { + this.applicationSubmissionService + .fetchSubmission(application.fileNumber) + .then( + (submission) => + (this.applicantType = + submission.inclGovernmentOwnsAllParcels === false ? 'L/FNG Initiated' : 'Land Owner') + ); + } + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/alcs-frontend/src/app/features/application/proposal/proposal.component.html b/alcs-frontend/src/app/features/application/proposal/proposal.component.html index a6da77d0b8..3d9bbde10a 100644 --- a/alcs-frontend/src/app/features/application/proposal/proposal.component.html +++ b/alcs-frontend/src/app/features/application/proposal/proposal.component.html @@ -38,6 +38,7 @@
Proposal Components - {{ application?.type?.label }}
+
From 57bbf33966e2c063cc2fc477b48141faee1121a7 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 28 Jul 2023 15:40:57 -0700 Subject: [PATCH 175/954] Add Incl/Excl Decision Components * Add Inclusion and Exclusion Decision Components with Copy Feature * Add new decision component column to store applicant type --- .../incl-excl/incl-excl.component.html | 17 +++++++++++ .../incl-excl/incl-excl.component.scss | 0 .../incl-excl/incl-excl.component.spec.ts | 26 ++++++++++++++++ .../incl-excl/incl-excl.component.ts | 11 +++++++ .../decision-component.component.html | 9 ++++++ .../decision-component.component.ts | 30 +++++++++++++++++++ .../incl-excl-input.component.html | 24 +++++++++++++++ .../incl-excl-input.component.scss | 0 .../incl-excl-input.component.spec.ts | 24 +++++++++++++++ .../incl-excl-input.component.ts | 11 +++++++ .../decision-components.component.ts | 20 +++++++++++++ .../decision-input-v2.component.scss | 6 ++++ .../decision-v2/decision-v2.component.html | 9 ++++++ .../application/decision/decision.module.ts | 4 +++ .../application-decision-v2.dto.ts | 9 +++++- .../application-decision-component.dto.ts | 7 +++++ .../application-decision-component.entity.ts | 8 +++++ .../application-decision-component.service.ts | 8 +++++ ...580462566-add_incl_excl_component_types.ts | 21 +++++++++++++ ...90580680063-add_dec_comp_applicant_type.ts | 25 ++++++++++++++++ 20 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.scss create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.html create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.scss create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.spec.ts create mode 100644 alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1690580462566-add_incl_excl_component_types.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1690580680063-add_dec_comp_applicant_type.ts diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html new file mode 100644 index 0000000000..abf3a14f1e --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html @@ -0,0 +1,17 @@ +
+ +
+ +
+
Applicant Type
+ {{ component.inclExclApplicantType }} + +
+ +
+
Expiry Date
+ {{ component.expiryDate | date }} + +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.spec.ts new file mode 100644 index 0000000000..9681c2d6ab --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.spec.ts @@ -0,0 +1,26 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; + +import { InclExclComponent } from './incl-excl.component'; + +describe('NaruComponent', () => { + let component: InclExclComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [InclExclComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(InclExclComponent); + component = fixture.componentInstance; + component.component = {} as DecisionComponentDto; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts new file mode 100644 index 0000000000..d183fc72d9 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; + +@Component({ + selector: 'app-incl-excl', + templateUrl: './incl-excl.component.html', + styleUrls: ['./incl-excl.component.scss'], +}) +export class InclExclComponent { + @Input() component!: DecisionComponentDto; +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html index 004bd7bfd4..543ab647d2 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html @@ -102,4 +102,13 @@ [form]="form" >
+
+ +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts index 968dfb1857..73c0e0b70f 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts @@ -64,6 +64,9 @@ export class DecisionComponentComponent implements OnInit { //subd subdApprovedLots = new FormControl([], [Validators.required]); + //incl/excl + applicantType = new FormControl(null, [Validators.required]); + // general alrArea = new FormControl(null, [Validators.required]); agCap = new FormControl(null, [Validators.required]); @@ -112,6 +115,12 @@ export class DecisionComponentComponent implements OnInit { case APPLICATION_DECISION_COMPONENT_TYPE.SUBD: this.patchSubdFields(); break; + case APPLICATION_DECISION_COMPONENT_TYPE.INCL: + this.patchInclExclFields(); + break; + case APPLICATION_DECISION_COMPONENT_TYPE.EXCL: + this.patchInclExclFields(); + break; default: this.toastService.showErrorToast('Wrong decision component type'); break; @@ -176,6 +185,12 @@ export class DecisionComponentComponent implements OnInit { case APPLICATION_DECISION_COMPONENT_TYPE.SUBD: dataChange = { ...dataChange, ...this.getSubdDataChange() }; break; + case APPLICATION_DECISION_COMPONENT_TYPE.INCL: + dataChange = { ...dataChange, ...this.getInclExclDataChange() }; + break; + case APPLICATION_DECISION_COMPONENT_TYPE.EXCL: + dataChange = { ...dataChange, ...this.getInclExclDataChange() }; + break; default: this.toastService.showErrorToast('Wrong decision component type'); break; @@ -246,6 +261,14 @@ export class DecisionComponentComponent implements OnInit { this.subdApprovedLots.setValue(this.data.subdApprovedLots ?? null); } + private patchInclExclFields() { + this.form.addControl('applicantType', this.applicantType); + this.form.addControl('expiryDate', this.expiryDate); + + this.applicantType.setValue(this.data.inclExclApplicantType ?? null); + this.expiryDate.setValue(this.data.expiryDate ? new Date(this.data.expiryDate) : null); + } + private getNfuDataChange(): NfuDecisionComponentDto { return { nfuType: this.nfuType.value ? this.nfuType.value : null, @@ -315,4 +338,11 @@ export class DecisionComponentComponent implements OnInit { markTouched() { this.subdInputComponent?.markAllAsTouched(); } + + private getInclExclDataChange() { + return { + inclExclApplicantType: this.applicantType.value ?? undefined, + expiryDate: this.expiryDate.value ? formatDateForApi(this.expiryDate.value) : null, + }; + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.html new file mode 100644 index 0000000000..b4203dc864 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.html @@ -0,0 +1,24 @@ +
+
+
+ Applicant Type* + + Land Owner + L/FNG Initiated + +
+ + + Expiry Date + + + + +
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.spec.ts new file mode 100644 index 0000000000..23c3b96223 --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InclExclInputComponent } from './incl-excl-input.component'; + +describe('InclExclInputComponent', () => { + let component: InclExclInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [InclExclInputComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(InclExclInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.ts new file mode 100644 index 0000000000..6da5009f8c --- /dev/null +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-incl-excl-input', + templateUrl: './incl-excl-input.component.html', + styleUrls: ['./incl-excl-input.component.scss'], +}) +export class InclExclInputComponent { + @Input() form!: FormGroup; +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index 19b14d17fc..1e9c5e205e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -144,6 +144,13 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView this.patchNaruFields(component); } + if ( + typeCode === APPLICATION_DECISION_COMPONENT_TYPE.INCL || + typeCode === APPLICATION_DECISION_COMPONENT_TYPE.EXCL + ) { + this.patchInclExclFields(component); + } + this.components.push(component); break; case APPLICATION_DECISION_COMPONENT_TYPE.NFUP: @@ -153,6 +160,8 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView case APPLICATION_DECISION_COMPONENT_TYPE.PFRS: case APPLICATION_DECISION_COMPONENT_TYPE.NARU: case APPLICATION_DECISION_COMPONENT_TYPE.SUBD: + case APPLICATION_DECISION_COMPONENT_TYPE.INCL: + case APPLICATION_DECISION_COMPONENT_TYPE.EXCL: this.components.push({ applicationDecisionComponentTypeCode: typeCode, applicationDecisionComponentType: this.decisionComponentTypes.find( @@ -201,6 +210,17 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView component.naruSubtypeCode = this.application.submittedApplication?.naruSubtype?.code; } + private patchInclExclFields(component: DecisionComponentDto) { + if (this.application.inclExclApplicantType) { + component.inclExclApplicantType = this.application.inclExclApplicantType; + } else { + component.inclExclApplicantType = + this.application.submittedApplication?.inclGovernmentOwnsAllParcels === false + ? 'L/FNG Initiated' + : 'Land Owner'; + } + } + private updateComponentsMenuItems() { this.decisionComponentTypes = this.decisionComponentTypes.map((e) => ({ ...e, diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss index e6e203b157..e2bbee1da8 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss @@ -160,4 +160,10 @@ margin-bottom: 0px !important; } } + + .toggle-label { + color: colors.$grey-dark; + display: block; + margin-bottom: 3px; + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html index 1cc2816ba7..b912c09605 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html @@ -261,6 +261,15 @@
Decision Components and Conditions
*ngIf="component.applicationDecisionComponentTypeCode === COMPONENT_TYPE.SUBD" [component]="component" > + +
diff --git a/alcs-frontend/src/app/features/application/decision/decision.module.ts b/alcs-frontend/src/app/features/application/decision/decision.module.ts index adf3641992..5e656899df 100644 --- a/alcs-frontend/src/app/features/application/decision/decision.module.ts +++ b/alcs-frontend/src/app/features/application/decision/decision.module.ts @@ -14,6 +14,7 @@ import { ConditionComponent } from './conditions/condition/condition.component'; import { ConditionsComponent } from './conditions/conditions.component'; import { DecisionV1DialogComponent } from './decision-v1/decision-v1-dialog/decision-v1-dialog.component'; import { DecisionV1Component } from './decision-v1/decision-v1.component'; +import { InclExclComponent } from './decision-v2/decision-component/incl-excl/incl-excl.component'; import { NaruComponent } from './decision-v2/decision-component/naru/naru.component'; import { NfupComponent } from './decision-v2/decision-component/nfup/nfup.component'; import { PfrsComponent } from './decision-v2/decision-component/pfrs/pfrs.component'; @@ -23,6 +24,7 @@ import { SubdComponent } from './decision-v2/decision-component/subd/subd.compon import { TurpComponent } from './decision-v2/decision-component/turp/turp.component'; import { DecisionDocumentsComponent } from './decision-v2/decision-documents/decision-documents.component'; import { DecisionComponentComponent } from './decision-v2/decision-input/decision-components/decision-component/decision-component.component'; +import { InclExclInputComponent } from './decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component'; import { NaruInputComponent } from './decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component'; import { NfuInputComponent } from './decision-v2/decision-input/decision-components/decision-component/nfu-input/nfu-input.component'; import { PfrsInputComponent } from './decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component'; @@ -96,6 +98,8 @@ export const decisionChildRoutes = [ PfrsComponent, NaruComponent, SubdComponent, + InclExclComponent, + InclExclInputComponent, NaruInputComponent, ConditionsComponent, ConditionComponent, diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts index 838db7e479..81df88bcf9 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts @@ -151,13 +151,18 @@ export interface SubdDecisionComponentDto { subdApprovedLots?: ProposedLot[]; } +export interface InclExclDecisionComponentDto { + inclExclApplicantType?: string | null; +} + export interface DecisionComponentDto extends NfuDecisionComponentDto, TurpDecisionComponentDto, PofoDecisionComponentDto, RosoDecisionComponentDto, NaruDecisionComponentDto, - SubdDecisionComponentDto { + SubdDecisionComponentDto, + InclExclDecisionComponentDto { uuid?: string; alrArea?: number | null; agCap?: string | null; @@ -188,6 +193,8 @@ export enum APPLICATION_DECISION_COMPONENT_TYPE { PFRS = 'PFRS', NARU = 'NARU', SUBD = 'SUBD', + INCL = 'INCL', + EXCL = 'EXCL', } export interface ApplicationDecisionConditionTypeDto extends BaseCodeDto {} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts index 1fa5cd93b5..a77ad3f11c 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts @@ -96,6 +96,10 @@ export class UpdateApplicationDecisionComponentDto { @IsArray() @IsOptional() subdApprovedLots?: ProposedLot[]; + + @IsString() + @IsOptional() + inclExclApplicantType?: string | null; } export class CreateApplicationDecisionComponentDto extends UpdateApplicationDecisionComponentDto { @@ -182,6 +186,9 @@ export class ApplicationDecisionComponentDto { @AutoMap(() => [ProposedLot]) subdApprovedLots: ProposedLot[]; + + @AutoMap(() => String) + inclExclApplicantType?: string; } export enum APPLICATION_DECISION_COMPONENT_TYPE { diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts index 51a31a9206..9005f4db9c 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts @@ -199,6 +199,14 @@ export class ApplicationDecisionComponent extends Base { }) subdApprovedLots: ProposedLot[]; + @AutoMap(() => String) + @Column({ + nullable: true, + type: 'text', + comment: 'Stores the applicant type for inclusion and exclusion components', + }) + inclExclApplicantType: string | null; + @AutoMap() @Column({ nullable: false }) applicationDecisionComponentTypeCode: string; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts index 096b502cd0..d5cd62582d 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts @@ -55,6 +55,14 @@ export class ApplicationDecisionComponentService { component.subdApprovedLots = updateDto.subdApprovedLots; } + //INCL / EXCL + if (updateDto.inclExclApplicantType !== undefined) { + component.inclExclApplicantType = updateDto.inclExclApplicantType; + component.expiryDate = updateDto.expiryDate + ? new Date(updateDto.expiryDate) + : null; + } + updatedComponents.push(component); } diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690580462566-add_incl_excl_component_types.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690580462566-add_incl_excl_component_types.ts new file mode 100644 index 0000000000..961cc8cb44 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690580462566-add_incl_excl_component_types.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addInclExclComponentTypes1690580462566 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + INSERT INTO alcs.application_decision_component_type + (audit_deleted_date_at, audit_created_at, audit_updated_at, audit_created_by, audit_updated_by, "label", code, description) + values + (null , now(), now(), 'seed-migration','seed-migration' , 'Inclusion', 'INCL', 'Inclusion'), + (null , now(), now(), 'seed-migration','seed-migration' , 'Exclusion', 'EXCL', 'Exclusion'); + `, + ); + } + + public async down(): Promise { + //No + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690580680063-add_dec_comp_applicant_type.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690580680063-add_dec_comp_applicant_type.ts new file mode 100644 index 0000000000..63e6704a76 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690580680063-add_dec_comp_applicant_type.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addDecCompApplicantType1690580680063 + implements MigrationInterface +{ + name = 'addDecCompApplicantType1690580680063'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_component" ADD "incl_excl_applicant_type" text`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_decision_component"."incl_excl_applicant_type" IS 'Stores the applicant type for inclusion and exclusion components'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_decision_component"."incl_excl_applicant_type" IS 'Stores the applicant type for inclusion and exclusion components'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_component" DROP COLUMN "incl_excl_applicant_type"`, + ); + } +} From 821dbd72414245a9134f1124fc9372285c74e491 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 28 Jul 2023 15:57:56 -0700 Subject: [PATCH 176/954] Code review feedback --- .../decision-component/decision-component.component.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts index 73c0e0b70f..7aebabd00a 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts @@ -116,8 +116,6 @@ export class DecisionComponentComponent implements OnInit { this.patchSubdFields(); break; case APPLICATION_DECISION_COMPONENT_TYPE.INCL: - this.patchInclExclFields(); - break; case APPLICATION_DECISION_COMPONENT_TYPE.EXCL: this.patchInclExclFields(); break; @@ -186,8 +184,6 @@ export class DecisionComponentComponent implements OnInit { dataChange = { ...dataChange, ...this.getSubdDataChange() }; break; case APPLICATION_DECISION_COMPONENT_TYPE.INCL: - dataChange = { ...dataChange, ...this.getInclExclDataChange() }; - break; case APPLICATION_DECISION_COMPONENT_TYPE.EXCL: dataChange = { ...dataChange, ...this.getInclExclDataChange() }; break; From d7fc7c34b63d9314ed0577a9e7cdbe2edb55191e Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 31 Jul 2023 09:27:47 -0700 Subject: [PATCH 177/954] Allow changing prescribed body with change app type dialog --- ...nge-application-type-dialog.component.html | 2 +- .../edit-submission.component.ts | 20 +++++++++---------- .../application-submission.service.ts | 4 ++++ 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html b/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html index 001d0a0494..12191c3f0d 100644 --- a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html +++ b/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html @@ -71,7 +71,7 @@
Are you sure you want to change your application type?
You will not be able to complete the remaining portion of the application until the notification and public hearing diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index 16fa1c1ae8..eaf916546f 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -204,11 +204,17 @@ export class ApplicationSubmissionReviewController { ); } + const creatingGuid = applicationSubmission.createdBy.bceidBusinessGuid; + const creatingGovernment = await this.localGovernmentService.getByGuid( + creatingGuid, + ); + if ( - userLocalGovernment.uuid === applicationSubmission.localGovernmentUuid && - primaryContact + creatingGovernment?.uuid === applicationSubmission.localGovernmentUuid && + primaryContact && + primaryContact.type.code === APPLICATION_OWNER.GOVERNMENT ) { - //Copy contact details over to government form + //Copy contact details over to government form when applying to self await this.applicationSubmissionReviewService.update( applicationSubmission.fileNumber, { From 72ca60e43840b6e85d1d445724e579e585aeda09 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 31 Jul 2023 10:40:48 -0700 Subject: [PATCH 179/954] Add FIXME to all placeholder links --- .../proposal/incl-proposal/incl-proposal.component.html | 2 +- .../proposal/naru-proposal/naru-proposal.component.html | 2 +- .../proposal/pfrs-proposal/pfrs-proposal.component.html | 8 ++++---- .../proposal/pofo-proposal/pofo-proposal.component.html | 6 +++--- .../proposal/roso-proposal/roso-proposal.component.html | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html index c7463b9985..9ae514abbd 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html @@ -126,7 +126,7 @@

Proposal

The ALR General Regulation does not require the {{ governmentName }} to complete a public hearing if all inclusion application parcel(s) are owned by the {{ governmentName }}.
- Please refer to Inclusion page on the ALC website for more information. + Please refer to FIXME: Inclusion page on the ALC website for more information. Proposal
Agri-tourism activities are defined in the Agricultural Land Reserve Use Regulation.
- Please refer to the Non-Adhering Residential Use page on the ALC website for more information + Please refer to the FIXME: Non-Adhering Residential Use page on the ALC website for more information - Characters left: {{ 4000 - fillTypeDescriptionText.textLength }} + Characters left: {{ 4000 - fillTypeToPlaceText.textLength }}
Soil & Fill Components warning
This field is required
+
Example: Aggregate, topsoil, structural fill, sand, gravel, etc
+
+ +
- + - Characters left: {{ 4000 - fillOriginDescriptionText.textLength }} + Characters left: {{ 4000 - fillOriginToPlaceText.textLength }}
Soil & Fill Components
This field is required
-
-

Project Dimensions

-
-
- -
- Enter area in hectares (ha) (Note: 0.01 ha is 100 m2) -
- - - ha - -
- warning -
This field is required
-
-
-
- -
- Enter depth in meters (m)  -
- - - m - -
- warning -
This field is required
-
-
-
- -
- Enter volume in cubic meters (m3) without a comma -
- - - m3 - -
- warning -
This field is required
-
-
+ + +

Project Duration

+
Estimated duration of the project
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts index b063aff119..a69d6e39d9 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts +++ b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts @@ -7,6 +7,7 @@ import { ApplicationSubmissionService } from '../../../../services/application-s import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { StepComponent } from '../../step.partial'; +import { SoilTableData } from '../soil-table/soil-table.component'; @Component({ selector: 'app-nfu-proposal', @@ -16,14 +17,13 @@ import { StepComponent } from '../../step.partial'; export class NfuProposalComponent extends StepComponent implements OnInit, OnDestroy { currentStep = EditApplicationSteps.Proposal; + fillTableData: SoilTableData = {}; + hectares = new FormControl(null, [Validators.required]); purpose = new FormControl(null, [Validators.required]); outsideLands = new FormControl(null, [Validators.required]); agricultureSupport = new FormControl(null, [Validators.required]); willImportFill = new FormControl(null, [Validators.required]); - totalFillPlacement = new FormControl(null, [Validators.required]); - maxFillDepth = new FormControl(null, [Validators.required]); - fillVolume = new FormControl(null, [Validators.required]); projectDurationAmount = new FormControl(null, [Validators.required]); projectDurationUnit = new FormControl(null, [Validators.required]); fillTypeDescription = new FormControl(null, [Validators.required]); @@ -35,9 +35,6 @@ export class NfuProposalComponent extends StepComponent implements OnInit, OnDes outsideLands: this.outsideLands, agricultureSupport: this.agricultureSupport, willImportFill: this.willImportFill, - totalFillPlacement: this.totalFillPlacement, - maxFillDepth: this.maxFillDepth, - fillVolume: this.fillVolume, projectDurationAmount: this.projectDurationAmount, projectDurationUnit: this.projectDurationUnit, fillTypeDescription: this.fillTypeDescription, @@ -61,15 +58,19 @@ export class NfuProposalComponent extends StepComponent implements OnInit, OnDes purpose: applicationSubmission.purpose, outsideLands: applicationSubmission.nfuOutsideLands, agricultureSupport: applicationSubmission.nfuAgricultureSupport, - totalFillPlacement: applicationSubmission.nfuTotalFillPlacement?.toString(), - maxFillDepth: applicationSubmission.nfuMaxFillDepth?.toString(), - fillVolume: applicationSubmission.nfuFillVolume?.toString(), projectDurationAmount: applicationSubmission.nfuProjectDurationAmount?.toString(), projectDurationUnit: applicationSubmission.nfuProjectDurationUnit, fillTypeDescription: applicationSubmission.nfuFillTypeDescription, fillOriginDescription: applicationSubmission.nfuFillOriginDescription, }); + this.fillTableData = { + volume: applicationSubmission.nfuFillVolume ?? undefined, + area: applicationSubmission.nfuTotalFillPlacement ?? undefined, + maximumDepth: applicationSubmission.nfuMaxFillDepth ?? undefined, + averageDepth: applicationSubmission.nfuAverageFillDepth ?? undefined, + }; + if (applicationSubmission.nfuWillImportFill !== null) { this.willImportFill.setValue(applicationSubmission.nfuWillImportFill ? 'true' : 'false'); this.onChangeFill(applicationSubmission.nfuWillImportFill ? 'true' : 'false'); @@ -93,9 +94,6 @@ export class NfuProposalComponent extends StepComponent implements OnInit, OnDes const nfuOutsideLands = this.outsideLands.getRawValue(); const nfuAgricultureSupport = this.agricultureSupport.getRawValue(); const nfuWillImportFill = this.willImportFill.getRawValue(); - const nfuTotalFillPlacement = this.totalFillPlacement.getRawValue(); - const nfuMaxFillDepth = this.maxFillDepth.getRawValue(); - const nfuFillVolume = this.fillVolume.getRawValue(); const nfuProjectDurationAmount = this.projectDurationAmount.getRawValue(); const nfuProjectDurationUnit = this.projectDurationUnit.getRawValue(); const nfuFillTypeDescription = this.fillTypeDescription.getRawValue(); @@ -107,9 +105,10 @@ export class NfuProposalComponent extends StepComponent implements OnInit, OnDes nfuOutsideLands, nfuAgricultureSupport, nfuWillImportFill: parseStringToBoolean(nfuWillImportFill), - nfuTotalFillPlacement: nfuTotalFillPlacement ? parseFloat(nfuTotalFillPlacement) : null, - nfuMaxFillDepth: nfuMaxFillDepth ? parseFloat(nfuMaxFillDepth) : null, - nfuFillVolume: nfuFillVolume ? parseFloat(nfuFillVolume) : null, + nfuTotalFillPlacement: this.fillTableData.area ?? null, + nfuMaxFillDepth: this.fillTableData.maximumDepth ?? null, + nfuAverageFillDepth: this.fillTableData.averageDepth ?? null, + nfuFillVolume: this.fillTableData.volume ?? null, nfuProjectDurationAmount: nfuProjectDurationAmount ? parseFloat(nfuProjectDurationAmount) : null, nfuProjectDurationUnit, nfuFillTypeDescription, @@ -123,29 +122,24 @@ export class NfuProposalComponent extends StepComponent implements OnInit, OnDes onChangeFill(value: string) { if (value === 'true') { - this.totalFillPlacement.enable(); - this.maxFillDepth.enable(); - this.fillVolume.enable(); this.projectDurationAmount.enable(); this.projectDurationUnit.enable(); this.fillTypeDescription.enable(); this.fillOriginDescription.enable(); } else { - this.totalFillPlacement.disable(); - this.maxFillDepth.disable(); - this.fillVolume.disable(); this.projectDurationAmount.disable(); this.projectDurationUnit.disable(); this.fillTypeDescription.disable(); this.fillOriginDescription.disable(); - this.totalFillPlacement.setValue(null); - this.maxFillDepth.setValue(null); - this.fillVolume.setValue(null); this.projectDurationAmount.setValue(null); this.projectDurationUnit.setValue(null); this.fillTypeDescription.setValue(null); this.fillOriginDescription.setValue(null); } } + + markDirty() { + this.form.markAsDirty(); + } } diff --git a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts index c831b683f0..d597bc128e 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts @@ -79,6 +79,7 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD nfuWillImportFill: boolean | null; nfuTotalFillPlacement: number | null; nfuMaxFillDepth: number | null; + nfuAverageFillDepth: number | null; nfuFillVolume: number | null; nfuProjectDurationAmount: number | null; nfuProjectDurationUnit: string | null; @@ -185,6 +186,7 @@ export interface ApplicationSubmissionUpdateDto { nfuWillImportFill?: boolean | null; nfuTotalFillPlacement?: number | null; nfuMaxFillDepth?: number | null; + nfuAverageFillDepth?: number | null; nfuFillVolume?: number | null; nfuProjectDurationAmount?: number | null; nfuProjectDurationUnit?: string | null; diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts index 4b162e1a1c..b54f16ba38 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts @@ -549,6 +549,7 @@ describe('ApplicationSubmissionValidatorService', () => { nfuFillOriginDescription: 'VALID', nfuTotalFillPlacement: 0.0, nfuMaxFillDepth: 1.5125, + nfuAverageFillDepth: 1261.21, nfuFillVolume: 742.1, nfuProjectDurationAmount: 12, nfuProjectDurationUnit: 'VALID', @@ -597,6 +598,7 @@ describe('ApplicationSubmissionValidatorService', () => { nfuFillOriginDescription: null, nfuTotalFillPlacement: 0.0, nfuMaxFillDepth: 1.5125, + nfuAverageFillDepth: 121, nfuFillVolume: 742.1, nfuProjectDurationAmount: 12, nfuProjectDurationUnit: 'VALID', diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts index 70a7013a65..ab7e22cbf1 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts @@ -414,6 +414,7 @@ export class ApplicationSubmissionValidatorService { !applicationSubmission.nfuFillOriginDescription || applicationSubmission.nfuTotalFillPlacement === null || applicationSubmission.nfuMaxFillDepth === null || + applicationSubmission.nfuAverageFillDepth === null || applicationSubmission.nfuFillVolume === null || applicationSubmission.nfuProjectDurationAmount === null || !applicationSubmission.nfuProjectDurationUnit diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts index 249009cbc4..244b7d5935 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts @@ -107,6 +107,9 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { @AutoMap(() => Number) nfuMaxFillDepth?: number | null; + @AutoMap(() => Number) + nfuAverageFillDepth?: number | null; + @AutoMap(() => Number) nfuFillVolume?: number | null; @@ -424,6 +427,10 @@ export class ApplicationSubmissionUpdateDto { @IsOptional() nfuMaxFillDepth?: number | null; + @IsNumber() + @IsOptional() + nfuAverageFillDepth?: number | null; + @IsNumber() @IsOptional() nfuFillVolume?: number | null; diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts index e13c5a21ba..69e14f3e61 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts @@ -239,6 +239,16 @@ export class ApplicationSubmission extends Base { }) nfuTotalFillPlacement: number | null; + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + nfuAverageFillDepth: number | null; + @AutoMap(() => Number) @Column({ type: 'decimal', diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index 76f9406ca3..d9da41cd68 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -655,38 +655,42 @@ export class ApplicationSubmissionService { updateDto.nfuOutsideLands || application.nfuOutsideLands; application.nfuAgricultureSupport = updateDto.nfuAgricultureSupport || application.nfuAgricultureSupport; - application.nfuWillImportFill = - updateDto.nfuWillImportFill !== undefined - ? updateDto.nfuWillImportFill - : application.nfuWillImportFill; - application.nfuTotalFillPlacement = - updateDto.nfuTotalFillPlacement !== undefined - ? updateDto.nfuTotalFillPlacement - : application.nfuTotalFillPlacement; - application.nfuMaxFillDepth = - updateDto.nfuMaxFillDepth !== undefined - ? updateDto.nfuMaxFillDepth - : application.nfuMaxFillDepth; - application.nfuFillVolume = - updateDto.nfuFillVolume !== undefined - ? updateDto.nfuFillVolume - : application.nfuFillVolume; - application.nfuProjectDurationUnit = - updateDto.nfuProjectDurationUnit !== undefined - ? updateDto.nfuProjectDurationUnit - : application.nfuProjectDurationUnit; - application.nfuProjectDurationAmount = - updateDto.nfuProjectDurationAmount !== undefined - ? updateDto.nfuProjectDurationAmount - : application.nfuProjectDurationAmount; - application.nfuFillTypeDescription = - updateDto.nfuFillTypeDescription !== undefined - ? updateDto.nfuFillTypeDescription - : application.nfuFillTypeDescription; - application.nfuFillOriginDescription = - updateDto.nfuFillOriginDescription !== undefined - ? updateDto.nfuFillOriginDescription - : application.nfuFillOriginDescription; + application.nfuWillImportFill = filterUndefined( + updateDto.nfuWillImportFill, + application.nfuWillImportFill, + ); + application.nfuTotalFillPlacement = filterUndefined( + updateDto.nfuTotalFillPlacement, + application.nfuTotalFillPlacement, + ); + application.nfuMaxFillDepth = filterUndefined( + updateDto.nfuMaxFillDepth, + application.nfuMaxFillDepth, + ); + application.nfuAverageFillDepth = filterUndefined( + updateDto.nfuAverageFillDepth, + application.nfuAverageFillDepth, + ); + application.nfuFillVolume = filterUndefined( + updateDto.nfuFillVolume, + application.nfuFillVolume, + ); + application.nfuProjectDurationUnit = filterUndefined( + updateDto.nfuProjectDurationUnit, + application.nfuProjectDurationUnit, + ); + application.nfuProjectDurationAmount = filterUndefined( + updateDto.nfuProjectDurationAmount, + application.nfuProjectDurationAmount, + ); + application.nfuFillTypeDescription = filterUndefined( + updateDto.nfuFillTypeDescription, + application.nfuFillTypeDescription, + ); + application.nfuFillOriginDescription = filterUndefined( + updateDto.nfuFillOriginDescription, + application.nfuFillOriginDescription, + ); return application; } diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts index 4b69a074bb..1bbc88e37a 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts @@ -291,6 +291,7 @@ export class GenerateSubmissionDocumentService { nfuFillOriginDescription: submission.nfuFillOriginDescription, nfuTotalFillPlacement: submission.nfuTotalFillPlacement, nfuMaxFillDepth: submission.nfuMaxFillDepth, + nfuAverageFillDepth: submission.nfuAverageFillDepth, nfuFillVolume: submission.nfuFillVolume, nfuProjectDurationAmount: submission.nfuProjectDurationAmount, nfuProjectDurationUnit: submission.nfuProjectDurationUnit, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1690999583806-add_average_fill_depth.ts b/services/apps/alcs/src/providers/typeorm/migrations/1690999583806-add_average_fill_depth.ts new file mode 100644 index 0000000000..0d766bfd73 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1690999583806-add_average_fill_depth.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addAverageFillDepth1690999583806 implements MigrationInterface { + name = 'addAverageFillDepth1690999583806'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "nfu_average_fill_depth" numeric(12,2)`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "nfu_average_fill_depth"`, + ); + } +} From dc8e95251d7f065ac1960920123fc914eca711dd Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 2 Aug 2023 12:56:40 -0700 Subject: [PATCH 192/954] noi docs --- .../noi/documents/documents_noi.py | 93 +++++++++++++++++ .../noi/documents/documents_noi.sql | 33 +++++++ .../documents/documents_noi_total_count.sql | 22 +++++ .../noi/documents/noi_documents.py | 99 +++++++++++++++++++ .../noi/documents/noi_documents.sql | 30 +++--- .../documents/noi_documents_total_count.sql | 23 +++++ .../sql/documents/documents.sql | 3 +- 7 files changed, 286 insertions(+), 17 deletions(-) create mode 100644 bin/migrate-oats-data/noi/documents/documents_noi.py create mode 100644 bin/migrate-oats-data/noi/documents/documents_noi.sql create mode 100644 bin/migrate-oats-data/noi/documents/documents_noi_total_count.sql create mode 100644 bin/migrate-oats-data/noi/documents/noi_documents.py create mode 100644 bin/migrate-oats-data/noi/documents/noi_documents_total_count.sql diff --git a/bin/migrate-oats-data/noi/documents/documents_noi.py b/bin/migrate-oats-data/noi/documents/documents_noi.py new file mode 100644 index 0000000000..e03bdaee79 --- /dev/null +++ b/bin/migrate-oats-data/noi/documents/documents_noi.py @@ -0,0 +1,93 @@ +from db import inject_conn_pool + +""" + This script connects to postgress version of OATS DB and transfers data from OATS documents table to ALCS document_noi table. + + NOTE: + Before performing document import you need to import applications from oats. +""" + + +def compile_document_insert_query(number_of_rows_to_insert): + """ + function takes the number of rows to insert and generates an SQL insert statement with upserts using the ON CONFLICT clause + """ + documents_to_insert = ",".join(["%s"] * number_of_rows_to_insert) + return f""" + INSERT INTO alcs."document_noi" (oats_document_id, file_name, oats_application_id, "source", + audit_created_by, file_key, mime_type, tags, "system") + VALUES {documents_to_insert} + ON CONFLICT (oats_document_id) DO UPDATE SET + oats_document_id = EXCLUDED.oats_document_id, + file_name = EXCLUDED.file_name, + oats_application_id = EXCLUDED.oats_application_id, + "source" = EXCLUDED."source", + audit_created_by = EXCLUDED.audit_created_by, + file_key = EXCLUDED.file_key, + mime_type = EXCLUDED.mime_type, + tags = EXCLUDED.tags, + "system" = EXCLUDED."system" + """ + + +@inject_conn_pool +def process_documents_noi(conn=None, batch_size=10000): + """ + function uses a decorator pattern @inject_conn_pool to inject a database connection pool to the function. It fetches the total count of documents and prints it to the console. Then, it fetches the documents to insert in batches using document IDs, constructs an insert query, and processes them. + """ + with conn.cursor() as cursor: + with open( + "noi/documents/documents_noi_total_count.sql", "r", encoding="utf-8" + ) as sql_file: + count_query = sql_file.read() + cursor.execute(count_query) + total_count = cursor.fetchone()[0] + print("Total count of noi documents to transfer:", total_count) + + failed_inserts = 0 + successful_inserts_count = 0 + last_document_id = 0 + + with open("noi/documents/documents_noi.sql", "r", encoding="utf-8") as sql_file: + documents_to_insert_sql = sql_file.read() + while True: + cursor.execute( + f"{documents_to_insert_sql} WHERE document_id > {last_document_id} ORDER BY document_id" + ) + rows = cursor.fetchmany(batch_size) + if not rows: + break + try: + documents_to_be_inserted_count = len(rows) + + insert_query = compile_document_insert_query( + documents_to_be_inserted_count + ) + cursor.execute(insert_query, rows) + conn.commit() + + last_document_id = rows[-1][0] + successful_inserts_count = ( + successful_inserts_count + documents_to_be_inserted_count + ) + + print( + f"retrieved/inserted items count: {documents_to_be_inserted_count}; total successfully inserted/updated documents so far {successful_inserts_count}; last inserted oats_document_id: {last_document_id}" + ) + except Exception as e: + conn.rollback() + print("Error", e) + failed_inserts += len(rows) + last_document_id = last_document_id + 1 + + print("Total amount of successful inserts:", successful_inserts_count) + print("Total amount of failed inserts:", failed_inserts) + + +@inject_conn_pool +def clean_documents_noi(conn=None): + print("Start documents_noi cleaning") + with conn.cursor() as cursor: + cursor.execute("DELETE FROM alcs.document_noi WHERE audit_created_by = 'oats_etl';") + conn.commit() + print(f"Deleted items count = {cursor.rowcount}") diff --git a/bin/migrate-oats-data/noi/documents/documents_noi.sql b/bin/migrate-oats-data/noi/documents/documents_noi.sql new file mode 100644 index 0000000000..23b362ef56 --- /dev/null +++ b/bin/migrate-oats-data/noi/documents/documents_noi.sql @@ -0,0 +1,33 @@ + WITH oats_documents_to_insert AS ( + SELECT + od.alr_application_id , + document_id , + document_code , + file_name , od.who_created + + from oats.oats_documents od + LEFT JOIN oats.oats_subject_properties osp + ON osp.subject_property_id = od.subject_property_id + AND osp.alr_application_id = od.alr_application_id + WHERE od.alr_application_id IS NOT NULL + AND document_code IS NOT NULL + AND od.issue_id IS NULL + AND od.planning_review_id IS NULL +) + SELECT + document_id::TEXT AS oats_document_id, + file_name, + alr_application_id::TEXT AS oats_application_id, + 'oats_etl' AS "source", + 'oats_etl' AS audit_created_by, + '/migrate/application/' || alr_application_id || '/' || document_id || '_' || file_name AS file_key, + 'pdf' AS mime_type, + '{"ORCS Classification: 85100-20"}'::TEXT[] AS tags, + CASE + WHEN who_created = 'PROXY_OATS_LOCGOV' THEN 'OATS_P' + WHEN who_created = 'PROXY_OATS_APPLICANT' THEN 'OATS_P' + ELSE 'OATS' + END AS "system" + FROM + oats_documents_to_insert oti + JOIN alcs.notice_of_intent noi ON noi.file_number = oti.alr_application_id::TEXT \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/documents/documents_noi_total_count.sql b/bin/migrate-oats-data/noi/documents/documents_noi_total_count.sql new file mode 100644 index 0000000000..f2c098300e --- /dev/null +++ b/bin/migrate-oats-data/noi/documents/documents_noi_total_count.sql @@ -0,0 +1,22 @@ +WITH + oats_documents_to_insert AS ( + SELECT + od.alr_application_id, + document_id, + document_code, + file_name + FROM + oats.oats_documents od + LEFT JOIN oats.oats_subject_properties osp ON osp.subject_property_id = od.subject_property_id + AND osp.alr_application_id = od.alr_application_id + WHERE + od.alr_application_id IS NOT NULL + AND document_code IS NOT NULL + AND od.issue_id IS NULL + AND od.planning_review_id IS NULL + ) +SELECT + count(*) +FROM + oats_documents_to_insert oti + JOIN alcs.notice_of_intent a ON a.file_number = oti.alr_application_id::TEXT \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/documents/noi_documents.py b/bin/migrate-oats-data/noi/documents/noi_documents.py new file mode 100644 index 0000000000..6000d41d09 --- /dev/null +++ b/bin/migrate-oats-data/noi/documents/noi_documents.py @@ -0,0 +1,99 @@ +from db import inject_conn_pool + +""" + This script links data from ALCS documents table to ALCS noi_documents table based on data from OATS. +""" + + +def compile_document_insert_query(number_of_rows_to_insert): + """ + function takes the number of rows to insert and generates an SQL insert statement with upserts using the ON CONFLICT clause + """ + documents_to_insert = ",".join(["%s"] * number_of_rows_to_insert) + return f""" + insert into alcs.noi_document + ( + application_uuid , + document_uuid , + type_code , + visibility_flags, + oats_document_id, + oats_application_id, + audit_created_by + ) + VALUES {documents_to_insert} + ON CONFLICT (oats_document_id, oats_application_id) DO UPDATE SET + application_uuid = EXCLUDED.application_uuid, + document_uuid = EXCLUDED.document_uuid, + type_code = EXCLUDED.type_code, + visibility_flags = EXCLUDED.visibility_flags, + audit_created_by = EXCLUDED.audit_created_by; + """ + + +@inject_conn_pool +def process_noi_documents(conn=None, batch_size=10000): + """ + function uses a decorator pattern @inject_conn_pool to inject a database connection pool to the function. It fetches the total count of documents and prints it to the console. Then, it fetches the documents to insert in batches using document IDs, constructs an insert query, and processes them. + """ + with conn.cursor() as cursor: + with open( + "noi/documents/noi_documents_total_count.sql", "r", encoding="utf-8" + ) as sql_file: + count_query = sql_file.read() + cursor.execute(count_query) + total_count = cursor.fetchone()[0] + print("Total count of noi documents to transfer:", total_count) + + failed_inserts = 0 + successful_inserts_count = 0 + last_document_id = 0 + + with open( + "noi/documents/noi_documents.sql", "r", encoding="utf-8" + ) as sql_file: + documents_to_insert_sql = sql_file.read() + while True: + cursor.execute( + f"{documents_to_insert_sql} WHERE oats_document_id > {last_document_id} ORDER BY oats_document_id" + ) + rows = cursor.fetchmany(batch_size) + if not rows: + break + try: + documents_to_be_inserted_count = len(rows) + + insert_query = compile_document_insert_query( + documents_to_be_inserted_count + ) + + cursor.execute(insert_query, rows) + conn.commit() + + last_document_id = rows[-1][4] + successful_inserts_count = ( + successful_inserts_count + documents_to_be_inserted_count + ) + + print( + f"retrieved/inserted items count: {documents_to_be_inserted_count}; total successfully inserted/updated items so far {successful_inserts_count}; last inserted oats_document_id: {last_document_id}" + ) + except Exception as e: + conn.rollback() + print("Error", e) + failed_inserts += len(rows) + last_document_id = last_document_id + 1 + + print("Total amount of successful inserts:", successful_inserts_count) + print("Total amount of failed inserts:", failed_inserts) + + +@inject_conn_pool +def clean_noi_documents(conn=None): + print("Start noi documents cleaning") + with conn.cursor() as cursor: + cursor.execute( + "DELETE FROM alcs.noi_document WHERE audit_created_by = 'oats_etl';" + ) + conn.commit() + print(f"Deleted items count = {cursor.rowcount}") diff --git a/bin/migrate-oats-data/noi/documents/noi_documents.sql b/bin/migrate-oats-data/noi/documents/noi_documents.sql index 732053abd4..2c62187162 100644 --- a/bin/migrate-oats-data/noi/documents/noi_documents.sql +++ b/bin/migrate-oats-data/noi/documents/noi_documents.sql @@ -1,36 +1,36 @@ WITH oats_documents_to_map AS ( SELECT - a.uuid AS application_uuid, + a.uuid AS noi_uuid, d.uuid AS document_uuid, adc.code, publicly_viewable_ind AS is_public, app_lg_viewable_ind AS is_app_lg, od.document_id AS oats_document_id, od.alr_application_id AS oats_application_id - from + FROM oats.oats_documents od - JOIN alcs."document" d + JOIN alcs."document_noi" d ON d.oats_document_id = od.document_id::TEXT - JOIN alcs.application_document_code adc + JOIN alcs.application_document_code adc -- using adc as it is the same mapping ON adc.oats_code = od.document_code - JOIN alcs.application a + JOIN alcs.notice_of_intent a ON a.file_number = od.alr_application_id::TEXT ) SELECT - otm.application_uuid, + otm.noi_uuid, otm.document_uuid, otm.code AS type_code, - (case when is_public = 'Y' and is_app_lg = 'Y' - then '{P, A, C, G}'::TEXT[] - when is_public = 'Y' - then '{P}'::TEXT[] - when is_app_lg='Y' - then '{A, C, G}'::TEXT[] - else '{}'::TEXT[] - end) AS visibility_flags, + (CASE WHEN is_public = 'Y' and is_app_lg = 'Y' + THEN '{P, A, C, G}'::TEXT[] + WHEN is_public = 'Y' + THEN '{P}'::TEXT[] + WHEN is_app_lg='Y' + THEN '{A, C, G}'::TEXT[] + ELSE '{}'::TEXT[] + END) AS visibility_flags, oats_document_id, oats_application_id, 'oats_etl' AS audit_created_by -from +FROM oats_documents_to_map otm \ No newline at end of file diff --git a/bin/migrate-oats-data/noi/documents/noi_documents_total_count.sql b/bin/migrate-oats-data/noi/documents/noi_documents_total_count.sql new file mode 100644 index 0000000000..21165ab8e1 --- /dev/null +++ b/bin/migrate-oats-data/noi/documents/noi_documents_total_count.sql @@ -0,0 +1,23 @@ +with oats_documents_to_map as ( + select + a.uuid AS application_uuid, + d.uuid AS document_uuid, + adc.code, + publicly_viewable_ind AS is_public, + app_lg_viewable_ind AS is_app_lg, + od.document_id AS oats_document_id, + od.alr_application_id AS oats_application_id + FROM oats.oats_documents od + + JOIN alcs."document_noi" d + ON d.oats_document_id = od.document_id::TEXT + + JOIN alcs.application_document_code adc -- reusing application table mapping + ON adc.oats_code = od.document_code + + JOIN alcs.notice_of_intent a + ON a.file_number = od.alr_application_id::TEXT +) +SELECT + count(*) +FROM oats_documents_to_map otm \ No newline at end of file diff --git a/bin/migrate-oats-data/sql/documents/documents.sql b/bin/migrate-oats-data/sql/documents/documents.sql index 703f9acecf..95ed0a0cda 100644 --- a/bin/migrate-oats-data/sql/documents/documents.sql +++ b/bin/migrate-oats-data/sql/documents/documents.sql @@ -30,5 +30,4 @@ END AS "system" FROM oats_documents_to_insert oti - JOIN alcs.application a ON a.file_number = oti.alr_application_id::text - JOIN alcs.notice_of_intent noi ON noi.file_number = oti.alr_application_id::text + JOIN alcs.application a ON a.file_number = oti.alr_application_id::text From 0c1c510b6e87ceacffe948cd1f240f746b991c51 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 2 Aug 2023 14:33:30 -0700 Subject: [PATCH 193/954] Add more subtexts and tooltips to Portal --- .../alcs-edit-submission.component.scss | 1 - .../edit-submission.component.scss | 1 - .../land-use/land-use.component.html | 22 +++++++++++++++++++ .../land-use/land-use.component.scss | 5 +++++ .../parcel-entry/parcel-entry.component.html | 9 ++++++-- .../tur-proposal/tur-proposal.component.html | 4 +--- 6 files changed, 35 insertions(+), 7 deletions(-) diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.scss b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.scss index 5dde460aa7..172d15447c 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.scss +++ b/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.scss @@ -336,7 +336,6 @@ .land-use-form { display: grid; grid-template-columns: 1fr; - row-gap: rem(24); .land-use-type-wrapper { display: flex; diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.scss b/portal-frontend/src/app/features/edit-submission/edit-submission.component.scss index d2b9828254..cb18914260 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.scss +++ b/portal-frontend/src/app/features/edit-submission/edit-submission.component.scss @@ -349,7 +349,6 @@ .land-use-form { display: grid; grid-template-columns: 1fr; - row-gap: rem(24); .land-use-type-wrapper { display: flex; diff --git a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html index 4d44599ce1..8343869399 100644 --- a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html +++ b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html @@ -20,6 +20,7 @@
Land Use of Parcel(s) under Application
+
You may describe multiple parcels collectively or individually.
+
+ + +
+
+
diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.scss new file mode 100644 index 0000000000..cc09826d94 --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.scss @@ -0,0 +1,73 @@ +@use '../../../../styles/colors'; + +.editable { + display: inline-block; + cursor: pointer; + border: 2px solid colors.$primary-color-dark; + padding: 8px; + border-radius: 2px; + font-size: 16px; + width: 100%; + min-height: 96px; + margin-left: -8px; + margin-right: -8px; +} + +.content { + display: flex; + align-items: stretch; + justify-content: space-between; + border-radius: 2px; + font-size: 16px; + border: 2px solid transparent; + margin-left: -8px; + margin-right: -8px; + min-height: 28px; +} + +.text { + padding: 8px; +} + +.content:hover { + border: 2px solid colors.$grey; + border-radius: 4px; + cursor: pointer; + + .edit-button { + color: colors.$white; + background-color: colors.$grey; + } + + mat-icon { + display: inline-block; + } +} + +.edit-button { + min-width: 32px; + display: flex; + align-items: center; + justify-content: center; + + mat-icon { + display: none; + } +} + +.editing.hidden { + display: none; +} + +.placeholder { + font-style: italic; + color: colors.$grey; +} + +.editable:hover { + border: 2px solid colors.$primary-color; +} + +.save { + color: colors.$primary-color; +} diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.spec.ts b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.spec.ts new file mode 100644 index 0000000000..a2dcf4e500 --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InlineTextareaEditComponent } from './inline-textarea-edit.component'; + +describe('InlineTextareaComponent', () => { + let component: InlineTextareaEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [InlineTextareaEditComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(InlineTextareaEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.ts new file mode 100644 index 0000000000..5f6daba83c --- /dev/null +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea-edit/inline-textarea-edit.component.ts @@ -0,0 +1,45 @@ +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; + +@Component({ + selector: 'app-inline-edit[value]', + templateUrl: './inline-textarea-edit.component.html', + styleUrls: ['./inline-textarea-edit.component.scss'], +}) +export class InlineTextareaEditComponent { + @Input() value: string = ''; + @Input() placeholder: string = 'Enter a value'; + @Output() save = new EventEmitter(); + + @ViewChild('editInput') textInput!: ElementRef; + + isEditing = false; + hasFocused = false; + pendingValue: undefined | string; + + constructor() {} + + startEdit() { + this.isEditing = true; + this.hasFocused = false; + this.pendingValue = this.value; + setTimeout(() => { + if (this.textInput) { + this.textInput.nativeElement.focus(); + } + }, 300); + } + + confirmEdit() { + if (this.pendingValue !== this.value && this.pendingValue !== undefined) { + this.save.emit(this.pendingValue); + this.value = this.pendingValue; + } + + this.isEditing = false; + } + + cancelEdit() { + this.isEditing = false; + this.pendingValue = this.value; + } +} diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.html b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.html index b3031b168b..8b4e4583c7 100644 --- a/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.html +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.html @@ -1,13 +1,13 @@ -
-
-
- {{ placeholder}} +
+ + Add text + {{ value }} -
-
- edit -
-
+ + +
+
diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.scss b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.scss index cc09826d94..5775eff0d0 100644 --- a/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.scss +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.scss @@ -1,73 +1,72 @@ @use '../../../../styles/colors'; -.editable { - display: inline-block; - cursor: pointer; - border: 2px solid colors.$primary-color-dark; - padding: 8px; - border-radius: 2px; - font-size: 16px; - width: 100%; - min-height: 96px; - margin-left: -8px; - margin-right: -8px; +.inline-number-wrapper { + padding-top: 4px; } -.content { - display: flex; - align-items: stretch; - justify-content: space-between; - border-radius: 2px; - font-size: 16px; - border: 2px solid transparent; - margin-left: -8px; - margin-right: -8px; - min-height: 28px; +.editing { + width: 100%; + display: inline-block; } -.text { - padding: 8px; +.editing.hidden { + display: none; } -.content:hover { - border: 2px solid colors.$grey; - border-radius: 4px; - cursor: pointer; - - .edit-button { - color: colors.$white; - background-color: colors.$grey; - } +.edit-button { + height: 24px; + width: 24px; + display: flex; + align-items: center; +} - mat-icon { - display: inline-block; - } +.edit-icon { + font-size: inherit; + line-height: 22px; } -.edit-button { - min-width: 32px; +.button-container { display: flex; - align-items: center; - justify-content: center; + justify-content: flex-end; - mat-icon { - display: none; + button:not(:last-child) { + margin-right: 2px !important; } } -.editing.hidden { - display: none; +.add { + cursor: pointer; } -.placeholder { - font-style: italic; - color: colors.$grey; +.save { + color: colors.$primary-color; } -.editable:hover { - border: 2px solid colors.$primary-color; +:host::ng-deep { + .mat-form-field-wrapper { + padding: 0 !important; + margin: 0 !important; + } + + button mat-icon { + overflow: visible; + } + + .mat-mdc-icon-button.mat-mdc-button-base { + padding: 0 !important; + } } -.save { - color: colors.$primary-color; +.editable { + width: 100%; + display: inline-block; + border: 2px solid colors.$primary-color-dark; + padding: 8px; + border-radius: 2px; + font-size: 16px; + width: 100%; + min-height: 96px; + margin-left: -8px; + margin-right: -8px; + background: none; } diff --git a/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.ts b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.ts index 4320990c37..34d879ecc0 100644 --- a/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.ts +++ b/alcs-frontend/src/app/shared/inline-editors/inline-textarea/inline-textarea.component.ts @@ -1,14 +1,14 @@ -import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { AfterContentChecked, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; @Component({ - selector: 'app-inline-edit[value]', + selector: 'app-inline-textarea[value]', templateUrl: './inline-textarea.component.html', styleUrls: ['./inline-textarea.component.scss'], }) export class InlineTextareaComponent { - @Input() value: string = ''; + @Input() value?: string | undefined; @Input() placeholder: string = 'Enter a value'; - @Output() save = new EventEmitter(); + @Output() save = new EventEmitter(); @ViewChild('editInput') textInput!: ElementRef; @@ -30,8 +30,8 @@ export class InlineTextareaComponent { } confirmEdit() { - if (this.pendingValue !== this.value && this.pendingValue !== undefined) { - this.save.emit(this.pendingValue); + if (this.pendingValue !== this.value) { + this.save.emit(this.pendingValue?.toString() ?? null); this.value = this.pendingValue; } diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html index 4506f99631..cb89e20f9d 100644 --- a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html @@ -28,7 +28,7 @@ Type - + Lot Road Dedication @@ -48,7 +48,7 @@ separatorLimit="9999999999" min="0.01" formControlName="{{ i }}-lotSize" - (change)='fireChanged()' + (change)="fireChanged()" /> ha @@ -67,13 +67,22 @@ separatorLimit="9999999999" min="0.01" formControlName="{{ i }}-lotAlrArea" - (change)='fireChanged()' + (change)="fireChanged()" /> ha + + diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts index c9a65c177e..0606104c47 100644 --- a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; import { AbstractControl, ControlValueAccessor, @@ -11,7 +11,12 @@ import { } from '@angular/forms'; import { MatTableDataSource } from '@angular/material/table'; -type ProposedLot = { type: 'Lot' | 'Road Dedication' | null; size: string | null; alrArea: string | null }; +type ProposedLot = { + type: 'Lot' | 'Road Dedication' | null; + size: string | null; + alrArea: string | null; + uuid: string | null; +}; @Component({ selector: 'app-lots-table', @@ -63,6 +68,7 @@ export class LotsTableFormComponent implements ControlValueAccessor, Validator { size: null, alrArea: null, type: null, + uuid: null, }); } @@ -84,6 +90,7 @@ export class LotsTableFormComponent implements ControlValueAccessor, Validator { this.form.addControl(`${index}-lotType`, new FormControl(lot.type, [Validators.required])); this.form.addControl(`${index}-lotSize`, new FormControl(lot.size, [Validators.required])); this.form.addControl(`${index}-lotAlrArea`, new FormControl(lot.alrArea, [Validators.required])); + this.form.addControl(`${index}-lotUuid`, new FormControl(lot.uuid)); }); } @@ -142,10 +149,12 @@ export class LotsTableFormComponent implements ControlValueAccessor, Validator { const lotType = this.form.controls[`${index}-lotType`].value; const lotSize = this.form.controls[`${index}-lotSize`].value; const lotAlrArea = this.form.controls[`${index}-lotAlrArea`].value; + const lotUuid = this.form.controls[`${index}-lotUuid`].value; proposedLots.push({ size: lotSize, type: lotType, alrArea: lotAlrArea, + uuid: lotUuid, }); } } diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 36ea036d66..f80164609a 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -44,11 +44,12 @@ import { InlineApplicantTypeComponent } from './inline-applicant-type/inline-app import { InlineBooleanComponent } from './inline-editors/inline-boolean/inline-boolean.component'; import { InlineDatepickerComponent } from './inline-editors/inline-datepicker/inline-datepicker.component'; import { InlineDropdownComponent } from './inline-editors/inline-dropdown/inline-dropdown.component'; +import { InlineNgSelectComponent } from './inline-editors/inline-ng-select/inline-ng-select.component'; import { InlineNumberComponent } from './inline-editors/inline-number/inline-number.component'; import { InlineReviewOutcomeComponent } from './inline-editors/inline-review-outcome/inline-review-outcome.component'; import { InlineTextComponent } from './inline-editors/inline-text/inline-text.component'; +import { InlineTextareaEditComponent } from './inline-editors/inline-textarea-edit/inline-textarea-edit.component'; import { InlineTextareaComponent } from './inline-editors/inline-textarea/inline-textarea.component'; -import { InlineNgSelectComponent } from './inline-editors/inline-ng-select/inline-ng-select.component'; import { LotsTableFormComponent } from './lots-table/lots-table-form.component'; import { MeetingOverviewComponent } from './meeting-overview/meeting-overview.component'; import { NoDataComponent } from './no-data/no-data.component'; @@ -74,6 +75,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen StartOfDayPipe, MeetingOverviewComponent, InlineApplicantTypeComponent, + InlineTextareaEditComponent, InlineTextareaComponent, InlineBooleanComponent, InlineNumberComponent, @@ -151,6 +153,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen StartOfDayPipe, MatTooltipModule, InlineApplicantTypeComponent, + InlineTextareaEditComponent, InlineTextareaComponent, InlineBooleanComponent, InlineNumberComponent, diff --git a/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.controller.spec.ts new file mode 100644 index 0000000000..d5b3509c72 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.controller.spec.ts @@ -0,0 +1,73 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { ApplicationDecisionProfile } from '../../../common/automapper/application-decision-v2.automapper.profile'; +import { ApplicationDecisionComponentLotController } from './application-decision-component-lot.controller'; +import { UpdateApplicationDecisionComponentLotDto } from './application-decision-component-lot.dto'; +import { ApplicationDecisionComponentLot } from './application-decision-component-lot.entity'; +import { ApplicationDecisionComponentLotService } from './application-decision-component-lot.service'; + +describe('ApplicationDecisionComponentLotController', () => { + let controller: ApplicationDecisionComponentLotController; + let mockApplicationDecisionComponentLotService: DeepMocked; + + beforeEach(async () => { + mockApplicationDecisionComponentLotService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [ApplicationDecisionComponentLotController], + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + providers: [ + ApplicationDecisionProfile, + { + provide: ApplicationDecisionComponentLotService, + useValue: mockApplicationDecisionComponentLotService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + ApplicationDecisionComponentLotController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should successfully call service to update', async () => { + mockApplicationDecisionComponentLotService.update.mockResolvedValue( + new ApplicationDecisionComponentLot(), + ); + + const updateDto = { + type: 'Lot', + alrArea: 1, + size: 1, + uuid: 'fake', + } as UpdateApplicationDecisionComponentLotDto; + + const result = await controller.update('fake', updateDto); + + expect(result).toBeDefined(); + expect(mockApplicationDecisionComponentLotService.update).toBeCalledTimes( + 1, + ); + expect(mockApplicationDecisionComponentLotService.update).toBeCalledWith( + 'fake', + updateDto, + ); + }); +}); diff --git a/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.controller.ts new file mode 100644 index 0000000000..673f5e7582 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.controller.ts @@ -0,0 +1,33 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Param, Patch } from '@nestjs/common'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { + ApplicationDecisionComponentLotDto, + UpdateApplicationDecisionComponentLotDto, +} from './application-decision-component-lot.dto'; +import { ApplicationDecisionComponentLot } from './application-decision-component-lot.entity'; +import { ApplicationDecisionComponentLotService } from './application-decision-component-lot.service'; + +@Controller('application-decision-component-lot') +@UserRoles(...ANY_AUTH_ROLE) +export class ApplicationDecisionComponentLotController { + constructor( + private componentLotService: ApplicationDecisionComponentLotService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Patch('/:uuid') + async update( + @Param('uuid') uuid: string, + @Body() updateDto: UpdateApplicationDecisionComponentLotDto, + ) { + const updatedLot = await this.componentLotService.update(uuid, updateDto); + return this.mapper.mapAsync( + updatedLot, + ApplicationDecisionComponentLot, + ApplicationDecisionComponentLotDto, + ); + } +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.dto.ts new file mode 100644 index 0000000000..35f604b2f0 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.dto.ts @@ -0,0 +1,27 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator'; + +export class UpdateApplicationDecisionComponentLotDto { + @IsString() + @IsOptional() + type: 'Lot' | 'Road Dedication' | null; + + @IsNumber() + @IsOptional() + alrArea: number | null; + + @IsNumber() + @IsOptional() + size: number | null; + + @IsString() + uuid: string; +} + +export class ApplicationDecisionComponentLotDto { + index: number; + componentUuid: string; + type: 'Lot' | 'Road Dedication' | null; + alrArea: number | null; + size: number | null; + uuid: string; +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.entity.ts new file mode 100644 index 0000000000..a8b4b7cab3 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.entity.ts @@ -0,0 +1,73 @@ +import { AutoMap } from '@automapper/classes'; +import { Type } from 'class-transformer'; +import { Column, Entity, ManyToMany, ManyToOne } from 'typeorm'; +import { Base } from '../../../common/entities/base.entity'; +import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; +import { ApplicationDecisionConditionToComponentLot } from '../application-condition-to-component-lot/application-decision-condition-to-component-lot.entity'; +import { ApplicationDecisionComponent } from '../application-decision-v2/application-decision/component/application-decision-component.entity'; + +export class ProposedLot { + type: 'Lot' | 'Road Dedication' | null; + alrArea?: number | null; + size: number | null; + planNumbers: string | null; +} + +//Contains the approved subdivision lots +@Entity() +export class ApplicationDecisionComponentLot extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @Column({ type: 'int' }) + index: number; + + @AutoMap(() => String) + @Column({ + type: 'text', + nullable: true, + }) + type: 'Lot' | 'Road Dedication' | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + alrArea?: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + size?: number | null; + + @AutoMap() + @Column() + componentUuid: string; + + @ManyToOne(() => ApplicationDecisionComponent) + @Type(() => ApplicationDecisionComponent) + component: ApplicationDecisionComponent; + + @ManyToMany( + () => ApplicationDecisionConditionToComponentLot, + (e) => e.componentLot, + { + cascade: ['soft-remove', 'insert', 'update', 'remove'], + }, + ) + conditionLots: ApplicationDecisionConditionToComponentLot[]; +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.service.spec.ts new file mode 100644 index 0000000000..cd43b14592 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.service.spec.ts @@ -0,0 +1,123 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { UpdateApplicationDecisionComponentLotDto } from './application-decision-component-lot.dto'; +import { ApplicationDecisionComponentLot } from './application-decision-component-lot.entity'; +import { ApplicationDecisionComponentLotService } from './application-decision-component-lot.service'; + +describe('ApplicationDecisionComponentLotService', () => { + let service: ApplicationDecisionComponentLotService; + let mockApplicationDecisionComponentLotRepository: DeepMocked< + Repository + >; + + beforeEach(async () => { + mockApplicationDecisionComponentLotRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApplicationDecisionComponentLotService, + { + provide: getRepositoryToken(ApplicationDecisionComponentLot), + useValue: mockApplicationDecisionComponentLotRepository, + }, + ], + }).compile(); + + service = module.get( + ApplicationDecisionComponentLotService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should update component lot', async () => { + const dto = { + alrArea: 1, + uuid: '1', + } as UpdateApplicationDecisionComponentLotDto; + + const lot = new ApplicationDecisionComponentLot(); + mockApplicationDecisionComponentLotRepository.findOneByOrFail.mockResolvedValue( + lot, + ); + const updatedLot = new ApplicationDecisionComponentLot({ + alrArea: dto.alrArea, + }); + mockApplicationDecisionComponentLotRepository.save.mockResolvedValue( + updatedLot, + ); + + const result = await service.update('1', dto); + + expect(result).toEqual(updatedLot); + expect(mockApplicationDecisionComponentLotRepository.save).toBeCalledWith( + updatedLot, + ); + expect(mockApplicationDecisionComponentLotRepository.save).toBeCalledTimes( + 1, + ); + expect( + mockApplicationDecisionComponentLotRepository.findOneByOrFail, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionComponentLotRepository.findOneByOrFail, + ).toBeCalledWith({ uuid: dto.uuid }); + }); + + it('should soft remove by componentUuid', async () => { + const lots = [ + new ApplicationDecisionComponentLot(), + new ApplicationDecisionComponentLot(), + ]; + mockApplicationDecisionComponentLotRepository.findBy.mockResolvedValue( + lots, + ); + mockApplicationDecisionComponentLotRepository.softRemove.mockResolvedValue( + [] as any, + ); + await service.softRemoveBy('1'); + expect(mockApplicationDecisionComponentLotRepository.findBy).toBeCalledWith( + { componentUuid: '1' }, + ); + expect( + mockApplicationDecisionComponentLotRepository.findBy, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionComponentLotRepository.softRemove, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionComponentLotRepository.softRemove, + ).toBeCalledWith(lots); + }); + + it('should soft remove by uuids', async () => { + const lots = [ + new ApplicationDecisionComponentLot(), + new ApplicationDecisionComponentLot(), + ]; + mockApplicationDecisionComponentLotRepository.findBy.mockResolvedValue( + lots, + ); + mockApplicationDecisionComponentLotRepository.softRemove.mockResolvedValue( + [] as any, + ); + await service.softRemove(['1', '2']); + + expect(mockApplicationDecisionComponentLotRepository.findBy).toBeCalledWith( + { uuid: In(['1', '2']) }, + ); + expect( + mockApplicationDecisionComponentLotRepository.findBy, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionComponentLotRepository.softRemove, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionComponentLotRepository.softRemove, + ).toBeCalledWith(lots); + }); +}); diff --git a/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.service.ts b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.service.ts new file mode 100644 index 0000000000..98dc5f46eb --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-component-lot/application-decision-component-lot.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { UpdateApplicationDecisionComponentLotDto } from './application-decision-component-lot.dto'; +import { ApplicationDecisionComponentLot } from './application-decision-component-lot.entity'; + +@Injectable() +export class ApplicationDecisionComponentLotService { + constructor( + @InjectRepository(ApplicationDecisionComponentLot) + private componentLotRepository: Repository, + ) {} + + async update( + uuid: string, + updateDto: UpdateApplicationDecisionComponentLotDto, + ) { + const existingLot = await this.componentLotRepository.findOneByOrFail({ + uuid, + }); + + existingLot.alrArea = updateDto.alrArea; + return await this.componentLotRepository.save(existingLot); + } + + async softRemoveBy(componentUuid: string) { + const componentLots = await this.componentLotRepository.findBy({ + componentUuid, + }); + + return await this.componentLotRepository.softRemove(componentLots); + } + + async softRemove(uuids: string[]) { + const componentLots = await this.componentLotRepository.findBy({ + uuid: In(uuids), + }); + + return await this.componentLotRepository.softRemove(componentLots); + } +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.dto.ts new file mode 100644 index 0000000000..92d5d2b0c8 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.dto.ts @@ -0,0 +1,7 @@ +export class ApplicationDecisionConditionToComponentLotDto { + componentLotUuid: string; + + conditionUuid: string; + + planNumbers: string; +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.spec.ts new file mode 100644 index 0000000000..464275aca8 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.spec.ts @@ -0,0 +1,81 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { ApplicationDecisionProfile } from '../../../common/automapper/application-decision-v2.automapper.profile'; +import { ApplicationConditionToComponentLotController } from './application-condition-to-component-lot.controller'; +import { ApplicationConditionToComponentLotService } from './application-condition-to-component-lot.service'; +import { ApplicationDecisionConditionToComponentLot } from './application-decision-condition-to-component-lot.entity'; + +describe('ApplicationConditionToComponentLotController', () => { + let controller: ApplicationConditionToComponentLotController; + let mockApplicationConditionToComponentLotService: DeepMocked; + + beforeEach(async () => { + mockApplicationConditionToComponentLotService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [ApplicationConditionToComponentLotController], + providers: [ + ApplicationDecisionProfile, + { + provide: ApplicationConditionToComponentLotService, + useValue: mockApplicationConditionToComponentLotService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + ApplicationConditionToComponentLotController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should successfully call service to update lot', async () => { + mockApplicationConditionToComponentLotService.createOrUpdate.mockResolvedValue( + new ApplicationDecisionConditionToComponentLot(), + ); + + const result = await controller.update('fake-1', 'fake-2', {} as any); + + expect(result).toBeDefined(); + expect( + mockApplicationConditionToComponentLotService.createOrUpdate, + ).toBeCalledTimes(1); + expect( + mockApplicationConditionToComponentLotService.createOrUpdate, + ).toBeCalledWith('fake-2', 'fake-1', {}); + }); + + it('should successfully call service to fetch lots', async () => { + mockApplicationConditionToComponentLotService.fetch.mockResolvedValue([ + new ApplicationDecisionConditionToComponentLot(), + ]); + + const result = await controller.get('fake-1', 'fake-2'); + + expect(result).toBeDefined(); + expect(mockApplicationConditionToComponentLotService.fetch).toBeCalledTimes( + 1, + ); + expect(mockApplicationConditionToComponentLotService.fetch).toBeCalledWith( + 'fake-1', + 'fake-2', + ); + }); +}); diff --git a/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.ts new file mode 100644 index 0000000000..b945567a54 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.ts @@ -0,0 +1,51 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Get, Param, Patch } from '@nestjs/common'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { ApplicationDecisionConditionToComponentLotDto } from './application-condition-to-component-lot.controller.dto'; +import { ApplicationConditionToComponentLotService } from './application-condition-to-component-lot.service'; +import { ApplicationDecisionConditionToComponentLot } from './application-decision-condition-to-component-lot.entity'; + +@Controller('application-condition-to-component-lot') +@UserRoles(...ANY_AUTH_ROLE) +export class ApplicationConditionToComponentLotController { + constructor( + private conditionLotService: ApplicationConditionToComponentLotService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Patch('/condition/:conditionUuid/component-lot/:lotUuid') + async update( + @Param('conditionUuid') conditionUuid: string, + @Param('lotUuid') lotUuid: string, + @Body() planNumbers: string | null, + ) { + const updatedLot = await this.conditionLotService.createOrUpdate( + lotUuid, + conditionUuid, + planNumbers, + ); + return this.mapper.mapAsync( + updatedLot, + ApplicationDecisionConditionToComponentLot, + ApplicationDecisionConditionToComponentLotDto, + ); + } + + @Get('/component/:componentUuid/condition/:conditionUuid') + async get( + @Param('componentUuid') componentUuid: string, + @Param('conditionUuid') conditionUuid: string, + ) { + const updatedLot = await this.conditionLotService.fetch( + componentUuid, + conditionUuid, + ); + return await this.mapper.mapArrayAsync( + updatedLot, + ApplicationDecisionConditionToComponentLot, + ApplicationDecisionConditionToComponentLotDto, + ); + } +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.service.spec.ts new file mode 100644 index 0000000000..576d274d74 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.service.spec.ts @@ -0,0 +1,145 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ApplicationConditionToComponentLotService } from './application-condition-to-component-lot.service'; +import { ApplicationDecisionConditionToComponentLot } from './application-decision-condition-to-component-lot.entity'; + +describe('ApplicationConditionToComponentLotService', () => { + let service: ApplicationConditionToComponentLotService; + let mockApplicationDecisionConditionToComponentLotRepository: DeepMocked< + Repository + >; + + beforeEach(async () => { + mockApplicationDecisionConditionToComponentLotRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApplicationConditionToComponentLotService, + { + provide: getRepositoryToken( + ApplicationDecisionConditionToComponentLot, + ), + useValue: mockApplicationDecisionConditionToComponentLotRepository, + }, + ], + }).compile(); + + service = module.get( + ApplicationConditionToComponentLotService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it(' it should successfully call repo to find data', async () => { + mockApplicationDecisionConditionToComponentLotRepository.find.mockResolvedValue( + [], + ); + + const result = await service.fetch('fake-1', 'fake-2'); + + expect(result).toBeDefined; + expect( + mockApplicationDecisionConditionToComponentLotRepository.find, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionConditionToComponentLotRepository.find, + ).toBeCalledWith({ + where: { + conditionUuid: 'fake-2', + componentLot: { + componentUuid: 'fake-1', + }, + }, + }); + }); + + it('should create new conditionLot', async () => { + mockApplicationDecisionConditionToComponentLotRepository.findOne.mockResolvedValue( + new ApplicationDecisionConditionToComponentLot({ + componentLotUuid: '1', + conditionUuid: '1', + }), + ); + + mockApplicationDecisionConditionToComponentLotRepository.save.mockImplementation( + (data: ApplicationDecisionConditionToComponentLot) => + Promise.resolve(data), + ); + + const result = await service.createOrUpdate('1', '1', 'plan1'); + + expect(result.planNumbers).toEqual('plan1'); + expect(result.componentLotUuid).toEqual('1'); + expect(result.conditionUuid).toEqual('1'); + expect( + mockApplicationDecisionConditionToComponentLotRepository.findOne, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionConditionToComponentLotRepository.findOne, + ).toBeCalledWith({ + where: { + componentLotUuid: '1', + conditionUuid: '1', + }, + }); + expect( + mockApplicationDecisionConditionToComponentLotRepository.save, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionConditionToComponentLotRepository.save, + ).toBeCalledWith( + new ApplicationDecisionConditionToComponentLot({ + componentLotUuid: '1', + conditionUuid: '1', + planNumbers: 'plan1', + }), + ); + }); + + it('should update an existing conditionLot', async () => { + const existingConditionLot = new ApplicationDecisionConditionToComponentLot( + { + conditionUuid: '2', + componentLotUuid: '2', + planNumbers: 'plan2', + }, + ); + + mockApplicationDecisionConditionToComponentLotRepository.findOne.mockResolvedValue( + existingConditionLot, + ); + + mockApplicationDecisionConditionToComponentLotRepository.save.mockImplementation( + (data: ApplicationDecisionConditionToComponentLot) => + Promise.resolve(data), + ); + + const result = await service.createOrUpdate('2', '2', 'updatedPlan'); + + expect(result.planNumbers).toEqual('updatedPlan'); + expect(result.componentLotUuid).toEqual('2'); + expect(result.conditionUuid).toEqual('2'); + expect( + mockApplicationDecisionConditionToComponentLotRepository.findOne, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionConditionToComponentLotRepository.findOne, + ).toBeCalledWith({ + where: { + componentLotUuid: '2', + conditionUuid: '2', + }, + }); + expect( + mockApplicationDecisionConditionToComponentLotRepository.save, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionConditionToComponentLotRepository.save, + ).toBeCalledWith(existingConditionLot); + }); +}); diff --git a/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.service.ts b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.service.ts new file mode 100644 index 0000000000..7b7a17f466 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ApplicationDecisionConditionToComponentLot } from './application-decision-condition-to-component-lot.entity'; + +@Injectable() +export class ApplicationConditionToComponentLotService { + constructor( + @InjectRepository(ApplicationDecisionConditionToComponentLot) + private conditionLotRepository: Repository, + ) {} + + async createOrUpdate( + componentLotUuid: string, + conditionUuid: string, + planNumbers: string | null, + ) { + let conditionLot = await this.conditionLotRepository.findOne({ + where: { + componentLotUuid, + conditionUuid, + }, + }); + + if (!conditionLot) { + conditionLot = new ApplicationDecisionConditionToComponentLot({ + conditionUuid, + componentLotUuid, + planNumbers, + }); + } else { + conditionLot.planNumbers = planNumbers; + } + + return await this.conditionLotRepository.save(conditionLot); + } + + async fetch(componentUuid: string, conditionUuid: string) { + return await this.conditionLotRepository.find({ + where: { + conditionUuid, + componentLot: { + componentUuid, + }, + }, + }); + } +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-decision-condition-to-component-lot.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-decision-condition-to-component-lot.entity.ts new file mode 100644 index 0000000000..9c034b8a71 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-condition-to-component-lot/application-decision-condition-to-component-lot.entity.ts @@ -0,0 +1,40 @@ +import { AutoMap } from '@automapper/classes'; +import { Column, Entity, ManyToOne } from 'typeorm'; +import { Base } from '../../../common/entities/base.entity'; +import { ApplicationDecisionComponentLot } from '../application-component-lot/application-decision-component-lot.entity'; +import { ApplicationDecisionCondition } from '../application-decision-condition/application-decision-condition.entity'; + +@Entity() +export class ApplicationDecisionConditionToComponentLot extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => String) + @Column({ + type: 'text', + }) + planNumbers?: string | null; + + @AutoMap() + @Column({ nullable: true }) + conditionUuid: string; + + @ManyToOne(() => ApplicationDecisionCondition, { + nullable: true, + persistence: false, + }) + condition: ApplicationDecisionCondition; + + @AutoMap() + @Column() + componentLotUuid: string; + + @ManyToOne(() => ApplicationDecisionComponentLot, { + persistence: false, + }) + componentLot: ApplicationDecisionComponentLot; +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity.ts new file mode 100644 index 0000000000..9bf4b6b3b3 --- /dev/null +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity.ts @@ -0,0 +1,62 @@ +import { AutoMap } from '@automapper/classes'; +import { + BaseEntity, + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, +} from 'typeorm'; +import { ApplicationDecisionCondition } from '../application-decision-condition/application-decision-condition.entity'; +import { ApplicationDecisionComponent } from '../application-decision-v2/application-decision/component/application-decision-component.entity'; + +@Entity() +export class ApplicationDecisionConditionComponentPlanNumber extends BaseEntity { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @PrimaryColumn({ + type: 'uuid', + }) + applicationDecisionConditionUuid: string; + + @AutoMap() + @PrimaryColumn({ + type: 'uuid', + }) + applicationDecisionComponentUuid: string; + + @AutoMap(() => String) + @Column({ + type: 'text', + nullable: true, + }) + planNumbers?: string | null; + + @ManyToOne( + () => ApplicationDecisionCondition, + (c) => c.conditionToComponentsWithPlanNumber, + { persistence: false }, + ) + @JoinColumn({ + name: 'application_decision_condition_uuid', + referencedColumnName: 'uuid', + }) + condition: ApplicationDecisionCondition; + + @ManyToOne( + () => ApplicationDecisionComponent, + (c) => c.componentToConditions, + { persistence: false }, + ) + @JoinColumn({ + name: 'application_decision_component_uuid', + referencedColumnName: 'uuid', + }) + component: ApplicationDecisionComponent; +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts index c38a6b7e34..d21d03ac91 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.controller.ts @@ -1,13 +1,15 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Patch, UseGuards } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; import { RolesGuard } from '../../../common/authorization/roles-guard.service'; import { UserRoles } from '../../../common/authorization/roles.decorator'; import { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { ApplicationDecisionConditionComponentPlanNumber } from '../application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity'; import { + ApplicationDecisionConditionComponentDto, ApplicationDecisionConditionDto, UpdateApplicationDecisionConditionDto, } from './application-decision-condition.dto'; @@ -45,4 +47,36 @@ export class ApplicationDecisionConditionController { ApplicationDecisionConditionDto, ); } + + @Get('/plan-numbers/:uuid') + @UserRoles(...ANY_AUTH_ROLE) + async getPlanNumbers(@Param('uuid') uuid: string) { + const planNumbers = await this.conditionService.getPlanNumbers(uuid); + + return await this.mapper.mapArrayAsync( + planNumbers, + ApplicationDecisionConditionComponentPlanNumber, + ApplicationDecisionConditionComponentDto, + ); + } + + @Patch('/plan-numbers/condition/:conditionUuid/component/:componentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async updateConditionPlanNumbers( + @Param('conditionUuid') conditionUuid: string, + @Param('componentUuid') componentUuid: string, + @Body() planNumbers: string | null, + ) { + const planNumber = await this.conditionService.updateConditionPlanNumbers( + conditionUuid, + componentUuid, + planNumbers, + ); + + return await this.mapper.mapAsync( + planNumber, + ApplicationDecisionConditionComponentPlanNumber, + ApplicationDecisionConditionComponentDto, + ); + } } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts index 6afa2209ed..2314b9dffe 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.dto.ts @@ -101,3 +101,21 @@ export class UpdateApplicationDecisionConditionServiceDto { completionDate?: Date | null; supersededDate?: Date | null; } + +export class ApplicationDecisionConditionComponentDto { + applicationDecisionConditionUuid: string; + applicationDecisionComponentUuid: string; + planNumbers: string | null; +} + +export class UpdateApplicationDecisionConditionComponentDto { + @IsString() + applicationDecisionConditionUuid: string; + + @IsString() + applicationDecisionComponentUuid: string; + + @IsString() + @IsOptional() + planNumbers: string | null; +} diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts index b3cc1f7031..f9fe054b38 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.entity.ts @@ -1,7 +1,16 @@ import { AutoMap } from '@automapper/classes'; -import { Column, Entity, JoinTable, ManyToMany, ManyToOne } from 'typeorm'; +import { + Column, + Entity, + JoinTable, + ManyToMany, + ManyToOne, + OneToMany, +} from 'typeorm'; import { Base } from '../../../common/entities/base.entity'; import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; +import { ApplicationDecisionConditionToComponentLot } from '../application-condition-to-component-lot/application-decision-condition-to-component-lot.entity'; +import { ApplicationDecisionConditionComponentPlanNumber } from '../application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity'; import { ApplicationDecisionComponent } from '../application-decision-v2/application-decision/component/application-decision-component.entity'; import { ApplicationDecision } from '../application-decision.entity'; import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; @@ -82,4 +91,15 @@ export class ApplicationDecisionCondition extends Base { name: 'application_decision_condition_component', }) components: ApplicationDecisionComponent[] | null; + + @OneToMany( + () => ApplicationDecisionConditionComponentPlanNumber, + (c) => c.condition, + { + cascade: ['insert', 'update', 'remove'], + }, + ) + conditionToComponentsWithPlanNumber: + | ApplicationDecisionConditionComponentPlanNumber[] + | null; } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts index e1df986106..b0dc5475b8 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.spec.ts @@ -2,6 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { ApplicationDecisionConditionToComponentLot } from '../application-condition-to-component-lot/application-decision-condition-to-component-lot.entity'; +import { ApplicationDecisionConditionComponentPlanNumber } from '../application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity'; import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; import { UpdateApplicationDecisionConditionDto } from './application-decision-condition.dto'; import { ApplicationDecisionCondition } from './application-decision-condition.entity'; @@ -15,10 +17,18 @@ describe('ApplicationDecisionConditionService', () => { let mockAppDecCondTypeRepository: DeepMocked< Repository >; + let mockApplicationDecisionConditionComponentPlanNumber: DeepMocked< + Repository + >; + let mockApplicationDecisionConditionToComponentLot: DeepMocked< + Repository + >; beforeEach(async () => { mockApplicationDecisionConditionRepository = createMock(); mockAppDecCondTypeRepository = createMock(); + mockApplicationDecisionConditionComponentPlanNumber = createMock(); + mockApplicationDecisionConditionToComponentLot = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -31,6 +41,18 @@ describe('ApplicationDecisionConditionService', () => { provide: getRepositoryToken(ApplicationDecisionConditionType), useValue: mockAppDecCondTypeRepository, }, + { + provide: getRepositoryToken( + ApplicationDecisionConditionComponentPlanNumber, + ), + useValue: mockApplicationDecisionConditionComponentPlanNumber, + }, + { + provide: getRepositoryToken( + ApplicationDecisionConditionToComponentLot, + ), + useValue: mockApplicationDecisionConditionToComponentLot, + }, ], }).compile(); @@ -68,15 +90,18 @@ describe('ApplicationDecisionConditionService', () => { new ApplicationDecisionCondition(), ]; + const mockTransaction = jest.fn(); + mockApplicationDecisionConditionRepository.manager.transaction = + mockTransaction; + mockApplicationDecisionConditionRepository.remove.mockResolvedValue( {} as ApplicationDecisionCondition, ); + mockApplicationDecisionConditionToComponentLot.find.mockResolvedValue([]); await service.remove(conditions); - expect( - mockApplicationDecisionConditionRepository.remove, - ).toHaveBeenCalledWith(conditions); + expect(mockTransaction).toBeCalledTimes(1); }); it('should create new components when given a DTO without a UUID', async () => { diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts index ad9698e8a8..30b1301417 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-condition/application-decision-condition.service.ts @@ -1,7 +1,9 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; +import { ApplicationDecisionConditionToComponentLot } from '../application-condition-to-component-lot/application-decision-condition-to-component-lot.entity'; +import { ApplicationDecisionConditionComponentPlanNumber } from '../application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity'; import { ApplicationDecisionComponent } from '../application-decision-v2/application-decision/component/application-decision-component.entity'; import { ApplicationDecisionConditionType } from './application-decision-condition-code.entity'; import { @@ -17,6 +19,10 @@ export class ApplicationDecisionConditionService { private repository: Repository, @InjectRepository(ApplicationDecisionConditionType) private typeRepository: Repository, + @InjectRepository(ApplicationDecisionConditionComponentPlanNumber) + private conditionComponentPlanNumbersRepository: Repository, + @InjectRepository(ApplicationDecisionConditionToComponentLot) + private conditionComponentLotRepository: Repository, ) {} async getOneOrFail(uuid: string) { @@ -92,6 +98,7 @@ export class ApplicationDecisionConditionService { 'Failed to find matching component', ); } + condition.components = mappedComponents; } else { condition.components = null; @@ -106,8 +113,19 @@ export class ApplicationDecisionConditionService { return updatedConditions; } - async remove(components: ApplicationDecisionCondition[]) { - await this.repository.remove(components); + async remove(conditions: ApplicationDecisionCondition[]) { + const lots = await this.conditionComponentLotRepository.find({ + where: { + conditionUuid: In(conditions.map((e) => e.uuid)), + }, + }); + + await this.repository.manager.transaction( + async (transactionalEntityManager) => { + await transactionalEntityManager.remove(lots); + await transactionalEntityManager.remove(conditions); + }, + ); } async update( @@ -117,4 +135,37 @@ export class ApplicationDecisionConditionService { await this.repository.update(existingCondition.uuid, updates); return await this.getOneOrFail(existingCondition.uuid); } + + async getPlanNumbers(uuid: string) { + return await this.conditionComponentPlanNumbersRepository.findBy({ + applicationDecisionConditionUuid: uuid, + }); + } + + async updateConditionPlanNumbers( + conditionUuid: string, + componentUuid: string, + planNumbers: string | null, + ) { + let conditionToComponent = + await this.conditionComponentPlanNumbersRepository.findOneBy({ + applicationDecisionComponentUuid: componentUuid, + applicationDecisionConditionUuid: conditionUuid, + }); + + if (conditionToComponent) { + conditionToComponent.planNumbers = planNumbers; + } else { + conditionToComponent = + new ApplicationDecisionConditionComponentPlanNumber({ + applicationDecisionComponentUuid: componentUuid, + applicationDecisionConditionUuid: conditionUuid, + planNumbers, + }); + } + + await this.conditionComponentPlanNumbersRepository.save( + conditionToComponent, + ); + } } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts index e9434b44f2..d2d1f47997 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts @@ -13,6 +13,13 @@ import { ApplicationModule } from '../../application/application.module'; import { BoardModule } from '../../board/board.module'; import { CardModule } from '../../card/card.module'; import { ApplicationCeoCriterionCode } from '../application-ceo-criterion/application-ceo-criterion.entity'; +import { ApplicationDecisionComponentLotController } from '../application-component-lot/application-decision-component-lot.controller'; +import { ApplicationDecisionComponentLot } from '../application-component-lot/application-decision-component-lot.entity'; +import { ApplicationDecisionComponentLotService } from '../application-component-lot/application-decision-component-lot.service'; +import { ApplicationConditionToComponentLotController } from '../application-condition-to-component-lot/application-condition-to-component-lot.controller'; +import { ApplicationConditionToComponentLotService } from '../application-condition-to-component-lot/application-condition-to-component-lot.service'; +import { ApplicationDecisionConditionToComponentLot } from '../application-condition-to-component-lot/application-decision-condition-to-component-lot.entity'; +import { ApplicationDecisionConditionComponentPlanNumber } from '../application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity'; import { ApplicationDecisionConditionType } from '../application-decision-condition/application-decision-condition-code.entity'; import { ApplicationDecisionConditionController } from '../application-decision-condition/application-decision-condition.controller'; import { ApplicationDecisionCondition } from '../application-decision-condition/application-decision-condition.entity'; @@ -61,6 +68,9 @@ import { LinkedResolutionOutcomeType } from './application-decision/linked-resol ApplicationSubmissionToSubmissionStatus, ApplicationSubmission, ApplicationSubmissionStatusType, + ApplicationDecisionComponentLot, + ApplicationDecisionConditionToComponentLot, + ApplicationDecisionConditionComponentPlanNumber, ]), forwardRef(() => BoardModule), ApplicationModule, @@ -81,11 +91,15 @@ import { LinkedResolutionOutcomeType } from './application-decision/linked-resol ApplicationDecisionProfile, ApplicationDecisionComponentService, ApplicationDecisionConditionService, + ApplicationDecisionComponentLotService, + ApplicationConditionToComponentLotService, ], controllers: [ ApplicationDecisionV2Controller, ApplicationDecisionComponentController, ApplicationDecisionConditionController, + ApplicationDecisionComponentLotController, + ApplicationConditionToComponentLotController, ], exports: [ApplicationDecisionV2Service], }) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts index b67a513d27..886114eca1 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts @@ -115,10 +115,13 @@ export class ApplicationDecisionV2Service { components: { applicationDecisionComponentType: true, naruSubtype: true, + lots: true, }, conditions: { type: true, - components: true, + components: { + lots: true, + }, }, }, }); @@ -207,6 +210,7 @@ export class ApplicationDecisionV2Service { }, components: { applicationDecisionComponentType: true, + lots: true, }, conditions: { type: true, diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts index a77ad3f11c..0cd48a06f7 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.dto.ts @@ -2,7 +2,7 @@ import { AutoMap } from '@automapper/classes'; import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; import { BaseCodeDto } from '../../../../../common/dtos/base.dto'; import { NaruSubtypeDto } from '../../../../../portal/application-submission/application-submission.dto'; -import { ProposedLot } from '../../../../../portal/application-submission/application-submission.entity'; +import { ApplicationDecisionComponentLotDto } from '../../../application-component-lot/application-decision-component-lot.dto'; export class ApplicationDecisionComponentTypeDto extends BaseCodeDto {} @@ -95,7 +95,7 @@ export class UpdateApplicationDecisionComponentDto { @IsArray() @IsOptional() - subdApprovedLots?: ProposedLot[]; + lots?: ApplicationDecisionComponentLotDto[]; @IsString() @IsOptional() @@ -184,8 +184,8 @@ export class ApplicationDecisionComponentDto { @AutoMap(() => NaruSubtypeDto) naruSubtype: NaruSubtypeDto; - @AutoMap(() => [ProposedLot]) - subdApprovedLots: ProposedLot[]; + @AutoMap(() => [ApplicationDecisionComponentLotDto]) + lots?: ApplicationDecisionComponentLotDto[]; @AutoMap(() => String) inclExclApplicantType?: string; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts index 9005f4db9c..0cbfd0ce42 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity.ts @@ -1,9 +1,17 @@ import { AutoMap } from '@automapper/classes'; -import { Column, Entity, Index, ManyToMany, ManyToOne } from 'typeorm'; +import { + Column, + Entity, + Index, + ManyToMany, + ManyToOne, + OneToMany, +} from 'typeorm'; import { Base } from '../../../../../common/entities/base.entity'; -import { ProposedLot } from '../../../../../portal/application-submission/application-submission.entity'; import { NaruSubtype } from '../../../../../portal/application-submission/naru-subtype/naru-subtype.entity'; import { ColumnNumericTransformer } from '../../../../../utils/column-numeric-transform'; +import { ApplicationDecisionComponentLot } from '../../../application-component-lot/application-decision-component-lot.entity'; +import { ApplicationDecisionConditionComponentPlanNumber } from '../../../application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity'; import { ApplicationDecisionCondition } from '../../../application-decision-condition/application-decision-condition.entity'; import { ApplicationDecision } from '../../../application-decision.entity'; import { ApplicationDecisionComponentType } from './application-decision-component-type.entity'; @@ -190,15 +198,6 @@ export class ApplicationDecisionComponent extends Base { @ManyToOne(() => NaruSubtype) naruSubtype: NaruSubtype; - @AutoMap(() => [ProposedLot]) - @Column({ - comment: 'JSONB Column containing the approved subdivision lots', - type: 'jsonb', - array: false, - default: () => `'[]'`, - }) - subdApprovedLots: ProposedLot[]; - @AutoMap(() => String) @Column({ nullable: true, @@ -227,4 +226,21 @@ export class ApplicationDecisionComponent extends Base { (condition) => condition.components, ) conditions: ApplicationDecisionCondition[]; + + @AutoMap(() => [ApplicationDecisionComponentLot]) + @OneToMany(() => ApplicationDecisionComponentLot, (lot) => lot.component, { + cascade: ['soft-remove', 'insert', 'update'], + }) + lots: ApplicationDecisionComponentLot[]; + + @OneToMany( + () => ApplicationDecisionConditionComponentPlanNumber, + (c) => c.component, + { + cascade: ['insert', 'update'], + }, + ) + componentToConditions: + | ApplicationDecisionConditionComponentPlanNumber[] + | null; } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.spec.ts index 4bba9495f1..c0a4f19563 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.spec.ts @@ -3,9 +3,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ServiceValidationException } from '../../../../../../../../libs/common/src/exceptions/base.exception'; +import { ApplicationDecisionComponentLot } from '../../../application-component-lot/application-decision-component-lot.entity'; +import { ApplicationDecisionComponentLotService } from '../../../application-component-lot/application-decision-component-lot.service'; import { APPLICATION_DECISION_COMPONENT_TYPE, CreateApplicationDecisionComponentDto, + UpdateApplicationDecisionComponentDto, } from './application-decision-component.dto'; import { ApplicationDecisionComponent } from './application-decision-component.entity'; import { ApplicationDecisionComponentService } from './application-decision-component.service'; @@ -15,9 +18,11 @@ describe('ApplicationDecisionComponentService', () => { let mockApplicationDecisionComponentRepository: DeepMocked< Repository >; + let mockApplicationDecisionComponentLotService: DeepMocked; beforeEach(async () => { mockApplicationDecisionComponentRepository = createMock(); + mockApplicationDecisionComponentLotService = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -26,6 +31,10 @@ describe('ApplicationDecisionComponentService', () => { provide: getRepositoryToken(ApplicationDecisionComponent), useValue: mockApplicationDecisionComponentRepository, }, + { + provide: ApplicationDecisionComponentLotService, + useValue: mockApplicationDecisionComponentLotService, + }, ], }).compile(); @@ -52,6 +61,9 @@ describe('ApplicationDecisionComponentService', () => { mockApplicationDecisionComponentRepository.findOneOrFail, ).toBeCalledWith({ where: { uuid: 'fake' }, + relations: { + lots: true, + }, }); expect(result).toBeDefined(); }); @@ -169,6 +181,9 @@ describe('ApplicationDecisionComponentService', () => { mockApplicationDecisionComponentRepository.findOneOrFail, ).toBeCalledWith({ where: { uuid: 'fake' }, + relations: { + lots: true, + }, }); expect(result[0].uuid).toEqual(mockDto.uuid); expect(result[0].alrArea).toEqual(mockDto.alrArea); @@ -248,6 +263,9 @@ describe('ApplicationDecisionComponentService', () => { mockApplicationDecisionComponentRepository.findOneOrFail, ).toBeCalledWith({ where: { uuid: 'fake' }, + relations: { + lots: true, + }, }); expect(result[0].uuid).toEqual(mockDto.uuid); expect(result[0].alrArea).toEqual(mockDto.alrArea); @@ -281,4 +299,40 @@ describe('ApplicationDecisionComponentService', () => { expect(mockValidationWrapper).toThrow(ServiceValidationException); }); + + it('should call lot service if there any lots to remove', async () => { + mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue({ + lots: [new ApplicationDecisionComponentLot({ uuid: 'remove' })], + } as ApplicationDecisionComponent); + mockApplicationDecisionComponentRepository.save.mockResolvedValue( + {} as ApplicationDecisionComponent, + ); + mockApplicationDecisionComponentLotService.softRemove.mockResolvedValue([]); + + const updateDtos = [ + { + uuid: 'fake', + lots: [ + { + index: 1, + componentUuid: '2', + type: null, + alrArea: null, + size: null, + }, + ], + } as UpdateApplicationDecisionComponentDto, + ]; + + const result = await service.createOrUpdate(updateDtos); + + expect(result).toBeDefined(); + expect( + mockApplicationDecisionComponentRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionComponentLotService.softRemove, + ).toBeCalledTimes(1); + expect(mockApplicationDecisionComponentRepository.save).toBeCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts index d5cd62582d..979a69bf59 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ServiceValidationException } from '../../../../../../../../libs/common/src/exceptions/base.exception'; +import { ApplicationDecisionComponentLot } from '../../../application-component-lot/application-decision-component-lot.entity'; +import { ApplicationDecisionComponentLotService } from '../../../application-component-lot/application-decision-component-lot.service'; import { APPLICATION_DECISION_COMPONENT_TYPE, CreateApplicationDecisionComponentDto, @@ -13,11 +15,15 @@ export class ApplicationDecisionComponentService { constructor( @InjectRepository(ApplicationDecisionComponent) private componentRepository: Repository, + private componentLotService: ApplicationDecisionComponentLotService, ) {} async getOneOrFail(uuid: string) { return this.componentRepository.findOneOrFail({ where: { uuid }, + relations: { + lots: true, + }, }); } @@ -51,9 +57,7 @@ export class ApplicationDecisionComponentService { this.patchNaruFields(component, updateDto); //SUBD - if (updateDto.subdApprovedLots) { - component.subdApprovedLots = updateDto.subdApprovedLots; - } + this.updateComponentLots(component, updateDto); //INCL / EXCL if (updateDto.inclExclApplicantType !== undefined) { @@ -73,6 +77,60 @@ export class ApplicationDecisionComponentService { return updatedComponents; } + private async updateComponentLots( + component: ApplicationDecisionComponent, + updateDto: CreateApplicationDecisionComponentDto, + ) { + if (updateDto.lots) { + if (updateDto.uuid) { + const lotsToRemove = component.lots + .filter((l1) => !updateDto.lots?.some((l2) => l1.uuid === l2.uuid)) + .map((l) => l.uuid); + + component.lots = component.lots.filter( + (l) => !lotsToRemove.includes(l.uuid), + ); + + updateDto.lots?.forEach((lot, index) => { + if (lot.uuid) { + const lotToUpdate = component.lots.find((e) => e.uuid === lot.uuid); + if (lotToUpdate) { + lotToUpdate.alrArea = lot.alrArea; + lotToUpdate.type = lot.type; + lotToUpdate.size = lot.size; + } + } else { + component.lots.push( + new ApplicationDecisionComponentLot({ + componentUuid: updateDto.uuid, + alrArea: lot.alrArea, + size: lot.size, + type: lot.type, + index: index + 1, + }), + ); + } + }); + + if (lotsToRemove.length > 0) { + await this.componentLotService.softRemove(lotsToRemove); + } + } else { + // this is a new component so it does not have lots and the lot is simply created + component.lots = updateDto.lots.map( + (e, index) => + new ApplicationDecisionComponentLot({ + componentUuid: updateDto.uuid, + alrArea: e.alrArea, + size: e.size, + type: e.type, + index: index + 1, + }), + ); + } + } + } + private patchNfuFields( component: ApplicationDecisionComponent, updateDto: CreateApplicationDecisionComponentDto, @@ -166,6 +224,9 @@ export class ApplicationDecisionComponentService { async softRemove(components: ApplicationDecisionComponent[]) { await this.componentRepository.softRemove(components); + components.forEach( + async (e) => await this.componentLotService.softRemoveBy(e.uuid), + ); } async getAllByApplicationUuid(applicationUuid: string) { diff --git a/services/apps/alcs/src/alcs/application/application-submission/application-submission.controller.ts b/services/apps/alcs/src/alcs/application/application-submission/application-submission.controller.ts index 9f37588d3a..d87f034145 100644 --- a/services/apps/alcs/src/alcs/application/application-submission/application-submission.controller.ts +++ b/services/apps/alcs/src/alcs/application/application-submission/application-submission.controller.ts @@ -60,6 +60,7 @@ export class ApplicationSubmissionController { if (!fileNumber) { throw new ServiceValidationException('File number is required'); } + await this.applicationSubmissionService.update(fileNumber, updateDto); return this.get(fileNumber); } diff --git a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts index 055c1fd8a2..c777a7f6d6 100644 --- a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts @@ -4,6 +4,7 @@ import { Injectable } from '@nestjs/common'; import { ApplicationCeoCriterionCode } from '../../alcs/application-decision/application-ceo-criterion/application-ceo-criterion.entity'; import { ApplicationDecisionConditionType } from '../../alcs/application-decision/application-decision-condition/application-decision-condition-code.entity'; import { + ApplicationDecisionConditionComponentDto, ApplicationDecisionConditionDto, ApplicationDecisionConditionTypeDto, } from '../../alcs/application-decision/application-decision-condition/application-decision-condition.dto'; @@ -12,6 +13,8 @@ import { ApplicationDecisionDocument } from '../../alcs/application-decision/app import { ApplicationDecisionMakerCode } from '../../alcs/application-decision/application-decision-maker/application-decision-maker.entity'; import { ApplicationDecisionChairReviewOutcomeType } from '../../alcs/application-decision/application-decision-outcome-type/application-decision-outcome-type.entity'; +import { ApplicationDecisionComponentLotDto } from '../../alcs/application-decision/application-component-lot/application-decision-component-lot.dto'; +import { ApplicationDecisionComponentLot } from '../../alcs/application-decision/application-component-lot/application-decision-component-lot.entity'; import { ApplicationDecisionMakerCodeDto } from '../../alcs/application-decision/application-decision-maker/decision-maker.dto'; import { ApplicationDecisionOutcomeCode } from '../../alcs/application-decision/application-decision-outcome.entity'; import { @@ -33,6 +36,9 @@ import { ApplicationDecision } from '../../alcs/application-decision/application import { PortalDecisionDto } from '../../portal/application-decision/application-decision.dto'; import { NaruSubtypeDto } from '../../portal/application-submission/application-submission.dto'; import { NaruSubtype } from '../../portal/application-submission/naru-subtype/naru-subtype.entity'; +import { ApplicationDecisionConditionToComponentLotDto } from '../../alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.dto'; +import { ApplicationDecisionConditionToComponentLot } from '../../alcs/application-decision/application-condition-to-component-lot/application-decision-condition-to-component-lot.entity'; +import { ApplicationDecisionConditionComponentPlanNumber } from '../../alcs/application-decision/application-decision-component-to-condition/application-decision-component-to-condition-plan-number.entity'; @Injectable() export class ApplicationDecisionProfile extends AutomapperProfile { @@ -56,6 +62,16 @@ export class ApplicationDecisionProfile extends AutomapperProfile { ), ), ), + forMember( + (ad) => ad.documents, + mapFrom((a) => + this.mapper.mapArray( + a.documents || [], + ApplicationDecisionDocument, + DecisionDocumentDto, + ), + ), + ), forMember( (a) => a.reconsiders, mapFrom((dec) => @@ -170,6 +186,18 @@ export class ApplicationDecisionProfile extends AutomapperProfile { this.mapper.map(a.naruSubtype, NaruSubtype, NaruSubtypeDto), ), ), + forMember( + (ad) => ad.lots, + mapFrom((a) => + a.lots + ? this.mapper.mapArray( + a.lots, + ApplicationDecisionComponentLot, + ApplicationDecisionComponentLotDto, + ) + : [], + ), + ), ); createMap( mapper, @@ -281,6 +309,28 @@ export class ApplicationDecisionProfile extends AutomapperProfile { LinkedResolutionOutcomeType, LinkedResolutionOutcomeTypeDto, ); + + createMap( + mapper, + ApplicationDecisionComponentLot, + ApplicationDecisionComponentLotDto, + forMember( + (a) => a.type, + mapFrom((ac) => ac.type), + ), + ); + + createMap( + mapper, + ApplicationDecisionConditionToComponentLot, + ApplicationDecisionConditionToComponentLotDto, + ); + + createMap( + mapper, + ApplicationDecisionConditionComponentPlanNumber, + ApplicationDecisionConditionComponentDto, + ); }; } } diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts index fcc7ba8cd9..59bb911eb4 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts @@ -21,6 +21,7 @@ export class ProposedLot { type: 'Lot' | 'Road Dedication' | null; alrArea?: number | null; size: number | null; + planNumbers: string | null; } @Entity() diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691021151956-plan_number.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691021151956-plan_number.ts new file mode 100644 index 0000000000..4bfede09a9 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691021151956-plan_number.ts @@ -0,0 +1,77 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class planNumber1691021151956 implements MigrationInterface { + name = 'planNumber1691021151956'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision" DROP CONSTRAINT "FK_e88e4a5dc4db0a7ba934b99dbe0"`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."application_decision_condition_to_component_lot" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "plan_numbers" text NOT NULL, "condition_uuid" uuid, "component_lot_uuid" uuid NOT NULL, CONSTRAINT "PK_9c03e6af9a3996fcccf250ca610" PRIMARY KEY ("uuid"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."application_decision_component_lot" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "index" integer NOT NULL, "type" text, "alr_area" numeric(12,2), "size" numeric(12,2), "component_uuid" uuid NOT NULL, CONSTRAINT "PK_5572331a2f8315a1efffa08928d" PRIMARY KEY ("uuid"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."application_decision_condition_component_plan_number" ("application_decision_condition_uuid" uuid NOT NULL, "application_decision_component_uuid" uuid NOT NULL, "plan_numbers" text, CONSTRAINT "PK_272dd82d4108601a66fcc786715" PRIMARY KEY ("application_decision_condition_uuid", "application_decision_component_uuid"))`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_component" DROP COLUMN "subd_approved_lots"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_to_component_lot" ADD CONSTRAINT "FK_6b5ee63fc75f3a551029c07bc38" FOREIGN KEY ("condition_uuid") REFERENCES "alcs"."application_decision_condition"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_to_component_lot" ADD CONSTRAINT "FK_c41ec91b3f32c215e495770a2e1" FOREIGN KEY ("component_lot_uuid") REFERENCES "alcs"."application_decision_component_lot"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_component_lot" ADD CONSTRAINT "FK_040a878c55a37efa00fbb10e196" FOREIGN KEY ("component_uuid") REFERENCES "alcs"."application_decision_component"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component_plan_number" ADD CONSTRAINT "FK_e9c8eae0a03c6816475ece8a702" FOREIGN KEY ("application_decision_condition_uuid") REFERENCES "alcs"."application_decision_condition"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component_plan_number" ADD CONSTRAINT "FK_0a2a0d208d27cd9d9a5577ac89b" FOREIGN KEY ("application_decision_component_uuid") REFERENCES "alcs"."application_decision_component"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision" ADD CONSTRAINT "FK_dc34f50291af0299bd44e8d0448" FOREIGN KEY ("chair_review_outcome_code") REFERENCES "alcs"."application_decision_chair_review_outcome_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision" DROP CONSTRAINT "FK_dc34f50291af0299bd44e8d0448"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component_plan_number" DROP CONSTRAINT "FK_0a2a0d208d27cd9d9a5577ac89b"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_component_plan_number" DROP CONSTRAINT "FK_e9c8eae0a03c6816475ece8a702"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_component_lot" DROP CONSTRAINT "FK_040a878c55a37efa00fbb10e196"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_to_component_lot" DROP CONSTRAINT "FK_c41ec91b3f32c215e495770a2e1"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_condition_to_component_lot" DROP CONSTRAINT "FK_6b5ee63fc75f3a551029c07bc38"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision_component" ADD "subd_approved_lots" jsonb NOT NULL DEFAULT '[]'`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."application_decision_condition_component_plan_number"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."application_decision_component_lot"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."application_decision_condition_to_component_lot"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision" ADD CONSTRAINT "FK_e88e4a5dc4db0a7ba934b99dbe0" FOREIGN KEY ("chair_review_outcome_code") REFERENCES "alcs"."application_decision_chair_review_outcome_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691094096225-table_comments.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691094096225-table_comments.ts new file mode 100644 index 0000000000..419e5f6faa --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691094096225-table_comments.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class tableComments1691094096225 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + COMMENT ON TABLE "alcs"."application_decision_condition_component_plan_number" IS 'Survey plan numbers associated with survey plan conditions on decision components'; + COMMENT ON TABLE "alcs"."application_decision_condition_component" IS 'Join table to link decision conditions with decision components'; + COMMENT ON TABLE "alcs"."application_decision_condition_to_component_lot" IS 'Join table to link approved subdivision lots between condition and components and provide plan numbers associated with survey plan per lot'; + COMMENT ON TABLE "alcs"."application_decision_component_lot" IS 'Approved lots on the subdivision decision component'; + `, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // not needed + } +} From 207fcead6174dbf8e7c31c3abbab444766e291bc Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 3 Aug 2023 15:51:15 -0700 Subject: [PATCH 205/954] Code Review Feedback --- .../notice-of-intent-document.service.spec.ts | 1 + .../notice-of-intent-meeting.controller.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.spec.ts index ebff119377..d9cdd8ca16 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.spec.ts @@ -248,6 +248,7 @@ describe('NoticeOfIntentDocumentService', () => { expect(res).toBeDefined(); }); + // TODO: Re-enabled when adding Step 7 // it('should set the type and description for multiple files', async () => { // const mockDocument1 = new NoticeOfIntentDocument({ // typeCode: DOCUMENT_TYPE.DECISION_DOCUMENT, diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.ts index 5bbda2e010..f0d00b7ef8 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.ts @@ -80,10 +80,10 @@ export class NoticeOfIntentMeetingController { } private async validateAndPrepareCreateData( - fileNumber: string, + uuid: string, meeting: CreateNoticeOfIntentMeetingDto, ) { - const noi = await this.noiService.getOrFailByUuid(fileNumber); + const noi = await this.noiService.getOrFailByUuid(uuid); const meetingType = ( await this.noiMeetingService.fetNoticeOfIntentMeetingTypes() ).find((e) => e.code === meeting.meetingTypeCode); From 664039f859385b5b9d74b101a720847d79fe9a8a Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 3 Aug 2023 15:53:38 -0700 Subject: [PATCH 206/954] removed migration & tweaked names to work with ALCS-1001 --- .../documents/noi_documents.py | 8 ++-- .../sql/documents_noi/noi_documents.sql | 2 +- .../noi_documents_total_count.sql | 4 +- .../migrations/1691007382208-add_noi_docs.ts | 47 ------------------- 4 files changed, 7 insertions(+), 54 deletions(-) delete mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691007382208-add_noi_docs.ts diff --git a/bin/migrate-oats-data/documents/noi_documents.py b/bin/migrate-oats-data/documents/noi_documents.py index 8d70741607..6a6e7ba727 100644 --- a/bin/migrate-oats-data/documents/noi_documents.py +++ b/bin/migrate-oats-data/documents/noi_documents.py @@ -11,9 +11,9 @@ def compile_document_insert_query(number_of_rows_to_insert): """ documents_to_insert = ",".join(["%s"] * number_of_rows_to_insert) return f""" - insert into alcs.noi_document + insert into alcs.notice_of_intent_document ( - application_uuid , + notice_of_intent_uuid , document_uuid , type_code , visibility_flags, @@ -23,7 +23,7 @@ def compile_document_insert_query(number_of_rows_to_insert): ) VALUES {documents_to_insert} ON CONFLICT (oats_document_id, oats_application_id) DO UPDATE SET - application_uuid = EXCLUDED.application_uuid, + notice_of_intent_uuid = EXCLUDED.notice_of_intnent_uuid, document_uuid = EXCLUDED.document_uuid, type_code = EXCLUDED.type_code, visibility_flags = EXCLUDED.visibility_flags, @@ -93,7 +93,7 @@ def clean_noi_documents(conn=None): print("Start noi documents cleaning") with conn.cursor() as cursor: cursor.execute( - "DELETE FROM alcs.noi_document WHERE audit_created_by = 'oats_etl';" + "DELETE FROM alcs.notice_of_intent_document WHERE audit_created_by = 'oats_etl';" ) conn.commit() print(f"Deleted items count = {cursor.rowcount}") diff --git a/bin/migrate-oats-data/sql/documents_noi/noi_documents.sql b/bin/migrate-oats-data/sql/documents_noi/noi_documents.sql index 1a91a46a6f..36998bc167 100644 --- a/bin/migrate-oats-data/sql/documents_noi/noi_documents.sql +++ b/bin/migrate-oats-data/sql/documents_noi/noi_documents.sql @@ -12,7 +12,7 @@ WITH oats.oats_documents od JOIN alcs."document" d ON d.oats_document_id = od.document_id::TEXT - JOIN alcs.application_document_code adc -- using adc as it is the same mapping + JOIN alcs.document_code adc ON adc.oats_code = od.document_code JOIN alcs.notice_of_intent a ON a.file_number = od.alr_application_id::TEXT diff --git a/bin/migrate-oats-data/sql/documents_noi/noi_documents_total_count.sql b/bin/migrate-oats-data/sql/documents_noi/noi_documents_total_count.sql index 3ad3fd9825..30bdd294ee 100644 --- a/bin/migrate-oats-data/sql/documents_noi/noi_documents_total_count.sql +++ b/bin/migrate-oats-data/sql/documents_noi/noi_documents_total_count.sql @@ -1,6 +1,6 @@ with oats_documents_to_map as ( select - a.uuid AS application_uuid, + a.uuid AS noi_uuid, d.uuid AS document_uuid, adc.code, publicly_viewable_ind AS is_public, @@ -12,7 +12,7 @@ with oats_documents_to_map as ( JOIN alcs."document" d ON d.oats_document_id = od.document_id::TEXT - JOIN alcs.application_document_code adc -- reusing application table mapping + JOIN alcs.document_code adc ON adc.oats_code = od.document_code JOIN alcs.notice_of_intent a diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691007382208-add_noi_docs.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691007382208-add_noi_docs.ts deleted file mode 100644 index ba7bb34be8..0000000000 --- a/services/apps/alcs/src/providers/typeorm/migrations/1691007382208-add_noi_docs.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm" - -export class addNoiDocs1691007382208 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `CREATE TABLE "alcs"."noi_document" ("uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "audit_created_by" text, "evidentiary_record_sorting" integer, "oats_document_id" text, "oats_application_id" text, "type_code" character varying NOT NULL, "visibility_flags" text array NOT NULL DEFAULT '{}', "application_uuid" uuid NOT NULL, "description" text, "document_uuid" uuid, CONSTRAINT "REL_17be9d534322486d932567196a62725e" UNIQUE ("document_uuid"), CONSTRAINT "PK_aa8a0b4ce8474054afe73e949c2d88f5" PRIMARY KEY ("uuid"))`, - ); - await queryRunner.query( - `ALTER TABLE "alcs"."noi_document" ADD CONSTRAINT "FK_703514379dc945f3bfc0d8e897d88086" FOREIGN KEY ("application_uuid") REFERENCES "alcs"."notice_of_intent"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "alcs"."noi_document" ADD CONSTRAINT "FK_be9edc8bd1864d20be37058fba08199b" FOREIGN KEY ("document_uuid") REFERENCES "alcs"."document"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query( - `ALTER TABLE "alcs"."noi_document" ALTER COLUMN "type_code" DROP NOT NULL`, - ); - await queryRunner.query( - `ALTER TABLE "alcs"."noi_document" ADD CONSTRAINT "FK_d5b10b5343374bf58cde7be5cefdcbdd" FOREIGN KEY ("type_code") REFERENCES "alcs"."application_document_code"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, - ); - await queryRunner.query(` - COMMENT ON COLUMN "alcs"."noi_document"."oats_document_id" IS 'used only for oats etl process'; - COMMENT ON COLUMN "alcs"."noi_document"."oats_application_id" IS 'used only for oats etl process'; - COMMENT ON COLUMN "alcs"."noi_document"."audit_created_by" IS 'used only for oats etl process'; - COMMENT ON TABLE "alcs"."noi_document" IS 'Links noi documents with the noi they''re saved to and logs other attributes'; - `, - ); - await queryRunner.query( - `ALTER TABLE "alcs"."noi_document" ADD CONSTRAINT "OATS_UNQ_DOCUMENTS" UNIQUE ("oats_document_id", "oats_application_id")`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE "alcs"."noi_document" DROP CONSTRAINT "REL_17be9d534322486d932567196a62725e"; - ALTER TABLE "alcs"."noi_document" DROP CONSTRAINT "PK_aa8a0b4ce8474054afe73e949c2d88f5"; - ALTER TABLE "alcs"."noi_document" DROP CONSTRAINT "FK_703514379dc945f3bfc0d8e897d88086"; - ALTER TABLE "alcs"."noi_document" DROP CONSTRAINT "FK_be9edc8bd1864d20be37058fba08199b"; - ALTER TABLE "alcs"."noi_document" DROP CONSTRAINT "FK_d5b10b5343374bf58cde7be5cefdcbdd"; - ALTER TABLE "alcs"."noi_document" DROP CONSTRAINT "OATS_UNQ_DOCUMENTS"; - DROP TABLE "alcs"."noi_document" - `, - - ); - } - -} From 4dd1ea9064bc294182aa83b5ee8ad31328a77a7a Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Thu, 3 Aug 2023 15:54:13 -0700 Subject: [PATCH 207/954] Remove expiry date from naru and tur proposals (#851) - Delete blank tur proposal component after removing only field - Update modules from non-existent tur component --- .../application/application.module.ts | 2 - .../proposal/naru/naru.component.html | 8 ---- .../proposal/proposal.component.html | 1 - .../proposal/tur/tur.component.html | 7 --- .../proposal/tur/tur.component.scss | 0 .../proposal/tur/tur.component.spec.ts | 43 ------------------- .../application/proposal/tur/tur.component.ts | 42 ------------------ 7 files changed, 103 deletions(-) delete mode 100644 alcs-frontend/src/app/features/application/proposal/tur/tur.component.html delete mode 100644 alcs-frontend/src/app/features/application/proposal/tur/tur.component.scss delete mode 100644 alcs-frontend/src/app/features/application/proposal/tur/tur.component.spec.ts delete mode 100644 alcs-frontend/src/app/features/application/proposal/tur/tur.component.ts diff --git a/alcs-frontend/src/app/features/application/application.module.ts b/alcs-frontend/src/app/features/application/application.module.ts index a26b8f313c..829b1b8818 100644 --- a/alcs-frontend/src/app/features/application/application.module.ts +++ b/alcs-frontend/src/app/features/application/application.module.ts @@ -30,7 +30,6 @@ import { ProposalComponent } from './proposal/proposal.component'; import { ExclProposalComponent } from './proposal/excl/excl.component'; import { SoilProposalComponent } from './proposal/soil/soil.component'; import { SubdProposalComponent } from './proposal/subd/subd.component'; -import { TurProposalComponent } from './proposal/tur/tur.component'; import { DecisionMeetingDialogComponent } from './review/decision-meeting-dialog/decision-meeting-dialog.component'; import { DecisionMeetingComponent } from './review/decision-meeting/decision-meeting.component'; import { ReviewComponent } from './review/review.component'; @@ -74,7 +73,6 @@ const routes: Routes = [ NfuProposalComponent, SubdProposalComponent, SoilProposalComponent, - TurProposalComponent, ExclProposalComponent, NaruProposalComponent, InclProposalComponent, diff --git a/alcs-frontend/src/app/features/application/proposal/naru/naru.component.html b/alcs-frontend/src/app/features/application/proposal/naru/naru.component.html index 461ba76b51..aeb7253ea3 100644 --- a/alcs-frontend/src/app/features/application/proposal/naru/naru.component.html +++ b/alcs-frontend/src/app/features/application/proposal/naru/naru.component.html @@ -1,11 +1,3 @@ -
-
Expiry Date
- -
-
Use End Date
Proposal Components - {{ application?.type?.label }} (save)="updateApplicationValue('agCapConsultant', $event)" >
-
diff --git a/alcs-frontend/src/app/features/application/proposal/tur/tur.component.html b/alcs-frontend/src/app/features/application/proposal/tur/tur.component.html deleted file mode 100644 index 13bf973675..0000000000 --- a/alcs-frontend/src/app/features/application/proposal/tur/tur.component.html +++ /dev/null @@ -1,7 +0,0 @@ -
-
Expiry Date
- -
diff --git a/alcs-frontend/src/app/features/application/proposal/tur/tur.component.scss b/alcs-frontend/src/app/features/application/proposal/tur/tur.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/alcs-frontend/src/app/features/application/proposal/tur/tur.component.spec.ts b/alcs-frontend/src/app/features/application/proposal/tur/tur.component.spec.ts deleted file mode 100644 index 3a0ad21212..0000000000 --- a/alcs-frontend/src/app/features/application/proposal/tur/tur.component.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { BehaviorSubject } from 'rxjs'; -import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; -import { ApplicationDto } from '../../../../services/application/application.dto'; -import { ToastService } from '../../../../services/toast/toast.service'; - -import { TurProposalComponent } from './tur.component'; - -describe('TurComponent', () => { - let component: TurProposalComponent; - let fixture: ComponentFixture; - let mockApplicationDetailService: DeepMocked; - let mockToastService: DeepMocked; - - beforeEach(async () => { - mockApplicationDetailService = createMock(); - mockToastService = createMock(); - - await TestBed.configureTestingModule({ - declarations: [TurProposalComponent], - providers: [ - { provide: ToastService, useValue: mockToastService }, - { - provide: ApplicationDetailService, - useValue: mockApplicationDetailService, - }, - ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - mockApplicationDetailService.$application = new BehaviorSubject(undefined); - - fixture = TestBed.createComponent(TurProposalComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alcs-frontend/src/app/features/application/proposal/tur/tur.component.ts b/alcs-frontend/src/app/features/application/proposal/tur/tur.component.ts deleted file mode 100644 index eda82878a1..0000000000 --- a/alcs-frontend/src/app/features/application/proposal/tur/tur.component.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Subject, takeUntil } from 'rxjs'; -import { ApplicationDetailService } from '../../../../services/application/application-detail.service'; -import { ApplicationDto, UpdateApplicationDto } from '../../../../services/application/application.dto'; -import { ToastService } from '../../../../services/toast/toast.service'; - -@Component({ - selector: 'app-proposal-tur', - templateUrl: './tur.component.html', - styleUrls: ['./tur.component.scss'], -}) -export class TurProposalComponent implements OnInit, OnDestroy { - $destroy = new Subject(); - application: ApplicationDto | undefined; - - constructor(private applicationDetailService: ApplicationDetailService, private toastService: ToastService) {} - - ngOnInit(): void { - this.applicationDetailService.$application.pipe(takeUntil(this.$destroy)).subscribe((application) => { - if (application) { - this.application = application; - } - }); - } - - async updateApplicationValue(field: keyof UpdateApplicationDto, value: string[] | string | number | null) { - const application = this.application; - if (application) { - const update = await this.applicationDetailService.updateApplication(application.fileNumber, { - [field]: value, - }); - if (update) { - this.toastService.showSuccessToast('Application updated'); - } - } - } - - ngOnDestroy(): void { - this.$destroy.next(); - this.$destroy.complete(); - } -} From 8fbe58f0532a3a888d384333cf6fdfd76745c3ca Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 3 Aug 2023 16:09:21 -0700 Subject: [PATCH 208/954] updated comment to specify noi --- bin/migrate-oats-data/documents/documents_noi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/documents/documents_noi.py b/bin/migrate-oats-data/documents/documents_noi.py index 1f5bd12047..b2e2fe638e 100644 --- a/bin/migrate-oats-data/documents/documents_noi.py +++ b/bin/migrate-oats-data/documents/documents_noi.py @@ -4,7 +4,7 @@ This script connects to postgress version of OATS DB and transfers data from OATS documents table to ALCS document table. NOTE: - Before performing document import you need to import applications from oats. + Before performing document_noi import you need to import noi from oats. """ From 6e21e2ce34bee95413104166ef60fa5adab4a601 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Thu, 3 Aug 2023 16:26:29 -0700 Subject: [PATCH 209/954] fix inconsistency in FK name between initial migration and dev/test (#852) --- .../typeorm/migrations/1691021151956-plan_number.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691021151956-plan_number.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691021151956-plan_number.ts index 4bfede09a9..9d761f800c 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1691021151956-plan_number.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691021151956-plan_number.ts @@ -5,7 +5,10 @@ export class planNumber1691021151956 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `ALTER TABLE "alcs"."application_decision" DROP CONSTRAINT "FK_e88e4a5dc4db0a7ba934b99dbe0"`, + `ALTER TABLE "alcs"."application_decision" DROP CONSTRAINT IF EXISTS "FK_e88e4a5dc4db0a7ba934b99dbe0"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision" DROP CONSTRAINT IF EXISTS "FK_dc34f50291af0299bd44e8d0448"`, ); await queryRunner.query( `CREATE TABLE "alcs"."application_decision_condition_to_component_lot" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "plan_numbers" text NOT NULL, "condition_uuid" uuid, "component_lot_uuid" uuid NOT NULL, CONSTRAINT "PK_9c03e6af9a3996fcccf250ca610" PRIMARY KEY ("uuid"))`, From 4cd48c9040944f8fdea63bfa1259243c532e1a84 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 3 Aug 2023 16:36:56 -0700 Subject: [PATCH 210/954] Bug Fixes for Edit Submission * Update toasts for some reason * Stop red toast / error returning from Submit when the PDF generation fails. --- .../application-owner.service.ts | 6 ++-- ...nerate-submission-document.service.spec.ts | 7 ++-- .../generate-submission-document.service.ts | 36 +++++++++++++------ .../pdf-generation.controller.ts | 6 ++-- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/portal-frontend/src/app/services/application-owner/application-owner.service.ts b/portal-frontend/src/app/services/application-owner/application-owner.service.ts index 2942812ab9..debb0ae2c4 100644 --- a/portal-frontend/src/app/services/application-owner/application-owner.service.ts +++ b/portal-frontend/src/app/services/application-owner/application-owner.service.ts @@ -53,7 +53,7 @@ export class ApplicationOwnerService { const res = await firstValueFrom( this.httpClient.patch(`${this.serviceUrl}/${uuid}`, updateDto) ); - this.toastService.showSuccessToast('Owner updated'); + this.toastService.showSuccessToast('Owner saved'); return res; } catch (e) { console.error(e); @@ -67,11 +67,11 @@ export class ApplicationOwnerService { const res = await firstValueFrom( this.httpClient.post(`${this.serviceUrl}/setPrimaryContact`, updateDto) ); - this.toastService.showSuccessToast('Primary Contact Updated'); + this.toastService.showSuccessToast('Application saved'); return res; } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to update Primary Contact, please try again later'); + this.toastService.showErrorToast('Failed to update Application, please try again later'); return undefined; } } diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts index 72a1fd31d0..69cf57d39a 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts @@ -166,12 +166,9 @@ describe('GenerateSubmissionDocumentService', () => { name: user.user.entity, }); - await expect(service.generate('fake', userEntity)).rejects.toMatchObject( - new ServiceNotFoundException( - `Could not find template for application submission type not a type`, - ), - ); + const res = await service.generate('fake', userEntity); + expect(res).toBeUndefined(); expect(mockCdogsService.generateDocument).toBeCalledTimes(0); }); diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts index e95c1ed148..43ccf334f5 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts @@ -1,6 +1,12 @@ import { CdogsService } from '@app/common/cdogs/cdogs.service'; import { ServiceNotFoundException } from '@app/common/exceptions/base.exception'; -import { forwardRef, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { + forwardRef, + HttpStatus, + Inject, + Injectable, + Logger, +} from '@nestjs/common'; import * as config from 'config'; import * as dayjs from 'dayjs'; import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; @@ -37,6 +43,8 @@ const NO_DATA = 'No Data'; @Injectable() export class GenerateSubmissionDocumentService { + private logger = new Logger(GenerateSubmissionDocumentService.name); + constructor( private documentGenerationService: CdogsService, @Inject(forwardRef(() => ApplicationSubmissionService)) @@ -59,19 +67,24 @@ export class GenerateSubmissionDocumentService { const template = await this.getPdfTemplateBySubmissionType(submission); - const pdf = await this.documentGenerationService.generateDocument( - `${fileNumber}_submission_Date_Time`, - `${config.get('CDOGS.TEMPLATE_FOLDER')}/${template.templateName}`, - template.payload, - ); + if (template) { + const pdf = await this.documentGenerationService.generateDocument( + `${fileNumber}_submission_Date_Time`, + `${config.get('CDOGS.TEMPLATE_FOLDER')}/${ + template.templateName + }`, + template.payload, + ); - return pdf; + return pdf; + } + return; } async generateAndAttach(fileNumber: string, user: User) { const generatedRes = await this.generate(fileNumber, user); - if (generatedRes.status === HttpStatus.OK) { + if (generatedRes && generatedRes.status === HttpStatus.OK) { await this.applicationDocumentService.attachDocumentAsBuffer({ fileNumber: fileNumber, fileName: `${fileNumber}_APP_Submission.pdf`, @@ -94,7 +107,7 @@ export class GenerateSubmissionDocumentService { async generateUpdate(fileNumber: string, user: User) { const generatedRes = await this.generate(fileNumber, user); - if (generatedRes.status === HttpStatus.OK) { + if (generatedRes && generatedRes.status === HttpStatus.OK) { const documents = await this.applicationDocumentService.list(fileNumber); const submissionDocuments = documents.filter( @@ -141,7 +154,7 @@ export class GenerateSubmissionDocumentService { private async getPdfTemplateBySubmissionType( submission: ApplicationSubmission, - ): Promise { + ): Promise { const documents = await this.applicationDocumentService.list( submission.fileNumber, ); @@ -162,9 +175,10 @@ export class GenerateSubmissionDocumentService { payload = this.populateSubdData(payload, submission, documents); return { payload, templateName: 'subd-submission-template.docx' }; default: - throw new ServiceNotFoundException( + this.logger.error( `Could not find template for application submission type ${submission.typeCode}`, ); + return; } } diff --git a/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.ts b/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.ts index 2f08599eb6..d488a8ab44 100644 --- a/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.ts +++ b/services/apps/alcs/src/portal/pdf-generation/pdf-generation.controller.ts @@ -25,8 +25,10 @@ export class PdfGenerationController { user, ); - resp.type('application/pdf'); - resp.send(result.data); + if (result) { + resp.type('application/pdf'); + resp.send(result.data); + } } @Get(':fileNumber/review') From 099cd708a56c359ebbe27ac62a9fd26295f20c70 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 3 Aug 2023 17:01:29 -0700 Subject: [PATCH 211/954] renamed files --- bin/migrate-oats-data/documents/__init__.py | 8 ++++---- ...on_documents.py => alcs_documents_to_app_documents.py} | 4 ++-- ...oi_documents.py => alcs_documents_to_noi_documents.py} | 6 +++--- ...cuments.py => oats_documents_to_alcs_documents_app.py} | 4 ++-- ...nts_noi.py => oats_documents_to_alcs_documents_noi.py} | 4 ++-- .../alcs_documents_to_app_documents.sql} | 0 .../alcs_documents_to_app_documents_total_count.sql} | 0 .../oats_documents_to_alcs_documents_app.sql} | 0 .../oats_documents_to_alcs_documents_app_total_count.sql} | 0 ..._documents.sql => alcs_documents_to_noi_documents.sql} | 0 ...ql => alcs_documents_to_noi_documents_total_count.sql} | 0 ...s_noi.sql => oats_documents_to_alcs_documents_noi.sql} | 0 ... oats_documents_to_alcs_documents_noi_total_count.sql} | 0 13 files changed, 13 insertions(+), 13 deletions(-) rename bin/migrate-oats-data/documents/{application_documents.py => alcs_documents_to_app_documents.py} (95%) rename bin/migrate-oats-data/documents/{noi_documents.py => alcs_documents_to_noi_documents.py} (92%) rename bin/migrate-oats-data/documents/{documents.py => oats_documents_to_alcs_documents_app.py} (94%) rename bin/migrate-oats-data/documents/{documents_noi.py => oats_documents_to_alcs_documents_noi.py} (94%) rename bin/migrate-oats-data/sql/{documents/application_documents.sql => documents_app/alcs_documents_to_app_documents.sql} (100%) rename bin/migrate-oats-data/sql/{documents/application_documents_total_count.sql => documents_app/alcs_documents_to_app_documents_total_count.sql} (100%) rename bin/migrate-oats-data/sql/{documents/documents.sql => documents_app/oats_documents_to_alcs_documents_app.sql} (100%) rename bin/migrate-oats-data/sql/{documents/documents_total_count.sql => documents_app/oats_documents_to_alcs_documents_app_total_count.sql} (100%) rename bin/migrate-oats-data/sql/documents_noi/{noi_documents.sql => alcs_documents_to_noi_documents.sql} (100%) rename bin/migrate-oats-data/sql/documents_noi/{noi_documents_total_count.sql => alcs_documents_to_noi_documents_total_count.sql} (100%) rename bin/migrate-oats-data/sql/documents_noi/{documents_noi.sql => oats_documents_to_alcs_documents_noi.sql} (100%) rename bin/migrate-oats-data/sql/documents_noi/{documents_noi_total_count.sql => oats_documents_to_alcs_documents_noi_total_count.sql} (100%) diff --git a/bin/migrate-oats-data/documents/__init__.py b/bin/migrate-oats-data/documents/__init__.py index 95beb4bba2..5038d11bb9 100644 --- a/bin/migrate-oats-data/documents/__init__.py +++ b/bin/migrate-oats-data/documents/__init__.py @@ -1,4 +1,4 @@ -from .documents import * -from .application_documents import * -from .documents_noi import * -from .noi_documents import * +from .alcs_documents_to_app_documents import * +from .oats_documents_to_alcs_documents_app import * +from .alcs_documents_to_noi_documents import * +from .oats_documents_to_alcs_documents_noi import * diff --git a/bin/migrate-oats-data/documents/application_documents.py b/bin/migrate-oats-data/documents/alcs_documents_to_app_documents.py similarity index 95% rename from bin/migrate-oats-data/documents/application_documents.py rename to bin/migrate-oats-data/documents/alcs_documents_to_app_documents.py index ea8b34c715..1eeb7124f6 100644 --- a/bin/migrate-oats-data/documents/application_documents.py +++ b/bin/migrate-oats-data/documents/alcs_documents_to_app_documents.py @@ -38,7 +38,7 @@ def process_application_documents(conn=None, batch_size=10000): """ with conn.cursor() as cursor: with open( - "sql/documents/application_documents_total_count.sql", "r", encoding="utf-8" + "sql/documents_app/alcs_documents_to_app_documents_total_count.sql", "r", encoding="utf-8" ) as sql_file: count_query = sql_file.read() cursor.execute(count_query) @@ -50,7 +50,7 @@ def process_application_documents(conn=None, batch_size=10000): last_document_id = 0 with open( - "sql/documents/application_documents.sql", "r", encoding="utf-8" + "sql/documents_app/alcs_documents_to_app_documents.sql", "r", encoding="utf-8" ) as sql_file: documents_to_insert_sql = sql_file.read() while True: diff --git a/bin/migrate-oats-data/documents/noi_documents.py b/bin/migrate-oats-data/documents/alcs_documents_to_noi_documents.py similarity index 92% rename from bin/migrate-oats-data/documents/noi_documents.py rename to bin/migrate-oats-data/documents/alcs_documents_to_noi_documents.py index 6a6e7ba727..b06b2101e1 100644 --- a/bin/migrate-oats-data/documents/noi_documents.py +++ b/bin/migrate-oats-data/documents/alcs_documents_to_noi_documents.py @@ -1,7 +1,7 @@ from db import inject_conn_pool """ - This script links data from ALCS documents table to ALCS noi_documents table based on data from OATS. + This script links data from ALCS documents table to ALCS notice_of_intent_document table based on data from OATS. """ @@ -38,7 +38,7 @@ def process_noi_documents(conn=None, batch_size=10000): """ with conn.cursor() as cursor: with open( - "sql/documents_noi/noi_documents_total_count.sql", "r", encoding="utf-8" + "sql/documents_noi/alcs_documents_to_noi_documents_total_count.sql", "r", encoding="utf-8" ) as sql_file: count_query = sql_file.read() cursor.execute(count_query) @@ -50,7 +50,7 @@ def process_noi_documents(conn=None, batch_size=10000): last_document_id = 0 with open( - "sql/documents_noi/noi_documents.sql", "r", encoding="utf-8" + "sql/documents_noi/alcs_documents_to_noi_documents.sql", "r", encoding="utf-8" ) as sql_file: documents_to_insert_sql = sql_file.read() while True: diff --git a/bin/migrate-oats-data/documents/documents.py b/bin/migrate-oats-data/documents/oats_documents_to_alcs_documents_app.py similarity index 94% rename from bin/migrate-oats-data/documents/documents.py rename to bin/migrate-oats-data/documents/oats_documents_to_alcs_documents_app.py index e8af18e96a..ecc953969f 100644 --- a/bin/migrate-oats-data/documents/documents.py +++ b/bin/migrate-oats-data/documents/oats_documents_to_alcs_documents_app.py @@ -37,7 +37,7 @@ def process_documents(conn=None, batch_size=10000): """ with conn.cursor() as cursor: with open( - "sql/documents/documents_total_count.sql", "r", encoding="utf-8" + "sql/documents_app/oats_documents_to_alcs_documents_app_total_count.sql", "r", encoding="utf-8" ) as sql_file: count_query = sql_file.read() cursor.execute(count_query) @@ -48,7 +48,7 @@ def process_documents(conn=None, batch_size=10000): successful_inserts_count = 0 last_document_id = 0 - with open("sql/documents/documents.sql", "r", encoding="utf-8") as sql_file: + with open("sql/documents_app/oats_documents_to_alcs_documents_app.sql", "r", encoding="utf-8") as sql_file: documents_to_insert_sql = sql_file.read() while True: cursor.execute( diff --git a/bin/migrate-oats-data/documents/documents_noi.py b/bin/migrate-oats-data/documents/oats_documents_to_alcs_documents_noi.py similarity index 94% rename from bin/migrate-oats-data/documents/documents_noi.py rename to bin/migrate-oats-data/documents/oats_documents_to_alcs_documents_noi.py index b2e2fe638e..294538da15 100644 --- a/bin/migrate-oats-data/documents/documents_noi.py +++ b/bin/migrate-oats-data/documents/oats_documents_to_alcs_documents_noi.py @@ -37,7 +37,7 @@ def process_documents_noi(conn=None, batch_size=10000): """ with conn.cursor() as cursor: with open( - "sql/documents_noi/documents_noi_total_count.sql", "r", encoding="utf-8" + "sql/documents_noi/oats_documents_to_alcs_documents_noi_total_count.sql", "r", encoding="utf-8" ) as sql_file: count_query = sql_file.read() cursor.execute(count_query) @@ -48,7 +48,7 @@ def process_documents_noi(conn=None, batch_size=10000): successful_inserts_count = 0 last_document_id = 0 - with open("sql/documents_noi/documents_noi.sql", "r", encoding="utf-8") as sql_file: + with open("sql/documents_noi/oats_documents_to_alcs_documents_noi.sql", "r", encoding="utf-8") as sql_file: documents_to_insert_sql = sql_file.read() while True: cursor.execute( diff --git a/bin/migrate-oats-data/sql/documents/application_documents.sql b/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents.sql similarity index 100% rename from bin/migrate-oats-data/sql/documents/application_documents.sql rename to bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents.sql diff --git a/bin/migrate-oats-data/sql/documents/application_documents_total_count.sql b/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents_total_count.sql similarity index 100% rename from bin/migrate-oats-data/sql/documents/application_documents_total_count.sql rename to bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents_total_count.sql diff --git a/bin/migrate-oats-data/sql/documents/documents.sql b/bin/migrate-oats-data/sql/documents_app/oats_documents_to_alcs_documents_app.sql similarity index 100% rename from bin/migrate-oats-data/sql/documents/documents.sql rename to bin/migrate-oats-data/sql/documents_app/oats_documents_to_alcs_documents_app.sql diff --git a/bin/migrate-oats-data/sql/documents/documents_total_count.sql b/bin/migrate-oats-data/sql/documents_app/oats_documents_to_alcs_documents_app_total_count.sql similarity index 100% rename from bin/migrate-oats-data/sql/documents/documents_total_count.sql rename to bin/migrate-oats-data/sql/documents_app/oats_documents_to_alcs_documents_app_total_count.sql diff --git a/bin/migrate-oats-data/sql/documents_noi/noi_documents.sql b/bin/migrate-oats-data/sql/documents_noi/alcs_documents_to_noi_documents.sql similarity index 100% rename from bin/migrate-oats-data/sql/documents_noi/noi_documents.sql rename to bin/migrate-oats-data/sql/documents_noi/alcs_documents_to_noi_documents.sql diff --git a/bin/migrate-oats-data/sql/documents_noi/noi_documents_total_count.sql b/bin/migrate-oats-data/sql/documents_noi/alcs_documents_to_noi_documents_total_count.sql similarity index 100% rename from bin/migrate-oats-data/sql/documents_noi/noi_documents_total_count.sql rename to bin/migrate-oats-data/sql/documents_noi/alcs_documents_to_noi_documents_total_count.sql diff --git a/bin/migrate-oats-data/sql/documents_noi/documents_noi.sql b/bin/migrate-oats-data/sql/documents_noi/oats_documents_to_alcs_documents_noi.sql similarity index 100% rename from bin/migrate-oats-data/sql/documents_noi/documents_noi.sql rename to bin/migrate-oats-data/sql/documents_noi/oats_documents_to_alcs_documents_noi.sql diff --git a/bin/migrate-oats-data/sql/documents_noi/documents_noi_total_count.sql b/bin/migrate-oats-data/sql/documents_noi/oats_documents_to_alcs_documents_noi_total_count.sql similarity index 100% rename from bin/migrate-oats-data/sql/documents_noi/documents_noi_total_count.sql rename to bin/migrate-oats-data/sql/documents_noi/oats_documents_to_alcs_documents_noi_total_count.sql From d99b9555850f2324208d48dec95e97288a8c9f56 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 4 Aug 2023 10:25:03 -0700 Subject: [PATCH 212/954] Re-organize Error Fields, Hints, and Character Limits * Update order for all proposal / input component steps. --- .../land-use/land-use.component.html | 22 ++++++-------- .../parcel-entry/parcel-entry.component.html | 13 ++++---- .../excl-proposal.component.html | 4 +-- .../incl-proposal.component.html | 8 ++--- .../naru-proposal.component.html | 30 +++++++++---------- .../nfu-proposal/nfu-proposal.component.html | 12 ++++---- .../pfrs-proposal.component.html | 18 +++++------ .../pofo-proposal.component.html | 18 +++++------ .../roso-proposal.component.html | 14 +++++---- .../subd-proposal.component.html | 10 +++---- .../tur-proposal/tur-proposal.component.html | 8 ++--- 11 files changed, 78 insertions(+), 79 deletions(-) diff --git a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html index 8343869399..72130d0cf4 100644 --- a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html +++ b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html @@ -32,7 +32,6 @@
Land Use of Parcel(s) under Application
maxlength="4000" > - Characters left: {{ 4000 - parcelsAgricultureDescriptionText.textLength }}
This field is required
-
- Example 1: PID 001-002-003: 60 ha hay crop, 40 ha grazing, 200 sheep.
Example 2: Parcel 1: 10% - blueberry crop, 30% vegetables, 60% hay. Parcel 2: 100% hay. -
-
+ Example 1: PID 001-002-003: 60 ha hay crop, 40 ha grazing, 200 sheep. Example 2: Parcel 1: 10% + blueberry crop, 30% vegetables, 60% hay. Parcel 2: 100% hay. + +
Characters left: {{ 4000 - parcelsAgricultureDescriptionText.textLength }}
@@ -72,9 +70,6 @@
maxlength="4000" > - Characters left: {{ 4000 - parcelsAgricultureImprovementDescriptionText.textLength }}
This field is required
-
Example: 40 ha of grazing land fenced in 2010.
+ Example: 40 ha of grazing land fenced in 2010. +
Characters left: {{ 4000 - parcelsAgricultureImprovementDescriptionText.textLength }}
-
Example: House and 100 square metre detached auto repair shop.
+ Example: House and 100 square metre detached auto repair shop. +
Characters left: {{ 4000 - parcelsNonAgricultureUseDescriptionText.textLength }}
Identify the land uses surrounding the parcel(s) under application.
diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html index a5bc951032..0cdffa38c9 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html @@ -83,6 +83,7 @@
Parcel Lookup
+
The area of the entire parcel in hectares, not just the area under application.
Parcel Lookup
-
+
@@ -133,19 +134,20 @@
Parcel Lookup
-
- Example: 2020-Mar-01 -
warning
This field is required
+
+ Example: 2020-Mar-01 +
- As determined by BC Assessment + As determined by + BC Assessment
Owner Information isConfirmedByApplicant.invalid && (isConfirmedByApplicant.dirty || isConfirmedByApplicant.touched) }" - class="" >I confirm that the owner information provided above matches the current Certificate of Title. Mismatched information can cause significant delays to processing time. diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html index c5afa0cf74..02917af9e0 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html @@ -91,11 +91,11 @@

Proposal

placeholder="Type comment" > - Characters left: {{ 4000 - purposeText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - purposeText.textLength }}
@@ -109,7 +109,7 @@

Proposal

placeholder="Type comment" > - Characters left: {{ 4000 - outsideLandsText.textLength }} +
Characters left: {{ 4000 - outsideLandsText.textLength }}
warning
This field is required
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html index b48dd099ea..c3d52c3413 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html @@ -55,11 +55,11 @@

Proposal

placeholder="Type comment" > - Characters left: {{ 4000 - purposeText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - purposeText.textLength }}
@@ -73,11 +73,11 @@

Proposal

placeholder="Type comment" > - Characters left: {{ 4000 - outsideLandsText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - outsideLandsText.textLength }}
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html index 9ae2050053..52abf7c448 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html @@ -50,11 +50,11 @@

Proposal

- Characters left: {{ 4000 - purposeText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - purposeText.textLength }}
@@ -97,7 +97,6 @@

Proposal

matInput > - Characters left: {{ 4000 - residenceNecessityText.textLength }}
Proposal warning
This field is required
+
Characters left: {{ 4000 - residenceNecessityText.textLength }}
@@ -123,7 +123,6 @@

Proposal

matInput > - Characters left: {{ 4000 - locationRationaleText.textLength }}
Proposal warning
This field is required
+
Characters left: {{ 4000 - locationRationaleText.textLength }}
@@ -151,11 +151,11 @@

Proposal

matInput > - Characters left: {{ 4000 - infrastructureText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - infrastructureText.textLength }}
@@ -196,7 +196,6 @@

Proposal

matInput > - Characters left: {{ 4000 - residenceNecessityText.textLength }}
Proposal warning
This field is required
+
Characters left: {{ 4000 - residenceNecessityText.textLength }}
@@ -219,7 +219,6 @@

Proposal

matInput > - Characters left: {{ 4000 - locationRationaleText.textLength }}
Proposal warning
This field is required
+
Characters left: {{ 4000 - locationRationaleText.textLength }}
@@ -247,11 +247,11 @@

Proposal

matInput > - Characters left: {{ 4000 - infrastructureText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - infrastructureText.textLength }}
@@ -316,7 +316,6 @@

Proposal

matInput > - Characters left: {{ 4000 - residenceNecessityText.textLength }}
Proposal warning
This field is required
+
Characters left: {{ 4000 - residenceNecessityText.textLength }}
@@ -339,7 +339,6 @@

Proposal

matInput > - Characters left: {{ 4000 - locationRationaleText.textLength }}
Proposal warning
This field is required
+
Characters left: {{ 4000 - locationRationaleText.textLength }}
@@ -367,11 +367,11 @@

Proposal

matInput > - Characters left: {{ 4000 - infrastructureText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - infrastructureText.textLength }}
@@ -389,11 +389,11 @@

Proposal

matInput > - Characters left: {{ 4000 - agriTourismText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - agriTourismText.textLength }}
@@ -415,7 +415,6 @@

Proposal

matInput > - Characters left: {{ 4000 - existingStructuresText.textLength }}
Proposal warning
This field is required
+
Characters left: {{ 4000 - existingStructuresText.textLength }}
@@ -477,12 +477,12 @@

Soil & Fill Components

- Characters left: {{ 4000 - fillTypeToPlaceText.textLength }}
warning
This field is required
-
Example: Aggregate, topsoil, structural fill, sand, gravel, etc
+ Example: Aggregate, topsoil, structural fill, sand, gravel, etc +
Characters left: {{ 4000 - fillTypeToPlaceText.textLength }}
@@ -498,11 +498,11 @@

Soil & Fill Components

matInput > - Characters left: {{ 4000 - fillOriginToPlaceText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - fillOriginToPlaceText.textLength }}
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html index c9d089321a..58ab5ea83a 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html @@ -50,11 +50,11 @@

Proposal

placeholder="Type comment" > - Characters left: {{ 4000 - purposeText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - purposeText.textLength }}
@@ -167,7 +167,6 @@

Soil & Fill Components

matInput > - Characters left: {{ 4000 - fillOriginToPlaceText.textLength }}
Soil & Fill Components warning
This field is required
+
Characters left: {{ 4000 - fillOriginToPlaceText.textLength }}
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html index cd61be54b1..d795b5f5f6 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html @@ -209,12 +209,12 @@

Project Duration

placeholder="Type comment" > - Characters left: {{ 4000 - fillTypeToPlaceText.textLength }}
warning
This field is required
-
Example: Aggregate, Extraction, Placer Mining, Peat Extraction, Soil etc.
+ Example: Aggregate, Extraction, Placer Mining, Peat Extraction, Soil etc. +
Characters left: {{ 4000 - fillTypeToPlaceText.textLength }}
@@ -230,12 +230,12 @@

Project Duration

placeholder="Type comment" > - Characters left: {{ 4000 - soilTypeRemovedText.textLength }}
warning
This field is required
-
Example: Aggregate, Extraction, Placer Mining, Peat Extraction, Soil etc.
+ Example: Aggregate, Extraction, Placer Mining, Peat Extraction, Soil etc. +
Characters left: {{ 4000 - soilTypeRemovedText.textLength }}
@@ -256,7 +256,6 @@

Project Duration

placeholder="Type comment" > - Characters left: {{ 4000 - soilAlternativeMeasuresText.textLength }}
Project Duration warning
This field is required
-
Example: Aggregate, Extraction, Placer Mining, Peat Extraction, Soil etc.
+ Example: Aggregate, Extraction, Placer Mining, Peat Extraction, Soil etc. +
Characters left: {{ 4000 - soilAlternativeMeasuresText.textLength }}
@@ -282,7 +282,6 @@

Project Duration

placeholder="Type comment" > - Characters left: {{ 4000 - reduceNegativeImpactsText.textLength }}
Project Duration warning
This field is required
-
+ Example: Project phasing, providing landscape screening, fencing, buffering, erosion and sediment control, temporary or permanent drainage, etc. -
+ +
Characters left: {{ 4000 - reduceNegativeImpactsText.textLength }}
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html index b5fa496a1e..4568248baf 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html @@ -82,11 +82,11 @@

Proposal

placeholder="Type comment" > - Characters left: {{ 4000 - purposeText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - purposeText.textLength }}
@@ -175,12 +175,12 @@

Project Duration

placeholder="Type comment" > - Characters left: {{ 4000 - fillTypeToPlaceText.textLength }}
warning
This field is required
-
Example: Aggregate, topsoil, structural fill, sand, gravel, etc
+ Example: Aggregate, topsoil, structural fill, sand, gravel, etc + Characters left: {{ 4000 - fillTypeToPlaceText.textLength }}
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html index ed68d2b5e5..6364510f0f 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html @@ -83,11 +83,11 @@

Proposal

placeholder="Type comment" > - Characters left: {{ 4000 - purposeText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - purposeText.textLength }}
@@ -175,13 +175,14 @@

Project Duration

placeholder="Type comment" > - Characters left: {{ 4000 - suitabilityText.textLength }}
warning
This field is required
-
Example: Aggregate, Extraction, Placer Mining, Peat Extraction, Soil etc.
+ Example: Aggregate, Extraction, Placer Mining, Peat Extraction, Soil etc. +
Characters left: {{ 4000 - suitabilityText.textLength }}
+
+
A visual representation of your proposal.
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html index facd717e1e..8defadda06 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html @@ -54,10 +54,10 @@
Documents needed for this step:
warning
This field is required
-
+ Example: If the proposal is to subdivide a 5 ha lot with road dedication from a 20 ha parcel, the total number of lots proposed is three. -
+
@@ -125,11 +125,11 @@
Documents needed for this step:
placeholder="Type comment" > - Characters left: {{ 4000 - purposeText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - purposeText.textLength }}
@@ -147,11 +147,11 @@
Documents needed for this step:
placeholder="Type comment" > - Characters left: {{ 4000 - suitabilityText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - suitabilityText.textLength }}
diff --git a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.html b/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.html index b719d744a2..0de9433b72 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.html +++ b/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.html @@ -43,11 +43,11 @@
Documents needed for this step:
placeholder="Type comment" > - Characters left: {{ 4000 - purposeText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - purposeText.textLength }}
@@ -108,11 +108,11 @@
Documents needed for this step:
placeholder="Type comment" > - Characters left: {{ 4000 - outsideLandsText.textLength }}
warning
This field is required
+
Characters left: {{ 4000 - outsideLandsText.textLength }}
From b80e09ff766646de15d400e2c6cdd78929878cb9 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 4 Aug 2023 11:06:52 -0700 Subject: [PATCH 213/954] Sort columns and implement column delete * Sort columns while mapping them * Hook up delete button and add to controller --- .../card-status-dialog/card-status-dialog.component.html | 2 +- .../card-status-dialog/card-status-dialog.component.ts | 5 +++++ .../admin/card-status/card-status.controller.spec.ts | 9 +++++++++ .../src/alcs/admin/card-status/card-status.controller.ts | 7 +++++++ .../src/alcs/card/card-status/card-status.service.ts | 6 ++++++ .../src/common/automapper/board.automapper.profile.ts | 4 +++- 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.html b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.html index 221f66fe25..1aff4398cc 100644 --- a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.html +++ b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.html @@ -37,7 +37,7 @@

{{ isEdit ? 'Edit' : 'Create' }} Column

- +
diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.ts b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.ts index c05c5a1d04..1f28c8751f 100644 --- a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.ts +++ b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.ts @@ -60,4 +60,9 @@ export class CardStatusDialogComponent implements OnInit { this.canDeleteReason = res.reason; } } + + async onDelete() { + await this.cardStatusService.delete(this.code); + this.dialogRef.close(true); + } } diff --git a/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.spec.ts b/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.spec.ts index 243b9ba436..ff9a9ce666 100644 --- a/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.spec.ts +++ b/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.spec.ts @@ -122,4 +122,13 @@ describe('CardStatusController', () => { expect(res.canDelete).toBeTruthy(); expect(mockBoardService.getBoardsWithStatus).toHaveBeenCalledTimes(1); }); + + it('should call the service for delete', async () => { + mockCardStatusService.delete.mockResolvedValue({} as any); + + const res = await controller.delete('FAKE-CODE'); + + expect(res).toBeDefined(); + expect(mockCardStatusService.delete).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.ts b/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.ts index 65de8f7ec0..ec87e8f11b 100644 --- a/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.ts +++ b/services/apps/alcs/src/alcs/admin/card-status/card-status.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, + Delete, Get, Param, Patch, @@ -81,6 +82,12 @@ export class CardStatusController { return await this.cardStatusService.update(code, updateDto); } + @Delete('/:code') + @UserRoles(AUTH_ROLE.ADMIN) + async delete(@Param('code') code: string) { + return await this.cardStatusService.delete(code); + } + @Post('') @UserRoles(AUTH_ROLE.ADMIN) async create(@Body() createDto: CardStatusDto) { diff --git a/services/apps/alcs/src/alcs/card/card-status/card-status.service.ts b/services/apps/alcs/src/alcs/card/card-status/card-status.service.ts index 85659cd05c..fb3338ea81 100644 --- a/services/apps/alcs/src/alcs/card/card-status/card-status.service.ts +++ b/services/apps/alcs/src/alcs/card/card-status/card-status.service.ts @@ -53,4 +53,10 @@ export class CardStatusService { const cards = await this.cardService.getByCardStatus(code); return cards.length; } + + async delete(code: string) { + const cardStatus = await this.getOneOrFail(code); + + return await this.cardStatusRepository.remove(cardStatus); + } } diff --git a/services/apps/alcs/src/common/automapper/board.automapper.profile.ts b/services/apps/alcs/src/common/automapper/board.automapper.profile.ts index 5e3037c840..396aaf7b67 100644 --- a/services/apps/alcs/src/common/automapper/board.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/board.automapper.profile.ts @@ -38,7 +38,9 @@ export class BoardAutomapperProfile extends AutomapperProfile { forMember( (ad) => ad.statuses, mapFrom((a) => - this.mapper.mapArray(a.statuses, BoardStatus, BoardStatusDto), + this.mapper + .mapArray(a.statuses, BoardStatus, BoardStatusDto) + .sort((a1, b) => a1.order - b.order), ), ), forMember( From 709c6b37f2a90319a0755d6b24876c42c95efe54 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 4 Aug 2023 12:00:27 -0700 Subject: [PATCH 214/954] Adjust field alignment for incl/excl decision components * Use correct field for null checks on conditions page for no data --- .../condition/condition.component.html | 2 +- .../incl-excl/incl-excl.component.html | 32 ++++++++++++------- .../decision-v2/decision-v2.component.scss | 3 +- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html index 62fba64e36..30a7671c59 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.html @@ -14,7 +14,7 @@

{{ condition.type.label }}

Component to Condition
{{ condition.componentLabelsStr }} - +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html index abf3a14f1e..3dadcfd6c4 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html @@ -1,16 +1,26 @@ -
- +
+ +
+
Applicant Type
+ {{ component.inclExclApplicantType }} + +
+
+
Expiry Date
+ {{ component.expiryDate | date }} + +
-
-
Applicant Type
- {{ component.inclExclApplicantType }} - -
- -
+
Expiry Date
{{ component.expiryDate | date }} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss index 8474044aec..09b54fbebb 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss @@ -155,6 +155,7 @@ .mdc-tab__text-label { color: colors.$black !important; } + .mdc-tab { margin-left: 8px; } @@ -178,7 +179,7 @@ } .lot-table { - margin-top:12px; + margin-top: 12px; display: grid; grid-template-columns: max-content max-content max-content 0.8fr; grid-column-gap: 36px; From 01b89cb30568f40178e1903db17cba56d65b6547 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 4 Aug 2023 16:01:34 -0700 Subject: [PATCH 215/954] Add NOI Submission Entities * Add backend for NOI Submissions * Rename ApplicationLocalGovernment -> LocalGovernment and move to own module * Move file number generation to the fileNumber service --- .../create-application-dialog.component.ts | 1 + .../local-government.controller.spec.ts | 6 +- .../local-government.controller.ts | 6 +- services/apps/alcs/src/alcs/alcs.module.ts | 3 + .../application-modification.dto.ts | 4 +- .../src/alcs/application/application.dto.ts | 6 +- .../alcs/application/application.entity.ts | 11 +- .../alcs/application/application.module.ts | 16 +- .../application/application.service.spec.ts | 22 +- .../alcs/application/application.service.ts | 29 +- .../notice-of-intent-type.dto.ts | 13 + .../notice-of-intent-type.entity.ts | 25 ++ .../src/alcs/commissioner/commissioner.dto.ts | 4 +- .../alcs/src/alcs/covenant/covenant.dto.ts | 4 +- .../alcs/src/alcs/covenant/covenant.entity.ts | 6 +- .../import/application-import.service.spec.ts | 15 +- .../alcs/import/application-import.service.ts | 4 +- .../src/alcs/import/noi-import.service.ts | 8 +- .../local-government.controller.spec.ts} | 26 +- .../local-government.controller.ts} | 16 +- .../local-government.dto.ts} | 2 +- .../local-government.entity.ts} | 8 +- .../local-government.module.ts | 12 + .../local-government.service.spec.ts} | 30 +- .../local-government.service.ts} | 16 +- .../notice-of-intent-modification.dto.ts | 4 +- .../notice-of-intent.controller.spec.ts | 1 + .../notice-of-intent.controller.ts | 6 +- .../notice-of-intent/notice-of-intent.dto.ts | 23 +- .../notice-of-intent.entity.ts | 29 +- .../notice-of-intent.module.ts | 12 +- .../notice-of-intent.service.spec.ts | 32 +- .../notice-of-intent.service.ts | 98 ++++- .../alcs-application.controller.spec.ts | 11 +- .../alcs-application.controller.ts | 6 +- .../planning-review/planning-review.dto.ts | 4 +- .../planning-review/planning-review.entity.ts | 6 +- .../alcs/src/alcs/search/search.module.ts | 4 +- .../application.automapper.profile.ts | 10 +- .../modification.automapper.profile.ts | 8 +- ...e-of-intent-decision.automapper.profile.ts | 8 +- ...of-intent-submission.automapper.profile.ts | 55 +++ .../src/file-number/file-number.constants.ts | 1 + .../file-number/file-number.service.spec.ts | 12 + .../src/file-number/file-number.service.ts | 8 + services/apps/alcs/src/main.module.ts | 5 + ...ation-submission-review.controller.spec.ts | 10 +- ...pplication-submission-review.controller.ts | 8 +- ...lication-submission-review.service.spec.ts | 4 +- .../application-submission-review.service.ts | 4 +- ...ation-submission-validator.service.spec.ts | 10 +- ...pplication-submission-validator.service.ts | 4 +- .../application-submission.controller.spec.ts | 10 +- .../application-submission.controller.ts | 8 +- .../application-submission.module.ts | 2 + .../application-submission.service.spec.ts | 19 +- .../application-submission.service.ts | 23 +- .../src/portal/code/code.controller.spec.ts | 12 +- .../alcs/src/portal/code/code.controller.ts | 8 +- ...ce-of-intent-submission.controller.spec.ts | 284 +++++++++++++++ .../notice-of-intent-submission.controller.ts | 152 ++++++++ .../notice-of-intent-submission.dto.ts | 146 ++++++++ .../notice-of-intent-submission.entity.ts | 186 ++++++++++ .../notice-of-intent-submission.module.ts | 26 ++ ...otice-of-intent-submission.service.spec.ts | 244 +++++++++++++ .../notice-of-intent-submission.service.ts | 344 ++++++++++++++++++ .../generate-review-document.service.spec.ts | 6 +- .../generate-review-document.service.ts | 4 +- ...nerate-submission-document.service.spec.ts | 6 +- .../generate-submission-document.service.ts | 4 +- .../1691185932823-add_noi_submissions.ts | 179 +++++++++ .../alcs/src/user/user.controller.spec.ts | 4 +- services/apps/alcs/src/user/user.module.ts | 7 +- .../apps/alcs/src/user/user.service.spec.ts | 16 +- services/apps/alcs/src/user/user.service.ts | 6 +- 75 files changed, 2084 insertions(+), 288 deletions(-) create mode 100644 services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.dto.ts create mode 100644 services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity.ts rename services/apps/alcs/src/alcs/{application/application-code/application-local-government/application-local-government.controller.spec.ts => local-government/local-government.controller.spec.ts} (59%) rename services/apps/alcs/src/alcs/{application/application-code/application-local-government/application-local-government.controller.ts => local-government/local-government.controller.ts} (51%) rename services/apps/alcs/src/alcs/{application/application-code/application-local-government/application-local-government.dto.ts => local-government/local-government.dto.ts} (66%) rename services/apps/alcs/src/alcs/{application/application-code/application-local-government/application-local-government.entity.ts => local-government/local-government.entity.ts} (70%) create mode 100644 services/apps/alcs/src/alcs/local-government/local-government.module.ts rename services/apps/alcs/src/alcs/{application/application-code/application-local-government/application-local-government.service.spec.ts => local-government/local-government.service.spec.ts} (65%) rename services/apps/alcs/src/alcs/{application/application-code/application-local-government/application-local-government.service.ts => local-government/local-government.service.ts} (84%) create mode 100644 services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts create mode 100644 services/apps/alcs/src/file-number/file-number.constants.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691185932823-add_noi_submissions.ts diff --git a/alcs-frontend/src/app/features/board/dialogs/application/create/create-application-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application/create/create-application-dialog.component.ts index d23960b1ab..00381c5b77 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/create/create-application-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/create/create-application-dialog.component.ts @@ -49,6 +49,7 @@ export class CreateApplicationDialogComponent implements OnInit, OnDestroy { }); this.localGovernmentService.list().then((res) => { + debugger; this.localGovernments = res; }); } diff --git a/services/apps/alcs/src/alcs/admin/local-government/local-government.controller.spec.ts b/services/apps/alcs/src/alcs/admin/local-government/local-government.controller.spec.ts index 6394a72a37..fd6230947b 100644 --- a/services/apps/alcs/src/alcs/admin/local-government/local-government.controller.spec.ts +++ b/services/apps/alcs/src/alcs/admin/local-government/local-government.controller.spec.ts @@ -3,12 +3,12 @@ import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; -import { ApplicationLocalGovernmentService } from '../../application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../../local-government/local-government.service'; import { LocalGovernmentController } from './local-government.controller'; describe('LocalGovernmentController', () => { let controller: LocalGovernmentController; - let mockLgService: DeepMocked; + let mockLgService: DeepMocked; beforeEach(async () => { mockLgService = createMock(); @@ -17,7 +17,7 @@ describe('LocalGovernmentController', () => { controllers: [LocalGovernmentController], providers: [ { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockLgService, }, { diff --git a/services/apps/alcs/src/alcs/admin/local-government/local-government.controller.ts b/services/apps/alcs/src/alcs/admin/local-government/local-government.controller.ts index df10d37802..1109ab79d8 100644 --- a/services/apps/alcs/src/alcs/admin/local-government/local-government.controller.ts +++ b/services/apps/alcs/src/alcs/admin/local-government/local-government.controller.ts @@ -10,7 +10,7 @@ import { } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; -import { ApplicationLocalGovernmentService } from '../../application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../../local-government/local-government.service'; import { AUTH_ROLE } from '../../../common/authorization/roles'; import { RolesGuard } from '../../../common/authorization/roles-guard.service'; import { UserRoles } from '../../../common/authorization/roles.decorator'; @@ -23,9 +23,7 @@ import { @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @UseGuards(RolesGuard) export class LocalGovernmentController { - constructor( - private localGovernmentService: ApplicationLocalGovernmentService, - ) {} + constructor(private localGovernmentService: LocalGovernmentService) {} @Get('/:pageIndex/:itemsPerPage') @UserRoles(AUTH_ROLE.ADMIN) diff --git a/services/apps/alcs/src/alcs/alcs.module.ts b/services/apps/alcs/src/alcs/alcs.module.ts index 645cbe05ec..65ab46e1ec 100644 --- a/services/apps/alcs/src/alcs/alcs.module.ts +++ b/services/apps/alcs/src/alcs/alcs.module.ts @@ -12,6 +12,7 @@ import { CommissionerModule } from './commissioner/commissioner.module'; import { CovenantModule } from './covenant/covenant.module'; import { HomeModule } from './home/home.module'; import { ImportModule } from './import/import.module'; +import { LocalGovernmentModule } from './local-government/local-government.module'; import { NoticeOfIntentDecisionModule } from './notice-of-intent-decision/notice-of-intent-decision.module'; import { NoticeOfIntentModule } from './notice-of-intent/notice-of-intent.module'; import { NotificationModule } from './notification/notification.module'; @@ -38,6 +39,7 @@ import { StaffJournalModule } from './staff-journal/staff-journal.module'; StaffJournalModule, NoticeOfIntentDecisionModule, SearchModule, + LocalGovernmentModule, RouterModule.register([ { path: 'alcs', module: ApplicationModule }, { path: 'alcs', module: CommentModule }, @@ -56,6 +58,7 @@ import { StaffJournalModule } from './staff-journal/staff-journal.module'; { path: 'alcs', module: StaffJournalModule }, { path: 'alcs', module: SearchModule }, { path: 'alcs', module: ApplicationSubmissionStatusModule }, + { path: 'alcs', module: LocalGovernmentModule }, ]), ], controllers: [], diff --git a/services/apps/alcs/src/alcs/application-decision/application-modification/application-modification.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-modification/application-modification.dto.ts index 9fbb6c16b5..155bf25c58 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-modification/application-modification.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-modification/application-modification.dto.ts @@ -10,7 +10,7 @@ import { IsString, } from 'class-validator'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; -import { ApplicationLocalGovernmentDto } from '../../application/application-code/application-local-government/application-local-government.dto'; +import { LocalGovernmentDto } from '../../local-government/local-government.dto'; import { CardDto } from '../../card/card.dto'; import { ApplicationRegionDto } from '../../code/application-code/application-region/application-region.dto'; import { ApplicationTypeDto } from '../../code/application-code/application-type/application-type.dto'; @@ -88,7 +88,7 @@ export class ApplicationForModificationDto { statusCode: string; applicant: string; region: ApplicationRegionDto; - localGovernment: ApplicationLocalGovernmentDto; + localGovernment: LocalGovernmentDto; decisionMeetings: ApplicationDecisionMeetingDto[]; } diff --git a/services/apps/alcs/src/alcs/application/application.dto.ts b/services/apps/alcs/src/alcs/application/application.dto.ts index c756dda3c2..db902aeb43 100644 --- a/services/apps/alcs/src/alcs/application/application.dto.ts +++ b/services/apps/alcs/src/alcs/application/application.dto.ts @@ -14,7 +14,7 @@ import { ApplicationDecisionMeetingDto } from '../application-decision/applicati import { CardDto } from '../card/card.dto'; import { ApplicationRegionDto } from '../code/application-code/application-region/application-region.dto'; import { ApplicationTypeDto } from '../code/application-code/application-type/application-type.dto'; -import { ApplicationLocalGovernmentDto } from './application-code/application-local-government/application-local-government.dto'; +import { LocalGovernmentDto } from '../local-government/local-government.dto'; export class AlcsApplicationSubmissionDto extends ApplicationSubmissionDetailedDto { primaryContact?: ApplicationOwnerDto; @@ -208,8 +208,8 @@ export class ApplicationDto { @AutoMap(() => ApplicationRegionDto) region: ApplicationRegionDto; - @AutoMap(() => ApplicationLocalGovernmentDto) - localGovernment: ApplicationLocalGovernmentDto; + @AutoMap(() => LocalGovernmentDto) + localGovernment: LocalGovernmentDto; @AutoMap(() => ApplicationDecisionMeetingDto) decisionMeetings: ApplicationDecisionMeetingDto[]; diff --git a/services/apps/alcs/src/alcs/application/application.entity.ts b/services/apps/alcs/src/alcs/application/application.entity.ts index 9955871366..423c222b3d 100644 --- a/services/apps/alcs/src/alcs/application/application.entity.ts +++ b/services/apps/alcs/src/alcs/application/application.entity.ts @@ -11,6 +11,7 @@ import { OneToOne, } from 'typeorm'; import { Base } from '../../common/entities/base.entity'; +import { FILE_NUMBER_SEQUENCE } from '../../file-number/file-number.constants'; import { ApplicationSubmissionReview } from '../../portal/application-submission-review/application-submission-review.entity'; import { ColumnNumericTransformer } from '../../utils/column-numeric-transform'; import { ApplicationDecisionMeeting } from '../application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.entity'; @@ -18,13 +19,11 @@ import { ApplicationReconsideration } from '../application-decision/application- import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; import { ApplicationType } from '../code/application-code/application-type/application-type.entity'; -import { ApplicationLocalGovernment } from './application-code/application-local-government/application-local-government.entity'; +import { LocalGovernment } from '../local-government/local-government.entity'; import { ApplicationDocument } from './application-document/application-document.entity'; import { ApplicationMeeting } from './application-meeting/application-meeting.entity'; import { ApplicationPaused } from './application-paused.entity'; -export const APPLICATION_FILE_NUMBER_SEQUENCE = 'alcs.alcs_file_number_seq'; - @Entity() export class Application extends Base { constructor(data?: Partial) { @@ -38,7 +37,7 @@ export class Application extends Base { @AutoMap() @Column({ unique: true, - default: () => `NEXTVAL('${APPLICATION_FILE_NUMBER_SEQUENCE}')`, + default: () => `NEXTVAL('${FILE_NUMBER_SEQUENCE}')`, }) fileNumber: string; @@ -141,8 +140,8 @@ export class Application extends Base { @Column({ nullable: true }) regionCode?: string; - @ManyToOne(() => ApplicationLocalGovernment, { nullable: true }) - localGovernment?: ApplicationLocalGovernment; + @ManyToOne(() => LocalGovernment, { nullable: true }) + localGovernment?: LocalGovernment; @Index() @Column({ diff --git a/services/apps/alcs/src/alcs/application/application.module.ts b/services/apps/alcs/src/alcs/application/application.module.ts index ba6ee7d797..6b678833f7 100644 --- a/services/apps/alcs/src/alcs/application/application.module.ts +++ b/services/apps/alcs/src/alcs/application/application.module.ts @@ -18,10 +18,11 @@ import { Board } from '../board/board.entity'; import { CardModule } from '../card/card.module'; import { ApplicationType } from '../code/application-code/application-type/application-type.entity'; import { CodeModule } from '../code/code.module'; +import { LocalGovernment } from '../local-government/local-government.entity'; +import { LocalGovernmentModule } from '../local-government/local-government.module'; import { NotificationModule } from '../notification/notification.module'; -import { ApplicationLocalGovernmentController } from './application-code/application-local-government/application-local-government.controller'; -import { ApplicationLocalGovernment } from './application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from './application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentController } from '../local-government/local-government.controller'; +import { LocalGovernmentService } from '../local-government/local-government.service'; import { DocumentCode } from '../../document/document-code.entity'; import { ApplicationDocumentController } from './application-document/application-document.controller'; import { ApplicationDocument } from './application-document/application-document.entity'; @@ -50,13 +51,13 @@ import { ApplicationService } from './application.service'; ApplicationMeeting, ApplicationDocument, DocumentCode, - ApplicationLocalGovernment, ApplicationParcel, Board, ApplicationSubmission, ApplicationSubmissionReview, ApplicationSubmissionStatusType, ApplicationSubmissionToSubmissionStatus, + LocalGovernment, ]), NotificationModule, DocumentModule, @@ -65,6 +66,7 @@ import { ApplicationService } from './application.service'; FileNumberModule, forwardRef(() => ApplicationSubmissionModule), ApplicationSubmissionStatusModule, + LocalGovernmentModule, ], providers: [ ApplicationService, @@ -76,7 +78,7 @@ import { ApplicationService } from './application.service'; ApplicationMeetingService, ApplicationPausedService, ApplicationDocumentService, - ApplicationLocalGovernmentService, + LocalGovernmentService, ApplicationSubmissionService, ApplicationSubmissionReviewService, ApplicationSubmissionProfile, @@ -85,7 +87,7 @@ import { ApplicationService } from './application.service'; ApplicationController, ApplicationMeetingController, ApplicationDocumentController, - ApplicationLocalGovernmentController, + LocalGovernmentController, ApplicationSubmissionController, ApplicationSubmissionReviewController, ApplicationParcelController, @@ -97,7 +99,7 @@ import { ApplicationService } from './application.service'; ApplicationSubtaskProfile, ApplicationMeetingService, ApplicationPausedService, - ApplicationLocalGovernmentService, + LocalGovernmentService, ApplicationDocumentService, ], }) diff --git a/services/apps/alcs/src/alcs/application/application.service.spec.ts b/services/apps/alcs/src/alcs/application/application.service.spec.ts index 68fbb111ac..794dc2aaa4 100644 --- a/services/apps/alcs/src/alcs/application/application.service.spec.ts +++ b/services/apps/alcs/src/alcs/application/application.service.spec.ts @@ -7,7 +7,6 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { FindOptionsRelations, Repository } from 'typeorm'; import { initApplicationMockEntity } from '../../../test/mocks/mockEntities'; import { ApplicationSubmissionStatusService } from '../../application-submission-status/application-submission-status.service'; -import { ApplicationSubmissionStatusType } from '../../application-submission-status/submission-status-type.entity'; import { SUBMISSION_STATUS } from '../../application-submission-status/submission-status.dto'; import { ApplicationSubmissionToSubmissionStatus } from '../../application-submission-status/submission-status.entity'; import { FileNumberService } from '../../file-number/file-number.service'; @@ -15,7 +14,7 @@ import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; import { ApplicationType } from '../code/application-code/application-type/application-type.entity'; import { CodeService } from '../code/code.service'; -import { ApplicationLocalGovernmentService } from './application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../local-government/local-government.service'; import { ApplicationTimeData, ApplicationTimeTrackingService, @@ -33,7 +32,7 @@ describe('ApplicationService', () => { let applicationTypeRepositoryMock: DeepMocked>; let applicationMockEntity; let mockApplicationTimeService: DeepMocked; - let mockApplicationLocalGovernmentService: DeepMocked; + let mockApplicationLocalGovernmentService: DeepMocked; let mockCodeService: DeepMocked; let mockFileNumberService: DeepMocked; let mockApplicationSubmissionStatusService: DeepMocked; @@ -82,7 +81,7 @@ describe('ApplicationService', () => { useValue: mockCodeService, }, { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockApplicationLocalGovernmentService, }, { @@ -259,21 +258,6 @@ describe('ApplicationService', () => { expect(applicationRepositoryMock.save).toHaveBeenCalledTimes(0); }); - it('should generate and return new fileNumber', async () => { - applicationRepositoryMock.findOne - .mockResolvedValueOnce({} as Application) - .mockResolvedValue(null); - applicationRepositoryMock.query.mockResolvedValue([ - { nextval: applicationMockEntity.uuid }, - ]); - - const result = await applicationService.generateNextFileNumber(); - - expect(applicationRepositoryMock.findOne).toHaveBeenCalledTimes(2); - expect(applicationRepositoryMock.query).toBeCalledTimes(2); - expect(result).toEqual(applicationMockEntity.uuid); - }); - it('should load deleted card', async () => { applicationRepositoryMock.findOne.mockResolvedValue(null); diff --git a/services/apps/alcs/src/alcs/application/application.service.ts b/services/apps/alcs/src/alcs/application/application.service.ts index 7ef4744334..117f581f24 100644 --- a/services/apps/alcs/src/alcs/application/application.service.ts +++ b/services/apps/alcs/src/alcs/application/application.service.ts @@ -22,7 +22,7 @@ import { FileNumberService } from '../../file-number/file-number.service'; import { Card } from '../card/card.entity'; import { ApplicationType } from '../code/application-code/application-type/application-type.entity'; import { CodeService } from '../code/code.service'; -import { ApplicationLocalGovernmentService } from './application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../local-government/local-government.service'; import { ApplicationTimeData, ApplicationTimeTrackingService, @@ -32,10 +32,7 @@ import { ApplicationUpdateServiceDto, CreateApplicationServiceDto, } from './application.dto'; -import { - Application, - APPLICATION_FILE_NUMBER_SEQUENCE, -} from './application.entity'; +import { Application } from './application.entity'; export const APPLICATION_EXPIRATION_DAY_RANGES = { ACTIVE_DAYS_START: 55, @@ -84,7 +81,7 @@ export class ApplicationService { private applicationTypeRepository: Repository, private applicationTimeTrackingService: ApplicationTimeTrackingService, private codeService: CodeService, - private localGovernmentService: ApplicationLocalGovernmentService, + private localGovernmentService: LocalGovernmentService, private fileNumberService: FileNumberService, private applicationSubmissionStatusService: ApplicationSubmissionStatusService, @InjectMapper() private applicationMapper: Mapper, @@ -424,26 +421,6 @@ export class ApplicationService { }); } - private async getNextFileNumber() { - const fileNumberArr = await this.applicationRepository.query( - `select nextval('${APPLICATION_FILE_NUMBER_SEQUENCE}') limit 1`, - ); - return fileNumberArr[0].nextval; - } - - async generateNextFileNumber(): Promise { - let fileNumber: string; - let application: Application | null = null; - do { - fileNumber = await this.getNextFileNumber(); - application = await this.applicationRepository.findOne({ - where: { fileNumber }, - }); - } while (application); - - return fileNumber; - } - async getUuid(fileNumber: string) { const application = await this.applicationRepository.findOneOrFail({ where: { diff --git a/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.dto.ts b/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.dto.ts new file mode 100644 index 0000000000..0f555620b9 --- /dev/null +++ b/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.dto.ts @@ -0,0 +1,13 @@ +import { AutoMap } from '@automapper/classes'; +import { BaseCodeDto } from '../../../../common/dtos/base.dto'; + +export class NoticeOfIntentTypeDto extends BaseCodeDto { + @AutoMap() + shortLabel: string; + + @AutoMap() + backgroundColor: string; + + @AutoMap() + textColor: string; +} diff --git a/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity.ts b/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity.ts new file mode 100644 index 0000000000..ca9959746a --- /dev/null +++ b/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity.ts @@ -0,0 +1,25 @@ +import { AutoMap } from '@automapper/classes'; +import { Column, Entity } from 'typeorm'; +import { BaseCodeEntity } from '../../../../common/entities/base.code.entity'; + +@Entity() +export class NoticeOfIntentType extends BaseCodeEntity { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @Column() + shortLabel: string; + + @AutoMap() + @Column({ type: 'text', default: '' }) + htmlDescription: string; + + @AutoMap() + @Column({ type: 'text', default: '' }) + portalLabel: string; +} diff --git a/services/apps/alcs/src/alcs/commissioner/commissioner.dto.ts b/services/apps/alcs/src/alcs/commissioner/commissioner.dto.ts index 815cfd0f75..5ca3594a2c 100644 --- a/services/apps/alcs/src/alcs/commissioner/commissioner.dto.ts +++ b/services/apps/alcs/src/alcs/commissioner/commissioner.dto.ts @@ -1,5 +1,5 @@ import { AutoMap } from '@automapper/classes'; -import { ApplicationLocalGovernmentDto } from '../application/application-code/application-local-government/application-local-government.dto'; +import { LocalGovernmentDto } from '../local-government/local-government.dto'; import { ApplicationRegionDto } from '../code/application-code/application-region/application-region.dto'; import { ApplicationTypeDto } from '../code/application-code/application-type/application-type.dto'; @@ -26,7 +26,7 @@ export class CommissionerApplicationDto { region: ApplicationRegionDto; @AutoMap() - localGovernment: ApplicationLocalGovernmentDto; + localGovernment: LocalGovernmentDto; @AutoMap() decisionDate: number; diff --git a/services/apps/alcs/src/alcs/covenant/covenant.dto.ts b/services/apps/alcs/src/alcs/covenant/covenant.dto.ts index b66b50cf6e..d379c8d120 100644 --- a/services/apps/alcs/src/alcs/covenant/covenant.dto.ts +++ b/services/apps/alcs/src/alcs/covenant/covenant.dto.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { IsNotEmpty, IsString } from 'class-validator'; -import { ApplicationLocalGovernmentDto } from '../application/application-code/application-local-government/application-local-government.dto'; +import { LocalGovernmentDto } from '../local-government/local-government.dto'; import { CardDto } from '../card/card.dto'; import { ApplicationRegionDto } from '../code/application-code/application-region/application-region.dto'; @@ -37,7 +37,7 @@ export class CovenantDto { card: CardDto; @AutoMap() - localGovernment: ApplicationLocalGovernmentDto; + localGovernment: LocalGovernmentDto; @AutoMap() region: ApplicationRegionDto; diff --git a/services/apps/alcs/src/alcs/covenant/covenant.entity.ts b/services/apps/alcs/src/alcs/covenant/covenant.entity.ts index 459305d994..4ac7f0e1c6 100644 --- a/services/apps/alcs/src/alcs/covenant/covenant.entity.ts +++ b/services/apps/alcs/src/alcs/covenant/covenant.entity.ts @@ -8,9 +8,9 @@ import { OneToOne, } from 'typeorm'; import { Base } from '../../common/entities/base.entity'; -import { ApplicationLocalGovernment } from '../application/application-code/application-local-government/application-local-government.entity'; import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; +import { LocalGovernment } from '../local-government/local-government.entity'; @Entity() export class Covenant extends Base { @@ -36,8 +36,8 @@ export class Covenant extends Base { @Type(() => Card) card: Card; - @ManyToOne(() => ApplicationLocalGovernment) - localGovernment: ApplicationLocalGovernment; + @ManyToOne(() => LocalGovernment) + localGovernment: LocalGovernment; @Index() @Column({ diff --git a/services/apps/alcs/src/alcs/import/application-import.service.spec.ts b/services/apps/alcs/src/alcs/import/application-import.service.spec.ts index b91edaf761..7205be8798 100644 --- a/services/apps/alcs/src/alcs/import/application-import.service.spec.ts +++ b/services/apps/alcs/src/alcs/import/application-import.service.spec.ts @@ -5,8 +5,8 @@ import * as timezone from 'dayjs/plugin/timezone'; import * as utc from 'dayjs/plugin/utc'; import { Volume, fs } from 'memfs'; import * as path from 'path'; -import { ApplicationLocalGovernment } from '../application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../local-government/local-government.entity'; +import { LocalGovernmentService } from '../local-government/local-government.service'; import { ApplicationMeeting } from '../application/application-meeting/application-meeting.entity'; import { ApplicationMeetingService } from '../application/application-meeting/application-meeting.service'; import { ApplicationPaused } from '../application/application-paused.entity'; @@ -33,7 +33,7 @@ describe('ImportService', () => { let mockApplicationservice: DeepMocked; let mockApplicationMeetingService: DeepMocked; let mockPausedService: DeepMocked; - let mockLocalGovernmentService: DeepMocked; + let mockLocalGovernmentService: DeepMocked; let mockBoardService: DeepMocked; let mockDataRow; @@ -45,8 +45,7 @@ describe('ImportService', () => { mockApplicationservice = createMock(); mockApplicationMeetingService = createMock(); mockPausedService = createMock(); - mockLocalGovernmentService = - createMock(); + mockLocalGovernmentService = createMock(); mockBoardService = createMock(); const module: TestingModule = await Test.createTestingModule({ @@ -65,7 +64,7 @@ describe('ImportService', () => { useValue: mockPausedService, }, { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockLocalGovernmentService, }, { @@ -92,7 +91,7 @@ describe('ImportService', () => { mockLocalGovernmentService.getByName.mockResolvedValue({ uuid: 'government-uuid', - } as ApplicationLocalGovernment); + } as LocalGovernment); }); it('should be defined', () => { @@ -168,7 +167,7 @@ describe('ImportService', () => { it('should should do a fallback search for local government', async () => { mockLocalGovernmentService.getByName.mockResolvedValueOnce(null); mockLocalGovernmentService.getByName.mockResolvedValueOnce( - {} as ApplicationLocalGovernment, + {} as LocalGovernment, ); await service.parseRow( mockDataRow, diff --git a/services/apps/alcs/src/alcs/import/application-import.service.ts b/services/apps/alcs/src/alcs/import/application-import.service.ts index c81b753cae..93039630a0 100644 --- a/services/apps/alcs/src/alcs/import/application-import.service.ts +++ b/services/apps/alcs/src/alcs/import/application-import.service.ts @@ -5,7 +5,7 @@ import * as timezone from 'dayjs/plugin/timezone'; import * as utc from 'dayjs/plugin/utc'; import * as fs from 'fs'; import * as path from 'path'; -import { ApplicationLocalGovernmentService } from '../application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../local-government/local-government.service'; import { ApplicationMeetingService } from '../application/application-meeting/application-meeting.service'; import { ApplicationPausedService } from '../application/application-paused/application-paused.service'; import { Application } from '../application/application.entity'; @@ -94,7 +94,7 @@ export class ApplicationImportService { private meetingService: ApplicationMeetingService, private pausedService: ApplicationPausedService, private boardService: BoardService, - private localGovernmentService: ApplicationLocalGovernmentService, + private localGovernmentService: LocalGovernmentService, ) {} importCsv() { diff --git a/services/apps/alcs/src/alcs/import/noi-import.service.ts b/services/apps/alcs/src/alcs/import/noi-import.service.ts index 2f11a38207..66732d1add 100644 --- a/services/apps/alcs/src/alcs/import/noi-import.service.ts +++ b/services/apps/alcs/src/alcs/import/noi-import.service.ts @@ -5,7 +5,7 @@ import * as timezone from 'dayjs/plugin/timezone'; import * as utc from 'dayjs/plugin/utc'; import * as fs from 'fs'; import * as path from 'path'; -import { ApplicationLocalGovernmentService } from '../application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../local-government/local-government.service'; import { BoardService } from '../board/board.service'; import { CardService } from '../card/card.service'; import { NoticeOfIntentDecisionService } from '../notice-of-intent-decision/notice-of-intent-decision.service'; @@ -79,7 +79,7 @@ export class NoticeOfIntentImportService { private noticeOfIntentService: NoticeOfIntentService, private meetingService: NoticeOfIntentMeetingService, private boardService: BoardService, - private localGovernmentService: ApplicationLocalGovernmentService, + private localGovernmentService: LocalGovernmentService, private cardService: CardService, private noticeOfIntentDecisionService: NoticeOfIntentDecisionService, ) {} @@ -164,12 +164,12 @@ export class NoticeOfIntentImportService { const noticeOfIntent = await this.noticeOfIntentService.create( { + typeCode: '', fileNumber: mappedRow.fileNumber, applicant: mappedRow.applicant || 'Unknown', - dateSubmittedToAlc: mappedRow.submittedToAlc.getTime(), + dateSubmittedToAlc: mappedRow.submittedToAlc, localGovernmentUuid: localGovernment.uuid, regionCode: regionCode, - boardCode: 'noi', }, vettingBoard, ); diff --git a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.controller.spec.ts b/services/apps/alcs/src/alcs/local-government/local-government.controller.spec.ts similarity index 59% rename from services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.controller.spec.ts rename to services/apps/alcs/src/alcs/local-government/local-government.controller.spec.ts index 1fa45f7008..33d5d787b8 100644 --- a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.controller.spec.ts +++ b/services/apps/alcs/src/alcs/local-government/local-government.controller.spec.ts @@ -3,27 +3,27 @@ import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; -import { mockKeyCloakProviders } from '../../../../../test/mocks/mockTypes'; -import { ApplicationLocalGovernmentController } from './application-local-government.controller'; -import { ApplicationLocalGovernment } from './application-local-government.entity'; -import { ApplicationLocalGovernmentService } from './application-local-government.service'; +import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; +import { LocalGovernmentController } from './local-government.controller'; +import { LocalGovernment } from './local-government.entity'; +import { LocalGovernmentService } from './local-government.service'; -describe('ApplicationLocalGovernmentController', () => { - let controller: ApplicationLocalGovernmentController; - let mockService: DeepMocked; +describe('LocalGovernmentController', () => { + let controller: LocalGovernmentController; + let mockService: DeepMocked; beforeEach(async () => { - mockService = createMock(); + mockService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ AutomapperModule.forRoot({ strategyInitializer: classes(), }), ], - controllers: [ApplicationLocalGovernmentController], + controllers: [LocalGovernmentController], providers: [ { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockService, }, { @@ -34,8 +34,8 @@ describe('ApplicationLocalGovernmentController', () => { ], }).compile(); - controller = module.get( - ApplicationLocalGovernmentController, + controller = module.get( + LocalGovernmentController, ); }); @@ -51,7 +51,7 @@ describe('ApplicationLocalGovernmentController', () => { code: 'code', label: 'label', }, - } as ApplicationLocalGovernment; + } as LocalGovernment; mockService.list.mockResolvedValue([mockGovernment]); const res = await mockService.list(); diff --git a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.controller.ts b/services/apps/alcs/src/alcs/local-government/local-government.controller.ts similarity index 51% rename from services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.controller.ts rename to services/apps/alcs/src/alcs/local-government/local-government.controller.ts index 33f876093f..e5a20081e2 100644 --- a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.controller.ts +++ b/services/apps/alcs/src/alcs/local-government/local-government.controller.ts @@ -1,23 +1,23 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; -import { ANY_AUTH_ROLE } from '../../../../common/authorization/roles'; -import { RolesGuard } from '../../../../common/authorization/roles-guard.service'; -import { UserRoles } from '../../../../common/authorization/roles.decorator'; -import { ApplicationLocalGovernmentDto } from './application-local-government.dto'; -import { ApplicationLocalGovernmentService } from './application-local-government.service'; +import { ANY_AUTH_ROLE } from '../../common/authorization/roles'; +import { RolesGuard } from '../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../common/authorization/roles.decorator'; +import { LocalGovernmentDto } from './local-government.dto'; +import { LocalGovernmentService } from './local-government.service'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('application-local-government') @UseGuards(RolesGuard) -export class ApplicationLocalGovernmentController { +export class LocalGovernmentController { constructor( - private applicationLocalGovernmentService: ApplicationLocalGovernmentService, + private applicationLocalGovernmentService: LocalGovernmentService, ) {} @Get() @UserRoles(...ANY_AUTH_ROLE) - async list(): Promise { + async list(): Promise { const localGovernments = await this.applicationLocalGovernmentService.listActive(); diff --git a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.dto.ts b/services/apps/alcs/src/alcs/local-government/local-government.dto.ts similarity index 66% rename from services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.dto.ts rename to services/apps/alcs/src/alcs/local-government/local-government.dto.ts index f902c2794e..6ea96f59e0 100644 --- a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.dto.ts +++ b/services/apps/alcs/src/alcs/local-government/local-government.dto.ts @@ -1,4 +1,4 @@ -export class ApplicationLocalGovernmentDto { +export class LocalGovernmentDto { uuid: string; name: string; preferredRegionCode: string; diff --git a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.entity.ts b/services/apps/alcs/src/alcs/local-government/local-government.entity.ts similarity index 70% rename from services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.entity.ts rename to services/apps/alcs/src/alcs/local-government/local-government.entity.ts index 59f98dcce6..11f31377f0 100644 --- a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.entity.ts +++ b/services/apps/alcs/src/alcs/local-government/local-government.entity.ts @@ -1,11 +1,11 @@ import { AutoMap } from '@automapper/classes'; import { Column, Entity, ManyToOne } from 'typeorm'; -import { Base } from '../../../../common/entities/base.entity'; -import { ApplicationRegion } from '../../../code/application-code/application-region/application-region.entity'; +import { Base } from '../../common/entities/base.entity'; +import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; @Entity() -export class ApplicationLocalGovernment extends Base { - constructor(data?: Partial) { +export class LocalGovernment extends Base { + constructor(data?: Partial) { super(); if (data) { Object.assign(this, data); diff --git a/services/apps/alcs/src/alcs/local-government/local-government.module.ts b/services/apps/alcs/src/alcs/local-government/local-government.module.ts new file mode 100644 index 0000000000..2ea7c3af0a --- /dev/null +++ b/services/apps/alcs/src/alcs/local-government/local-government.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { LocalGovernment } from './local-government.entity'; +import { LocalGovernmentService } from './local-government.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([LocalGovernment])], + providers: [LocalGovernmentService], + controllers: [], + exports: [LocalGovernmentService], +}) +export class LocalGovernmentModule {} diff --git a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.spec.ts b/services/apps/alcs/src/alcs/local-government/local-government.service.spec.ts similarity index 65% rename from services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.spec.ts rename to services/apps/alcs/src/alcs/local-government/local-government.service.spec.ts index 55b19fb52c..6ea3b6cc2e 100644 --- a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.spec.ts +++ b/services/apps/alcs/src/alcs/local-government/local-government.service.spec.ts @@ -2,13 +2,13 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { ApplicationLocalGovernment } from './application-local-government.entity'; -import { ApplicationLocalGovernmentService } from './application-local-government.service'; +import { LocalGovernment } from './local-government.entity'; +import { LocalGovernmentService } from './local-government.service'; -describe('ApplicationLocalGovernmentService', () => { - let mockRepository: DeepMocked>; +describe('LocalGovernmentService', () => { + let mockRepository: DeepMocked>; - let service: ApplicationLocalGovernmentService; + let service: LocalGovernmentService; const mockLocalGovernments = [ { @@ -18,22 +18,20 @@ describe('ApplicationLocalGovernmentService', () => { ]; beforeEach(async () => { - mockRepository = createMock>(); + mockRepository = createMock>(); mockRepository.find.mockResolvedValue(mockLocalGovernments); const module: TestingModule = await Test.createTestingModule({ providers: [ - ApplicationLocalGovernmentService, + LocalGovernmentService, { - provide: getRepositoryToken(ApplicationLocalGovernment), + provide: getRepositoryToken(LocalGovernment), useValue: mockRepository, }, ], }).compile(); - service = module.get( - ApplicationLocalGovernmentService, - ); + service = module.get(LocalGovernmentService); }); it('should be defined', () => { @@ -50,7 +48,7 @@ describe('ApplicationLocalGovernmentService', () => { it('should call repository on getByUuId', async () => { const uuid = 'fake'; - mockRepository.findOne.mockResolvedValue({} as ApplicationLocalGovernment); + mockRepository.findOne.mockResolvedValue(new LocalGovernment()); await service.getByUuid(uuid); @@ -66,7 +64,7 @@ describe('ApplicationLocalGovernmentService', () => { }); it('should call repository on create', async () => { - mockRepository.save.mockResolvedValue({} as ApplicationLocalGovernment); + mockRepository.save.mockResolvedValue(new LocalGovernment()); await service.create({ name: 'name', @@ -81,10 +79,8 @@ describe('ApplicationLocalGovernmentService', () => { }); it('should call repository on update', async () => { - mockRepository.findOneOrFail.mockResolvedValue( - new ApplicationLocalGovernment(), - ); - mockRepository.save.mockResolvedValue({} as ApplicationLocalGovernment); + mockRepository.findOneOrFail.mockResolvedValue(new LocalGovernment()); + mockRepository.save.mockResolvedValue(new LocalGovernment()); await service.update('', { name: 'name', diff --git a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts b/services/apps/alcs/src/alcs/local-government/local-government.service.ts similarity index 84% rename from services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts rename to services/apps/alcs/src/alcs/local-government/local-government.service.ts index c9f542e873..7c85715163 100644 --- a/services/apps/alcs/src/alcs/application/application-code/application-local-government/application-local-government.service.ts +++ b/services/apps/alcs/src/alcs/local-government/local-government.service.ts @@ -1,20 +1,20 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsWhere, ILike, Repository } from 'typeorm'; -import { HolidayEntity } from '../../../admin/holiday/holiday.entity'; +import { HolidayEntity } from '../admin/holiday/holiday.entity'; import { LocalGovernmentCreateDto, LocalGovernmentUpdateDto, -} from '../../../admin/local-government/local-government.dto'; -import { ApplicationLocalGovernment } from './application-local-government.entity'; +} from '../admin/local-government/local-government.dto'; +import { LocalGovernment } from './local-government.entity'; @Injectable() -export class ApplicationLocalGovernmentService { - private logger: Logger = new Logger(ApplicationLocalGovernmentService.name); +export class LocalGovernmentService { + private logger: Logger = new Logger(LocalGovernmentService.name); constructor( - @InjectRepository(ApplicationLocalGovernment) - private repository: Repository, + @InjectRepository(LocalGovernment) + private repository: Repository, ) {} async list() { @@ -79,7 +79,7 @@ export class ApplicationLocalGovernmentService { } async create(createDto: LocalGovernmentCreateDto) { - const newGovernment = new ApplicationLocalGovernment(); + const newGovernment = new LocalGovernment(); newGovernment.name = createDto.name; newGovernment.bceidBusinessGuid = createDto.bceidBusinessGuid; newGovernment.isFirstNation = createDto.isFirstNation; diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts index f7618f13d8..385ca4c487 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts @@ -8,7 +8,7 @@ import { IsString, } from 'class-validator'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; -import { ApplicationLocalGovernmentDto } from '../../application/application-code/application-local-government/application-local-government.dto'; +import { LocalGovernmentDto } from '../../local-government/local-government.dto'; import { CardDto } from '../../card/card.dto'; import { ApplicationRegionDto } from '../../code/application-code/application-region/application-region.dto'; import { ApplicationTypeDto } from '../../code/application-code/application-type/application-type.dto'; @@ -75,7 +75,7 @@ export class NoticeOfIntentForModificationDto { applicant: string; region: ApplicationRegionDto; retroactive: boolean; - localGovernment: ApplicationLocalGovernmentDto; + localGovernment: LocalGovernmentDto; } export class NoticeOfIntentModificationDto { diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.spec.ts index cf6a8602e5..30483de2fb 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.spec.ts @@ -62,6 +62,7 @@ describe('NoticeOfIntentController', () => { regionCode: 'region-code', boardCode: 'fake', dateSubmittedToAlc: 0, + typeCode: '', }); expect(mockBoardService.getOneOrFail).toHaveBeenCalledTimes(1); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.ts index 4310e21267..ee68803698 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.ts @@ -6,6 +6,7 @@ import { ROLES_ALLOWED_BOARDS, } from '../../common/authorization/roles'; import { UserRoles } from '../../common/authorization/roles.decorator'; +import { formatIncomingDate } from '../../utils/incoming-date.formatter'; import { BoardService } from '../board/board.service'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; import { @@ -52,7 +53,10 @@ export class NoticeOfIntentController { }); const createdNoi = await this.noticeOfIntentService.create( - createDto, + { + ...createDto, + dateSubmittedToAlc: formatIncomingDate(createDto.dateSubmittedToAlc), + }, board, ); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts index acd45e2cdf..55747821fc 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts @@ -6,9 +6,10 @@ import { IsNumber, IsOptional, IsString, + IsUUID, } from 'class-validator'; import { BaseCodeDto } from '../../common/dtos/base.dto'; -import { ApplicationLocalGovernmentDto } from '../application/application-code/application-local-government/application-local-government.dto'; +import { LocalGovernmentDto } from '../local-government/local-government.dto'; import { CardDto } from '../card/card.dto'; import { ApplicationRegionDto } from '../code/application-code/application-region/application-region.dto'; @@ -41,6 +42,10 @@ export class CreateNoticeOfIntentDto { @IsString() @IsNotEmpty() boardCode: string; + + @IsString() + @IsOptional() + typeCode: string; } export class NoticeOfIntentDto { @@ -57,7 +62,7 @@ export class NoticeOfIntentDto { card: CardDto; @AutoMap() - localGovernment: ApplicationLocalGovernmentDto; + localGovernment: LocalGovernmentDto; @AutoMap() region: ApplicationRegionDto; @@ -103,6 +108,10 @@ export class UpdateNoticeOfIntentDto { @IsNumber() dateAcknowledgedComplete?: number; + @IsOptional() + @IsUUID() + localGovernmentUuid?: string; + @IsString() @IsOptional() summary?: string; @@ -115,3 +124,13 @@ export class UpdateNoticeOfIntentDto { @IsOptional() retroactive?: boolean; } + +export class CreateNoticeOfIntentServiceDto { + fileNumber: string; + applicant: string; + typeCode: string; + dateSubmittedToAlc?: Date | null | undefined; + regionCode?: string; + localGovernmentUuid?: string; + source?: 'ALCS' | 'APPLICANT'; +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts index f3cb99e4b9..f36dfc11ac 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts @@ -11,9 +11,11 @@ import { OneToOne, } from 'typeorm'; import { Base } from '../../common/entities/base.entity'; -import { ApplicationLocalGovernment } from '../application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernment } from '../local-government/local-government.entity'; import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; +import { ApplicationType } from '../code/application-code/application-type/application-type.entity'; +import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; @Entity() @@ -40,26 +42,27 @@ export class NoticeOfIntent extends Base { @Type(() => Card) card: Card | null; - @ManyToOne(() => ApplicationLocalGovernment) - localGovernment: ApplicationLocalGovernment; + @ManyToOne(() => LocalGovernment, { nullable: true }) + localGovernment?: LocalGovernment; @Index() @Column({ type: 'uuid', + nullable: true, }) - localGovernmentUuid: string; + localGovernmentUuid?: string; + + @ManyToOne(() => ApplicationRegion, { nullable: true }) + region?: ApplicationRegion; - @ManyToOne(() => ApplicationRegion) - region: ApplicationRegion; + @Column({ nullable: true }) + regionCode?: string; @ManyToMany(() => NoticeOfIntentSubtype) @JoinTable() @AutoMap(() => [NoticeOfIntentSubtype]) subtype: NoticeOfIntentSubtype[]; - @Column() - regionCode: string; - @AutoMap(() => String) @Column({ type: 'text', nullable: true }) summary: string | null; @@ -108,4 +111,12 @@ export class NoticeOfIntent extends Base { nullable: true, }) decisionDate: Date | null; + + @ManyToOne(() => NoticeOfIntentType, { + nullable: false, + }) + type: NoticeOfIntentType; + + @Column() + typeCode: string; } diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts index 5f72b652b5..8876b3f228 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts @@ -6,6 +6,9 @@ import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; import { BoardModule } from '../board/board.module'; import { CardModule } from '../card/card.module'; +import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { CodeModule } from '../code/code.module'; +import { LocalGovernmentModule } from '../local-government/local-government.module'; import { NoticeOfIntentDocumentController } from './notice-of-intent-document/notice-of-intent-document.controller'; import { NoticeOfIntentDocument } from './notice-of-intent-document/notice-of-intent-document.entity'; import { NoticeOfIntentDocumentService } from './notice-of-intent-document/notice-of-intent-document.service'; @@ -27,6 +30,7 @@ import { NoticeOfIntentService } from './notice-of-intent.service'; NoticeOfIntent, NoticeOfIntentMeeting, NoticeOfIntentMeetingType, + NoticeOfIntentType, NoticeOfIntentSubtype, NoticeOfIntentDocument, DocumentCode, @@ -35,6 +39,8 @@ import { NoticeOfIntentService } from './notice-of-intent.service'; CardModule, FileNumberModule, DocumentModule, + CodeModule, + LocalGovernmentModule, ], providers: [ NoticeOfIntentService, @@ -47,6 +53,10 @@ import { NoticeOfIntentService } from './notice-of-intent.service'; NoticeOfIntentMeetingController, NoticeOfIntentDocumentController, ], - exports: [NoticeOfIntentService, NoticeOfIntentMeetingService], + exports: [ + NoticeOfIntentService, + NoticeOfIntentMeetingService, + NoticeOfIntentDocumentService, + ], }) export class NoticeOfIntentModule {} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts index b048168233..eb5d47adc1 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts @@ -9,6 +9,11 @@ import { FileNumberService } from '../../file-number/file-number.service'; import { Board } from '../board/board.entity'; import { Card } from '../card/card.entity'; import { CardService } from '../card/card.service'; +import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; +import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { CodeService } from '../code/code.service'; +import { LocalGovernment } from '../local-government/local-government.entity'; +import { LocalGovernmentService } from '../local-government/local-government.service'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; import { NoticeOfIntent } from './notice-of-intent.entity'; import { NoticeOfIntentService } from './notice-of-intent.service'; @@ -17,14 +22,20 @@ describe('NoticeOfIntentService', () => { let service: NoticeOfIntentService; let mockCardService: DeepMocked; let mockRepository: DeepMocked>; + let mockTypeRepository: DeepMocked>; let mockSubtypeRepository: DeepMocked>; let mockFileNumberService: DeepMocked; + let mockLocalGovernmentService: DeepMocked; + let mockCodeService: DeepMocked; beforeEach(async () => { mockCardService = createMock(); mockRepository = createMock(); mockFileNumberService = createMock(); mockSubtypeRepository = createMock(); + mockTypeRepository = createMock(); + mockLocalGovernmentService = createMock(); + mockCodeService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -39,6 +50,10 @@ describe('NoticeOfIntentService', () => { provide: getRepositoryToken(NoticeOfIntent), useValue: mockRepository, }, + { + provide: getRepositoryToken(NoticeOfIntentType), + useValue: mockTypeRepository, + }, { provide: getRepositoryToken(NoticeOfIntentSubtype), useValue: mockSubtypeRepository, @@ -51,6 +66,14 @@ describe('NoticeOfIntentService', () => { provide: FileNumberService, useValue: mockFileNumberService, }, + { + provide: LocalGovernmentService, + useValue: mockLocalGovernmentService, + }, + { + provide: CodeService, + useValue: mockCodeService, + }, ], }).compile(); @@ -69,6 +92,10 @@ describe('NoticeOfIntentService', () => { mockRepository.save.mockResolvedValue(new NoticeOfIntent()); mockCardService.create.mockResolvedValue(mockCard); mockFileNumberService.checkValidFileNumber.mockResolvedValue(true); + mockCodeService.fetchRegion.mockResolvedValue(new ApplicationRegion()); + mockTypeRepository.findOneOrFail.mockResolvedValue( + new NoticeOfIntentType(), + ); const res = await service.create( { @@ -76,8 +103,8 @@ describe('NoticeOfIntentService', () => { fileNumber: '1512311', localGovernmentUuid: 'fake-uuid', regionCode: 'region-code', - boardCode: 'fake', - dateSubmittedToAlc: 0, + typeCode: '', + dateSubmittedToAlc: new Date(0), }, fakeBoard, ); @@ -87,6 +114,7 @@ describe('NoticeOfIntentService', () => { expect(mockCardService.create).toHaveBeenCalledTimes(1); expect(mockRepository.save).toHaveBeenCalledTimes(1); expect(mockRepository.save.mock.calls[0][0].card).toBe(mockCard); + expect(mockTypeRepository.findOneOrFail).toHaveBeenCalledTimes(1); }); it('should call through to the repo for get by card', async () => { diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts index 2283615faa..862e199ffd 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts @@ -1,4 +1,7 @@ -import { ServiceNotFoundException } from '@app/common/exceptions/base.exception'; +import { + ServiceNotFoundException, + ServiceValidationException, +} from '@app/common/exceptions/base.exception'; import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; @@ -14,13 +17,17 @@ import { import { FileNumberService } from '../../file-number/file-number.service'; import { formatIncomingDate } from '../../utils/incoming-date.formatter'; import { filterUndefined } from '../../utils/undefined'; +import { LocalGovernmentService } from '../local-government/local-government.service'; import { ApplicationTimeData } from '../application/application-time-tracking.service'; import { Board } from '../board/board.entity'; import { CARD_TYPE } from '../card/card-type/card-type.entity'; +import { Card } from '../card/card.entity'; import { CardService } from '../card/card.service'; +import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { CodeService } from '../code/code.service'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; import { - CreateNoticeOfIntentDto, + CreateNoticeOfIntentServiceDto, NoticeOfIntentDto, UpdateNoticeOfIntentDto, } from './notice-of-intent.dto'; @@ -46,31 +53,52 @@ export class NoticeOfIntentService { private cardService: CardService, @InjectRepository(NoticeOfIntent) private repository: Repository, + @InjectRepository(NoticeOfIntentType) + private typeRepository: Repository, @InjectRepository(NoticeOfIntentSubtype) private subtypeRepository: Repository, @InjectMapper() private mapper: Mapper, private fileNumberService: FileNumberService, + private codeService: CodeService, + private localGovernmentService: LocalGovernmentService, ) {} - async create(createDto: CreateNoticeOfIntentDto, board: Board) { + async create( + createDto: CreateNoticeOfIntentServiceDto, + board?: Board, + persist = true, + ) { await this.fileNumberService.checkValidFileNumber(createDto.fileNumber); + const type = await this.typeRepository.findOneOrFail({ + where: { + code: createDto.typeCode, + }, + }); + const noticeOfIntent = new NoticeOfIntent({ localGovernmentUuid: createDto.localGovernmentUuid, fileNumber: createDto.fileNumber, regionCode: createDto.regionCode, applicant: createDto.applicant, - dateSubmittedToAlc: formatIncomingDate(createDto.dateSubmittedToAlc), + dateSubmittedToAlc: createDto.dateSubmittedToAlc, + type, }); - noticeOfIntent.card = await this.cardService.create( - CARD_TYPE.NOI, - board, - false, - ); - const savedNoticeOfIntent = await this.repository.save(noticeOfIntent); + if (board) { + noticeOfIntent.card = await this.cardService.create( + CARD_TYPE.NOI, + board, + false, + ); + } + + if (persist) { + const savedNoticeOfIntent = await this.repository.save(noticeOfIntent); - return this.getOrFailByUuid(savedNoticeOfIntent.uuid); + return this.getOrFailByUuid(savedNoticeOfIntent.uuid); + } + return noticeOfIntent; } async getOrFailByUuid(uuid: string) { @@ -187,6 +215,9 @@ export class NoticeOfIntentService { updateDto.summary, noticeOfIntent.summary, ); + if (updateDto.localGovernmentUuid) { + noticeOfIntent.localGovernmentUuid = updateDto.localGovernmentUuid; + } if (updateDto.subtype) { const subtypes = await this.listSubtypes(); @@ -311,4 +342,49 @@ export class NoticeOfIntentService { }); return noticeOfIntent.fileNumber; } + + async fetchTypes() { + return this.typeRepository.find(); + } + + async submit(createDto: CreateNoticeOfIntentServiceDto) { + const existingNoticeOfIntent = await this.repository.findOne({ + where: { fileNumber: createDto.fileNumber }, + }); + + if (!existingNoticeOfIntent) { + throw new ServiceValidationException( + `Notice of Intent with file number does not exist ${createDto.fileNumber}`, + ); + } + + if (!createDto.localGovernmentUuid) { + throw new ServiceValidationException( + `Local government is not set for notice of intent ${createDto.fileNumber}`, + ); + } + + let region = createDto.regionCode + ? await this.codeService.fetchRegion(createDto.regionCode) + : undefined; + + if (!region) { + const localGov = await this.localGovernmentService.getByUuid( + createDto.localGovernmentUuid, + ); + region = localGov?.preferredRegion; + } + + existingNoticeOfIntent.fileNumber = createDto.fileNumber; + existingNoticeOfIntent.applicant = createDto.applicant; + existingNoticeOfIntent.dateSubmittedToAlc = + createDto.dateSubmittedToAlc || null; + existingNoticeOfIntent.localGovernmentUuid = createDto.localGovernmentUuid; + existingNoticeOfIntent.typeCode = createDto.typeCode; + existingNoticeOfIntent.region = region; + existingNoticeOfIntent.card = new Card(); + + await this.repository.save(existingNoticeOfIntent); + return this.getByFileNumber(createDto.fileNumber); + } } diff --git a/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.spec.ts b/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.spec.ts index 8063db82ee..7f518855ae 100644 --- a/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.spec.ts +++ b/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.spec.ts @@ -4,15 +4,16 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; +import { FileNumberService } from '../../file-number/file-number.service'; import { ApplicationService } from '../application/application.service'; import { ApplicationGrpcController } from './alcs-application.controller'; describe('ApplicationGrpcController', () => { let controller: ApplicationGrpcController; - let mockApplicationService: DeepMocked; + let mockFileNumberService: DeepMocked; beforeEach(async () => { - mockApplicationService = createMock(); + mockFileNumberService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -21,7 +22,7 @@ describe('ApplicationGrpcController', () => { }), ], providers: [ - { provide: ApplicationService, useValue: mockApplicationService }, + { provide: FileNumberService, useValue: mockFileNumberService }, { provide: ClsService, useValue: {}, @@ -43,12 +44,12 @@ describe('ApplicationGrpcController', () => { it('should call through to service on generateNumber', async () => { const fileNumber = 'file-id'; - mockApplicationService.generateNextFileNumber.mockResolvedValue(fileNumber); + mockFileNumberService.generateNextFileNumber.mockResolvedValue(fileNumber); const res = await controller.generateFileNumber({ ApplicationFileNumberGenerateGrpcRequest: {}, }); expect(res).toEqual({ fileNumber }); - expect(mockApplicationService.generateNextFileNumber).toBeCalledTimes(1); + expect(mockFileNumberService.generateNextFileNumber).toBeCalledTimes(1); }); }); diff --git a/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.ts b/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.ts index 60fa1fdac1..9445876960 100644 --- a/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.ts +++ b/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.ts @@ -1,6 +1,6 @@ import { Controller, Logger } from '@nestjs/common'; import { GrpcMethod } from '@nestjs/microservices'; -import { ApplicationService } from '../application/application.service'; +import { FileNumberService } from '../../file-number/file-number.service'; import { ApplicationFileNumberGenerateGrpcResponse } from './alcs-application.message.interface'; import { AlcsApplicationService, @@ -11,7 +11,7 @@ import { export class ApplicationGrpcController implements AlcsApplicationService { private logger = new Logger(ApplicationGrpcController.name); - constructor(private applicationService: ApplicationService) {} + constructor(private fileNumberService: FileNumberService) {} @GrpcMethod(GRPC_APPLICATION_SERVICE_NAME, 'generateFileNumber') async generateFileNumber( @@ -19,7 +19,7 @@ export class ApplicationGrpcController implements AlcsApplicationService { ApplicationFileNumberGenerateGrpcRequest: any, ): Promise { return { - fileNumber: await this.applicationService.generateNextFileNumber(), + fileNumber: await this.fileNumberService.generateNextFileNumber(), }; } } diff --git a/services/apps/alcs/src/alcs/planning-review/planning-review.dto.ts b/services/apps/alcs/src/alcs/planning-review/planning-review.dto.ts index 12811c84c5..43c84b96eb 100644 --- a/services/apps/alcs/src/alcs/planning-review/planning-review.dto.ts +++ b/services/apps/alcs/src/alcs/planning-review/planning-review.dto.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { IsNotEmpty, IsString, MaxLength } from 'class-validator'; -import { ApplicationLocalGovernmentDto } from '../application/application-code/application-local-government/application-local-government.dto'; +import { LocalGovernmentDto } from '../local-government/local-government.dto'; import { CardDto } from '../card/card.dto'; import { ApplicationRegionDto } from '../code/application-code/application-region/application-region.dto'; @@ -35,7 +35,7 @@ export class PlanningReviewDto { card: CardDto; @AutoMap() - localGovernment: ApplicationLocalGovernmentDto; + localGovernment: LocalGovernmentDto; @AutoMap() region: ApplicationRegionDto; diff --git a/services/apps/alcs/src/alcs/planning-review/planning-review.entity.ts b/services/apps/alcs/src/alcs/planning-review/planning-review.entity.ts index 27d8567f4c..908e4f9d3e 100644 --- a/services/apps/alcs/src/alcs/planning-review/planning-review.entity.ts +++ b/services/apps/alcs/src/alcs/planning-review/planning-review.entity.ts @@ -8,9 +8,9 @@ import { OneToOne, } from 'typeorm'; import { Base } from '../../common/entities/base.entity'; -import { ApplicationLocalGovernment } from '../application/application-code/application-local-government/application-local-government.entity'; import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; +import { LocalGovernment } from '../local-government/local-government.entity'; @Entity() export class PlanningReview extends Base { @@ -36,8 +36,8 @@ export class PlanningReview extends Base { @Type(() => Card) card: Card; - @ManyToOne(() => ApplicationLocalGovernment) - localGovernment: ApplicationLocalGovernment; + @ManyToOne(() => LocalGovernment) + localGovernment: LocalGovernment; @Index() @Column({ diff --git a/services/apps/alcs/src/alcs/search/search.module.ts b/services/apps/alcs/src/alcs/search/search.module.ts index 3206a2c546..38c9051ac4 100644 --- a/services/apps/alcs/src/alcs/search/search.module.ts +++ b/services/apps/alcs/src/alcs/search/search.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationProfile } from '../../common/automapper/application.automapper.profile'; -import { ApplicationLocalGovernment } from '../application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernment } from '../local-government/local-government.entity'; import { Application } from '../application/application.entity'; import { Covenant } from '../covenant/covenant.entity'; import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; @@ -16,7 +16,7 @@ import { SearchService } from './search.service'; NoticeOfIntent, PlanningReview, Covenant, - ApplicationLocalGovernment, + LocalGovernment, ]), ], providers: [SearchService, ApplicationProfile], diff --git a/services/apps/alcs/src/common/automapper/application.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application.automapper.profile.ts index bf15e21b83..27c1632a43 100644 --- a/services/apps/alcs/src/common/automapper/application.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application.automapper.profile.ts @@ -4,8 +4,8 @@ import { Injectable } from '@nestjs/common'; import { ApplicationDecisionMeetingDto } from '../../alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.dto'; import { ApplicationDecisionMeeting } from '../../alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.entity'; -import { ApplicationLocalGovernmentDto } from '../../alcs/application/application-code/application-local-government/application-local-government.dto'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernmentDto } from '../../alcs/local-government/local-government.dto'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; import { ApplicationDocumentDto } from '../../alcs/application/application-document/application-document.dto'; import { DocumentCode } from '../../document/document-code.entity'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; @@ -41,11 +41,7 @@ export class ApplicationProfile extends AutomapperProfile { createMap(mapper, ApplicationRegion, ApplicationRegionDto); createMap(mapper, ApplicationMeetingType, ApplicationMeetingTypeDto); - createMap( - mapper, - ApplicationLocalGovernment, - ApplicationLocalGovernmentDto, - ); + createMap(mapper, LocalGovernment, LocalGovernmentDto); createMap( mapper, diff --git a/services/apps/alcs/src/common/automapper/modification.automapper.profile.ts b/services/apps/alcs/src/common/automapper/modification.automapper.profile.ts index d36852e11f..5d9d5e2de5 100644 --- a/services/apps/alcs/src/common/automapper/modification.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/modification.automapper.profile.ts @@ -12,8 +12,8 @@ import { ApplicationModificationOutcomeCodeDto, } from '../../alcs/application-decision/application-modification/application-modification.dto'; import { ApplicationModification } from '../../alcs/application-decision/application-modification/application-modification.entity'; -import { ApplicationLocalGovernmentDto } from '../../alcs/application/application-code/application-local-government/application-local-government.dto'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernmentDto } from '../../alcs/local-government/local-government.dto'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; import { Application } from '../../alcs/application/application.entity'; import { CardDto } from '../../alcs/card/card.dto'; import { Card } from '../../alcs/card/card.entity'; @@ -41,8 +41,8 @@ export class ModificationProfile extends AutomapperProfile { mapFrom((a) => this.mapper.map( a.localGovernment, - ApplicationLocalGovernment, - ApplicationLocalGovernmentDto, + LocalGovernment, + LocalGovernmentDto, ), ), ), diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts index a77b937053..13e4c79d8f 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts @@ -1,10 +1,10 @@ import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; -import { ApplicationLocalGovernmentDto } from '../../alcs/application/application-code/application-local-government/application-local-government.dto'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernmentDto } from '../../alcs/local-government/local-government.dto'; import { CardDto } from '../../alcs/card/card.dto'; import { Card } from '../../alcs/card/card.entity'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; import { NoticeOfIntentDecisionDocument } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; import { NoticeOfIntentDecisionOutcome } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-outcome.entity'; import { @@ -126,8 +126,8 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { mapFrom((a) => this.mapper.map( a.localGovernment, - ApplicationLocalGovernment, - ApplicationLocalGovernmentDto, + LocalGovernment, + LocalGovernmentDto, ), ), ), diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts new file mode 100644 index 0000000000..d7adff6063 --- /dev/null +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts @@ -0,0 +1,55 @@ +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { + NoticeOfIntentSubmissionDetailedDto, + NoticeOfIntentSubmissionDto, +} from '../../portal/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; + +@Injectable() +export class NoticeOfIntentSubmissionProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap( + mapper, + NoticeOfIntentSubmission, + NoticeOfIntentSubmissionDto, + forMember( + (a) => a.createdAt, + mapFrom((ad) => { + return ad.auditCreatedAt.getTime(); + }), + ), + forMember( + (a) => a.updatedAt, + mapFrom((ad) => { + return ad.auditUpdatedAt?.getTime(); + }), + ), + ); + + createMap( + mapper, + NoticeOfIntentSubmission, + NoticeOfIntentSubmissionDetailedDto, + forMember( + (a) => a.createdAt, + mapFrom((ad) => { + return ad.auditCreatedAt.getTime(); + }), + ), + forMember( + (a) => a.updatedAt, + mapFrom((ad) => { + return ad.auditUpdatedAt?.getTime(); + }), + ), + ); + }; + } +} diff --git a/services/apps/alcs/src/file-number/file-number.constants.ts b/services/apps/alcs/src/file-number/file-number.constants.ts new file mode 100644 index 0000000000..c2e13dce75 --- /dev/null +++ b/services/apps/alcs/src/file-number/file-number.constants.ts @@ -0,0 +1 @@ +export const FILE_NUMBER_SEQUENCE = 'alcs.alcs_file_number_seq'; diff --git a/services/apps/alcs/src/file-number/file-number.service.spec.ts b/services/apps/alcs/src/file-number/file-number.service.spec.ts index a932bcf448..b108979436 100644 --- a/services/apps/alcs/src/file-number/file-number.service.spec.ts +++ b/services/apps/alcs/src/file-number/file-number.service.spec.ts @@ -70,4 +70,16 @@ describe('FileNumberService', () => { expect(mockCovenantRepo.exist).toHaveBeenCalledTimes(1); expect(mockNOIRepo.exist).toHaveBeenCalledTimes(1); }); + + it('should generate and return new fileNumber', async () => { + mockAppRepo.findOne + .mockResolvedValueOnce({} as Application) + .mockResolvedValue(null); + mockAppRepo.query.mockResolvedValue([{ nextval: '2512' }]); + + const result = await service.generateNextFileNumber(); + + expect(mockAppRepo.query).toBeCalledTimes(1); + expect(result).toEqual('2512'); + }); }); diff --git a/services/apps/alcs/src/file-number/file-number.service.ts b/services/apps/alcs/src/file-number/file-number.service.ts index 6711681734..03f3b1580e 100644 --- a/services/apps/alcs/src/file-number/file-number.service.ts +++ b/services/apps/alcs/src/file-number/file-number.service.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { Application } from '../alcs/application/application.entity'; import { Covenant } from '../alcs/covenant/covenant.entity'; import { NoticeOfIntent } from '../alcs/notice-of-intent/notice-of-intent.entity'; +import { FILE_NUMBER_SEQUENCE } from './file-number.constants'; @Injectable() export class FileNumberService { @@ -42,4 +43,11 @@ export class FileNumberService { } return true; } + + async generateNextFileNumber() { + const fileNumberArr = await this.applicationRepo.query( + `select nextval('${FILE_NUMBER_SEQUENCE}') limit 1`, + ); + return fileNumberArr[0].nextval; + } } diff --git a/services/apps/alcs/src/main.module.ts b/services/apps/alcs/src/main.module.ts index 045e0d91b7..ab4f586986 100644 --- a/services/apps/alcs/src/main.module.ts +++ b/services/apps/alcs/src/main.module.ts @@ -11,11 +11,14 @@ import { ClsModule } from 'nestjs-cls'; import { LoggerModule } from 'nestjs-pino'; import { CdogsModule } from '../../../libs/common/src/cdogs/cdogs.module'; import { AlcsModule } from './alcs/alcs.module'; +import { LocalGovernmentController } from './alcs/local-government/local-government.controller'; +import { LocalGovernmentModule } from './alcs/local-government/local-government.module'; import { AuthorizationFilter } from './common/authorization/authorization.filter'; import { AuthorizationModule } from './common/authorization/authorization.module'; import { AuditSubscriber } from './common/entities/audit.subscriber'; import { DocumentModule } from './document/document.module'; import { FileNumberModule } from './file-number/file-number.module'; +import { FileNumberService } from './file-number/file-number.service'; import { HealthCheck } from './healthcheck/healthcheck.entity'; import { LogoutController } from './logout/logout.controller'; import { MainController } from './main.controller'; @@ -23,6 +26,7 @@ import { MainService } from './main.service'; import { PortalModule } from './portal/portal.module'; import { TypeormConfigService } from './providers/typeorm/typeorm.service'; import { UserModule } from './user/user.module'; +import { NoticeOfIntentSubmissionModule } from './portal/notice-of-intent-submission/notice-of-intent-submission.module'; @Module({ imports: [ @@ -62,6 +66,7 @@ import { UserModule } from './user/user.module'; PortalModule, UserModule, FileNumberModule, + NoticeOfIntentSubmissionModule, ], controllers: [MainController, LogoutController], providers: [ diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts index 0471d0e0ce..6634bf72f5 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts @@ -3,8 +3,8 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { DocumentCode, DOCUMENT_TYPE, @@ -38,14 +38,14 @@ describe('ApplicationSubmissionReviewController', () => { let controller: ApplicationSubmissionReviewController; let mockAppReviewService: DeepMocked; let mockAppSubmissionService: DeepMocked; - let mockLGService: DeepMocked; + let mockLGService: DeepMocked; let mockAppDocService: DeepMocked; let mockAppValidatorService: DeepMocked; let mockAppService: DeepMocked; let mockEmailService: DeepMocked; let mockApplicationSubmissionStatusService: DeepMocked; - const mockLG = new ApplicationLocalGovernment({ + const mockLG = new LocalGovernment({ isFirstNation: false, isActive: true, bceidBusinessGuid: '', @@ -86,7 +86,7 @@ describe('ApplicationSubmissionReviewController', () => { useValue: mockAppSubmissionService, }, { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockLGService, }, { diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index eaf916546f..9ebca6b312 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -14,8 +14,8 @@ import { UseGuards, } from '@nestjs/common'; import { generateStatusHtml } from '../../../../../templates/emails/submission-status.template'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { ApplicationService } from '../../alcs/application/application.service'; import { ApplicationSubmissionStatusService } from '../../application-submission-status/application-submission-status.service'; @@ -46,7 +46,7 @@ export class ApplicationSubmissionReviewController { private applicationSubmissionService: ApplicationSubmissionService, private applicationSubmissionReviewService: ApplicationSubmissionReviewService, private applicationDocumentService: ApplicationDocumentService, - private localGovernmentService: ApplicationLocalGovernmentService, + private localGovernmentService: LocalGovernmentService, private applicationValidatorService: ApplicationSubmissionValidatorService, private applicationService: ApplicationService, private emailService: EmailService, @@ -236,7 +236,7 @@ export class ApplicationSubmissionReviewController { private async sendStatusEmail( applicationSubmission: ApplicationSubmission, fileNumber: string, - userLocalGovernment: ApplicationLocalGovernment, + userLocalGovernment: LocalGovernment, primaryContact: ApplicationOwner, ) { if (primaryContact.email) { diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.spec.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.spec.ts index e812446209..332d184eb5 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.spec.ts @@ -5,7 +5,7 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; import { DocumentCode, DOCUMENT_TYPE, @@ -27,7 +27,7 @@ describe('ApplicationSubmissionReviewService', () => { let mockAppDocumentService: DeepMocked; let mockAppService: DeepMocked; - const mockLocalGovernment = new ApplicationLocalGovernment({ + const mockLocalGovernment = new LocalGovernment({ isFirstNation: true, isActive: true, }); diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.ts index f0dbb9be1d..93c25f7da2 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.service.ts @@ -7,7 +7,7 @@ import { InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; @@ -243,7 +243,7 @@ export class ApplicationSubmissionReviewService { async mapToDto( review: ApplicationSubmissionReview, - localGovernment: ApplicationLocalGovernment, + localGovernment: LocalGovernment, ): Promise { const mappedReview = await this.mapper.mapAsync( review, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts index bd1b82cae5..1e6e28cb0b 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts @@ -1,8 +1,8 @@ import { ServiceValidationException } from '@app/common/exceptions/base.exception'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { DocumentCode, DOCUMENT_TYPE, @@ -26,7 +26,7 @@ function includesError(errors: Error[], target: Error) { describe('ApplicationSubmissionValidatorService', () => { let service: ApplicationSubmissionValidatorService; - let mockLGService: DeepMocked; + let mockLGService: DeepMocked; let mockAppParcelService: DeepMocked; let mockAppDocumentService: DeepMocked; @@ -39,7 +39,7 @@ describe('ApplicationSubmissionValidatorService', () => { providers: [ ApplicationSubmissionValidatorService, { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockLGService, }, { @@ -407,7 +407,7 @@ describe('ApplicationSubmissionValidatorService', () => { }); it('should accept local government when its valid', async () => { - const mockLg = new ApplicationLocalGovernment({ + const mockLg = new LocalGovernment({ uuid: 'lg-uuid', name: 'lg', bceidBusinessGuid: 'CATS', diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts index 0a83049629..aed07bd264 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts @@ -1,6 +1,6 @@ import { ServiceValidationException } from '@app/common/exceptions/base.exception'; import { Injectable, Logger } from '@nestjs/common'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; @@ -37,7 +37,7 @@ export class ApplicationSubmissionValidatorService { ); constructor( - private localGovernmentService: ApplicationLocalGovernmentService, + private localGovernmentService: LocalGovernmentService, private appParcelService: ApplicationParcelService, private appDocumentService: ApplicationDocumentService, ) {} diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts index cc28740ba7..104ef46165 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts @@ -5,8 +5,8 @@ import { BadRequestException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { Application } from '../../alcs/application/application.entity'; import { ApplicationSubmissionStatusType } from '../../application-submission-status/submission-status-type.entity'; @@ -30,7 +30,7 @@ describe('ApplicationSubmissionController', () => { let controller: ApplicationSubmissionController; let mockAppSubmissionService: DeepMocked; let mockDocumentService: DeepMocked; - let mockLgService: DeepMocked; + let mockLgService: DeepMocked; let mockAppValidationService: DeepMocked; const localGovernmentUuid = 'local-government'; @@ -56,7 +56,7 @@ describe('ApplicationSubmissionController', () => { useValue: mockDocumentService, }, { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockLgService, }, { @@ -100,7 +100,7 @@ describe('ApplicationSubmissionController', () => { mockAppSubmissionService.mapToDTOs.mockResolvedValue([]); mockLgService.list.mockResolvedValue([ - new ApplicationLocalGovernment({ + new LocalGovernment({ uuid: localGovernmentUuid, bceidBusinessGuid, name: 'fake-name', diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts index 71440fe831..b41a28faeb 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts @@ -10,8 +10,8 @@ import { Req, UseGuards, } from '@nestjs/common'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { SUBMISSION_STATUS } from '../../application-submission-status/submission-status.dto'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; import { User } from '../../user/user.entity'; @@ -29,7 +29,7 @@ export class ApplicationSubmissionController { constructor( private applicationSubmissionService: ApplicationSubmissionService, - private localGovernmentService: ApplicationLocalGovernmentService, + private localGovernmentService: LocalGovernmentService, private applicationSubmissionValidatorService: ApplicationSubmissionValidatorService, ) {} @@ -161,7 +161,7 @@ export class ApplicationSubmissionController { uuid, req.user.entity, ); - let localGovernment: ApplicationLocalGovernment | null = null; + let localGovernment: LocalGovernment | null = null; if (user.bceidBusinessGuid) { localGovernment = await this.localGovernmentService.getByGuid( diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts index 15e64ad7c3..2f8049c052 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts @@ -9,6 +9,7 @@ import { ApplicationOwnerProfile } from '../../common/automapper/application-own import { ApplicationParcelProfile } from '../../common/automapper/application-parcel.automapper.profile'; import { ApplicationSubmissionProfile } from '../../common/automapper/application-submission.automapper.profile'; import { DocumentModule } from '../../document/document.module'; +import { FileNumberModule } from '../../file-number/file-number.module'; import { PdfGenerationModule } from '../pdf-generation/pdf-generation.module'; import { ApplicationOwnerType } from './application-owner/application-owner-type/application-owner-type.entity'; import { ApplicationOwnerController } from './application-owner/application-owner.controller'; @@ -41,6 +42,7 @@ import { NaruSubtype } from './naru-subtype/naru-subtype.entity'; forwardRef(() => DocumentModule), forwardRef(() => PdfGenerationModule), ApplicationSubmissionStatusModule, + FileNumberModule, ], providers: [ ApplicationSubmissionService, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts index 0b7a272490..405ec5bfcf 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts @@ -5,8 +5,8 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { Application } from '../../alcs/application/application.entity'; @@ -17,6 +17,7 @@ import { ApplicationSubmissionStatusType } from '../../application-submission-st import { SUBMISSION_STATUS } from '../../application-submission-status/submission-status.dto'; import { ApplicationSubmissionToSubmissionStatus } from '../../application-submission-status/submission-status.entity'; import { ApplicationSubmissionProfile } from '../../common/automapper/application-submission.automapper.profile'; +import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; import { GenerateReviewDocumentService } from '../pdf-generation/generate-review-document.service'; import { GenerateSubmissionDocumentService } from '../pdf-generation/generate-submission-document.service'; @@ -33,11 +34,12 @@ describe('ApplicationSubmissionService', () => { >; let mockNaruSubtypeRepository: DeepMocked>; let mockApplicationService: DeepMocked; - let mockLGService: DeepMocked; + let mockLGService: DeepMocked; let mockAppDocService: DeepMocked; let mockGenerateSubmissionDocumentService: DeepMocked; let mockGenerateReviewDocumentService: DeepMocked; let mockApplicationSubmissionStatusService: DeepMocked; + let mockFileNumberService: DeepMocked; beforeEach(async () => { mockRepository = createMock(); @@ -49,6 +51,7 @@ describe('ApplicationSubmissionService', () => { mockGenerateReviewDocumentService = createMock(); mockNaruSubtypeRepository = createMock(); mockApplicationSubmissionStatusService = createMock(); + mockFileNumberService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -72,7 +75,7 @@ describe('ApplicationSubmissionService', () => { useValue: mockApplicationService, }, { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockLGService, }, { @@ -95,6 +98,10 @@ describe('ApplicationSubmissionService', () => { provide: ApplicationSubmissionStatusService, useValue: mockApplicationSubmissionStatusService, }, + { + provide: FileNumberService, + useValue: mockFileNumberService, + }, ], }).compile(); @@ -155,7 +162,7 @@ describe('ApplicationSubmissionService', () => { new ApplicationSubmissionStatusType(), ); mockRepository.save.mockResolvedValue(new ApplicationSubmission()); - mockApplicationService.generateNextFileNumber.mockResolvedValue(fileId); + mockFileNumberService.generateNextFileNumber.mockResolvedValue(fileId); mockApplicationService.create.mockResolvedValue(new Application()); mockApplicationSubmissionStatusService.setInitialStatuses.mockResolvedValue( {} as any, @@ -203,7 +210,7 @@ describe('ApplicationSubmissionService', () => { const res = await service.getForGovernmentByFileId( '', - new ApplicationLocalGovernment({ + new LocalGovernment({ uuid: '', name: '', isFirstNation: false, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index d270290229..a8235eb325 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -7,8 +7,8 @@ import { InjectMapper } from '@automapper/nestjs'; import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsRelations, Repository } from 'typeorm'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { Application } from '../../alcs/application/application.entity'; @@ -17,6 +17,7 @@ import { ApplicationSubmissionStatusService } from '../../application-submission import { ApplicationSubmissionStatusType } from '../../application-submission-status/submission-status-type.entity'; import { SUBMISSION_STATUS } from '../../application-submission-status/submission-status.dto'; import { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; +import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; import { filterUndefined } from '../../utils/undefined'; import { ApplicationSubmissionReview } from '../application-submission-review/application-submission-review.entity'; @@ -61,7 +62,8 @@ export class ApplicationSubmissionService { @InjectRepository(NaruSubtype) private naruSubtypeRepository: Repository, private applicationService: ApplicationService, - private localGovernmentService: ApplicationLocalGovernmentService, + private fileNumberService: FileNumberService, + private localGovernmentService: LocalGovernmentService, private applicationDocumentService: ApplicationDocumentService, @Inject(forwardRef(() => GenerateSubmissionDocumentService)) private submissionDocumentGenerationService: GenerateSubmissionDocumentService, @@ -105,7 +107,7 @@ export class ApplicationSubmissionService { } async create(type: string, createdBy: User, prescribedBody?: string) { - const fileNumber = await this.applicationService.generateNextFileNumber(); + const fileNumber = await this.fileNumberService.generateNextFileNumber(); await this.applicationService.create( { @@ -303,7 +305,7 @@ export class ApplicationSubmissionService { }); } - async getForGovernment(localGovernment: ApplicationLocalGovernment) { + async getForGovernment(localGovernment: LocalGovernment) { if (!localGovernment.bceidBusinessGuid) { throw new Error("Cannot load by governments that don't have guids"); } @@ -340,10 +342,7 @@ export class ApplicationSubmissionService { ); } - async getForGovernmentByUuid( - uuid: string, - localGovernment: ApplicationLocalGovernment, - ) { + async getForGovernmentByUuid(uuid: string, localGovernment: LocalGovernment) { if (!localGovernment.bceidBusinessGuid) { throw new Error("Cannot load by governments that don't have guids"); } @@ -396,7 +395,7 @@ export class ApplicationSubmissionService { async getForGovernmentByFileId( fileNumber: string, - localGovernment: ApplicationLocalGovernment, + localGovernment: LocalGovernment, ) { if (!localGovernment.bceidBusinessGuid) { throw new Error("Cannot load by governments that don't have guids"); @@ -554,7 +553,7 @@ export class ApplicationSubmissionService { async mapToDTOs( apps: ApplicationSubmission[], user: User, - userGovernment?: ApplicationLocalGovernment, + userGovernment?: LocalGovernment, ) { const types = await this.applicationService.fetchApplicationTypes(); @@ -585,7 +584,7 @@ export class ApplicationSubmissionService { async mapToDetailedDTO( application: ApplicationSubmission, - userGovernment?: ApplicationLocalGovernment, + userGovernment?: LocalGovernment, ) { const types = await this.applicationService.fetchApplicationTypes(); const mappedApp = this.mapper.map( diff --git a/services/apps/alcs/src/portal/code/code.controller.spec.ts b/services/apps/alcs/src/portal/code/code.controller.spec.ts index 671d02300c..b97d56bbab 100644 --- a/services/apps/alcs/src/portal/code/code.controller.spec.ts +++ b/services/apps/alcs/src/portal/code/code.controller.spec.ts @@ -4,8 +4,8 @@ import { DeepMocked, createMock } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { ApplicationService } from '../../alcs/application/application.service'; import { CardType } from '../../alcs/card/card-type/card-type.entity'; @@ -16,7 +16,7 @@ import { CodeController } from './code.controller'; describe('CodeController', () => { let portalController: CodeController; - let mockLgService: DeepMocked; + let mockLgService: DeepMocked; let mockAppService: DeepMocked; let mockCardService: DeepMocked; let mockAppDocService: DeepMocked; @@ -39,7 +39,7 @@ describe('CodeController', () => { providers: [ CodeController, { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockLgService, }, { @@ -69,7 +69,7 @@ describe('CodeController', () => { portalController = app.get(CodeController); mockLgService.listActive.mockResolvedValue([ - new ApplicationLocalGovernment({ + new LocalGovernment({ uuid: 'fake-uuid', name: 'fake-name', isFirstNation: false, @@ -105,7 +105,7 @@ describe('CodeController', () => { it('should set the matches flag correctly when users guid matches government', async () => { const matchingGuid = 'guid'; mockLgService.listActive.mockResolvedValue([ - new ApplicationLocalGovernment({ + new LocalGovernment({ uuid: 'fake-uuid', name: 'fake-name', isFirstNation: false, diff --git a/services/apps/alcs/src/portal/code/code.controller.ts b/services/apps/alcs/src/portal/code/code.controller.ts index f4fe4dcf8f..2766f79064 100644 --- a/services/apps/alcs/src/portal/code/code.controller.ts +++ b/services/apps/alcs/src/portal/code/code.controller.ts @@ -1,8 +1,8 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { Controller, Get, Req, UseGuards } from '@nestjs/common'; -import { ApplicationLocalGovernment } from '../../alcs/application/application-code/application-local-government/application-local-government.entity'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { DocumentCode } from '../../document/document-code.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { ApplicationService } from '../../alcs/application/application.service'; @@ -24,7 +24,7 @@ export interface LocalGovernmentDto { export class CodeController { constructor( @InjectMapper() private mapper: Mapper, - private localGovernmentService: ApplicationLocalGovernmentService, + private localGovernmentService: LocalGovernmentService, private applicationService: ApplicationService, private applicationDocumentService: ApplicationDocumentService, private cardService: CardService, @@ -61,7 +61,7 @@ export class CodeController { } private mapLocalGovernments( - governments: ApplicationLocalGovernment[], + governments: LocalGovernment[], user: User, ): LocalGovernmentDto[] { return governments.map((government) => ({ diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts new file mode 100644 index 0000000000..5cd63a99a3 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts @@ -0,0 +1,284 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; +import { User } from '../../user/user.entity'; +import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; +import { + NoticeOfIntentSubmissionDetailedDto, + NoticeOfIntentSubmissionDto, +} from './notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; + +describe('NoticeOfIntentSubmissionController', () => { + let controller: NoticeOfIntentSubmissionController; + let mockNoiSubmissionService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockLgService: DeepMocked; + + const localGovernmentUuid = 'local-government'; + const applicant = 'fake-applicant'; + const bceidBusinessGuid = 'business-guid'; + + beforeEach(async () => { + mockNoiSubmissionService = createMock(); + mockDocumentService = createMock(); + mockLgService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [NoticeOfIntentSubmissionController], + providers: [ + NoticeOfIntentSubmissionProfile, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoiSubmissionService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockDocumentService, + }, + { + provide: LocalGovernmentService, + useValue: mockLgService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + }).compile(); + + controller = module.get( + NoticeOfIntentSubmissionController, + ); + + mockNoiSubmissionService.update.mockResolvedValue( + new NoticeOfIntentSubmission({ + applicant: applicant, + localGovernmentUuid, + }), + ); + + mockNoiSubmissionService.create.mockResolvedValue('2'); + mockNoiSubmissionService.getIfCreatorByFileNumber.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + mockNoiSubmissionService.verifyAccessByFileId.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + mockNoiSubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + mockNoiSubmissionService.mapToDTOs.mockResolvedValue([]); + mockLgService.list.mockResolvedValue([ + new LocalGovernment({ + uuid: localGovernmentUuid, + bceidBusinessGuid, + name: 'fake-name', + isFirstNation: false, + }), + ]); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call out to service when fetching notice of intents', async () => { + mockNoiSubmissionService.getByUser.mockResolvedValue([]); + + const submissions = await controller.getSubmissions({ + user: { + entity: new User(), + }, + }); + + expect(submissions).toBeDefined(); + expect(mockNoiSubmissionService.getByUser).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when cancelling an notice of intent', async () => { + const mockApplication = new NoticeOfIntentSubmission({}); + + mockNoiSubmissionService.mapToDTOs.mockResolvedValue([ + {} as NoticeOfIntentSubmissionDto, + ]); + mockNoiSubmissionService.verifyAccessByUuid.mockResolvedValue( + mockApplication, + ); + + const noticeOfIntentSubmission = await controller.cancel('file-id', { + user: { + entity: new User(), + }, + }); + + expect(noticeOfIntentSubmission).toBeDefined(); + //expect(mockNoiSubmissionService.cancel).toHaveBeenCalledTimes(1); + expect(mockNoiSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockNoiSubmissionService.verifyAccessByUuid).toHaveBeenCalledWith( + 'file-id', + new User(), + ); + }); + + // it('should throw an exception when trying to cancel a notice of intent that is not in progress', async () => { + // const mockNoi = new NoticeOfIntentSubmission(); + // mockNoiSubmissionService.verifyAccessByUuid.mockResolvedValue({ + // ...mockNoi, + // status: new ApplicationSubmissionStatusType({ + // code: SUBMISSION_STATUS.CANCELLED, + // }), + // } as any); + // + // const promise = controller.cancel('file-id', { + // user: { + // entity: new User(), + // }, + // }); + // + // await expect(promise).rejects.toMatchObject( + // new BadRequestException('Can only cancel in progress Applications'), + // ); + // //expect(mockNoiSubmissionService.cancel).toHaveBeenCalledTimes(0); + // expect(mockNoiSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + // 1, + // ); + // expect(mockNoiSubmissionService.verifyAccessByUuid).toHaveBeenCalledWith( + // 'file-id', + // new User(), + // ); + // }); + + it('should call out to service when fetching a notice of intent', async () => { + mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as NoticeOfIntentSubmissionDetailedDto, + ); + + const noticeOfIntent = controller.getSubmission( + { + user: { + entity: new User(), + }, + }, + '', + ); + + expect(noticeOfIntent).toBeDefined(); + expect(mockNoiSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should fetch notice of intent by bceid if user has same guid as a local government', async () => { + mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as NoticeOfIntentSubmissionDetailedDto, + ); + mockNoiSubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission({ + localGovernmentUuid: '', + }), + ); + + const noiSubmission = controller.getSubmission( + { + user: { + entity: new User({ + bceidBusinessGuid: 'guid', + }), + }, + }, + '', + ); + + expect(noiSubmission).toBeDefined(); + expect(mockNoiSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should call out to service when creating an notice of intent', async () => { + mockNoiSubmissionService.create.mockResolvedValue(''); + mockNoiSubmissionService.mapToDTOs.mockResolvedValue([ + {} as NoticeOfIntentSubmissionDto, + ]); + + const noiSubmission = await controller.create( + { + user: { + entity: new User(), + }, + }, + { + type: '', + }, + ); + + expect(noiSubmission).toBeDefined(); + expect(mockNoiSubmissionService.create).toHaveBeenCalledTimes(1); + }); + + it('should call out to service for update and map', async () => { + mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as NoticeOfIntentSubmissionDetailedDto, + ); + + await controller.update( + 'file-id', + { + localGovernmentUuid, + applicant, + }, + { + user: { + entity: new User(), + }, + }, + ); + + expect(mockNoiSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockNoiSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(1); + }); + + it('should call out to service on submitAlcs', async () => { + const mockFileId = 'file-id'; + mockNoiSubmissionService.submitToAlcs.mockResolvedValue( + new NoticeOfIntent(), + ); + mockNoiSubmissionService.getIfCreatorByUuid.mockResolvedValue( + new NoticeOfIntentSubmission({ + typeCode: 'TURP', + }), + ); + + await controller.submitAsApplicant(mockFileId, { + user: { + entity: new User(), + }, + }); + + expect(mockNoiSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockNoiSubmissionService.submitToAlcs).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts new file mode 100644 index 0000000000..74a25c4db0 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts @@ -0,0 +1,152 @@ +import { + Body, + Controller, + Get, + Logger, + Param, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; +import { User } from '../../user/user.entity'; +import { + NoticeOfIntentSubmissionCreateDto, + NoticeOfIntentSubmissionUpdateDto, +} from './notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; + +@Controller('notice-of-intent-submission') +@UseGuards(PortalAuthGuard) +export class NoticeOfIntentSubmissionController { + private logger: Logger = new Logger(NoticeOfIntentSubmissionController.name); + + constructor( + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + ) {} + + @Get() + async getSubmissions(@Req() req) { + const user = req.user.entity as User; + + if (user.bceidBusinessGuid) { + //TODO: Business accounts? + } + + const applications = await this.noticeOfIntentSubmissionService.getByUser( + user, + ); + return this.noticeOfIntentSubmissionService.mapToDTOs(applications); + } + + @Get('/application/:fileId') + async getSubmissionByFileId(@Req() req, @Param('fileId') fileId: string) { + const user = req.user.entity as User; + + const submission = + await this.noticeOfIntentSubmissionService.verifyAccessByFileId( + fileId, + user, + ); + + return await this.noticeOfIntentSubmissionService.mapToDetailedDTO( + submission, + ); + } + + @Get('/:uuid') + async getSubmission(@Req() req, @Param('uuid') uuid: string) { + const user = req.user.entity as User; + + const submission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid(uuid, user); + + return await this.noticeOfIntentSubmissionService.mapToDetailedDTO( + submission, + ); + } + + @Post() + async create(@Req() req, @Body() body: NoticeOfIntentSubmissionCreateDto) { + const { type } = body; + const user = req.user.entity as User; + const newFileNumber = await this.noticeOfIntentSubmissionService.create( + type, + user, + ); + return { + fileId: newFileNumber, + }; + } + + @Put('/:uuid') + async update( + @Param('uuid') uuid: string, + @Body() updateDto: NoticeOfIntentSubmissionUpdateDto, + @Req() req, + ) { + const submission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + uuid, + req.user.entity, + ); + + const updatedSubmission = await this.noticeOfIntentSubmissionService.update( + submission.uuid, + updateDto, + ); + + return await this.noticeOfIntentSubmissionService.mapToDetailedDTO( + updatedSubmission, + ); + } + + @Post('/:uuid/cancel') + async cancel(@Param('uuid') uuid: string, @Req() req) { + const user = req.user.entity; + + const noticeOfIntentSubmission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + uuid, + req.user.entity, + ); + + //TODO: Hook this up + //await this.noticeOfIntentSubmissionService.cancel(noticeOfIntentSubmission); + + return { + cancelled: true, + }; + } + + @Post('/alcs/submit/:uuid') + async submitAsApplicant(@Param('uuid') uuid: string, @Req() req) { + const noticeOfIntentSubmission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + uuid, + req.user.entity, + ); + + const validationResult = { + noticeOfIntentSubmission, + errors: [], + }; + + if (validationResult) { + const validatedApplicationSubmission = + validationResult.noticeOfIntentSubmission; + await this.noticeOfIntentSubmissionService.submitToAlcs( + validatedApplicationSubmission, + ); + // return await this.noticeOfIntentSubmissionService.updateStatus( + // noticeOfIntentSubmission, + // SUBMISSION_STATUS.SUBMITTED_TO_ALC, + // ); + } else { + //this.logger.debug(validationResult.errors); + //throw new BadRequestException('Invalid Application'); + } + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts new file mode 100644 index 0000000000..f83d517ec9 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -0,0 +1,146 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsBoolean, + IsNotEmpty, + IsOptional, + IsString, + IsUUID, + MaxLength, +} from 'class-validator'; + +export const MAX_DESCRIPTION_FIELD_LENGTH = 4000; + +export class NoticeOfIntentSubmissionDto { + @AutoMap() + fileNumber: string; + + @AutoMap() + uuid: string; + + @AutoMap() + createdAt: number; + + @AutoMap() + updatedAt: number; + + @AutoMap() + applicant: string; + + @AutoMap() + localGovernmentUuid: string; + + @AutoMap() + type: string; + + canEdit: boolean; + canView: boolean; +} + +export class NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmissionDto { + @AutoMap(() => String) + purpose: string | null; + @AutoMap() + parcelsAgricultureDescription: string; + @AutoMap() + parcelsAgricultureImprovementDescription: string; + @AutoMap() + parcelsNonAgricultureUseDescription: string; + @AutoMap() + northLandUseType: string; + @AutoMap() + northLandUseTypeDescription: string; + @AutoMap() + eastLandUseType: string; + @AutoMap() + eastLandUseTypeDescription: string; + @AutoMap() + southLandUseType: string; + @AutoMap() + southLandUseTypeDescription: string; + @AutoMap() + westLandUseType: string; + @AutoMap() + westLandUseTypeDescription: string; + @AutoMap(() => String) + primaryContactOwnerUuid?: string | null; +} + +export class NoticeOfIntentSubmissionCreateDto { + @IsString() + @IsNotEmpty() + type: string; + + @IsString() + @IsOptional() + prescribedBody?: string; +} + +export class NoticeOfIntentSubmissionUpdateDto { + @IsString() + @IsOptional() + applicant?: string; + + @IsString() + @IsOptional() + @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) + purpose?: string; + + @IsUUID() + @IsOptional() + localGovernmentUuid?: string; + + @IsString() + @IsOptional() + typeCode?: string; + + @IsString() + @IsOptional() + @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) + parcelsAgricultureDescription?: string; + + @IsString() + @IsOptional() + @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) + parcelsAgricultureImprovementDescription?: string; + + @IsString() + @IsOptional() + @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) + parcelsNonAgricultureUseDescription?: string; + + @IsString() + @IsOptional() + northLandUseType?: string; + + @IsString() + @IsOptional() + northLandUseTypeDescription?: string; + + @IsString() + @IsOptional() + eastLandUseType?: string; + + @IsString() + @IsOptional() + eastLandUseTypeDescription?: string; + + @IsString() + @IsOptional() + southLandUseType?: string; + + @IsString() + @IsOptional() + southLandUseTypeDescription?: string; + + @IsString() + @IsOptional() + westLandUseType?: string; + + @IsString() + @IsOptional() + westLandUseTypeDescription?: string; + + @IsBoolean() + @IsOptional() + hasOtherParcelsInCommunity?: boolean | null; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts new file mode 100644 index 0000000000..8379f8e287 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts @@ -0,0 +1,186 @@ +import { AutoMap } from '@automapper/classes'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; +import { Base } from '../../common/entities/base.entity'; +import { User } from '../../user/user.entity'; + +@Entity() +export class NoticeOfIntentSubmission extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @PrimaryGeneratedColumn('uuid') + uuid: string; + + @AutoMap({}) + @Column({ + comment: 'File Number of attached application', + }) + fileNumber: string; + + @AutoMap({}) + @Column({ + comment: 'Indicates whether submission is currently draft or not', + default: false, + }) + isDraft: boolean; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: 'The Applicants name on the application', + nullable: true, + }) + applicant?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'uuid', + comment: 'UUID from ALCS System of the Local Government', + nullable: true, + }) + localGovernmentUuid?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: 'The Applicants name on the application', + nullable: true, + }) + purpose?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'Quantify and describe in detail all agriculture that currently takes place on the parcel(s).', + nullable: true, + }) + parcelsAgricultureDescription?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'Quantify and describe in detail all agricultural improvements made to the parcel(s).', + nullable: true, + }) + parcelsAgricultureImprovementDescription?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'Quantify and describe all non-agricultural uses that currently take place on the parcel(s).', + nullable: true, + }) + parcelsNonAgricultureUseDescription?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'The land uses surrounding the parcel(s) under application on the North.', + nullable: true, + }) + northLandUseType?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'Description of the land uses surrounding the parcel(s) under application on the North.', + nullable: true, + }) + northLandUseTypeDescription?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'The land uses surrounding the parcel(s) under application on the East.', + nullable: true, + }) + eastLandUseType?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'Description of the land uses surrounding the parcel(s) under application on the East.', + nullable: true, + }) + eastLandUseTypeDescription?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'The land uses surrounding the parcel(s) under application on the South.', + nullable: true, + }) + southLandUseType?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'Description of the land uses surrounding the parcel(s) under application on the South.', + nullable: true, + }) + southLandUseTypeDescription?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'The land uses surrounding the parcel(s) under application on the West.', + nullable: true, + }) + westLandUseType?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'Description of the land uses surrounding the parcel(s) under application on the West.', + nullable: true, + }) + westLandUseTypeDescription?: string | null; + + @AutoMap() + @ManyToOne(() => User) + createdBy: User; + + @Column({ + type: 'text', + nullable: true, + comment: 'Stores Uuid of Owner Selected as Primary Contact', + }) + primaryContactOwnerUuid?: string | null; + + @AutoMap() + @Column({ + comment: 'Notice of Intent Type Code', + }) + typeCode: string; + + @AutoMap(() => NoticeOfIntent) + @ManyToOne(() => NoticeOfIntent) + @JoinColumn({ + name: 'file_number', + referencedColumnName: 'fileNumber', + }) + noticeOfIntent: NoticeOfIntent; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts new file mode 100644 index 0000000000..ee7ddf80ef --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts @@ -0,0 +1,26 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BoardModule } from '../../alcs/board/board.module'; +import { LocalGovernmentModule } from '../../alcs/local-government/local-government.module'; +import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; +import { AuthorizationModule } from '../../common/authorization/authorization.module'; +import { DocumentModule } from '../../document/document.module'; +import { FileNumberModule } from '../../file-number/file-number.module'; +import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; +import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([NoticeOfIntentSubmission]), + forwardRef(() => NoticeOfIntentModule), + AuthorizationModule, + forwardRef(() => DocumentModule), + forwardRef(() => BoardModule), + LocalGovernmentModule, + FileNumberModule, + ], + controllers: [NoticeOfIntentSubmissionController], + providers: [NoticeOfIntentSubmissionService], +}) +export class NoticeOfIntentSubmissionModule {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts new file mode 100644 index 0000000000..cb9c2ecb2d --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -0,0 +1,244 @@ +import { BaseServiceException } 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 { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NoticeOfIntentType } from '../../alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; +import { FileNumberService } from '../../file-number/file-number.service'; +import { User } from '../../user/user.entity'; +import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; + +describe('NoticeOfIntentSubmissionService', () => { + let service: NoticeOfIntentSubmissionService; + let mockRepository: DeepMocked>; + let mockNoiService: DeepMocked; + let mockLGService: DeepMocked; + let mockNoiDocService: DeepMocked; + let mockFileNumberService: DeepMocked; + + beforeEach(async () => { + mockRepository = createMock(); + mockNoiService = createMock(); + mockLGService = createMock(); + mockNoiDocService = createMock(); + mockFileNumberService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + providers: [ + NoticeOfIntentSubmissionService, + NoticeOfIntentSubmissionProfile, + { + provide: getRepositoryToken(NoticeOfIntentSubmission), + useValue: mockRepository, + }, + { + provide: NoticeOfIntentService, + useValue: mockNoiService, + }, + { + provide: LocalGovernmentService, + useValue: mockLGService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNoiDocService, + }, + { + provide: FileNumberService, + useValue: mockFileNumberService, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentSubmissionService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should return the fetched application', async () => { + const application = new NoticeOfIntentSubmission(); + mockRepository.findOne.mockResolvedValue(application); + + const app = await service.getOrFailByFileNumber(''); + expect(app).toBe(application); + }); + + it('should return the fetched application when fetching with user', async () => { + const application = new NoticeOfIntentSubmission(); + mockRepository.findOne.mockResolvedValue(application); + + const app = await service.getIfCreatorByFileNumber('', new User()); + expect(app).toBe(application); + }); + + it('should throw an exception if the application is not found the fetched application', async () => { + mockRepository.findOne.mockResolvedValue(null); + + const promise = service.getIfCreatorByFileNumber('', new User()); + await expect(promise).rejects.toMatchObject( + new Error(`Failed to load application with File ID `), + ); + }); + + it("should throw an error if application doesn't exist", async () => { + mockRepository.findOne.mockResolvedValue(null); + + const promise = service.getOrFailByFileNumber(''); + await expect(promise).rejects.toMatchObject( + new Error('Failed to find document'), + ); + }); + + it('save a new noi for create', async () => { + const fileId = 'file-id'; + mockRepository.findOne.mockResolvedValue(null); + mockRepository.save.mockResolvedValue(new NoticeOfIntentSubmission()); + mockFileNumberService.generateNextFileNumber.mockResolvedValue(fileId); + mockNoiService.create.mockResolvedValue(new NoticeOfIntent()); + + const fileNumber = await service.create('type', new User()); + + expect(fileNumber).toEqual(fileId); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + expect(mockNoiService.create).toHaveBeenCalledTimes(1); + }); + + it('should call through for get by user', async () => { + const application = new NoticeOfIntentSubmission(); + mockRepository.find.mockResolvedValue([application]); + + const res = await service.getByUser(new User()); + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(res.length).toEqual(1); + expect(res[0]).toBe(application); + }); + + it('should call through for getByFileId', async () => { + const application = new NoticeOfIntentSubmission(); + mockRepository.findOne.mockResolvedValue(application); + + const res = await service.getByFileNumber('', new User()); + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + expect(res).toBe(application); + }); + + it('should use application type service for mapping DTOs', async () => { + const applicant = 'Bruce Wayne'; + const typeCode = 'fake-code'; + + mockNoiService.fetchTypes.mockResolvedValue([ + new NoticeOfIntentType({ + code: typeCode, + portalLabel: 'portalLabel', + htmlDescription: 'htmlDescription', + label: 'label', + }), + ]); + + const noiSubmission = new NoticeOfIntentSubmission({ + applicant, + typeCode: typeCode, + auditCreatedAt: new Date(), + }); + mockRepository.findOne.mockResolvedValue(noiSubmission); + + const res = await service.mapToDTOs([noiSubmission]); + expect(mockNoiService.fetchTypes).toHaveBeenCalledTimes(1); + expect(res[0].type).toEqual('label'); + expect(res[0].applicant).toEqual(applicant); + }); + + it('should fail on submitToAlcs if error', async () => { + const applicant = 'Bruce Wayne'; + const typeCode = 'fake-code'; + const fileNumber = 'fake'; + const localGovernmentUuid = 'fake-uuid'; + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + fileNumber, + applicant, + typeCode, + localGovernmentUuid, + }); + + mockNoiService.submit.mockRejectedValue(new Error()); + + await expect( + service.submitToAlcs(noticeOfIntentSubmission), + ).rejects.toMatchObject( + new BaseServiceException(`Failed to submit application: ${fileNumber}`), + ); + }); + + it('should call out to service on submitToAlcs', async () => { + const applicant = 'Bruce Wayne'; + const typeCode = 'fake-code'; + const fileNumber = 'fake'; + const localGovernmentUuid = 'fake-uuid'; + const mockNoiSubmission = new NoticeOfIntentSubmission({ + fileNumber, + applicant, + typeCode, + localGovernmentUuid, + }); + const mockNoticeOfIntent = new NoticeOfIntent({ + dateSubmittedToAlc: new Date(), + }); + + mockNoiService.submit.mockResolvedValue(mockNoticeOfIntent); + await service.submitToAlcs(mockNoiSubmission); + + expect(mockNoiService.submit).toBeCalledTimes(1); + }); + + it('should update fields if notice of intent exists', async () => { + const applicant = 'Bruce Wayne'; + const typeCode = 'fake-code'; + const fileNumber = 'fake'; + const localGovernmentUuid = 'fake-uuid'; + + const mockNoiSubmission = new NoticeOfIntentSubmission({ + fileNumber, + applicant: 'incognito', + typeCode: 'fake', + localGovernmentUuid: 'uuid', + }); + + mockRepository.findOne.mockResolvedValue(mockNoiSubmission); + mockRepository.save.mockResolvedValue(mockNoiSubmission); + mockNoiService.update.mockResolvedValue(new NoticeOfIntent()); + + const result = await service.update(fileNumber, { + applicant, + typeCode, + localGovernmentUuid, + }); + + expect(mockRepository.save).toBeCalledTimes(1); + expect(mockRepository.findOne).toBeCalledTimes(2); + expect(result).toEqual( + new NoticeOfIntentSubmission({ + fileNumber, + applicant, + typeCode, + localGovernmentUuid, + }), + ); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts new file mode 100644 index 0000000000..8a116f9c00 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -0,0 +1,344 @@ +import { + BaseServiceException, + ServiceNotFoundException, +} from '@app/common/exceptions/base.exception'; +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { FindOptionsRelations, Repository } from 'typeorm'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +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 { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; +import { FileNumberService } from '../../file-number/file-number.service'; +import { User } from '../../user/user.entity'; +import { filterUndefined } from '../../utils/undefined'; +import { + NoticeOfIntentSubmissionDetailedDto, + NoticeOfIntentSubmissionDto, + NoticeOfIntentSubmissionUpdateDto, +} from './notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; + +@Injectable() +export class NoticeOfIntentSubmissionService { + private logger: Logger = new Logger(NoticeOfIntentSubmissionService.name); + + private DEFAULT_RELATIONS: FindOptionsRelations = { + //TODO + }; + + constructor( + @InjectRepository(NoticeOfIntentSubmission) + private noticeOfIntentSubmissionRepository: Repository, + private noticeOfIntentService: NoticeOfIntentService, + private localGovernmentService: LocalGovernmentService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private fileNumberService: FileNumberService, + @InjectMapper() private mapper: Mapper, + ) {} + + async getOrFailByFileNumber(fileNumber: string) { + const noticeOfIntent = + await this.noticeOfIntentSubmissionRepository.findOne({ + where: { + fileNumber, + isDraft: false, + }, + relations: this.DEFAULT_RELATIONS, + }); + if (!noticeOfIntent) { + throw new Error('Failed to find document'); + } + return noticeOfIntent; + } + + async getOrFailByUuid( + uuid: string, + relations: FindOptionsRelations = {}, + ) { + const noticeOfIntent = + await this.noticeOfIntentSubmissionRepository.findOne({ + where: { + uuid, + }, + relations, + }); + if (!noticeOfIntent) { + throw new Error('Failed to find document'); + } + return noticeOfIntent; + } + + async create(type: string, createdBy: User) { + const fileNumber = await this.fileNumberService.generateNextFileNumber(); + + await this.noticeOfIntentService.create({ + fileNumber, + applicant: 'Unknown', + typeCode: type, + }); + + //TODO: Set Initial Status here + + const noiSubmission = new NoticeOfIntentSubmission({ + fileNumber, + typeCode: type, + createdBy, + }); + + await this.noticeOfIntentSubmissionRepository.save(noiSubmission); + + return fileNumber; + } + + async update( + submissionUuid: string, + updateDto: NoticeOfIntentSubmissionUpdateDto, + ) { + const noticeOfIntentSubmission = await this.getOrFailByUuid(submissionUuid); + + noticeOfIntentSubmission.applicant = updateDto.applicant; + noticeOfIntentSubmission.purpose = filterUndefined( + updateDto.purpose, + noticeOfIntentSubmission.purpose, + ); + noticeOfIntentSubmission.typeCode = + updateDto.typeCode || noticeOfIntentSubmission.typeCode; + noticeOfIntentSubmission.localGovernmentUuid = + updateDto.localGovernmentUuid; + + await this.noticeOfIntentSubmissionRepository.save( + noticeOfIntentSubmission, + ); + + if (!noticeOfIntentSubmission.isDraft && updateDto.localGovernmentUuid) { + await this.noticeOfIntentService.update( + noticeOfIntentSubmission.fileNumber, + { + localGovernmentUuid: updateDto.localGovernmentUuid, + }, + ); + } + + return this.getOrFailByUuid(submissionUuid, this.DEFAULT_RELATIONS); + } + + // + // async submitToAlcs( + // application: ValidatedApplicationSubmission, + // user: User, + // applicationReview?: ApplicationSubmissionReview, + // ) { + // let submittedApp: Application | null = null; + // + // const shouldCreateCard = applicationReview?.isAuthorized ?? true; + // try { + // submittedApp = await this.noticeOfIntentService.submit( + // { + // fileNumber: application.fileNumber, + // applicant: application.applicant, + // localGovernmentUuid: application.localGovernmentUuid, + // typeCode: application.typeCode, + // dateSubmittedToAlc: new Date(), + // }, + // shouldCreateCard, + // ); + // + // await this.updateStatus( + // application, + // SUBMISSION_STATUS.SUBMITTED_TO_ALC, + // submittedApp.dateSubmittedToAlc, + // ); + // + // this.generateAndAttachPdfs( + // application.fileNumber, + // user, + // !!applicationReview, + // ); + // } catch (ex) { + // this.logger.error(ex); + // throw new BaseServiceException( + // `Failed to submit application: ${application.fileNumber}`, + // ); + // } + // + // return submittedApp; + // } + // + // private async generateAndAttachPdfs( + // fileNumber: string, + // user: User, + // hasReview: boolean, + // ) { + // try { + // await this.submissionDocumentGenerationService.generateAndAttach( + // fileNumber, + // user, + // ); + // + // if (hasReview) { + // await this.generateReviewDocumentService.generateAndAttach( + // fileNumber, + // user, + // ); + // } + // } catch (e) { + // this.logger.error(`Error generating the document on submission${e}`); + // } + // } + + getByUser(user: User) { + return this.noticeOfIntentSubmissionRepository.find({ + where: { + createdBy: { + uuid: user.uuid, + }, + isDraft: false, + }, + order: { + auditUpdatedAt: 'DESC', + }, + }); + } + + async getByFileNumber(fileNumber: string, user: User) { + return await this.noticeOfIntentSubmissionRepository.findOne({ + where: { + fileNumber, + createdBy: { + uuid: user.uuid, + }, + isDraft: false, + }, + relations: this.DEFAULT_RELATIONS, + }); + } + + async getByUuid(uuid: string) { + return await this.noticeOfIntentSubmissionRepository.findOne({ + where: { + uuid, + }, + relations: { + ...this.DEFAULT_RELATIONS, + createdBy: true, + }, + }); + } + + async getIfCreatorByFileNumber(fileNumber: string, user: User) { + const applicationSubmission = await this.getByFileNumber(fileNumber, user); + if (!applicationSubmission) { + throw new ServiceNotFoundException( + `Failed to load application with File ID ${fileNumber}`, + ); + } + return applicationSubmission; + } + + async getIfCreatorByUuid(uuid: string, user: User) { + const applicationSubmission = await this.getByUuid(uuid); + if ( + !applicationSubmission || + applicationSubmission.createdBy.uuid !== user.uuid + ) { + throw new ServiceNotFoundException( + `Failed to load application with ID ${uuid}`, + ); + } + return applicationSubmission; + } + + async verifyAccessByFileId(fileId: string, user: User) { + const overlappingRoles = ROLES_ALLOWED_APPLICATIONS.filter((value) => + user.clientRoles!.includes(value), + ); + if (overlappingRoles.length > 0) { + return await this.noticeOfIntentSubmissionRepository.findOneOrFail({ + where: { + fileNumber: fileId, + isDraft: false, + }, + relations: this.DEFAULT_RELATIONS, + }); + } + + return await this.getIfCreatorByFileNumber(fileId, user); + } + + async verifyAccessByUuid(submissionUuid: string, user: User) { + const overlappingRoles = ROLES_ALLOWED_APPLICATIONS.filter((value) => + user.clientRoles!.includes(value), + ); + if (overlappingRoles.length > 0) { + return await this.noticeOfIntentSubmissionRepository.findOneOrFail({ + where: { + uuid: submissionUuid, + }, + relations: { + ...this.DEFAULT_RELATIONS, + }, + }); + } + + return await this.getIfCreatorByUuid(submissionUuid, user); + } + + async mapToDTOs(apps: NoticeOfIntentSubmission[]) { + const types = await this.noticeOfIntentService.fetchTypes(); + + return apps.map((app) => { + return { + ...this.mapper.map( + app, + NoticeOfIntentSubmission, + NoticeOfIntentSubmissionDto, + ), + type: types.find((type) => type.code === app.typeCode)!.label, + canEdit: true, //TODO + canView: true, //TODO + }; + }); + } + + async mapToDetailedDTO(application: NoticeOfIntentSubmission) { + const types = await this.noticeOfIntentService.fetchTypes(); + const mappedApp = this.mapper.map( + application, + NoticeOfIntentSubmission, + NoticeOfIntentSubmissionDetailedDto, + ); + return { + ...mappedApp, + type: types.find((type) => type.code === application.typeCode)!.label, + canEdit: true, + canView: true, + }; + } + + async submitToAlcs(noticeOfIntent: NoticeOfIntentSubmission) { + try { + const submittedNoi = await this.noticeOfIntentService.submit({ + fileNumber: noticeOfIntent.fileNumber, + applicant: noticeOfIntent.applicant!, //TODO: Remove ! once validation is implemented + localGovernmentUuid: noticeOfIntent.localGovernmentUuid!, + typeCode: noticeOfIntent.typeCode, + dateSubmittedToAlc: new Date(), + }); + + // await this.updateStatus( + // application, + // SUBMISSION_STATUS.SUBMITTED_TO_ALC, + // submittedApp.dateSubmittedToAlc, + // ); + return submittedNoi; + } catch (ex) { + this.logger.error(ex); + throw new BaseServiceException( + `Failed to submit application: ${noticeOfIntent.fileNumber}`, + ); + } + } +} diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-review-document.service.spec.ts b/services/apps/alcs/src/portal/pdf-generation/generate-review-document.service.spec.ts index de279896eb..39f548ea6e 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-review-document.service.spec.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-review-document.service.spec.ts @@ -3,7 +3,7 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { Application } from '../../alcs/application/application.entity'; import { ApplicationService } from '../../alcs/application/application.service'; @@ -21,7 +21,7 @@ describe('GenerateReviewDocumentService', () => { let service: GenerateReviewDocumentService; let mockCdogsService: DeepMocked; let mockApplicationSubmissionService: DeepMocked; - let mockApplicationLocalGovernmentService: DeepMocked; + let mockApplicationLocalGovernmentService: DeepMocked; let mockApplicationService: DeepMocked; let mockApplicationDocumentService: DeepMocked; let mockSubmissionReviewService: DeepMocked; @@ -49,7 +49,7 @@ describe('GenerateReviewDocumentService', () => { useValue: mockApplicationSubmissionService, }, { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockApplicationLocalGovernmentService, }, { provide: ApplicationService, useValue: mockApplicationService }, diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-review-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-review-document.service.ts index 991c900118..ab9b437617 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-review-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-review-document.service.ts @@ -6,7 +6,7 @@ import * as config from 'config'; import * as dayjs from 'dayjs'; import * as timezone from 'dayjs/plugin/timezone'; import * as utc from 'dayjs/plugin/utc'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocument, @@ -41,7 +41,7 @@ export class GenerateReviewDocumentService { @Inject(forwardRef(() => ApplicationSubmissionReviewService)) private applicationSubmissionReviewService: ApplicationSubmissionReviewService, private applicationService: ApplicationService, - private localGovernmentService: ApplicationLocalGovernmentService, + private localGovernmentService: LocalGovernmentService, @InjectMapper() private mapper: Mapper, ) {} diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts index 69cf57d39a..099a47ba7b 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.spec.ts @@ -6,7 +6,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import * as dayjs from 'dayjs'; import * as timezone from 'dayjs/plugin/timezone'; import * as utc from 'dayjs/plugin/utc'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; @@ -30,7 +30,7 @@ describe('GenerateSubmissionDocumentService', () => { let service: GenerateSubmissionDocumentService; let mockCdogsService: DeepMocked; let mockApplicationSubmissionService: DeepMocked; - let mockApplicationLocalGovernmentService: DeepMocked; + let mockApplicationLocalGovernmentService: DeepMocked; let mockApplicationService: DeepMocked; let mockApplicationParcelService: DeepMocked; let mockApplicationOwnerService: DeepMocked; @@ -56,7 +56,7 @@ describe('GenerateSubmissionDocumentService', () => { useValue: mockApplicationSubmissionService, }, { - provide: ApplicationLocalGovernmentService, + provide: LocalGovernmentService, useValue: mockApplicationLocalGovernmentService, }, { provide: ApplicationService, useValue: mockApplicationService }, diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts index 43ccf334f5..68992e01a4 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts @@ -9,7 +9,7 @@ import { } from '@nestjs/common'; import * as config from 'config'; import * as dayjs from 'dayjs'; -import { ApplicationLocalGovernmentService } from '../../alcs/application/application-code/application-local-government/application-local-government.service'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocument, @@ -49,7 +49,7 @@ export class GenerateSubmissionDocumentService { private documentGenerationService: CdogsService, @Inject(forwardRef(() => ApplicationSubmissionService)) private applicationSubmissionService: ApplicationSubmissionService, - private localGovernmentService: ApplicationLocalGovernmentService, + private localGovernmentService: LocalGovernmentService, private applicationService: ApplicationService, @Inject(forwardRef(() => ApplicationParcelService)) private parcelService: ApplicationParcelService, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691185932823-add_noi_submissions.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691185932823-add_noi_submissions.ts new file mode 100644 index 0000000000..aacbc8933a --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691185932823-add_noi_submissions.ts @@ -0,0 +1,179 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNoiSubmissions1691185932823 implements MigrationInterface { + name = 'addNoiSubmissions1691185932823'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application" DROP CONSTRAINT "FK_58853fcb8957e8b2c131cc12da1"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."covenant" DROP CONSTRAINT "FK_0fa742d800bc0e0b3f2451e0f0b"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP CONSTRAINT "FK_7e78db4d1c5afb16374253b42d4"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."planning_review" DROP CONSTRAINT "FK_5a57c8d407eb6132ed39cb8fe6f"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_local_government" RENAME TO "local_government"`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_type" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "label" character varying NOT NULL, "code" text NOT NULL, "description" text NOT NULL, "short_label" character varying NOT NULL, "html_description" text NOT NULL DEFAULT '', "portal_label" text NOT NULL DEFAULT '', CONSTRAINT "UQ_2a02e4d5838272a54b4a9f7c9c8" UNIQUE ("description"), CONSTRAINT "PK_7b4b618e6aaf6c0205d36a5e2ca" PRIMARY KEY ("code"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_submission" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "file_number" character varying NOT NULL, "is_draft" boolean NOT NULL DEFAULT false, "applicant" character varying, "local_government_uuid" uuid, "purpose" character varying, "parcels_agriculture_description" text, "parcels_agriculture_improvement_description" text, "parcels_non_agriculture_use_description" text, "north_land_use_type" text, "north_land_use_type_description" text, "east_land_use_type" text, "east_land_use_type_description" text, "south_land_use_type" text, "south_land_use_type_description" text, "west_land_use_type" text, "west_land_use_type_description" text, "primary_contact_owner_uuid" text, "type_code" character varying NOT NULL, "created_by_uuid" uuid, CONSTRAINT "PK_373b13feac5362050544ee1dd62" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."file_number" IS 'File Number of attached application'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."is_draft" IS 'Indicates whether submission is currently draft or not'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."applicant" IS 'The Applicants name on the application'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."local_government_uuid" IS 'UUID from ALCS System of the Local Government'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."purpose" IS 'The Applicants name on the application'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."parcels_agriculture_description" IS 'Quantify and describe in detail all agriculture that currently takes place on the parcel(s).'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."parcels_agriculture_improvement_description" IS 'Quantify and describe in detail all agricultural improvements made to the parcel(s).'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."parcels_non_agriculture_use_description" IS 'Quantify and describe all non-agricultural uses that currently take place on the parcel(s).'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."north_land_use_type" IS 'The land uses surrounding the parcel(s) under application on the North.'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."north_land_use_type_description" IS 'Description of the land uses surrounding the parcel(s) under application on the North.'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."east_land_use_type" IS 'The land uses surrounding the parcel(s) under application on the East.'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."east_land_use_type_description" IS 'Description of the land uses surrounding the parcel(s) under application on the East.'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."south_land_use_type" IS 'The land uses surrounding the parcel(s) under application on the South.'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."south_land_use_type_description" IS 'Description of the land uses surrounding the parcel(s) under application on the South.'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."west_land_use_type" IS 'The land uses surrounding the parcel(s) under application on the West.'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."west_land_use_type_description" IS 'Description of the land uses surrounding the parcel(s) under application on the West.'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."primary_contact_owner_uuid" IS 'Stores Uuid of Owner Selected as Primary Contact'; COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."type_code" IS 'Notice of Intent Type Code'`, + ); + + //Add a type + await queryRunner.query(` + INSERT INTO "alcs"."notice_of_intent_type" ("audit_deleted_date_at", "audit_created_at", "audit_updated_at", "audit_created_by", "audit_updated_by", "label", "code", "description", "short_label", "html_description", "portal_label") VALUES + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Placement of Fill', 'POFO', 'Placement of Fill Only', 'SOIL', 'Choose this option if you are only proposing to place fill on ALR land under + Section 20.3(1)(c) of the Agricultural Land Commission Act. +

+ Fill includes but is not limited to: +
    +
  • Aggregate
  • +
  • Topsoil
  • +
  • Structural Fill
  • +
  • Sand
  • +
  • Gravel
  • +
+ ', 'Placement of Fill within the ALR'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Placement of Fill/Removal of Soil', 'PFRS', 'Placement of Fill and Removal of Soil', 'SOIL', 'Choose this option if you are proposing to remove soil and place fill on ALR land under + Section 20.3(1)(c) of the Agricultural Land Commission Act. +

+ Soil includes but is not limited to: +
    +
  • Aggregate Extraction
  • +
  • Placer Mining
  • +
  • Peat Extraction
  • +
  • Soil Removal
  • +
+
+ Fill includes but is not limited to: +
    +
  • Aggregate
  • +
  • Topsoil
  • +
  • Structural Fill
  • +
  • Sand
  • +
  • Gravel
  • +
+ ', 'Removal of Soil (Extraction) and Placement of Fill within the ALR'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Removal of Soil', 'ROSO', 'Removal of Soil Only', 'SOIL', 'Choose this option if you are only proposing to remove soil from ALR land under + Section 20.3(1)(c) of the Agricultural Land Commission Act. +

+ Soil includes but is not limited to: +
    +
  • Aggregate Extraction
  • +
  • Placer Mining
  • +
  • Peat Extraction
  • +
  • Soil Removal
  • +
+ ', 'Removal of Soil (Extraction) within the ALR'); + `); + + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD "type_code" text NOT NULL DEFAULT 'POFO'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ALTER COLUMN "type_code" DROP DEFAULT`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP CONSTRAINT "FK_d3247037b5d69365c94a6e5ddc9"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ALTER COLUMN "local_government_uuid" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ALTER COLUMN "region_code" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."local_government" ADD CONSTRAINT "FK_b1d9c7304c6cde02bc651fbd954" FOREIGN KEY ("preferred_region_code") REFERENCES "alcs"."application_region"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application" ADD CONSTRAINT "FK_58853fcb8957e8b2c131cc12da1" FOREIGN KEY ("local_government_uuid") REFERENCES "alcs"."local_government"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."covenant" ADD CONSTRAINT "FK_0fa742d800bc0e0b3f2451e0f0b" FOREIGN KEY ("local_government_uuid") REFERENCES "alcs"."local_government"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD CONSTRAINT "FK_7e78db4d1c5afb16374253b42d4" FOREIGN KEY ("local_government_uuid") REFERENCES "alcs"."local_government"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD CONSTRAINT "FK_d3247037b5d69365c94a6e5ddc9" FOREIGN KEY ("region_code") REFERENCES "alcs"."application_region"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD CONSTRAINT "FK_7b4b618e6aaf6c0205d36a5e2ca" FOREIGN KEY ("type_code") REFERENCES "alcs"."notice_of_intent_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."planning_review" ADD CONSTRAINT "FK_5a57c8d407eb6132ed39cb8fe6f" FOREIGN KEY ("local_government_uuid") REFERENCES "alcs"."local_government"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD CONSTRAINT "FK_ad65e20b0787e13906e0f36f1cf" FOREIGN KEY ("created_by_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD CONSTRAINT "FK_07e0aa0c43cb5a2bfc2c00282d4" FOREIGN KEY ("file_number") REFERENCES "alcs"."notice_of_intent"("file_number") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP CONSTRAINT "FK_07e0aa0c43cb5a2bfc2c00282d4"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP CONSTRAINT "FK_ad65e20b0787e13906e0f36f1cf"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."planning_review" DROP CONSTRAINT "FK_5a57c8d407eb6132ed39cb8fe6f"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP CONSTRAINT "FK_7b4b618e6aaf6c0205d36a5e2ca"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP CONSTRAINT "FK_d3247037b5d69365c94a6e5ddc9"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP CONSTRAINT "FK_7e78db4d1c5afb16374253b42d4"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."covenant" DROP CONSTRAINT "FK_0fa742d800bc0e0b3f2451e0f0b"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application" DROP CONSTRAINT "FK_58853fcb8957e8b2c131cc12da1"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."local_government" DROP CONSTRAINT "FK_b1d9c7304c6cde02bc651fbd954"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ALTER COLUMN "region_code" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ALTER COLUMN "local_government_uuid" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD CONSTRAINT "FK_d3247037b5d69365c94a6e5ddc9" FOREIGN KEY ("region_code") REFERENCES "alcs"."application_region"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP COLUMN "type_code"`, + ); + await queryRunner.query(`DROP TABLE "alcs"."notice_of_intent_submission"`); + await queryRunner.query(`DROP TABLE "alcs"."notice_of_intent_type"`); + await queryRunner.query( + `ALTER TABLE "alcs"."local_government" RENAME TO "application_local_government"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."planning_review" ADD CONSTRAINT "FK_5a57c8d407eb6132ed39cb8fe6f" FOREIGN KEY ("local_government_uuid") REFERENCES "alcs"."application_local_government"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD CONSTRAINT "FK_7e78db4d1c5afb16374253b42d4" FOREIGN KEY ("local_government_uuid") REFERENCES "alcs"."application_local_government"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."covenant" ADD CONSTRAINT "FK_0fa742d800bc0e0b3f2451e0f0b" FOREIGN KEY ("local_government_uuid") REFERENCES "alcs"."application_local_government"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application" ADD CONSTRAINT "FK_58853fcb8957e8b2c131cc12da1" FOREIGN KEY ("local_government_uuid") REFERENCES "alcs"."application_local_government"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/services/apps/alcs/src/user/user.controller.spec.ts b/services/apps/alcs/src/user/user.controller.spec.ts index 25e4bffa35..5fdc849c28 100644 --- a/services/apps/alcs/src/user/user.controller.spec.ts +++ b/services/apps/alcs/src/user/user.controller.spec.ts @@ -3,7 +3,7 @@ import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; -import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernment } from '../alcs/local-government/local-government.entity'; import { UserProfile } from '../common/automapper/user.automapper.profile'; import { initMockUserDto, @@ -147,7 +147,7 @@ describe('UserController', () => { const governmentName = 'Government'; mockUserService.getUserLocalGovernment.mockResolvedValue( - new ApplicationLocalGovernment({ + new LocalGovernment({ name: governmentName, }), ); diff --git a/services/apps/alcs/src/user/user.module.ts b/services/apps/alcs/src/user/user.module.ts index 9269c64bdb..247e156a85 100644 --- a/services/apps/alcs/src/user/user.module.ts +++ b/services/apps/alcs/src/user/user.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernment } from '../alcs/local-government/local-government.entity'; import { UserProfile } from '../common/automapper/user.automapper.profile'; import { EmailModule } from '../providers/email/email.module'; import { UserController } from './user.controller'; @@ -8,10 +8,7 @@ import { User } from './user.entity'; import { UserService } from './user.service'; @Module({ - imports: [ - TypeOrmModule.forFeature([ApplicationLocalGovernment, User]), - EmailModule, - ], + imports: [TypeOrmModule.forFeature([LocalGovernment, User]), EmailModule], providers: [UserService, UserProfile], exports: [UserService, EmailModule], controllers: [UserController], diff --git a/services/apps/alcs/src/user/user.service.spec.ts b/services/apps/alcs/src/user/user.service.spec.ts index fe94069eb8..7503389e71 100644 --- a/services/apps/alcs/src/user/user.service.spec.ts +++ b/services/apps/alcs/src/user/user.service.spec.ts @@ -8,7 +8,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import * as config from 'config'; import { Repository } from 'typeorm'; import { initUserMockEntity } from '../../test/mocks/mockEntities'; -import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernment } from '../alcs/local-government/local-government.entity'; import { UserProfile } from '../common/automapper/user.automapper.profile'; import { EmailService } from '../providers/email/email.service'; import { User } from './user.entity'; @@ -17,9 +17,7 @@ import { UserService } from './user.service'; describe('UserService', () => { let service: UserService; let mockUserRepository: DeepMocked>; - let mockGovernmentRepository: DeepMocked< - Repository - >; + let mockGovernmentRepository: DeepMocked>; let emailServiceMock: DeepMocked; const email = 'bruce.wayne@gotham.com'; @@ -39,7 +37,7 @@ describe('UserService', () => { useValue: mockUserRepository, }, { - provide: getRepositoryToken(ApplicationLocalGovernment), + provide: getRepositoryToken(LocalGovernment), useValue: mockGovernmentRepository, }, { provide: EmailService, useValue: emailServiceMock }, @@ -152,9 +150,7 @@ describe('UserService', () => { }); it('should not call repository if user does not have a bc business guid', async () => { - mockGovernmentRepository.findOne.mockResolvedValue( - new ApplicationLocalGovernment(), - ); + mockGovernmentRepository.findOne.mockResolvedValue(new LocalGovernment()); const res = await service.getUserLocalGovernment(new User()); @@ -163,9 +159,7 @@ describe('UserService', () => { }); it('should call repository if user has a bc business guid', async () => { - mockGovernmentRepository.findOne.mockResolvedValue( - new ApplicationLocalGovernment(), - ); + mockGovernmentRepository.findOne.mockResolvedValue(new LocalGovernment()); const res = await service.getUserLocalGovernment( new User({ diff --git a/services/apps/alcs/src/user/user.service.ts b/services/apps/alcs/src/user/user.service.ts index be498d7f86..2679f3c729 100644 --- a/services/apps/alcs/src/user/user.service.ts +++ b/services/apps/alcs/src/user/user.service.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { IConfig } from 'config'; import { Repository } from 'typeorm'; -import { ApplicationLocalGovernment } from '../alcs/application/application-code/application-local-government/application-local-government.entity'; +import { LocalGovernment } from '../alcs/local-government/local-government.entity'; import { EmailService } from '../providers/email/email.service'; import { CreateUserDto } from './user.dto'; import { User } from './user.entity'; @@ -23,8 +23,8 @@ export class UserService { private userRepository: Repository, @InjectMapper() private userMapper: Mapper, private emailService: EmailService, - @InjectRepository(ApplicationLocalGovernment) - private localGovernmentRepository: Repository, + @InjectRepository(LocalGovernment) + private localGovernmentRepository: Repository, @Inject(CONFIG_TOKEN) private config: IConfig, ) {} From 23189b5e7d5644f9f1b8812c45916795f77a074b Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 8 Aug 2023 13:06:42 -0700 Subject: [PATCH 216/954] Code Review Feedback --- .../create-application-dialog.component.ts | 1 - .../alcs-application.controller.spec.ts | 1 - services/apps/alcs/src/main.module.ts | 3 -- .../notice-of-intent-submission.controller.ts | 2 + .../notice-of-intent-submission.entity.ts | 2 +- ...otice-of-intent-submission.service.spec.ts | 46 ++++++++++--------- .../notice-of-intent-submission.service.ts | 34 +++++++------- .../1691525221371-adjust_column_comments.ts | 23 ++++++++++ 8 files changed, 67 insertions(+), 45 deletions(-) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691525221371-adjust_column_comments.ts diff --git a/alcs-frontend/src/app/features/board/dialogs/application/create/create-application-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application/create/create-application-dialog.component.ts index 00381c5b77..d23960b1ab 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/create/create-application-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/create/create-application-dialog.component.ts @@ -49,7 +49,6 @@ export class CreateApplicationDialogComponent implements OnInit, OnDestroy { }); this.localGovernmentService.list().then((res) => { - debugger; this.localGovernments = res; }); } diff --git a/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.spec.ts b/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.spec.ts index 7f518855ae..8501f448f4 100644 --- a/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.spec.ts +++ b/services/apps/alcs/src/alcs/obsolete-application-grpc/alcs-application.controller.spec.ts @@ -5,7 +5,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; import { FileNumberService } from '../../file-number/file-number.service'; -import { ApplicationService } from '../application/application.service'; import { ApplicationGrpcController } from './alcs-application.controller'; describe('ApplicationGrpcController', () => { diff --git a/services/apps/alcs/src/main.module.ts b/services/apps/alcs/src/main.module.ts index ab4f586986..dcfb85af1c 100644 --- a/services/apps/alcs/src/main.module.ts +++ b/services/apps/alcs/src/main.module.ts @@ -11,14 +11,11 @@ import { ClsModule } from 'nestjs-cls'; import { LoggerModule } from 'nestjs-pino'; import { CdogsModule } from '../../../libs/common/src/cdogs/cdogs.module'; import { AlcsModule } from './alcs/alcs.module'; -import { LocalGovernmentController } from './alcs/local-government/local-government.controller'; -import { LocalGovernmentModule } from './alcs/local-government/local-government.module'; import { AuthorizationFilter } from './common/authorization/authorization.filter'; import { AuthorizationModule } from './common/authorization/authorization.module'; import { AuditSubscriber } from './common/entities/audit.subscriber'; import { DocumentModule } from './document/document.module'; import { FileNumberModule } from './file-number/file-number.module'; -import { FileNumberService } from './file-number/file-number.service'; import { HealthCheck } from './healthcheck/healthcheck.entity'; import { LogoutController } from './logout/logout.controller'; import { MainController } from './main.controller'; diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts index 74a25c4db0..38ab0f9f00 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts @@ -140,11 +140,13 @@ export class NoticeOfIntentSubmissionController { await this.noticeOfIntentSubmissionService.submitToAlcs( validatedApplicationSubmission, ); + //TODO: Uncomment when we add status // return await this.noticeOfIntentSubmissionService.updateStatus( // noticeOfIntentSubmission, // SUBMISSION_STATUS.SUBMITTED_TO_ALC, // ); } else { + //TODO: Uncomment when we add validation //this.logger.debug(validationResult.errors); //throw new BadRequestException('Invalid Application'); } diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts index 8379f8e287..b088547362 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts @@ -55,7 +55,7 @@ export class NoticeOfIntentSubmission extends Base { @AutoMap(() => String) @Column({ type: 'varchar', - comment: 'The Applicants name on the application', + comment: 'The purpose of the application', nullable: true, }) purpose?: string | null; diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts index cb9c2ecb2d..51c346abe8 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -72,37 +72,39 @@ describe('NoticeOfIntentSubmissionService', () => { expect(service).toBeDefined(); }); - it('should return the fetched application', async () => { - const application = new NoticeOfIntentSubmission(); - mockRepository.findOne.mockResolvedValue(application); + it('should return the fetched notice of intent', async () => { + const noiSubmission = new NoticeOfIntentSubmission(); + mockRepository.findOne.mockResolvedValue(noiSubmission); const app = await service.getOrFailByFileNumber(''); - expect(app).toBe(application); + expect(app).toBe(noiSubmission); }); - it('should return the fetched application when fetching with user', async () => { - const application = new NoticeOfIntentSubmission(); - mockRepository.findOne.mockResolvedValue(application); + it('should return the fetched notice of intent when fetching with user', async () => { + const noiSubmission = new NoticeOfIntentSubmission(); + mockRepository.findOne.mockResolvedValue(noiSubmission); const app = await service.getIfCreatorByFileNumber('', new User()); - expect(app).toBe(application); + expect(app).toBe(noiSubmission); }); - it('should throw an exception if the application is not found the fetched application', async () => { + it('should throw an exception if the notice of intent is not found', async () => { mockRepository.findOne.mockResolvedValue(null); - const promise = service.getIfCreatorByFileNumber('', new User()); + const promise = service.getIfCreatorByFileNumber('file-number', new User()); await expect(promise).rejects.toMatchObject( - new Error(`Failed to load application with File ID `), + new Error( + `Failed to load notice of intent submission with File ID file-number`, + ), ); }); - it("should throw an error if application doesn't exist", async () => { + it("should throw an error if notice of intent doesn't exist", async () => { mockRepository.findOne.mockResolvedValue(null); const promise = service.getOrFailByFileNumber(''); await expect(promise).rejects.toMatchObject( - new Error('Failed to find document'), + new Error('Failed to find notice of intent submission'), ); }); @@ -121,25 +123,25 @@ describe('NoticeOfIntentSubmissionService', () => { }); it('should call through for get by user', async () => { - const application = new NoticeOfIntentSubmission(); - mockRepository.find.mockResolvedValue([application]); + const noiSubmission = new NoticeOfIntentSubmission(); + mockRepository.find.mockResolvedValue([noiSubmission]); const res = await service.getByUser(new User()); expect(mockRepository.find).toHaveBeenCalledTimes(1); expect(res.length).toEqual(1); - expect(res[0]).toBe(application); + expect(res[0]).toBe(noiSubmission); }); it('should call through for getByFileId', async () => { - const application = new NoticeOfIntentSubmission(); - mockRepository.findOne.mockResolvedValue(application); + const noiSubmission = new NoticeOfIntentSubmission(); + mockRepository.findOne.mockResolvedValue(noiSubmission); const res = await service.getByFileNumber('', new User()); expect(mockRepository.findOne).toHaveBeenCalledTimes(1); - expect(res).toBe(application); + expect(res).toBe(noiSubmission); }); - it('should use application type service for mapping DTOs', async () => { + it('should use notice of intent type service for mapping DTOs', async () => { const applicant = 'Bruce Wayne'; const typeCode = 'fake-code'; @@ -182,7 +184,9 @@ describe('NoticeOfIntentSubmissionService', () => { await expect( service.submitToAlcs(noticeOfIntentSubmission), ).rejects.toMatchObject( - new BaseServiceException(`Failed to submit application: ${fileNumber}`), + new BaseServiceException( + `Failed to submit notice of intent: ${fileNumber}`, + ), ); }); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index 8a116f9c00..ca59616dbc 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -49,7 +49,7 @@ export class NoticeOfIntentSubmissionService { relations: this.DEFAULT_RELATIONS, }); if (!noticeOfIntent) { - throw new Error('Failed to find document'); + throw new Error('Failed to find notice of intent submission'); } return noticeOfIntent; } @@ -66,7 +66,7 @@ export class NoticeOfIntentSubmissionService { relations, }); if (!noticeOfIntent) { - throw new Error('Failed to find document'); + throw new Error('Failed to find submission'); } return noticeOfIntent; } @@ -125,7 +125,7 @@ export class NoticeOfIntentSubmissionService { return this.getOrFailByUuid(submissionUuid, this.DEFAULT_RELATIONS); } - // + //TODO: Uncomment when adding submitting // async submitToAlcs( // application: ValidatedApplicationSubmission, // user: User, @@ -229,26 +229,23 @@ export class NoticeOfIntentSubmissionService { } async getIfCreatorByFileNumber(fileNumber: string, user: User) { - const applicationSubmission = await this.getByFileNumber(fileNumber, user); - if (!applicationSubmission) { + const noiSubmission = await this.getByFileNumber(fileNumber, user); + if (!noiSubmission) { throw new ServiceNotFoundException( - `Failed to load application with File ID ${fileNumber}`, + `Failed to load notice of intent submission with File ID ${fileNumber}`, ); } - return applicationSubmission; + return noiSubmission; } async getIfCreatorByUuid(uuid: string, user: User) { - const applicationSubmission = await this.getByUuid(uuid); - if ( - !applicationSubmission || - applicationSubmission.createdBy.uuid !== user.uuid - ) { + const noiSubmission = await this.getByUuid(uuid); + if (!noiSubmission || noiSubmission.createdBy.uuid !== user.uuid) { throw new ServiceNotFoundException( - `Failed to load application with ID ${uuid}`, + `Failed to load notice of intent submission with ID ${uuid}`, ); } - return applicationSubmission; + return noiSubmission; } async verifyAccessByFileId(fileId: string, user: User) { @@ -303,16 +300,16 @@ export class NoticeOfIntentSubmissionService { }); } - async mapToDetailedDTO(application: NoticeOfIntentSubmission) { + async mapToDetailedDTO(noiSubmission: NoticeOfIntentSubmission) { const types = await this.noticeOfIntentService.fetchTypes(); const mappedApp = this.mapper.map( - application, + noiSubmission, NoticeOfIntentSubmission, NoticeOfIntentSubmissionDetailedDto, ); return { ...mappedApp, - type: types.find((type) => type.code === application.typeCode)!.label, + type: types.find((type) => type.code === noiSubmission.typeCode)!.label, canEdit: true, canView: true, }; @@ -328,6 +325,7 @@ export class NoticeOfIntentSubmissionService { dateSubmittedToAlc: new Date(), }); + //TODO: Uncomment once statuses are added // await this.updateStatus( // application, // SUBMISSION_STATUS.SUBMITTED_TO_ALC, @@ -337,7 +335,7 @@ export class NoticeOfIntentSubmissionService { } catch (ex) { this.logger.error(ex); throw new BaseServiceException( - `Failed to submit application: ${noticeOfIntent.fileNumber}`, + `Failed to submit notice of intent: ${noticeOfIntent.fileNumber}`, ); } } diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691525221371-adjust_column_comments.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691525221371-adjust_column_comments.ts new file mode 100644 index 0000000000..9619dd5e42 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691525221371-adjust_column_comments.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class adjustColumnComments1691525221371 implements MigrationInterface { + name = 'adjustColumnComments1691525221371'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."local_government" DROP CONSTRAINT "FK_b7e4525de796ada01f43f464d9d"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."purpose" IS 'The purpose of the application'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."purpose" IS 'The Applicants name on the application'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."local_government" ADD CONSTRAINT "FK_b7e4525de796ada01f43f464d9d" FOREIGN KEY ("preferred_region_code") REFERENCES "alcs"."application_region"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} From a310b5f189fd7e61923186e0e375660505ea3499 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 8 Aug 2023 09:25:15 -0700 Subject: [PATCH 217/954] Move applications to their own folder --- portal-frontend/src/app/app-routing.module.ts | 8 +++---- portal-frontend/src/app/app.module.ts | 12 +++++----- .../alcs-edit-submission.component.html | 0 .../alcs-edit-submission.component.scss | 4 ++-- .../alcs-edit-submission.component.spec.ts | 12 +++++----- .../alcs-edit-submission.component.ts | 22 +++++++++---------- .../alcs-edit-submission.module.ts | 4 ++-- .../confirm-publish-dialog.component.html | 0 .../confirm-publish-dialog.component.scss | 2 +- .../confirm-publish-dialog.component.spec.ts | 0 .../confirm-publish-dialog.component.ts | 0 ...ve-file-confirmation-dialog.component.html | 0 ...e-file-confirmation-dialog.component.scss} | 0 ...file-confirmation-dialog.component.spec.ts | 0 ...move-file-confirmation-dialog.component.ts | 0 .../application-details.component.html | 0 .../application-details.component.scss | 4 ++-- .../application-details.component.spec.ts | 12 +++++----- .../application-details.component.ts | 14 ++++++------ .../application-details.module.ts | 2 +- .../excl-details/excl-details.component.html | 0 .../excl-details/excl-details.component.scss | 0 .../excl-details.component.spec.ts | 4 ++-- .../excl-details/excl-details.component.ts | 6 ++--- .../incl-details/incl-details.component.html | 0 .../incl-details/incl-details.component.scss | 0 .../incl-details.component.spec.ts | 6 ++--- .../incl-details/incl-details.component.ts | 8 +++---- .../naru-details/naru-details.component.html | 0 .../naru-details/naru-details.component.scss | 2 +- .../naru-details.component.spec.ts | 4 ++-- .../naru-details/naru-details.component.ts | 6 ++--- .../nfu-details/nfu-details.component.html | 0 .../nfu-details/nfu-details.component.scss | 0 .../nfu-details/nfu-details.component.spec.ts | 0 .../nfu-details/nfu-details.component.ts | 2 +- .../parcel/parcel.component.html | 0 .../parcel/parcel.component.scss | 2 +- .../parcel/parcel.component.spec.ts | 8 +++---- .../parcel/parcel.component.ts | 20 ++++++++--------- .../pfrs-details/pfrs-details.component.html | 0 .../pfrs-details/pfrs-details.component.scss | 2 +- .../pfrs-details.component.spec.ts | 4 ++-- .../pfrs-details/pfrs-details.component.ts | 6 ++--- .../pofo-details/pofo-details.component.html | 0 .../pofo-details/pofo-details.component.scss | 2 +- .../pofo-details.component.spec.ts | 4 ++-- .../pofo-details/pofo-details.component.ts | 6 ++--- .../roso-details/roso-details.component.html | 0 .../roso-details/roso-details.component.scss | 2 +- .../roso-details.component.spec.ts | 4 ++-- .../roso-details/roso-details.component.ts | 6 ++--- .../subd-details/subd-details.component.html | 0 .../subd-details/subd-details.component.scss | 2 +- .../subd-details.component.spec.ts | 4 ++-- .../subd-details/subd-details.component.ts | 10 ++++----- .../tur-details/tur-details.component.html | 0 .../tur-details/tur-details.component.scss | 0 .../tur-details/tur-details.component.spec.ts | 2 +- .../tur-details/tur-details.component.ts | 6 ++--- ...nge-application-type-dialog.component.html | 0 ...nge-application-type-dialog.component.scss | 4 ++-- ...-application-type-dialog.component.spec.ts | 4 ++-- ...hange-application-type-dialog.component.ts | 8 +++---- .../edit-submission-base.module.ts | 2 +- .../edit-submission.component.html | 0 .../edit-submission.component.scss | 4 ++-- .../edit-submission.component.spec.ts | 12 +++++----- .../edit-submission.component.ts | 22 +++++++++---------- .../edit-submission/edit-submission.module.ts | 4 ++-- .../edit-submission/files-step.partial.ts | 6 ++--- .../land-use/land-use.component.html | 0 .../land-use/land-use.component.scss} | 4 ++-- .../land-use/land-use.component.spec.ts | 4 ++-- .../land-use/land-use.component.ts | 4 ++-- .../other-attachments.component.html | 0 .../other-attachments.component.scss | 4 ++-- .../other-attachments.component.spec.ts | 10 ++++----- .../other-attachments.component.ts | 8 +++---- ...-parcel-confirmation-dialog.component.html | 0 ...parcel-confirmation-dialog.component.scss} | 0 ...rcel-confirmation-dialog.component.spec.ts | 0 ...er-parcel-confirmation-dialog.component.ts | 0 .../other-parcels.component.html | 0 .../other-parcels.component.scss} | 4 ++-- .../other-parcels.component.spec.ts | 8 +++---- .../other-parcels/other-parcels.component.ts | 18 +++++++-------- ...lication-crown-owner-dialog.component.html | 0 ...lication-crown-owner-dialog.component.scss | 4 ++-- ...ation-crown-owner-dialog.component.spec.ts | 2 +- ...pplication-crown-owner-dialog.component.ts | 4 ++-- .../application-owner-dialog.component.html | 0 .../application-owner-dialog.component.scss | 4 ++-- ...application-owner-dialog.component.spec.ts | 6 ++--- .../application-owner-dialog.component.ts | 12 +++++----- .../application-owners-dialog.component.html | 0 .../application-owners-dialog.component.scss | 0 ...pplication-owners-dialog.component.spec.ts | 2 +- .../application-owners-dialog.component.ts | 4 ++-- .../delete-parcel-dialog.component.html | 0 .../delete-parcel-dialog.component.scss} | 0 .../delete-parcel-dialog.component.spec.ts | 2 +- .../delete-parcel-dialog.component.ts | 4 ++-- .../parcel-details.component.html | 0 .../parcel-details.component.scss | 0 .../parcel-details.component.spec.ts | 8 +++---- .../parcel-details.component.ts | 12 +++++----- ...l-entry-confirmation-dialog.component.html | 0 ...-entry-confirmation-dialog.component.scss} | 2 +- ...ntry-confirmation-dialog.component.spec.ts | 0 ...cel-entry-confirmation-dialog.component.ts | 0 .../parcel-entry/parcel-entry.component.html | 0 .../parcel-entry/parcel-entry.component.scss | 4 ++-- .../parcel-entry.component.spec.ts | 12 +++++----- .../parcel-entry/parcel-entry.component.ts | 18 +++++++-------- .../parcel-owners.component.html | 0 .../parcel-owners.component.scss | 4 ++-- .../parcel-owners.component.spec.ts | 4 ++-- .../parcel-owners/parcel-owners.component.ts | 6 ++--- .../primary-contact.component.html | 0 .../primary-contact.component.scss | 4 ++-- .../primary-contact.component.spec.ts | 14 ++++++------ .../primary-contact.component.ts | 12 +++++----- .../excl-proposal.component.html | 0 .../excl-proposal.component.scss | 2 +- .../excl-proposal.component.spec.ts | 8 +++---- .../excl-proposal/excl-proposal.component.ts | 10 ++++----- .../incl-proposal.component.html | 0 .../incl-proposal.component.scss | 2 +- .../incl-proposal.component.spec.ts | 12 +++++----- .../incl-proposal/incl-proposal.component.ts | 14 ++++++------ ...subtype-confirmation-dialog.component.html | 0 ...ubtype-confirmation-dialog.component.scss} | 2 +- ...type-confirmation-dialog.component.spec.ts | 0 ...e-subtype-confirmation-dialog.component.ts | 0 .../naru-proposal.component.html | 0 .../naru-proposal.component.scss | 2 +- .../naru-proposal.component.spec.ts | 10 ++++----- .../naru-proposal/naru-proposal.component.ts | 12 +++++----- .../nfu-proposal/nfu-proposal.component.html | 0 .../nfu-proposal/nfu-proposal.component.scss | 5 +++++ .../nfu-proposal.component.spec.ts | 4 ++-- .../nfu-proposal/nfu-proposal.component.ts | 6 ++--- .../pfrs-proposal.component.html | 0 .../pfrs-proposal.component.scss | 5 +++++ .../pfrs-proposal.component.spec.ts | 10 ++++----- .../pfrs-proposal/pfrs-proposal.component.ts | 14 ++++++------ .../pofo-proposal.component.html | 0 .../pofo-proposal.component.scss | 5 +++++ .../pofo-proposal.component.spec.ts | 10 ++++----- .../pofo-proposal/pofo-proposal.component.ts | 12 +++++----- .../roso-proposal.component.html | 0 .../roso-proposal.component.scss | 5 +++++ .../roso-proposal.component.spec.ts | 10 ++++----- .../roso-proposal/roso-proposal.component.ts | 12 +++++----- .../soil-table/soil-table.component.html | 0 .../soil-table/soil-table.component.scss | 2 +- .../soil-table/soil-table.component.spec.ts | 0 .../soil-table/soil-table.component.ts | 0 .../subd-proposal.component.html | 0 .../subd-proposal.component.scss | 2 +- .../subd-proposal.component.spec.ts | 10 ++++----- .../subd-proposal/subd-proposal.component.ts | 12 +++++----- .../tur-proposal/tur-proposal.component.html | 0 .../tur-proposal/tur-proposal.component.scss | 2 +- .../tur-proposal.component.spec.ts | 8 +++---- .../tur-proposal/tur-proposal.component.ts | 8 +++---- .../review-and-submit.component.html | 0 .../review-and-submit.component.scss | 0 .../review-and-submit.component.spec.ts | 10 ++++----- .../review-and-submit.component.ts | 12 +++++----- .../submit-confirmation-dialog.component.html | 0 .../submit-confirmation-dialog.component.scss | 2 +- ...bmit-confirmation-dialog.component.spec.ts | 0 .../submit-confirmation-dialog.component.ts | 0 .../select-government.component.html | 0 .../select-government.component.scss | 0 .../select-government.component.spec.ts | 6 ++--- .../select-government.component.ts | 6 ++--- .../edit-submission/step.partial.ts | 2 +- .../return-application-dialog.component.html | 0 .../return-application-dialog.component.scss | 4 ++-- ...eturn-application-dialog.component.spec.ts | 2 +- .../return-application-dialog.component.ts | 2 +- .../review-attachments.component.html | 0 .../review-attachments.component.scss} | 2 +- .../review-attachments.component.spec.ts | 8 +++---- .../review-attachments.component.ts | 8 +++---- .../review-contact-information.component.html | 0 .../review-contact-information.component.scss | 0 ...view-contact-information.component.spec.ts | 4 ++-- .../review-contact-information.component.ts | 2 +- .../review-ocp/review-ocp.component.html | 0 .../review-ocp/review-ocp.component.scss | 0 .../review-ocp/review-ocp.component.spec.ts | 4 ++-- .../review-ocp/review-ocp.component.ts | 2 +- .../review-resolution.component.html | 0 .../review-resolution.component.scss | 0 .../review-resolution.component.spec.ts | 4 ++-- .../review-resolution.component.ts | 2 +- .../review-submission.component.html | 0 .../review-submission.component.scss | 4 ++-- .../review-submission.component.spec.ts | 14 ++++++------ .../review-submission.component.ts | 18 +++++++-------- .../review-submission.module.ts | 4 ++-- .../review-submit-fng.component.html | 0 .../review-submit-fng.component.scss | 4 ++-- .../review-submit-fng.component.spec.ts | 14 ++++++------ .../review-submit-fng.component.ts | 16 +++++++------- .../review-submit.component.html | 0 .../review-submit.component.scss | 4 ++-- .../review-submit.component.spec.ts | 12 +++++----- .../review-submit/review-submit.component.ts | 16 +++++++------- .../review-zoning.component.html | 0 .../review-zoning.component.scss | 0 .../review-zoning.component.spec.ts | 4 ++-- .../review-zoning/review-zoning.component.ts | 2 +- .../alc-review/alc-review.component.html | 0 .../alc-review/alc-review.component.scss | 4 ++-- .../alc-review/alc-review.component.spec.ts | 2 +- .../alc-review/alc-review.component.ts | 6 ++--- .../decisions/decisions.component.html | 0 .../decisions/decisions.component.scss | 4 ++-- .../decisions/decisions.component.spec.ts | 2 +- .../decisions/decisions.component.ts | 4 ++-- .../submission-documents.component.html | 0 .../submission-documents.component.scss | 4 ++-- .../submission-documents.component.spec.ts | 2 +- .../submission-documents.component.ts | 4 ++-- .../lfng-review/lfng-review.component.html | 0 .../lfng-review/lfng-review.component.scss | 4 ++-- .../lfng-review/lfng-review.component.spec.ts | 12 +++++----- .../lfng-review/lfng-review.component.ts | 12 +++++----- .../view-submission.component.html | 0 .../view-submission.component.scss | 6 ++--- .../view-submission.component.spec.ts | 12 +++++----- .../view-submission.component.ts | 18 +++++++-------- .../land-use/land-use.component.scss | 5 ----- .../other-parcels.component.scss | 5 ----- .../nfu-proposal/nfu-proposal.component.scss | 5 ----- .../review-attachments.component.scss | 5 ----- 241 files changed, 512 insertions(+), 512 deletions(-) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/alcs-edit-submission.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/alcs-edit-submission.component.scss (98%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/alcs-edit-submission.component.spec.ts (75%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/alcs-edit-submission.component.ts (90%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/alcs-edit-submission.module.ts (88%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.scss (91%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.spec.ts (100%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.ts (100%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html (100%) rename portal-frontend/src/app/features/{edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.scss => applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.scss} (100%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.spec.ts (100%) rename portal-frontend/src/app/features/{ => applications}/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.ts (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/application-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/application-details.component.scss (96%) rename portal-frontend/src/app/features/{ => applications}/application-details/application-details.component.spec.ts (78%) rename portal-frontend/src/app/features/{ => applications}/application-details/application-details.component.ts (87%) rename portal-frontend/src/app/features/{ => applications}/application-details/application-details.module.ts (95%) rename portal-frontend/src/app/features/{ => applications}/application-details/excl-details/excl-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/excl-details/excl-details.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/excl-details/excl-details.component.spec.ts (82%) rename portal-frontend/src/app/features/{ => applications}/application-details/excl-details/excl-details.component.ts (87%) rename portal-frontend/src/app/features/{ => applications}/application-details/incl-details/incl-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/incl-details/incl-details.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/incl-details/incl-details.component.spec.ts (80%) rename portal-frontend/src/app/features/{ => applications}/application-details/incl-details/incl-details.component.ts (87%) rename portal-frontend/src/app/features/{ => applications}/application-details/naru-details/naru-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/naru-details/naru-details.component.scss (77%) rename portal-frontend/src/app/features/{ => applications}/application-details/naru-details/naru-details.component.spec.ts (83%) rename portal-frontend/src/app/features/{ => applications}/application-details/naru-details/naru-details.component.ts (84%) rename portal-frontend/src/app/features/{ => applications}/application-details/nfu-details/nfu-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/nfu-details/nfu-details.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/nfu-details/nfu-details.component.spec.ts (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/nfu-details/nfu-details.component.ts (87%) rename portal-frontend/src/app/features/{ => applications}/application-details/parcel/parcel.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/parcel/parcel.component.scss (93%) rename portal-frontend/src/app/features/{ => applications}/application-details/parcel/parcel.component.spec.ts (79%) rename portal-frontend/src/app/features/{ => applications}/application-details/parcel/parcel.component.ts (88%) rename portal-frontend/src/app/features/{ => applications}/application-details/pfrs-details/pfrs-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/pfrs-details/pfrs-details.component.scss (83%) rename portal-frontend/src/app/features/{ => applications}/application-details/pfrs-details/pfrs-details.component.spec.ts (83%) rename portal-frontend/src/app/features/{ => applications}/application-details/pfrs-details/pfrs-details.component.ts (87%) rename portal-frontend/src/app/features/{ => applications}/application-details/pofo-details/pofo-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/pofo-details/pofo-details.component.scss (77%) rename portal-frontend/src/app/features/{ => applications}/application-details/pofo-details/pofo-details.component.spec.ts (83%) rename portal-frontend/src/app/features/{ => applications}/application-details/pofo-details/pofo-details.component.ts (86%) rename portal-frontend/src/app/features/{ => applications}/application-details/roso-details/roso-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/roso-details/roso-details.component.scss (77%) rename portal-frontend/src/app/features/{ => applications}/application-details/roso-details/roso-details.component.spec.ts (83%) rename portal-frontend/src/app/features/{ => applications}/application-details/roso-details/roso-details.component.ts (86%) rename portal-frontend/src/app/features/{ => applications}/application-details/subd-details/subd-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/subd-details/subd-details.component.scss (79%) rename portal-frontend/src/app/features/{ => applications}/application-details/subd-details/subd-details.component.spec.ts (83%) rename portal-frontend/src/app/features/{ => applications}/application-details/subd-details/subd-details.component.ts (83%) rename portal-frontend/src/app/features/{ => applications}/application-details/tur-details/tur-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/tur-details/tur-details.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/application-details/tur-details/tur-details.component.spec.ts (88%) rename portal-frontend/src/app/features/{ => applications}/application-details/tur-details/tur-details.component.ts (85%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/change-application-type-dialog/change-application-type-dialog.component.scss (94%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/change-application-type-dialog/change-application-type-dialog.component.spec.ts (87%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts (92%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/edit-submission-base.module.ts (98%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/edit-submission.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/edit-submission.component.scss (98%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/edit-submission.component.spec.ts (72%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/edit-submission.component.ts (91%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/edit-submission.module.ts (84%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/files-step.partial.ts (87%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/land-use/land-use.component.html (100%) rename portal-frontend/src/app/features/{edit-submission/proposal/roso-proposal/roso-proposal.component.scss => applications/edit-submission/land-use/land-use.component.scss} (55%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/land-use/land-use.component.spec.ts (86%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/land-use/land-use.component.ts (95%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-attachments/other-attachments.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-attachments/other-attachments.component.scss (83%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-attachments/other-attachments.component.spec.ts (79%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-attachments/other-attachments.component.ts (91%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.html (100%) rename portal-frontend/src/app/features/{edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss => applications/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.scss} (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.spec.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-parcels/other-parcels.component.html (100%) rename portal-frontend/src/app/features/{edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.scss => applications/edit-submission/other-parcels/other-parcels.component.scss} (57%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-parcels/other-parcels.component.spec.ts (82%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/other-parcels/other-parcels.component.ts (92%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss (72%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts (92%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts (94%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss (75%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts (85%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts (92%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts (91%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts (87%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.html (100%) rename portal-frontend/src/app/features/{edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.scss => applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.scss} (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.spec.ts (93%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.ts (91%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-details.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-details.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-details.component.spec.ts (83%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-details.component.ts (92%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html (100%) rename portal-frontend/src/app/features/{alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.scss => applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss} (86%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-entry/parcel-entry.component.scss (94%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-entry/parcel-entry.component.spec.ts (79%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts (95%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss (66%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts (82%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts (90%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/primary-contact/primary-contact.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/primary-contact/primary-contact.component.scss (90%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/primary-contact/primary-contact.component.spec.ts (74%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/primary-contact/primary-contact.component.ts (94%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/excl-proposal/excl-proposal.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/excl-proposal/excl-proposal.component.scss (66%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts (79%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/excl-proposal/excl-proposal.component.ts (88%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/incl-proposal/incl-proposal.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/incl-proposal/incl-proposal.component.scss (66%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts (74%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/incl-proposal/incl-proposal.component.ts (88%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.html (100%) rename portal-frontend/src/app/features/{edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.scss => applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.scss} (86%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.spec.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/naru-proposal/naru-proposal.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/naru-proposal/naru-proposal.component.scss (82%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/naru-proposal/naru-proposal.component.spec.ts (76%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/naru-proposal/naru-proposal.component.ts (93%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html (100%) create mode 100644 portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.scss rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/nfu-proposal/nfu-proposal.component.spec.ts (84%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts (94%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html (100%) create mode 100644 portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.scss rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.spec.ts (77%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts (93%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html (100%) create mode 100644 portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.scss rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/pofo-proposal/pofo-proposal.component.spec.ts (77%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts (91%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/roso-proposal/roso-proposal.component.html (100%) create mode 100644 portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.scss rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/roso-proposal/roso-proposal.component.spec.ts (77%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/roso-proposal/roso-proposal.component.ts (91%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/soil-table/soil-table.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/soil-table/soil-table.component.scss (95%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/soil-table/soil-table.component.spec.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/soil-table/soil-table.component.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/subd-proposal/subd-proposal.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/subd-proposal/subd-proposal.component.scss (62%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/subd-proposal/subd-proposal.component.spec.ts (77%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/subd-proposal/subd-proposal.component.ts (89%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/tur-proposal/tur-proposal.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/tur-proposal/tur-proposal.component.scss (62%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/tur-proposal/tur-proposal.component.spec.ts (79%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/proposal/tur-proposal/tur-proposal.component.ts (90%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/review-and-submit/review-and-submit.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/review-and-submit/review-and-submit.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/review-and-submit/review-and-submit.component.spec.ts (79%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/review-and-submit/review-and-submit.component.ts (79%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss (81%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/select-government/select-government.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/select-government/select-government.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/select-government/select-government.component.spec.ts (81%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/select-government/select-government.component.ts (94%) rename portal-frontend/src/app/features/{ => applications}/edit-submission/step.partial.ts (86%) rename portal-frontend/src/app/features/{ => applications}/review-submission/return-application-dialog/return-application-dialog.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/return-application-dialog/return-application-dialog.component.scss (93%) rename portal-frontend/src/app/features/{ => applications}/review-submission/return-application-dialog/return-application-dialog.component.spec.ts (90%) rename portal-frontend/src/app/features/{ => applications}/review-submission/return-application-dialog/return-application-dialog.component.ts (92%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-attachments/review-attachments.component.html (100%) rename portal-frontend/src/app/features/{edit-submission/proposal/pofo-proposal/pofo-proposal.component.scss => applications/review-submission/review-attachments/review-attachments.component.scss} (69%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-attachments/review-attachments.component.spec.ts (78%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-attachments/review-attachments.component.ts (91%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-contact-information/review-contact-information.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-contact-information/review-contact-information.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-contact-information/review-contact-information.component.spec.ts (84%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-contact-information/review-contact-information.component.ts (96%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-ocp/review-ocp.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-ocp/review-ocp.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-ocp/review-ocp.component.spec.ts (83%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-ocp/review-ocp.component.ts (96%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-resolution/review-resolution.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-resolution/review-resolution.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-resolution/review-resolution.component.spec.ts (83%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-resolution/review-resolution.component.ts (95%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submission.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submission.component.scss (97%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submission.component.spec.ts (77%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submission.component.ts (89%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submission.module.ts (92%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submit-fng/review-submit-fng.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submit-fng/review-submit-fng.component.scss (94%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submit-fng/review-submit-fng.component.spec.ts (72%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submit-fng/review-submit-fng.component.ts (85%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submit/review-submit.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submit/review-submit.component.scss (94%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submit/review-submit.component.spec.ts (73%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-submit/review-submit.component.ts (89%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-zoning/review-zoning.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-zoning/review-zoning.component.scss (100%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-zoning/review-zoning.component.spec.ts (81%) rename portal-frontend/src/app/features/{ => applications}/review-submission/review-zoning/review-zoning.component.ts (97%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/alc-review.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/alc-review.component.scss (59%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/alc-review.component.spec.ts (86%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/alc-review.component.ts (81%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/decisions/decisions.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/decisions/decisions.component.scss (82%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/decisions/decisions.component.spec.ts (88%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/decisions/decisions.component.ts (80%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/submission-documents/submission-documents.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/submission-documents/submission-documents.component.scss (75%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts (89%) rename portal-frontend/src/app/features/{ => applications}/view-submission/alc-review/submission-documents/submission-documents.component.ts (86%) rename portal-frontend/src/app/features/{ => applications}/view-submission/lfng-review/lfng-review.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/view-submission/lfng-review/lfng-review.component.scss (95%) rename portal-frontend/src/app/features/{ => applications}/view-submission/lfng-review/lfng-review.component.spec.ts (68%) rename portal-frontend/src/app/features/{ => applications}/view-submission/lfng-review/lfng-review.component.ts (87%) rename portal-frontend/src/app/features/{ => applications}/view-submission/view-submission.component.html (100%) rename portal-frontend/src/app/features/{ => applications}/view-submission/view-submission.component.scss (96%) rename portal-frontend/src/app/features/{ => applications}/view-submission/view-submission.component.spec.ts (76%) rename portal-frontend/src/app/features/{ => applications}/view-submission/view-submission.component.ts (79%) delete mode 100644 portal-frontend/src/app/features/edit-submission/land-use/land-use.component.scss delete mode 100644 portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.scss delete mode 100644 portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.scss delete mode 100644 portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.scss diff --git a/portal-frontend/src/app/app-routing.module.ts b/portal-frontend/src/app/app-routing.module.ts index 8dab9039df..41b3578a9c 100644 --- a/portal-frontend/src/app/app-routing.module.ts +++ b/portal-frontend/src/app/app-routing.module.ts @@ -4,7 +4,7 @@ import { AuthorizationComponent } from './features/authorization/authorization.c import { HomeComponent } from './features/home/home.component'; import { LandingPageComponent } from './features/landing-page/landing-page.component'; import { LoginComponent } from './features/login/login.component'; -import { ViewSubmissionComponent } from './features/view-submission/view-submission.component'; +import { ViewSubmissionComponent } from './features/applications/view-submission/view-submission.component'; import { AlcsAuthGuard } from './services/authentication/alcs-auth.guard'; import { AuthGuard } from './services/authentication/auth.guard'; @@ -40,21 +40,21 @@ const routes: Routes = [ title: 'Edit Application', path: 'application/:fileId/edit', canActivate: [AuthGuard], - loadChildren: () => import('./features/edit-submission/edit-submission.module').then((m) => m.EditSubmissionModule), + loadChildren: () => import('./features/applications/edit-submission/edit-submission.module').then((m) => m.EditSubmissionModule), }, { title: 'ALCS Edit Application', path: 'alcs/application/:fileId/edit', canActivate: [AlcsAuthGuard], loadChildren: () => - import('./features/alcs-edit-submission/alcs-edit-submission.module').then((m) => m.AlcsEditSubmissionModule), + import('./features/applications/alcs-edit-submission/alcs-edit-submission.module').then((m) => m.AlcsEditSubmissionModule), }, { title: 'Review Application', path: 'application/:fileId/review', canActivate: [AuthGuard], loadChildren: () => - import('./features/review-submission/review-submission.module').then((m) => m.ReviewSubmissionModule), + import('./features/applications/review-submission/review-submission.module').then((m) => m.ReviewSubmissionModule), }, { path: '', redirectTo: '/login', pathMatch: 'full' }, ]; diff --git a/portal-frontend/src/app/app.module.ts b/portal-frontend/src/app/app.module.ts index d768dce4cd..25d2d3d63d 100644 --- a/portal-frontend/src/app/app.module.ts +++ b/portal-frontend/src/app/app.module.ts @@ -8,17 +8,17 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { provideNgxMask } from 'ngx-mask'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { ApplicationDetailsModule } from './features/application-details/application-details.module'; +import { ApplicationDetailsModule } from './features/applications/application-details/application-details.module'; import { AuthorizationComponent } from './features/authorization/authorization.component'; import { CreateApplicationDialogComponent } from './features/create-application-dialog/create-application-dialog.component'; import { ApplicationListComponent } from './features/home/application-list/application-list.component'; import { HomeComponent } from './features/home/home.component'; import { LandingPageComponent } from './features/landing-page/landing-page.component'; import { LoginComponent } from './features/login/login.component'; -import { AlcReviewComponent } from './features/view-submission/alc-review/alc-review.component'; -import { SubmissionDocumentsComponent } from './features/view-submission/alc-review/submission-documents/submission-documents.component'; -import { LfngReviewComponent } from './features/view-submission/lfng-review/lfng-review.component'; -import { ViewSubmissionComponent } from './features/view-submission/view-submission.component'; +import { AlcReviewComponent } from './features/applications/view-submission/alc-review/alc-review.component'; +import { SubmissionDocumentsComponent } from './features/applications/view-submission/alc-review/submission-documents/submission-documents.component'; +import { LfngReviewComponent } from './features/applications/view-submission/lfng-review/lfng-review.component'; +import { ViewSubmissionComponent } from './features/applications/view-submission/view-submission.component'; import { AuthInterceptorService } from './services/authentication/auth-interceptor.service'; import { TokenRefreshService } from './services/authentication/token-refresh.service'; import { ConfirmationDialogComponent } from './shared/confirmation-dialog/confirmation-dialog.component'; @@ -26,7 +26,7 @@ import { ConfirmationDialogService } from './shared/confirmation-dialog/confirma import { FooterComponent } from './shared/footer/footer.component'; import { HeaderComponent } from './shared/header/header.component'; import { SharedModule } from './shared/shared.module'; -import { DecisionsComponent } from './features/view-submission/alc-review/decisions/decisions.component'; +import { DecisionsComponent } from './features/applications/view-submission/alc-review/decisions/decisions.component'; @NgModule({ declarations: [ diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.html similarity index 100% rename from portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.html rename to portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.html diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.scss b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.scss similarity index 98% rename from portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.scss rename to portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.scss index 172d15447c..680d1f6272 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.scss +++ b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.scss @@ -1,5 +1,5 @@ -@use '../../../styles/functions' as *; -@use '../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; .header { margin: rem(24) 0; diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.spec.ts b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.spec.ts similarity index 75% rename from portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.spec.ts rename to portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.spec.ts index de987d58ff..296671f363 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.spec.ts @@ -3,14 +3,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { RouterTestingModule } from '@angular/router/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { ApplicationSubmissionDraftService } from '../../services/application-submission/application-submission-draft.service'; -import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { CodeService } from '../../services/code/code.service'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDraftService } from '../../../services/application-submission/application-submission-draft.service'; +import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../services/code/code.service'; import { MatDialogModule } from '@angular/material/dialog'; -import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; -import { ToastService } from '../../services/toast/toast.service'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../services/toast/toast.service'; import { AlcsEditSubmissionComponent } from './alcs-edit-submission.component'; describe('AlcsEditSubmissionComponent', () => { diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts similarity index 90% rename from portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts rename to portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts index 32486f33ba..3fdefdbced 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.component.ts +++ b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts @@ -4,16 +4,16 @@ import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { getDiff } from 'recursive-diff'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; -import { environment } from '../../../environments/environment'; -import { ApplicationDocumentDto } from '../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { ApplicationSubmissionDraftService } from '../../services/application-submission/application-submission-draft.service'; -import { ApplicationSubmissionDetailedDto } from '../../services/application-submission/application-submission.dto'; -import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; -import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { ToastService } from '../../services/toast/toast.service'; -import { CustomStepperComponent } from '../../shared/custom-stepper/custom-stepper.component'; -import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { environment } from '../../../../environments/environment'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDraftService } from '../../../services/application-submission/application-submission-draft.service'; +import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { ToastService } from '../../../services/toast/toast.service'; +import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; +import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; import { EditApplicationSteps } from '../edit-submission/edit-submission.component'; import { LandUseComponent } from '../edit-submission/land-use/land-use.component'; import { OtherAttachmentsComponent } from '../edit-submission/other-attachments/other-attachments.component'; @@ -28,7 +28,7 @@ import { SubdProposalComponent } from '../edit-submission/proposal/subd-proposal import { TurProposalComponent } from '../edit-submission/proposal/tur-proposal/tur-proposal.component'; import { SelectGovernmentComponent } from '../edit-submission/select-government/select-government.component'; import { ConfirmPublishDialogComponent } from './confirm-publish-dialog/confirm-publish-dialog.component'; -import { scrollToElement } from '../../shared/utils/scroll-helper'; +import { scrollToElement } from '../../../shared/utils/scroll-helper'; import { ExclProposalComponent } from '../edit-submission/proposal/excl-proposal/excl-proposal.component'; import { InclProposalComponent } from '../edit-submission/proposal/incl-proposal/incl-proposal.component'; diff --git a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.module.ts b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.module.ts similarity index 88% rename from portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.module.ts rename to portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.module.ts index c20cb72dbf..1afea8940e 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/alcs-edit-submission.module.ts +++ b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CanDeactivateGuard } from '../../shared/guard/can-deactivate.guard'; -import { SharedModule } from '../../shared/shared.module'; +import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; +import { SharedModule } from '../../../shared/shared.module'; import { EditSubmissionBaseModule } from '../edit-submission/edit-submission-base.module'; import { AlcsEditSubmissionComponent } from './alcs-edit-submission.component'; import { ConfirmPublishDialogComponent } from './confirm-publish-dialog/confirm-publish-dialog.component'; diff --git a/portal-frontend/src/app/features/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.html b/portal-frontend/src/app/features/applications/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.html rename to portal-frontend/src/app/features/applications/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.html diff --git a/portal-frontend/src/app/features/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.scss b/portal-frontend/src/app/features/applications/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.scss similarity index 91% rename from portal-frontend/src/app/features/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.scss rename to portal-frontend/src/app/features/applications/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.scss index 5baabab3ff..67d44d5da0 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.scss +++ b/portal-frontend/src/app/features/applications/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/functions' as *; +@use '../../../../../styles/functions' as *; .mat-dialog-actions { justify-content: flex-end; diff --git a/portal-frontend/src/app/features/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.spec.ts similarity index 100% rename from portal-frontend/src/app/features/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.spec.ts diff --git a/portal-frontend/src/app/features/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.ts b/portal-frontend/src/app/features/applications/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.ts similarity index 100% rename from portal-frontend/src/app/features/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.ts rename to portal-frontend/src/app/features/applications/alcs-edit-submission/confirm-publish-dialog/confirm-publish-dialog.component.ts diff --git a/portal-frontend/src/app/features/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html b/portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html rename to portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.scss b/portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.scss similarity index 100% rename from portal-frontend/src/app/features/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.scss rename to portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.scss diff --git a/portal-frontend/src/app/features/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.spec.ts similarity index 100% rename from portal-frontend/src/app/features/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.spec.ts diff --git a/portal-frontend/src/app/features/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.ts b/portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.ts similarity index 100% rename from portal-frontend/src/app/features/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.ts rename to portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.ts diff --git a/portal-frontend/src/app/features/application-details/application-details.component.html b/portal-frontend/src/app/features/applications/application-details/application-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/application-details.component.html rename to portal-frontend/src/app/features/applications/application-details/application-details.component.html diff --git a/portal-frontend/src/app/features/application-details/application-details.component.scss b/portal-frontend/src/app/features/applications/application-details/application-details.component.scss similarity index 96% rename from portal-frontend/src/app/features/application-details/application-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/application-details.component.scss index 07593da153..8df832ca0f 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.scss +++ b/portal-frontend/src/app/features/applications/application-details/application-details.component.scss @@ -1,5 +1,5 @@ -@use '../../../styles/functions' as *; -@use '../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; :host::ng-deep { .view-grid-item { diff --git a/portal-frontend/src/app/features/application-details/application-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/application-details.component.spec.ts similarity index 78% rename from portal-frontend/src/app/features/application-details/application-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/application-details.component.spec.ts index 640a2cd8b3..0554a68a05 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/application-details.component.spec.ts @@ -3,12 +3,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { CodeService } from '../../services/code/code.service'; -import { ToastService } from '../../services/toast/toast.service'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../services/code/code.service'; +import { ToastService } from '../../../services/toast/toast.service'; import { ApplicationDetailsComponent } from './application-details.component'; diff --git a/portal-frontend/src/app/features/application-details/application-details.component.ts b/portal-frontend/src/app/features/applications/application-details/application-details.component.ts similarity index 87% rename from portal-frontend/src/app/features/application-details/application-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/application-details.component.ts index 071fe7ed2f..c8ac8740c3 100644 --- a/portal-frontend/src/app/features/application-details/application-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/application-details.component.ts @@ -5,13 +5,13 @@ import { ApplicationDocumentDto, DOCUMENT_SOURCE, DOCUMENT_TYPE, -} from '../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { APPLICATION_OWNER, ApplicationOwnerDetailedDto } from '../../services/application-owner/application-owner.dto'; -import { PARCEL_TYPE } from '../../services/application-parcel/application-parcel.dto'; -import { ApplicationSubmissionDetailedDto } from '../../services/application-submission/application-submission.dto'; -import { LocalGovernmentDto } from '../../services/code/code.dto'; -import { CodeService } from '../../services/code/code.service'; +} from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { APPLICATION_OWNER, ApplicationOwnerDetailedDto } from '../../../services/application-owner/application-owner.dto'; +import { PARCEL_TYPE } from '../../../services/application-parcel/application-parcel.dto'; +import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { LocalGovernmentDto } from '../../../services/code/code.dto'; +import { CodeService } from '../../../services/code/code.service'; @Component({ selector: 'app-application-details', diff --git a/portal-frontend/src/app/features/application-details/application-details.module.ts b/portal-frontend/src/app/features/applications/application-details/application-details.module.ts similarity index 95% rename from portal-frontend/src/app/features/application-details/application-details.module.ts rename to portal-frontend/src/app/features/applications/application-details/application-details.module.ts index 8a7ee11774..d7eac27fe1 100644 --- a/portal-frontend/src/app/features/application-details/application-details.module.ts +++ b/portal-frontend/src/app/features/applications/application-details/application-details.module.ts @@ -1,7 +1,7 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { NgxMaskPipe } from 'ngx-mask'; -import { SharedModule } from '../../shared/shared.module'; +import { SharedModule } from '../../../shared/shared.module'; import { ApplicationDetailsComponent } from './application-details.component'; import { InclDetailsComponent } from './incl-details/incl-details.component'; import { NaruDetailsComponent } from './naru-details/naru-details.component'; diff --git a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.html b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/excl-details/excl-details.component.html rename to portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.html diff --git a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.scss b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.scss similarity index 100% rename from portal-frontend/src/app/features/application-details/excl-details/excl-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.scss diff --git a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.spec.ts similarity index 82% rename from portal-frontend/src/app/features/application-details/excl-details/excl-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.spec.ts index e021e28723..1f858adc91 100644 --- a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.spec.ts @@ -2,8 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeepMocked } from '@golevelup/ts-jest'; import { ExclDetailsComponent } from './excl-details.component'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; describe('ExclDetailsComponent', () => { let component: ExclDetailsComponent; diff --git a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.ts b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts similarity index 87% rename from portal-frontend/src/app/features/application-details/excl-details/excl-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts index d7ea9ecdf9..0d9ce89d03 100644 --- a/portal-frontend/src/app/features/application-details/excl-details/excl-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; @Component({ selector: 'app-excl-details', diff --git a/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.html b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/incl-details/incl-details.component.html rename to portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.html diff --git a/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.scss b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.scss similarity index 100% rename from portal-frontend/src/app/features/application-details/incl-details/incl-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.scss diff --git a/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.spec.ts similarity index 80% rename from portal-frontend/src/app/features/application-details/incl-details/incl-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.spec.ts index beb030ce61..d49a691006 100644 --- a/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.spec.ts @@ -1,9 +1,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { UserDto } from '../../../services/authentication/authentication.dto'; -import { AuthenticationService } from '../../../services/authentication/authentication.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { UserDto } from '../../../../services/authentication/authentication.dto'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; import { InclDetailsComponent } from './incl-details.component'; describe('InclDetailsComponent', () => { diff --git a/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.ts b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts similarity index 87% rename from portal-frontend/src/app/features/application-details/incl-details/incl-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts index d248a469a5..b81fd9271c 100644 --- a/portal-frontend/src/app/features/application-details/incl-details/incl-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts @@ -1,10 +1,10 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { AuthenticationService } from '../../../services/authentication/authentication.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; @Component({ selector: 'app-incl-details', diff --git a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/naru-details/naru-details.component.html rename to portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.html diff --git a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.scss b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.scss similarity index 77% rename from portal-frontend/src/app/features/application-details/naru-details/naru-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.scss index 2444367585..0c540ce5fb 100644 --- a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.scss +++ b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/functions' as *; +@use '../../../../../styles/functions' as *; .soil-table { display: grid; diff --git a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.spec.ts similarity index 83% rename from portal-frontend/src/app/features/application-details/naru-details/naru-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.spec.ts index fb5c4c9ed4..70c8dae4a4 100644 --- a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; import { NaruDetailsComponent } from './naru-details.component'; diff --git a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.ts b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts similarity index 84% rename from portal-frontend/src/app/features/application-details/naru-details/naru-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts index b24c447d6f..bcbb2644a9 100644 --- a/portal-frontend/src/app/features/application-details/naru-details/naru-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; @Component({ selector: 'app-naru-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.html b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.html rename to portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html diff --git a/portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.scss b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.scss similarity index 100% rename from portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.scss diff --git a/portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.spec.ts similarity index 100% rename from portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.spec.ts diff --git a/portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.ts b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.ts similarity index 87% rename from portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.ts index 6a59556491..5a4f0a54d4 100644 --- a/portal-frontend/src/app/features/application-details/nfu-details/nfu-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; @Component({ selector: 'app-nfu-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/application-details/parcel/parcel.component.html b/portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/parcel/parcel.component.html rename to portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.html diff --git a/portal-frontend/src/app/features/application-details/parcel/parcel.component.scss b/portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.scss similarity index 93% rename from portal-frontend/src/app/features/application-details/parcel/parcel.component.scss rename to portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.scss index def979ba59..ecf9d4a24c 100644 --- a/portal-frontend/src/app/features/application-details/parcel/parcel.component.scss +++ b/portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/functions' as *; +@use '../../../../../styles/functions' as *; .owner-information { display: grid; diff --git a/portal-frontend/src/app/features/application-details/parcel/parcel.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.spec.ts similarity index 79% rename from portal-frontend/src/app/features/application-details/parcel/parcel.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.spec.ts index 48836143c3..72ddecbc10 100644 --- a/portal-frontend/src/app/features/application-details/parcel/parcel.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.spec.ts @@ -3,10 +3,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; import { ParcelComponent } from './parcel.component'; describe('ParcelComponent', () => { diff --git a/portal-frontend/src/app/features/application-details/parcel/parcel.component.ts b/portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.ts similarity index 88% rename from portal-frontend/src/app/features/application-details/parcel/parcel.component.ts rename to portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.ts index 85bfaa28a7..e06e3389d8 100644 --- a/portal-frontend/src/app/features/application-details/parcel/parcel.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/parcel/parcel.component.ts @@ -2,21 +2,21 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; import { getDiff } from 'recursive-diff'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationOwnerDto } from '../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; import { ApplicationParcelDto, ApplicationParcelUpdateDto, PARCEL_OWNERSHIP_TYPE, PARCEL_TYPE, -} from '../../../services/application-parcel/application-parcel.dto'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { BaseCodeDto } from '../../../shared/dto/base.dto'; -import { formatBooleanToYesNoString } from '../../../shared/utils/boolean-helper'; -import { getLetterCombinations } from '../../../shared/utils/number-to-letter-helper'; +} from '../../../../services/application-parcel/application-parcel.dto'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { BaseCodeDto } from '../../../../shared/dto/base.dto'; +import { formatBooleanToYesNoString } from '../../../../shared/utils/boolean-helper'; +import { getLetterCombinations } from '../../../../shared/utils/number-to-letter-helper'; export class ApplicationParcelBasicValidation { // indicates general validity check state, including owner related information diff --git a/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.html b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.html rename to portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.html diff --git a/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.scss b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.scss similarity index 83% rename from portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.scss index 4fc6435f3f..f81fc4394b 100644 --- a/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.scss +++ b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/functions' as *; +@use '../../../../../styles/functions' as *; .soil-table { display: grid; diff --git a/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.spec.ts similarity index 83% rename from portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.spec.ts index 1fa5655aa5..0c306faca0 100644 --- a/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; import { PfrsDetailsComponent } from './pfrs-details.component'; diff --git a/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.ts b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts similarity index 87% rename from portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts index 398fedc975..d719c83504 100644 --- a/portal-frontend/src/app/features/application-details/pfrs-details/pfrs-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; @Component({ selector: 'app-pfrs-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.html b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.html rename to portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.html diff --git a/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.scss b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.scss similarity index 77% rename from portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.scss index 2444367585..0c540ce5fb 100644 --- a/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.scss +++ b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/functions' as *; +@use '../../../../../styles/functions' as *; .soil-table { display: grid; diff --git a/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.spec.ts similarity index 83% rename from portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.spec.ts index 4e7a277f6b..cadffa92a8 100644 --- a/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; import { PofoDetailsComponent } from './pofo-details.component'; diff --git a/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.ts b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts similarity index 86% rename from portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts index da763c693c..c412d258e5 100644 --- a/portal-frontend/src/app/features/application-details/pofo-details/pofo-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; @Component({ selector: 'app-pofo-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.html b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/roso-details/roso-details.component.html rename to portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.html diff --git a/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.scss b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.scss similarity index 77% rename from portal-frontend/src/app/features/application-details/roso-details/roso-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.scss index 2444367585..0c540ce5fb 100644 --- a/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.scss +++ b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/functions' as *; +@use '../../../../../styles/functions' as *; .soil-table { display: grid; diff --git a/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.spec.ts similarity index 83% rename from portal-frontend/src/app/features/application-details/roso-details/roso-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.spec.ts index c3cbf56750..3257d7345c 100644 --- a/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; import { RosoDetailsComponent } from './roso-details.component'; diff --git a/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.ts b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts similarity index 86% rename from portal-frontend/src/app/features/application-details/roso-details/roso-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts index 175830279f..7b4d0d5c5e 100644 --- a/portal-frontend/src/app/features/application-details/roso-details/roso-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; @Component({ selector: 'app-roso-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.html b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/subd-details/subd-details.component.html rename to portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.html diff --git a/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.scss b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.scss similarity index 79% rename from portal-frontend/src/app/features/application-details/subd-details/subd-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.scss index 8cbdd6b2f5..8e046d8314 100644 --- a/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.scss +++ b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/functions' as *; +@use '../../../../../styles/functions' as *; .parcel-table { display: grid; diff --git a/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.spec.ts similarity index 83% rename from portal-frontend/src/app/features/application-details/subd-details/subd-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.spec.ts index a5e8298700..f50de4439c 100644 --- a/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; import { SubdDetailsComponent } from './subd-details.component'; diff --git a/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.ts b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts similarity index 83% rename from portal-frontend/src/app/features/application-details/subd-details/subd-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts index 22c034eadc..61ee6278fe 100644 --- a/portal-frontend/src/app/features/application-details/subd-details/subd-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts @@ -1,10 +1,10 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { PARCEL_TYPE } from '../../../services/application-parcel/application-parcel.dto'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { PARCEL_TYPE } from '../../../../services/application-parcel/application-parcel.dto'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; @Component({ selector: 'app-subd-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.html b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.html similarity index 100% rename from portal-frontend/src/app/features/application-details/tur-details/tur-details.component.html rename to portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.html diff --git a/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.scss b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.scss similarity index 100% rename from portal-frontend/src/app/features/application-details/tur-details/tur-details.component.scss rename to portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.scss diff --git a/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.spec.ts similarity index 88% rename from portal-frontend/src/app/features/application-details/tur-details/tur-details.component.spec.ts rename to portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.spec.ts index bcfa395b2d..f3125dd612 100644 --- a/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { TurDetailsComponent } from './tur-details.component'; diff --git a/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.ts b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts similarity index 85% rename from portal-frontend/src/app/features/application-details/tur-details/tur-details.component.ts rename to portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts index d9dee09f1b..dc196ca276 100644 --- a/portal-frontend/src/app/features/application-details/tur-details/tur-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts @@ -1,8 +1,8 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; @Component({ selector: 'app-tur-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html rename to portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.html diff --git a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.scss b/portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.scss similarity index 94% rename from portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.scss index ce2d1e3d58..454f80895e 100644 --- a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.scss @@ -1,5 +1,5 @@ -@use '../../../../styles/functions' as *; -@use '../../../../styles/colors'; +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; .step-header { padding: rem(16) 0; diff --git a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.spec.ts similarity index 87% rename from portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.spec.ts index 3f2c3cd898..d435997d09 100644 --- a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.spec.ts @@ -3,8 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatRadioModule } from '@angular/material/radio'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { CodeService } from '../../../services/code/code.service'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../../services/code/code.service'; import { ChangeApplicationTypeDialogComponent } from './change-application-type-dialog.component'; describe('ChangeApplicationTypeDialogComponent', () => { diff --git a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts similarity index 92% rename from portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts index 5ce7921a58..88577ac71f 100644 --- a/portal-frontend/src/app/features/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/change-application-type-dialog/change-application-type-dialog.component.ts @@ -1,10 +1,10 @@ import { AfterViewChecked, Component, Inject, OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatRadioChange } from '@angular/material/radio'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { ApplicationTypeDto } from '../../../services/code/code.dto'; -import { CodeService } from '../../../services/code/code.service'; -import { scrollToElement } from '../../../shared/utils/scroll-helper'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationTypeDto } from '../../../../services/code/code.dto'; +import { CodeService } from '../../../../services/code/code.service'; +import { scrollToElement } from '../../../../shared/utils/scroll-helper'; export enum ApplicationChangeTypeStepsEnum { warning = 0, diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts similarity index 98% rename from portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts rename to portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts index 71801160f1..c70691f53a 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts @@ -7,7 +7,7 @@ import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; -import { SharedModule } from '../../shared/shared.module'; +import { SharedModule } from '../../../shared/shared.module'; import { ApplicationDetailsModule } from '../application-details/application-details.module'; import { ChangeApplicationTypeDialogComponent } from './change-application-type-dialog/change-application-type-dialog.component'; import { EditSubmissionComponent } from './edit-submission.component'; diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/edit-submission.component.html rename to portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.html diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.scss b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.scss similarity index 98% rename from portal-frontend/src/app/features/edit-submission/edit-submission.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.scss index cb18914260..e745545965 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.scss @@ -1,5 +1,5 @@ -@use '../../../styles/functions' as *; -@use '../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; .header { margin: rem(24) 0; diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.spec.ts similarity index 72% rename from portal-frontend/src/app/features/edit-submission/edit-submission.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.spec.ts index 4c6636f950..b7d58a31c0 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.spec.ts @@ -2,13 +2,13 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { RouterTestingModule } from '@angular/router/testing'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewService } from '../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { CodeService } from '../../services/code/code.service'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../services/code/code.service'; import { MatDialogModule } from '@angular/material/dialog'; -import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; -import { ToastService } from '../../services/toast/toast.service'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../services/toast/toast.service'; import { EditSubmissionComponent } from './edit-submission.component'; describe('EditSubmissionComponent', () => { diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts similarity index 91% rename from portal-frontend/src/app/features/edit-submission/edit-submission.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts index 93f48f15ee..e1240cebe5 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts @@ -3,17 +3,17 @@ import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild } from '@angular import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto } from '../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewService } from '../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionDetailedDto } from '../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { CodeService } from '../../services/code/code.service'; -import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; -import { ToastService } from '../../services/toast/toast.service'; -import { CustomStepperComponent } from '../../shared/custom-stepper/custom-stepper.component'; -import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; -import { scrollToElement } from '../../shared/utils/scroll-helper'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../services/code/code.service'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../services/toast/toast.service'; +import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; +import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; +import { scrollToElement } from '../../../shared/utils/scroll-helper'; import { ChangeApplicationTypeDialogComponent } from './change-application-type-dialog/change-application-type-dialog.component'; import { LandUseComponent } from './land-use/land-use.component'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; diff --git a/portal-frontend/src/app/features/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.module.ts similarity index 84% rename from portal-frontend/src/app/features/edit-submission/edit-submission.module.ts rename to portal-frontend/src/app/features/applications/edit-submission/edit-submission.module.ts index d784d512bb..bc02d6b043 100644 --- a/portal-frontend/src/app/features/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.module.ts @@ -1,8 +1,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; -import { CanDeactivateGuard } from '../../shared/guard/can-deactivate.guard'; -import { SharedModule } from '../../shared/shared.module'; +import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; +import { SharedModule } from '../../../shared/shared.module'; import { EditSubmissionBaseModule } from './edit-submission-base.module'; import { EditSubmissionComponent } from './edit-submission.component'; import { StepComponent } from './step.partial'; diff --git a/portal-frontend/src/app/features/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts similarity index 87% rename from portal-frontend/src/app/features/edit-submission/files-step.partial.ts rename to portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts index 012d128750..2840e92774 100644 --- a/portal-frontend/src/app/features/edit-submission/files-step.partial.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts @@ -1,9 +1,9 @@ import { Component, Input } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { FileHandle } from '../../shared/file-drag-drop/drag-drop.directive'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { FileHandle } from '../../../shared/file-drag-drop/drag-drop.directive'; import { RemoveFileConfirmationDialogComponent } from '../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; import { StepComponent } from './step.partial'; diff --git a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html b/portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/land-use/land-use.component.html rename to portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.scss similarity index 55% rename from portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.scss index 8223c57759..26003d9afa 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.scss @@ -1,5 +1,5 @@ @use '../../../../../styles/functions' as *; -section { - margin-top: rem(36); +h5 { + margin-top: rem(12) !important; } diff --git a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.spec.ts similarity index 86% rename from portal-frontend/src/app/features/edit-submission/land-use/land-use.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.spec.ts index 804f60a96f..340875b1a2 100644 --- a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.spec.ts @@ -3,8 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { LandUseComponent } from './land-use.component'; diff --git a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts b/portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.ts similarity index 95% rename from portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.ts index 6701fb7145..b6b66ecd15 100644 --- a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/land-use/land-use.component.ts @@ -2,8 +2,8 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { EditApplicationSteps } from '../edit-submission.component'; import { StepComponent } from '../step.partial'; diff --git a/portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.html b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.html rename to portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.html diff --git a/portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.scss b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.scss similarity index 83% rename from portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.scss index 601281e4d4..7a223440c8 100644 --- a/portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.scss @@ -1,5 +1,5 @@ -@use '../../../../styles/functions' as *; -@use '../../../../styles/colors'; +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; section { margin-top: rem(32); diff --git a/portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.spec.ts similarity index 79% rename from portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.spec.ts index 81478babdd..3ff3b56c7d 100644 --- a/portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.spec.ts @@ -4,11 +4,11 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { CodeService } from '../../../services/code/code.service'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../../services/code/code.service'; import { OtherAttachmentsComponent } from './other-attachments.component'; diff --git a/portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.ts b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts similarity index 91% rename from portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts index 64c47ce011..2984985c68 100644 --- a/portal-frontend/src/app/features/edit-submission/other-attachments/other-attachments.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts @@ -9,10 +9,10 @@ import { ApplicationDocumentUpdateDto, DOCUMENT_SOURCE, DOCUMENT_TYPE, -} from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { CodeService } from '../../../services/code/code.service'; +} from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../../services/code/code.service'; import { EditApplicationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.html rename to portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.html diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.scss similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.scss diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.spec.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.spec.ts diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component.ts diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.html b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.html rename to portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.scss similarity index 57% rename from portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.scss index 8223c57759..93a134c009 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.scss @@ -1,5 +1,5 @@ @use '../../../../../styles/functions' as *; -section { - margin-top: rem(36); +p { + margin: rem(16) 0 !important; } diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.spec.ts similarity index 82% rename from portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.spec.ts index c975222467..1153e907eb 100644 --- a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.spec.ts @@ -4,10 +4,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { ToastService } from '../../../services/toast/toast.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ToastService } from '../../../../services/toast/toast.service'; import { OtherParcelsComponent } from './other-parcels.component'; diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts similarity index 92% rename from portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts index 0af4fccb8a..d15c92e1f3 100644 --- a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts @@ -8,19 +8,19 @@ import { APPLICATION_OWNER, ApplicationOwnerDetailedDto, ApplicationOwnerDto, -} from '../../../services/application-owner/application-owner.dto'; +} from '../../../../services/application-owner/application-owner.dto'; import { ApplicationParcelDto, ApplicationParcelUpdateDto, PARCEL_TYPE, -} from '../../../services/application-parcel/application-parcel.dto'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { ToastService } from '../../../services/toast/toast.service'; -import { formatBooleanToString } from '../../../shared/utils/boolean-helper'; -import { getLetterCombinations } from '../../../shared/utils/number-to-letter-helper'; -import { parseStringToBoolean } from '../../../shared/utils/string-helper'; +} from '../../../../services/application-parcel/application-parcel.dto'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; +import { getLetterCombinations } from '../../../../shared/utils/number-to-letter-helper'; +import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../edit-submission.component'; import { DeleteParcelDialogComponent } from '../parcel-details/delete-parcel/delete-parcel-dialog.component'; import { ParcelEntryFormData } from '../parcel-details/parcel-entry/parcel-entry.component'; diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss similarity index 72% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss index c5273686ac..a4fd5b9b0f 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../styles/functions' as *; -@use '../../../../../styles/colors'; +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; .actions { button:not(:last-child) { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts similarity index 92% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts index 950dd0a70d..97ab2104aa 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts @@ -2,7 +2,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; import { ApplicationCrownOwnerDialogComponent } from './application-crown-owner-dialog.component'; diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts similarity index 94% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts index b36df6d272..fc2981a6fe 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts @@ -6,8 +6,8 @@ import { ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, -} from '../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +} from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; @Component({ selector: 'app-application-crown-owner-dialog', diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss similarity index 75% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss index b5c967defe..2af88008a7 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../styles/functions' as *; -@use '../../../../../styles/colors'; +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; .actions { button:not(:last-child) { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts similarity index 85% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts index 3c2df1f4ac..8e9cd1894e 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts @@ -2,9 +2,9 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; -import { CodeService } from '../../../../services/code/code.service'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { CodeService } from '../../../../../services/code/code.service'; import { ApplicationOwnerDialogComponent } from './application-owner-dialog.component'; diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts similarity index 92% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts index c826a75c7a..37fe30f179 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts @@ -7,17 +7,17 @@ import { ApplicationDocumentTypeDto, DOCUMENT_SOURCE, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { APPLICATION_OWNER, ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, -} from '../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; -import { CodeService } from '../../../../services/code/code.service'; -import { FileHandle } from '../../../../shared/file-drag-drop/drag-drop.directive'; +} from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { CodeService } from '../../../../../services/code/code.service'; +import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; @Component({ diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts similarity index 91% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts index 9a2dcddf62..002133241d 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts @@ -2,7 +2,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; import { ApplicationOwnersDialogComponent } from './application-owners-dialog.component'; describe('ApplicationOwnersDialogComponent', () => { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts similarity index 87% rename from portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts index 53ae86801d..83c7335b90 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; @Component({ selector: 'app-application-owner-dialog', diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.html rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.scss b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.scss similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.scss diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.spec.ts similarity index 93% rename from portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.spec.ts index f495c53d83..9beff5eaf0 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.spec.ts @@ -4,7 +4,7 @@ import { HttpClient } from '@angular/common/http'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; import { DeleteParcelDialogComponent } from './delete-parcel-dialog.component'; describe('DeleteParcelDialogComponent', () => { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.ts similarity index 91% rename from portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.ts index 83161500ec..f86e0979ab 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.ts @@ -1,6 +1,6 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; export enum ApplicationParcelDeleteStepsEnum { warning = 0, @@ -44,7 +44,7 @@ export class DeleteParcelDialogComponent { async onDelete() { const result = await this.applicationParcelService.deleteMany([this.parcelUuid]); - + if (result) { this.onCancel(true); } diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.html rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.html diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.scss b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.scss similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.scss diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.spec.ts similarity index 83% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.spec.ts index 23ecb2e9d1..14df0a689b 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.spec.ts @@ -4,10 +4,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ToastService } from '../../../services/toast/toast.service'; +import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; import { ParcelDetailsComponent } from './parcel-details.component'; describe('ParcelDetailsComponent', () => { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts similarity index 92% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts index 52fb6bbc48..6c2f6e4cee 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-details.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts @@ -2,16 +2,16 @@ import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { BehaviorSubject, takeUntil } from 'rxjs'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; import { ApplicationParcelDto, ApplicationParcelUpdateDto, PARCEL_TYPE, -} from '../../../services/application-parcel/application-parcel.dto'; -import { ApplicationParcelService } from '../../../services/application-parcel/application-parcel.service'; -import { ToastService } from '../../../services/toast/toast.service'; -import { parseStringToBoolean } from '../../../shared/utils/string-helper'; +} from '../../../../services/application-parcel/application-parcel.dto'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../edit-submission.component'; import { StepComponent } from '../step.partial'; import { DeleteParcelDialogComponent } from './delete-parcel/delete-parcel-dialog.component'; diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html diff --git a/portal-frontend/src/app/features/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.scss b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss similarity index 86% rename from portal-frontend/src/app/features/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss index 5258112251..c4e03e5c59 100644 --- a/portal-frontend/src/app/features/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss @@ -1,4 +1,4 @@ -@use '../../../../styles/functions' as *; +@use '../../../../../../../styles/functions' as *; .margin-bottom-1 { margin-bottom: rem(16); diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.scss b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.scss similarity index 94% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.scss index 6ee187a180..223b120fa2 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../styles/functions' as *; -@use '../../../../../styles/colors'; +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; .owner-option { display: flex; diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.spec.ts similarity index 79% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.spec.ts index 6fc99c190b..9df02aa086 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.spec.ts @@ -5,12 +5,12 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatDialog } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; -import { ApplicationParcelDto } from '../../../../services/application-parcel/application-parcel.dto'; -import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; -import { ParcelService } from '../../../../services/parcel/parcel.service'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { ApplicationParcelDto } from '../../../../../services/application-parcel/application-parcel.dto'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { ParcelService } from '../../../../../services/parcel/parcel.service'; import { ParcelEntryComponent } from './parcel-entry.component'; describe('ParcelEntryComponent', () => { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts similarity index 95% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts index fc8aec1045..b6567a7ea4 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts @@ -6,18 +6,18 @@ import { BehaviorSubject } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; import { ApplicationParcelDto, PARCEL_OWNERSHIP_TYPE, -} from '../../../../services/application-parcel/application-parcel.dto'; -import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; -import { ParcelService } from '../../../../services/parcel/parcel.service'; -import { FileHandle } from '../../../../shared/file-drag-drop/drag-drop.directive'; -import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; +} from '../../../../../services/application-parcel/application-parcel.dto'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { ParcelService } from '../../../../../services/parcel/parcel.service'; +import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; +import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; import { ApplicationCrownOwnerDialogComponent } from '../application-crown-owner-dialog/application-crown-owner-dialog.component'; import { ApplicationOwnerDialogComponent } from '../application-owner-dialog/application-owner-dialog.component'; diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss similarity index 66% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss index 926bc5711a..fb105e09eb 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../styles/functions' as *; -@use '../../../../../styles/colors'; +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; .actions { button:not(:last-child) { diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts similarity index 82% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts index 23032e93d9..36430e28e6 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts @@ -1,8 +1,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; -import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { ParcelOwnersComponent } from './parcel-owners.component'; diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts similarity index 90% rename from portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts index 42cbd8be63..760e03f11a 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts @@ -1,8 +1,8 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { ApplicationOwnerDto, APPLICATION_OWNER } from '../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; -import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { ApplicationOwnerDto, APPLICATION_OWNER } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { ApplicationCrownOwnerDialogComponent } from '../application-crown-owner-dialog/application-crown-owner-dialog.component'; import { ApplicationOwnerDialogComponent } from '../application-owner-dialog/application-owner-dialog.component'; diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.html rename to portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.html diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.scss b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.scss similarity index 90% rename from portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.scss index 56c38ab52e..ba5b4f2a09 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.scss @@ -1,5 +1,5 @@ -@use '../../../../styles/functions' as *; -@use '../../../../styles/colors'; +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; h4 { margin-bottom: rem(24) !important; diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.spec.ts similarity index 74% rename from portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.spec.ts index db1a76b2cd..1bc4cce935 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.spec.ts @@ -3,13 +3,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { UserDto } from '../../../services/authentication/authentication.dto'; -import { AuthenticationService } from '../../../services/authentication/authentication.service'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { UserDto } from '../../../../services/authentication/authentication.dto'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; import { PrimaryContactComponent } from './primary-contact.component'; diff --git a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts similarity index 94% rename from portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts index ed32798620..cc5f41607d 100644 --- a/portal-frontend/src/app/features/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts @@ -3,12 +3,12 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { AuthenticationService } from '../../../services/authentication/authentication.service'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; import { EditApplicationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.scss similarity index 66% rename from portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.scss index 8ad03f05a0..1a30448410 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../styles/functions' as *; +@use '../../../../../../styles/functions' as *; section { margin-top: rem(36); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts similarity index 79% rename from portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts index 38c8bc753e..4cc2056ebb 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.spec.ts @@ -4,10 +4,10 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { ExclProposalComponent } from './excl-proposal.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts similarity index 88% rename from portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts index 9205eb6159..638e8569b5 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/excl-proposal/excl-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts @@ -6,11 +6,11 @@ import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; -import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.scss similarity index 66% rename from portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.scss index b435af700d..952b837267 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../styles/functions' as *; +@use '../../../../../../styles/functions' as *; section { margin-top: rem(32); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts similarity index 74% rename from portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts index bda063fcdc..a6d0e67ae7 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.spec.ts @@ -3,13 +3,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { UserDto } from '../../../../services/authentication/authentication.dto'; -import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { UserDto } from '../../../../../services/authentication/authentication.dto'; +import { AuthenticationService } from '../../../../../services/authentication/authentication.service'; import { InclProposalComponent } from './incl-proposal.component'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; describe('InclProposalComponent', () => { let component: InclProposalComponent; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts similarity index 88% rename from portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts index 9aac67ce46..f56da91fb4 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/incl-proposal/incl-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts @@ -1,20 +1,20 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; -import { AuthenticationService } from '../../../../services/authentication/authentication.service'; -import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; -import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +import { AuthenticationService } from '../../../../../services/authentication/authentication.service'; +import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; +import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { FilesStepComponent } from '../../files-step.partial'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { MatDialog } from '@angular/material/dialog'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { ApplicationDocumentDto, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; +} from '../../../../../services/application-document/application-document.dto'; import { EditApplicationSteps } from '../../edit-submission.component'; import { takeUntil } from 'rxjs'; -import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; interface InclForm { hectares: FormControl; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.html diff --git a/portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.scss similarity index 86% rename from portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.scss index c0b978492d..c4e03e5c59 100644 --- a/portal-frontend/src/app/features/edit-submission/parcel-details/delete-parcel/delete-parcel-dialog.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../styles/functions' as *; +@use '../../../../../../../styles/functions' as *; .margin-bottom-1 { margin-bottom: rem(16); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.spec.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.spec.ts diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component.ts diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.scss similarity index 82% rename from portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.scss index b9a8a44ecd..b3f3123a12 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../styles/functions' as *; +@use '../../../../../../styles/functions' as *; section { margin-top: rem(36); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.spec.ts similarity index 76% rename from portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.spec.ts index 3a729be056..8436de1c4e 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.spec.ts @@ -4,11 +4,11 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; -import { CodeService } from '../../../../services/code/code.service'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../../../services/code/code.service'; import { NaruProposalComponent } from './naru-proposal.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts similarity index 93% rename from portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts index 74642f793d..7a5a8ff95b 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/naru-proposal/naru-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts @@ -7,15 +7,15 @@ import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto, NaruSubtypeDto, -} from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; -import { CodeService } from '../../../../services/code/code.service'; -import { formatBooleanToYesNoString } from '../../../../shared/utils/boolean-helper'; +} from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../../../services/code/code.service'; +import { formatBooleanToYesNoString } from '../../../../../shared/utils/boolean-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; import { SoilTableData } from '../soil-table/soil-table.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.scss new file mode 100644 index 0000000000..56caa68a82 --- /dev/null +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.scss @@ -0,0 +1,5 @@ +@use '../../../../../../styles/functions' as *; + +section { + margin-top: rem(36); +} diff --git a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.spec.ts similarity index 84% rename from portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.spec.ts index 823f9fb6ae..c565dfdf9d 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.spec.ts @@ -3,8 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { NfuProposalComponent } from './nfu-proposal.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts similarity index 94% rename from portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts index a69d6e39d9..ccb753d4c9 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts @@ -2,9 +2,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; -import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { StepComponent } from '../../step.partial'; import { SoilTableData } from '../soil-table/soil-table.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.scss new file mode 100644 index 0000000000..56caa68a82 --- /dev/null +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.scss @@ -0,0 +1,5 @@ +@use '../../../../../../styles/functions' as *; + +section { + margin-top: rem(36); +} diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.spec.ts similarity index 77% rename from portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.spec.ts index 156e96014b..72ff08d305 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.spec.ts @@ -4,11 +4,11 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { PfrsProposalComponent } from './pfrs-proposal.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts similarity index 93% rename from portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts index 85f439bdf2..07a2f4552d 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts @@ -6,13 +6,13 @@ import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; -import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; -import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; -import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; +import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; +import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; import { SoilTableData } from '../soil-table/soil-table.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.scss new file mode 100644 index 0000000000..56caa68a82 --- /dev/null +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.scss @@ -0,0 +1,5 @@ +@use '../../../../../../styles/functions' as *; + +section { + margin-top: rem(36); +} diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.spec.ts similarity index 77% rename from portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.spec.ts index b508ea45c5..8c73afeec4 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.spec.ts @@ -4,11 +4,11 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { PofoProposalComponent } from './pofo-proposal.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts similarity index 91% rename from portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts index 0c874af44c..b3ccbb4b96 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts @@ -6,12 +6,12 @@ import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; -import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; -import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; +import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; import { SoilTableData } from '../soil-table/soil-table.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.scss new file mode 100644 index 0000000000..56caa68a82 --- /dev/null +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.scss @@ -0,0 +1,5 @@ +@use '../../../../../../styles/functions' as *; + +section { + margin-top: rem(36); +} diff --git a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.spec.ts similarity index 77% rename from portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.spec.ts index f0b8aca06b..6c17a24fe7 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.spec.ts @@ -4,11 +4,11 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { RosoProposalComponent } from './roso-proposal.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts similarity index 91% rename from portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts index 1500bd6da3..83812714ac 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/roso-proposal/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts @@ -6,12 +6,12 @@ import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; -import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; -import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; +import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; import { SoilTableData } from '../soil-table/soil-table.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/soil-table/soil-table.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/soil-table/soil-table.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/soil-table/soil-table.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.scss similarity index 95% rename from portal-frontend/src/app/features/edit-submission/proposal/soil-table/soil-table.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.scss index b53e708d53..d1987e6e0e 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/soil-table/soil-table.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../styles/functions' as *; +@use '../../../../../../styles/functions' as *; .container { border: 1px solid #000; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/soil-table/soil-table.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.spec.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/soil-table/soil-table.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.spec.ts diff --git a/portal-frontend/src/app/features/edit-submission/proposal/soil-table/soil-table.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/soil-table/soil-table.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.ts diff --git a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.scss similarity index 62% rename from portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.scss index b50b55c96b..c56600402d 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../styles/functions' as *; +@use '../../../../../../styles/functions' as *; section { margin-top: rem(36); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.spec.ts similarity index 77% rename from portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.spec.ts index 0a9de1f967..688c0570db 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.spec.ts @@ -4,11 +4,11 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { SubdProposalComponent } from './subd-proposal.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts similarity index 89% rename from portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts index 86b663dcd1..e1bfec8990 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/subd-proposal/subd-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts @@ -7,12 +7,12 @@ import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { PARCEL_TYPE } from '../../../../services/application-parcel/application-parcel.dto'; -import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { PARCEL_TYPE } from '../../../../../services/application-parcel/application-parcel.dto'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.html rename to portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.scss similarity index 62% rename from portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.scss index 4526212950..fd01b5a150 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../styles/functions' as *; +@use '../../../../../../styles/functions' as *; section { margin-top: rem(36); diff --git a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.spec.ts similarity index 79% rename from portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.spec.ts index bf47a5864c..c501f29ee4 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.spec.ts @@ -4,10 +4,10 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { TurProposalComponent } from './tur-proposal.component'; diff --git a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts similarity index 90% rename from portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts index f0f9a2e831..0c85764f3e 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/tur-proposal/tur-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts @@ -6,10 +6,10 @@ import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { ApplicationSubmissionUpdateDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.html b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.html rename to portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.html diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.scss b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.scss similarity index 100% rename from portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.scss diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.spec.ts similarity index 79% rename from portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.spec.ts index 69089069ef..acf4ab4aad 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.spec.ts @@ -4,11 +4,11 @@ import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { CodeService } from '../../../services/code/code.service'; -import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; -import { ToastService } from '../../../services/toast/toast.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../../services/code/code.service'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../../services/toast/toast.service'; import { ReviewAndSubmitComponent } from './review-and-submit.component'; diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts similarity index 79% rename from portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts index b20cc04cd3..49dbe47874 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/review-and-submit.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts @@ -2,12 +2,12 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { BehaviorSubject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { CodeService } from '../../../services/code/code.service'; -import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; -import { ToastService } from '../../../services/toast/toast.service'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../../services/code/code.service'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../../services/toast/toast.service'; import { StepComponent } from '../step.partial'; import { SubmitConfirmationDialogComponent } from './submit-confirmation-dialog/submit-confirmation-dialog.component'; diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html rename to portal-frontend/src/app/features/applications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss similarity index 81% rename from portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss index d4f7e0ae48..93f22cbbcb 100644 --- a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../styles/functions' as *; +@use '../../../../../../styles/functions' as *; .checkbox { margin: rem(10) 0; diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts diff --git a/portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts similarity index 100% rename from portal-frontend/src/app/features/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.html b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.html similarity index 100% rename from portal-frontend/src/app/features/edit-submission/select-government/select-government.component.html rename to portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.html diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.scss b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.scss similarity index 100% rename from portal-frontend/src/app/features/edit-submission/select-government/select-government.component.scss rename to portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.scss diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.spec.ts similarity index 81% rename from portal-frontend/src/app/features/edit-submission/select-government/select-government.component.spec.ts rename to portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.spec.ts index 65c6a4522f..c0e86d5608 100644 --- a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.spec.ts @@ -3,9 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatAutocomplete } from '@angular/material/autocomplete'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { CodeService } from '../../../services/code/code.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { CodeService } from '../../../../services/code/code.service'; import { SelectGovernmentComponent } from './select-government.component'; diff --git a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts similarity index 94% rename from portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts rename to portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts index 9acdf56fd4..2fb8d05845 100644 --- a/portal-frontend/src/app/features/edit-submission/select-government/select-government.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts @@ -3,9 +3,9 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { Router } from '@angular/router'; import { map, Observable, startWith, takeUntil } from 'rxjs'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { LocalGovernmentDto } from '../../../services/code/code.dto'; -import { CodeService } from '../../../services/code/code.service'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { LocalGovernmentDto } from '../../../../services/code/code.dto'; +import { CodeService } from '../../../../services/code/code.service'; import { EditApplicationSteps } from '../edit-submission.component'; import { StepComponent } from '../step.partial'; diff --git a/portal-frontend/src/app/features/edit-submission/step.partial.ts b/portal-frontend/src/app/features/applications/edit-submission/step.partial.ts similarity index 86% rename from portal-frontend/src/app/features/edit-submission/step.partial.ts rename to portal-frontend/src/app/features/applications/edit-submission/step.partial.ts index c88bffdbb5..b874e123ef 100644 --- a/portal-frontend/src/app/features/edit-submission/step.partial.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/step.partial.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; import { BehaviorSubject, Subject } from 'rxjs'; -import { ApplicationSubmissionDetailedDto } from '../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; @Component({ selector: 'app-step', diff --git a/portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.html b/portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.html rename to portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.html diff --git a/portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.scss b/portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.scss similarity index 93% rename from portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.scss rename to portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.scss index b4db98c980..5c9b16b19e 100644 --- a/portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.scss +++ b/portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.scss @@ -1,5 +1,5 @@ -@use '../../../../styles/functions' as *; -@use '../../../../styles/colors'; +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; .return-application-modal { width: 80vw; diff --git a/portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.spec.ts similarity index 90% rename from portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.spec.ts rename to portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.spec.ts index 36d99ea003..fabd328ecd 100644 --- a/portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.spec.ts @@ -4,7 +4,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatRadioModule } from '@angular/material/radio'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReturnApplicationDialogComponent } from './return-application-dialog.component'; describe('ReturnApplicationDialogComponent', () => { diff --git a/portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.ts b/portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.ts similarity index 92% rename from portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.ts rename to portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.ts index 803d69ef03..31da37f008 100644 --- a/portal-frontend/src/app/features/review-submission/return-application-dialog/return-application-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/return-application-dialog/return-application-dialog.component.ts @@ -1,6 +1,6 @@ import { Component, Inject, OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; export enum ReturnApplicationStepEnum { reason = 0, diff --git a/portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.html b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.html similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.html rename to portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.html diff --git a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.scss b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.scss similarity index 69% rename from portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.scss rename to portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.scss index 8223c57759..74a771ed0c 100644 --- a/portal-frontend/src/app/features/edit-submission/proposal/pofo-proposal/pofo-proposal.component.scss +++ b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.scss @@ -1,5 +1,5 @@ @use '../../../../../styles/functions' as *; section { - margin-top: rem(36); + margin-bottom: rem(24); } diff --git a/portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.spec.ts similarity index 78% rename from portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.spec.ts rename to portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.spec.ts index f76fcb0c13..5405ebec90 100644 --- a/portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.spec.ts @@ -3,10 +3,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReviewAttachmentsComponent } from './review-attachments.component'; diff --git a/portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts similarity index 91% rename from portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.ts rename to portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts index b15894ef63..8e8b0f4f06 100644 --- a/portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts @@ -5,10 +5,10 @@ import { ApplicationDocumentDto, DOCUMENT_SOURCE, DOCUMENT_TYPE, -} from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; -import { FileHandle } from '../../../shared/file-drag-drop/drag-drop.directive'; +} from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; +import { FileHandle } from '../../../../shared/file-drag-drop/drag-drop.directive'; import { ReviewApplicationFngSteps, ReviewApplicationSteps } from '../review-submission.component'; @Component({ diff --git a/portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.html b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.html similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.html rename to portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.html diff --git a/portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.scss b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.scss similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.scss rename to portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.scss diff --git a/portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.spec.ts similarity index 84% rename from portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.spec.ts rename to portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.spec.ts index e6eddbcf91..006ffafea8 100644 --- a/portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.spec.ts @@ -3,8 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReviewContactInformationComponent } from './review-contact-information.component'; diff --git a/portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.ts similarity index 96% rename from portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.ts rename to portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.ts index 2127f3ba91..40644c8efe 100644 --- a/portal-frontend/src/app/features/review-submission/review-contact-information/review-contact-information.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReviewApplicationSteps } from '../review-submission.component'; @Component({ diff --git a/portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.html b/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.html similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.html rename to portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.html diff --git a/portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.scss b/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.scss similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.scss rename to portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.scss diff --git a/portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.spec.ts similarity index 83% rename from portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.spec.ts rename to portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.spec.ts index e4eec16439..b66188e8d8 100644 --- a/portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.spec.ts @@ -3,8 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReviewOcpComponent } from './review-ocp.component'; diff --git a/portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.ts similarity index 96% rename from portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.ts rename to portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.ts index 3ef0acb117..76305b82c6 100644 --- a/portal-frontend/src/app/features/review-submission/review-ocp/review-ocp.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.ts @@ -3,7 +3,7 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReviewApplicationSteps } from '../review-submission.component'; @Component({ diff --git a/portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.html b/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.html similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.html rename to portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.html diff --git a/portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.scss b/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.scss similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.scss rename to portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.scss diff --git a/portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.spec.ts similarity index 83% rename from portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.spec.ts rename to portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.spec.ts index 42afb3116c..bafadbc900 100644 --- a/portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.spec.ts @@ -3,8 +3,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReviewResolutionComponent } from './review-resolution.component'; diff --git a/portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.ts similarity index 95% rename from portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.ts rename to portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.ts index 3120e39b4c..ada417fcd3 100644 --- a/portal-frontend/src/app/features/review-submission/review-resolution/review-resolution.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.ts @@ -2,7 +2,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angu import { FormControl, FormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReviewApplicationFngSteps, ReviewApplicationSteps } from '../review-submission.component'; @Component({ diff --git a/portal-frontend/src/app/features/review-submission/review-submission.component.html b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.html similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-submission.component.html rename to portal-frontend/src/app/features/applications/review-submission/review-submission.component.html diff --git a/portal-frontend/src/app/features/review-submission/review-submission.component.scss b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.scss similarity index 97% rename from portal-frontend/src/app/features/review-submission/review-submission.component.scss rename to portal-frontend/src/app/features/applications/review-submission/review-submission.component.scss index f06c995246..99408b2af6 100644 --- a/portal-frontend/src/app/features/review-submission/review-submission.component.scss +++ b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.scss @@ -1,5 +1,5 @@ -@use '../../../styles/functions' as *; -@use '../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; .header { margin: rem(24) 0; diff --git a/portal-frontend/src/app/features/review-submission/review-submission.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.spec.ts similarity index 77% rename from portal-frontend/src/app/features/review-submission/review-submission.component.spec.ts rename to portal-frontend/src/app/features/applications/review-submission/review-submission.component.spec.ts index b5e75d3097..bb92624e83 100644 --- a/portal-frontend/src/app/features/review-submission/review-submission.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.spec.ts @@ -4,13 +4,13 @@ import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; -import { ToastService } from '../../services/toast/toast.service'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../services/toast/toast.service'; import { ReviewSubmissionComponent } from './review-submission.component'; describe('ReviewSubmissionComponent', () => { diff --git a/portal-frontend/src/app/features/review-submission/review-submission.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.ts similarity index 89% rename from portal-frontend/src/app/features/review-submission/review-submission.component.ts rename to portal-frontend/src/app/features/applications/review-submission/review-submission.component.ts index f5ba423738..8948e8625a 100644 --- a/portal-frontend/src/app/features/review-submission/review-submission.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.ts @@ -3,20 +3,20 @@ import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, Observable, Subject, combineLatest, of, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto } from '../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewService } from '../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionDto } from '../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; -import { ToastService } from '../../services/toast/toast.service'; -import { CustomStepperComponent } from '../../shared/custom-stepper/custom-stepper.component'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionDto } from '../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../services/toast/toast.service'; +import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; import { ReturnApplicationDialogComponent } from './return-application-dialog/return-application-dialog.component'; import { ReviewContactInformationComponent } from './review-contact-information/review-contact-information.component'; import { ReviewOcpComponent } from './review-ocp/review-ocp.component'; import { ReviewResolutionComponent } from './review-resolution/review-resolution.component'; import { ReviewZoningComponent } from './review-zoning/review-zoning.component'; -import { scrollToElement } from '../../shared/utils/scroll-helper'; +import { scrollToElement } from '../../../shared/utils/scroll-helper'; export enum ReviewApplicationSteps { ContactInformation = 0, diff --git a/portal-frontend/src/app/features/review-submission/review-submission.module.ts b/portal-frontend/src/app/features/applications/review-submission/review-submission.module.ts similarity index 92% rename from portal-frontend/src/app/features/review-submission/review-submission.module.ts rename to portal-frontend/src/app/features/applications/review-submission/review-submission.module.ts index 61923730bf..729ec1b3f8 100644 --- a/portal-frontend/src/app/features/review-submission/review-submission.module.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submission.module.ts @@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; -import { CanDeactivateGuard } from '../../shared/guard/can-deactivate.guard'; -import { SharedModule } from '../../shared/shared.module'; +import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; +import { SharedModule } from '../../../shared/shared.module'; import { ReturnApplicationDialogComponent } from './return-application-dialog/return-application-dialog.component'; import { ReviewSubmissionComponent } from './review-submission.component'; import { ReviewAttachmentsComponent } from './review-attachments/review-attachments.component'; diff --git a/portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.html b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.html similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.html rename to portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.html diff --git a/portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.scss b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.scss similarity index 94% rename from portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.scss rename to portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.scss index 4224790083..df8cff354c 100644 --- a/portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.scss +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.scss @@ -1,5 +1,5 @@ -@use '../../../../styles/functions' as *; -@use '../../../../styles/colors'; +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; section { margin-bottom: rem(24); diff --git a/portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.spec.ts similarity index 72% rename from portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.spec.ts rename to portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.spec.ts index 75663302cd..48641df391 100644 --- a/portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.spec.ts @@ -2,13 +2,13 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionDto } from '../../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; -import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; import { ReviewSubmitFngComponent } from './review-submit-fng.component'; diff --git a/portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts similarity index 85% rename from portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.ts rename to portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts index 869e4323cc..b729afc8d9 100644 --- a/portal-frontend/src/app/features/review-submission/review-submit-fng/review-submit-fng.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts @@ -2,14 +2,14 @@ import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output import { MatExpansionPanel } from '@angular/material/expansion'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionDto } from '../../../services/application-submission/application-submission.dto'; -import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; -import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; -import { MOBILE_BREAKPOINT } from '../../../shared/utils/breakpoints'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionDto } from '../../../../services/application-submission/application-submission.dto'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; +import { CustomStepperComponent } from '../../../../shared/custom-stepper/custom-stepper.component'; +import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; import { ReviewApplicationFngSteps } from '../review-submission.component'; @Component({ diff --git a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.html similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.html rename to portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.html diff --git a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.scss b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.scss similarity index 94% rename from portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.scss rename to portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.scss index d0fe465eb5..6eb113b667 100644 --- a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.scss +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.scss @@ -1,5 +1,5 @@ -@use '../../../../styles/functions' as *; -@use '../../../../styles/colors'; +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; section { margin-bottom: rem(24); diff --git a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.spec.ts similarity index 73% rename from portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.spec.ts rename to portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.spec.ts index 9570eac44a..4bfb483bb5 100644 --- a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.spec.ts @@ -2,12 +2,12 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionDto } from '../../../services/application-submission/application-submission.dto'; -import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionDto } from '../../../../services/application-submission/application-submission.dto'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; import { ReviewSubmitComponent } from './review-submit.component'; diff --git a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts similarity index 89% rename from portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.ts rename to portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts index f618a70cd9..b14ca7b5f0 100644 --- a/portal-frontend/src/app/features/review-submission/review-submit/review-submit.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts @@ -2,14 +2,14 @@ import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output import { MatExpansionPanel } from '@angular/material/expansion'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionDto } from '../../../services/application-submission/application-submission.dto'; -import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; -import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; -import { MOBILE_BREAKPOINT } from '../../../shared/utils/breakpoints'; +import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionDto } from '../../../../services/application-submission/application-submission.dto'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; +import { CustomStepperComponent } from '../../../../shared/custom-stepper/custom-stepper.component'; +import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; import { ReviewApplicationSteps } from '../review-submission.component'; @Component({ diff --git a/portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.html b/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.html similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.html rename to portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.html diff --git a/portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.scss b/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.scss similarity index 100% rename from portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.scss rename to portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.scss diff --git a/portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.spec.ts similarity index 81% rename from portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.spec.ts rename to portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.spec.ts index 38fea723a3..acdf3870c0 100644 --- a/portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.spec.ts @@ -2,8 +2,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReviewZoningComponent } from './review-zoning.component'; diff --git a/portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.ts similarity index 97% rename from portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.ts rename to portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.ts index b16d6d1f2e..7da567bed9 100644 --- a/portal-frontend/src/app/features/review-submission/review-zoning/review-zoning.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.ts @@ -3,7 +3,7 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ReviewApplicationSteps } from '../review-submission.component'; @Component({ diff --git a/portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.html b/portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.html similarity index 100% rename from portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.html rename to portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.html diff --git a/portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.scss b/portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.scss similarity index 59% rename from portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.scss rename to portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.scss index 31f5106ab0..7ca8d1c637 100644 --- a/portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.scss +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.scss @@ -1,5 +1,5 @@ -@use '../../../../styles/functions' as *; -@use '../../../../styles/colors'; +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; .warning { background-color: rgba(colors.$accent-color-light, 0.5); diff --git a/portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.spec.ts b/portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.spec.ts similarity index 86% rename from portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.spec.ts rename to portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.spec.ts index 86fa3ff54c..62a72d72d0 100644 --- a/portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.spec.ts +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { AlcReviewComponent } from './alc-review.component'; diff --git a/portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.ts b/portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.ts similarity index 81% rename from portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.ts rename to portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.ts index df59d564a4..ced2da29b9 100644 --- a/portal-frontend/src/app/features/view-submission/alc-review/alc-review.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/alc-review.component.ts @@ -1,12 +1,12 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { SUBMISSION_STATUS, ApplicationSubmissionDetailedDto, -} from '../../../services/application-submission/application-submission.dto'; +} from '../../../../services/application-submission/application-submission.dto'; @Component({ selector: 'app-alc-review', diff --git a/portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.html b/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.html similarity index 100% rename from portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.html rename to portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.html diff --git a/portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.scss b/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.scss similarity index 82% rename from portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.scss rename to portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.scss index e633e135a7..c92c6fb6a8 100644 --- a/portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.scss +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../styles/functions' as *; -@use '../../../../../styles/colors'; +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; .decision-table { padding: rem(8); diff --git a/portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.spec.ts b/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.spec.ts similarity index 88% rename from portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.spec.ts rename to portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.spec.ts index f1bdfd411f..f56a7194b9 100644 --- a/portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.spec.ts +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock } from '@golevelup/ts-jest'; -import { ApplicationDecisionService } from '../../../../services/application-decision/application-decision.service'; +import { ApplicationDecisionService } from '../../../../../services/application-decision/application-decision.service'; import { DecisionsComponent } from './decisions.component'; diff --git a/portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.ts b/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.ts similarity index 80% rename from portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.ts rename to portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.ts index f1a9c03618..f0c910a4b0 100644 --- a/portal-frontend/src/app/features/view-submission/alc-review/decisions/decisions.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { PortalDecisionDto } from '../../../../services/application-decision/application-decision.dto'; -import { ApplicationDecisionService } from '../../../../services/application-decision/application-decision.service'; +import { PortalDecisionDto } from '../../../../../services/application-decision/application-decision.dto'; +import { ApplicationDecisionService } from '../../../../../services/application-decision/application-decision.service'; @Component({ selector: 'app-decisions[fileNumber]', diff --git a/portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.html b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.html similarity index 100% rename from portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.html rename to portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.html diff --git a/portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.scss b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.scss similarity index 75% rename from portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.scss rename to portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.scss index e27ec16b36..0027146765 100644 --- a/portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.scss +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../styles/colors'; -@use '../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; +@use '../../../../../../styles/functions' as *; .header { display: flex; diff --git a/portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts similarity index 89% rename from portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts rename to portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts index 8956bfbfd1..09eb1292d3 100644 --- a/portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts @@ -1,7 +1,7 @@ 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 { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { SubmissionDocumentsComponent } from './submission-documents.component'; diff --git a/portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.ts b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.ts similarity index 86% rename from portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.ts rename to portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.ts index c4500e4858..e833e82712 100644 --- a/portal-frontend/src/app/features/view-submission/alc-review/submission-documents/submission-documents.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.ts @@ -2,8 +2,8 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; @Component({ selector: 'app-submission-documents', diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.html similarity index 100% rename from portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.html rename to portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.html diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.scss b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.scss similarity index 95% rename from portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.scss rename to portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.scss index 9d4b7c0db9..b2921ee5b8 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.scss +++ b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.scss @@ -1,5 +1,5 @@ -@use '../../../../styles/functions' as *; -@use '../../../../styles/colors'; +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; .warning { align-items: center; diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.spec.ts b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.spec.ts similarity index 68% rename from portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.spec.ts rename to portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.spec.ts index ebc8959a27..d1556cc200 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.spec.ts +++ b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.spec.ts @@ -1,12 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; -import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; import { LfngReviewComponent } from './lfng-review.component'; diff --git a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts similarity index 87% rename from portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts rename to portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts index 013e425394..91d1d69272 100644 --- a/portal-frontend/src/app/features/view-submission/lfng-review/lfng-review.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts @@ -5,15 +5,15 @@ import { ApplicationDocumentDto, DOCUMENT_SOURCE, DOCUMENT_TYPE, -} from '../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +} from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionDetailedDto, SUBMISSION_STATUS, -} from '../../../services/application-submission/application-submission.dto'; -import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +} from '../../../../services/application-submission/application-submission.dto'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; @Component({ selector: 'app-lfng-review', diff --git a/portal-frontend/src/app/features/view-submission/view-submission.component.html b/portal-frontend/src/app/features/applications/view-submission/view-submission.component.html similarity index 100% rename from portal-frontend/src/app/features/view-submission/view-submission.component.html rename to portal-frontend/src/app/features/applications/view-submission/view-submission.component.html diff --git a/portal-frontend/src/app/features/view-submission/view-submission.component.scss b/portal-frontend/src/app/features/applications/view-submission/view-submission.component.scss similarity index 96% rename from portal-frontend/src/app/features/view-submission/view-submission.component.scss rename to portal-frontend/src/app/features/applications/view-submission/view-submission.component.scss index b667637177..473a1ad530 100644 --- a/portal-frontend/src/app/features/view-submission/view-submission.component.scss +++ b/portal-frontend/src/app/features/applications/view-submission/view-submission.component.scss @@ -1,6 +1,6 @@ -@use '../../../styles/functions' as *; -@use '../../../styles/colors'; -@use '../../../styles/navigation-wrapper'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; +@use '../../../../styles/navigation-wrapper'; :host::ng-deep { .content { diff --git a/portal-frontend/src/app/features/view-submission/view-submission.component.spec.ts b/portal-frontend/src/app/features/applications/view-submission/view-submission.component.spec.ts similarity index 76% rename from portal-frontend/src/app/features/view-submission/view-submission.component.spec.ts rename to portal-frontend/src/app/features/applications/view-submission/view-submission.component.spec.ts index 32f56c5b24..c09acad8ed 100644 --- a/portal-frontend/src/app/features/view-submission/view-submission.component.spec.ts +++ b/portal-frontend/src/app/features/applications/view-submission/view-submission.component.spec.ts @@ -3,12 +3,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; -import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; +import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { ViewSubmissionComponent } from './view-submission.component'; diff --git a/portal-frontend/src/app/features/view-submission/view-submission.component.ts b/portal-frontend/src/app/features/applications/view-submission/view-submission.component.ts similarity index 79% rename from portal-frontend/src/app/features/view-submission/view-submission.component.ts rename to portal-frontend/src/app/features/applications/view-submission/view-submission.component.ts index 9656634886..cd8a17a6e8 100644 --- a/portal-frontend/src/app/features/view-submission/view-submission.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/view-submission.component.ts @@ -1,18 +1,18 @@ import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto } from '../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../services/application-document/application-document.service'; -import { ApplicationSubmissionReviewDto } from '../../services/application-submission-review/application-submission-review.dto'; -import { ApplicationSubmissionReviewService } from '../../services/application-submission-review/application-submission-review.service'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationSubmissionReviewDto } from '../../../services/application-submission-review/application-submission-review.dto'; +import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionDetailedDto, SUBMISSION_STATUS, -} from '../../services/application-submission/application-submission.dto'; -import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { PdfGenerationService } from '../../services/pdf-generation/pdf-generation.service'; -import { ConfirmationDialogService } from '../../shared/confirmation-dialog/confirmation-dialog.service'; -import { MOBILE_BREAKPOINT } from '../../shared/utils/breakpoints'; +} from '../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; +import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { MOBILE_BREAKPOINT } from '../../../shared/utils/breakpoints'; enum MOBILE_STEP { INTRODUCTION = 0, diff --git a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.scss b/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.scss deleted file mode 100644 index 12009e11fb..0000000000 --- a/portal-frontend/src/app/features/edit-submission/land-use/land-use.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@use '../../../../styles/functions' as *; - -h5 { - margin-top: rem(12) !important; -} diff --git a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.scss b/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.scss deleted file mode 100644 index 154584d3bb..0000000000 --- a/portal-frontend/src/app/features/edit-submission/other-parcels/other-parcels.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@use '../../../../styles/functions' as *; - -p { - margin: rem(16) 0 !important; -} diff --git a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.scss b/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.scss deleted file mode 100644 index 8223c57759..0000000000 --- a/portal-frontend/src/app/features/edit-submission/proposal/nfu-proposal/nfu-proposal.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@use '../../../../../styles/functions' as *; - -section { - margin-top: rem(36); -} diff --git a/portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.scss b/portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.scss deleted file mode 100644 index 84bef3449e..0000000000 --- a/portal-frontend/src/app/features/review-submission/review-attachments/review-attachments.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@use '../../../../styles/functions' as *; - -section { - margin-bottom: rem(24); -} From 6f6c8ec32ac659abc16dd6b73e52373184ff9222 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 8 Aug 2023 14:36:26 -0700 Subject: [PATCH 218/954] Add front-end for NOI Creation * Update create dialog for NOIs * Add module for editing NOIs * Add landing page for viewing NOIs --- portal-frontend/src/app/app-routing.module.ts | 29 +++- portal-frontend/src/app/app.module.ts | 10 +- ...iew-application-submission.component.html} | 0 ...iew-application-submission.component.scss} | 0 ...-application-submission.component.spec.ts} | 10 +- ... view-application-submission.component.ts} | 8 +- .../create-application-dialog.component.html | 119 -------------- .../create-submission-dialog.component.html | 150 ++++++++++++++++++ .../create-submission-dialog.component.scss} | 0 ...reate-submission-dialog.component.spec.ts} | 17 +- .../create-submission-dialog.component.ts} | 88 ++++++---- .../src/app/features/home/home.component.ts | 4 +- .../edit-submission.component.html | 1 + .../edit-submission.component.scss | 0 .../edit-submission.component.spec.ts | 23 +++ .../edit-submission.component.ts | 10 ++ .../edit-submission/edit-submission.module.ts | 18 +++ ...notice-of-intent-submission.component.html | 1 + ...notice-of-intent-submission.component.scss | 0 ...ice-of-intent-submission.component.spec.ts | 22 +++ ...w-notice-of-intent-submission.component.ts | 8 + .../src/app/services/code/code.dto.ts | 10 +- .../src/app/services/code/code.service.ts | 3 +- .../application-submission-draft.service.ts | 79 +++++++++ ...of-intent-submission-draft.service.spec.ts | 95 +++++++++++ .../notice-of-intent-submission.dto.ts | 45 ++++++ ...otice-of-intent-submission.service.spec.ts | 132 +++++++++++++++ .../notice-of-intent-submission.service.ts | 123 ++++++++++++++ .../notice-of-intent.service.ts | 8 +- .../src/portal/code/code.controller.spec.ts | 8 + .../alcs/src/portal/code/code.controller.ts | 5 + .../notice-of-intent-submission.dto.ts | 4 - ...otice-of-intent-submission.service.spec.ts | 4 +- .../notice-of-intent-submission.service.ts | 4 +- .../apps/alcs/src/portal/portal.module.ts | 5 + 35 files changed, 856 insertions(+), 187 deletions(-) rename portal-frontend/src/app/features/applications/view-submission/{view-submission.component.html => view-application-submission.component.html} (100%) rename portal-frontend/src/app/features/applications/view-submission/{view-submission.component.scss => view-application-submission.component.scss} (100%) rename portal-frontend/src/app/features/applications/view-submission/{view-submission.component.spec.ts => view-application-submission.component.spec.ts} (88%) rename portal-frontend/src/app/features/applications/view-submission/{view-submission.component.ts => view-application-submission.component.ts} (94%) delete mode 100644 portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html create mode 100644 portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html rename portal-frontend/src/app/features/{create-application-dialog/create-application-dialog.component.scss => create-submission-dialog/create-submission-dialog.component.scss} (100%) rename portal-frontend/src/app/features/{create-application-dialog/create-application-dialog.component.spec.ts => create-submission-dialog/create-submission-dialog.component.spec.ts} (62%) rename portal-frontend/src/app/features/{create-application-dialog/create-application-dialog.component.ts => create-submission-dialog/create-submission-dialog.component.ts} (55%) create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-submission/application-submission-draft.service.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission-draft.service.spec.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.service.ts diff --git a/portal-frontend/src/app/app-routing.module.ts b/portal-frontend/src/app/app-routing.module.ts index 41b3578a9c..486dcbdb63 100644 --- a/portal-frontend/src/app/app-routing.module.ts +++ b/portal-frontend/src/app/app-routing.module.ts @@ -4,7 +4,8 @@ import { AuthorizationComponent } from './features/authorization/authorization.c import { HomeComponent } from './features/home/home.component'; import { LandingPageComponent } from './features/landing-page/landing-page.component'; import { LoginComponent } from './features/login/login.component'; -import { ViewSubmissionComponent } from './features/applications/view-submission/view-submission.component'; +import { ViewApplicationSubmissionComponent } from './features/applications/view-submission/view-application-submission.component'; +import { ViewNoticeOfIntentSubmissionComponent } from './features/notice-of-intents/view-submission/view-notice-of-intent-submission.component'; import { AlcsAuthGuard } from './services/authentication/alcs-auth.guard'; import { AuthGuard } from './services/authentication/auth.guard'; @@ -33,28 +34,46 @@ const routes: Routes = [ { title: 'View Application', path: 'application/:fileId', - component: ViewSubmissionComponent, + component: ViewApplicationSubmissionComponent, canActivate: [AuthGuard], }, { title: 'Edit Application', path: 'application/:fileId/edit', canActivate: [AuthGuard], - loadChildren: () => import('./features/applications/edit-submission/edit-submission.module').then((m) => m.EditSubmissionModule), + loadChildren: () => + import('./features/applications/edit-submission/edit-submission.module').then((m) => m.EditSubmissionModule), }, { title: 'ALCS Edit Application', path: 'alcs/application/:fileId/edit', canActivate: [AlcsAuthGuard], loadChildren: () => - import('./features/applications/alcs-edit-submission/alcs-edit-submission.module').then((m) => m.AlcsEditSubmissionModule), + import('./features/applications/alcs-edit-submission/alcs-edit-submission.module').then( + (m) => m.AlcsEditSubmissionModule + ), }, { title: 'Review Application', path: 'application/:fileId/review', canActivate: [AuthGuard], loadChildren: () => - import('./features/applications/review-submission/review-submission.module').then((m) => m.ReviewSubmissionModule), + import('./features/applications/review-submission/review-submission.module').then( + (m) => m.ReviewSubmissionModule + ), + }, + { + title: 'View Notice of Intent', + path: 'notice-of-intent/:fileId', + component: ViewNoticeOfIntentSubmissionComponent, + canActivate: [AuthGuard], + }, + { + title: 'Edit Notice of Intent', + path: 'notice-of-intent/:fileId/edit', + canActivate: [AuthGuard], + loadChildren: () => + import('./features/notice-of-intents/edit-submission/edit-submission.module').then((m) => m.EditSubmissionModule), }, { path: '', redirectTo: '/login', pathMatch: 'full' }, ]; diff --git a/portal-frontend/src/app/app.module.ts b/portal-frontend/src/app/app.module.ts index 25d2d3d63d..57700ac0e9 100644 --- a/portal-frontend/src/app/app.module.ts +++ b/portal-frontend/src/app/app.module.ts @@ -10,7 +10,7 @@ import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { ApplicationDetailsModule } from './features/applications/application-details/application-details.module'; import { AuthorizationComponent } from './features/authorization/authorization.component'; -import { CreateApplicationDialogComponent } from './features/create-application-dialog/create-application-dialog.component'; +import { CreateSubmissionDialogComponent } from './features/create-submission-dialog/create-submission-dialog.component'; import { ApplicationListComponent } from './features/home/application-list/application-list.component'; import { HomeComponent } from './features/home/home.component'; import { LandingPageComponent } from './features/landing-page/landing-page.component'; @@ -18,7 +18,8 @@ import { LoginComponent } from './features/login/login.component'; import { AlcReviewComponent } from './features/applications/view-submission/alc-review/alc-review.component'; import { SubmissionDocumentsComponent } from './features/applications/view-submission/alc-review/submission-documents/submission-documents.component'; import { LfngReviewComponent } from './features/applications/view-submission/lfng-review/lfng-review.component'; -import { ViewSubmissionComponent } from './features/applications/view-submission/view-submission.component'; +import { ViewApplicationSubmissionComponent } from './features/applications/view-submission/view-application-submission.component'; +import { ViewNoticeOfIntentSubmissionComponent } from './features/notice-of-intents/view-submission/view-notice-of-intent-submission.component'; import { AuthInterceptorService } from './services/authentication/auth-interceptor.service'; import { TokenRefreshService } from './services/authentication/token-refresh.service'; import { ConfirmationDialogComponent } from './shared/confirmation-dialog/confirmation-dialog.component'; @@ -37,10 +38,11 @@ import { DecisionsComponent } from './features/applications/view-submission/alc- HomeComponent, AuthorizationComponent, ApplicationListComponent, - CreateApplicationDialogComponent, + CreateSubmissionDialogComponent, LandingPageComponent, ConfirmationDialogComponent, - ViewSubmissionComponent, + ViewApplicationSubmissionComponent, + ViewNoticeOfIntentSubmissionComponent, LfngReviewComponent, AlcReviewComponent, SubmissionDocumentsComponent, diff --git a/portal-frontend/src/app/features/applications/view-submission/view-submission.component.html b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.html similarity index 100% rename from portal-frontend/src/app/features/applications/view-submission/view-submission.component.html rename to portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.html diff --git a/portal-frontend/src/app/features/applications/view-submission/view-submission.component.scss b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.scss similarity index 100% rename from portal-frontend/src/app/features/applications/view-submission/view-submission.component.scss rename to portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.scss diff --git a/portal-frontend/src/app/features/applications/view-submission/view-submission.component.spec.ts b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.spec.ts similarity index 88% rename from portal-frontend/src/app/features/applications/view-submission/view-submission.component.spec.ts rename to portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.spec.ts index c09acad8ed..60ef872ed3 100644 --- a/portal-frontend/src/app/features/applications/view-submission/view-submission.component.spec.ts +++ b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.spec.ts @@ -10,11 +10,11 @@ import { ApplicationSubmissionService } from '../../../services/application-subm import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { ViewSubmissionComponent } from './view-submission.component'; +import { ViewApplicationSubmissionComponent } from './view-application-submission.component'; describe('ViewSubmissionComponent', () => { - let component: ViewSubmissionComponent; - let fixture: ComponentFixture; + let component: ViewApplicationSubmissionComponent; + let fixture: ComponentFixture; let mockRoute; let mockAppService: DeepMocked; @@ -64,11 +64,11 @@ describe('ViewSubmissionComponent', () => { useValue: {}, }, ], - declarations: [ViewSubmissionComponent], + declarations: [ViewApplicationSubmissionComponent], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(ViewSubmissionComponent); + fixture = TestBed.createComponent(ViewApplicationSubmissionComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/portal-frontend/src/app/features/applications/view-submission/view-submission.component.ts b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.ts similarity index 94% rename from portal-frontend/src/app/features/applications/view-submission/view-submission.component.ts rename to portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.ts index cd8a17a6e8..4645450ce7 100644 --- a/portal-frontend/src/app/features/applications/view-submission/view-submission.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.ts @@ -22,11 +22,11 @@ enum MOBILE_STEP { } @Component({ - selector: 'app-view-submission', - templateUrl: './view-submission.component.html', - styleUrls: ['./view-submission.component.scss'], + selector: 'app-view-application-submission', + templateUrl: './view-application-submission.component.html', + styleUrls: ['./view-application-submission.component.scss'], }) -export class ViewSubmissionComponent implements OnInit, OnDestroy { +export class ViewApplicationSubmissionComponent implements OnInit, OnDestroy { application: ApplicationSubmissionDetailedDto | undefined; $application = new BehaviorSubject(undefined); $applicationDocuments = new BehaviorSubject([]); diff --git a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html b/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html deleted file mode 100644 index ed0c6a90db..0000000000 --- a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.html +++ /dev/null @@ -1,119 +0,0 @@ -
-
-

Create New

-

- The governmental or prescribed public body that is applying to exclude land: -

-
- -
- -
- Select an option to learn more about the {{ currentStepIndex === 0 ? 'submission type' : 'application type' }}. -
-
- - - {{ subType.label }} - - -
-
- - - {{ appType.portalLabel }} - - -
- -
-
- {{ - selectedSubmissionType?.label - }} - -
-
-
- {{ selectedAppType?.portalLabel }} -
-
- Read {{ readMoreClicked ? 'Less' : 'More' }} -
-
- - - -
-
- -
-
- - -
-
- - -
-
- - -
-
diff --git a/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html new file mode 100644 index 0000000000..b24b1f3975 --- /dev/null +++ b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html @@ -0,0 +1,150 @@ +
+
+

Create New

+

+ The governmental or prescribed public body that is applying to exclude land: +

+
+ +
+ +
Select an option to learn more about the submission type.
+
+ + + {{ subType.label }} + + +
+
+
+ {{ selectedSubmissionType.label }} +
+
+
+ Read {{ readMoreClicked ? 'Less' : 'More' }} +
+ +
Select an option to learn more about the application type.
+
+ + + {{ appType.portalLabel }} + + +
+ +
+
+ {{ selectedAppType.portalLabel }} +
+
+ Read {{ readMoreClicked ? 'Less' : 'More' }} +
+
+ +
Select an option to learn more about the notice of intent type.
+
+ + + {{ appType.portalLabel }} + + +
+ +
+
+ {{ selectedNoiType.portalLabel }} +
+
+ Read {{ readMoreClicked ? 'Less' : 'More' }} +
+
+ + + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.scss b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.scss similarity index 100% rename from portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.scss rename to portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.scss diff --git a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.spec.ts b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.spec.ts similarity index 62% rename from portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.spec.ts rename to portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.spec.ts index 29867fe928..f260209156 100644 --- a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.spec.ts @@ -4,11 +4,12 @@ import { MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { MatRadioModule } from '@angular/material/radio'; import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; import { CodeService } from '../../services/code/code.service'; -import { CreateApplicationDialogComponent } from './create-application-dialog.component'; +import { NoticeOfIntentSubmissionService } from '../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { CreateSubmissionDialogComponent } from './create-submission-dialog.component'; -describe('CreateApplicationDialogComponent', () => { - let component: CreateApplicationDialogComponent; - let fixture: ComponentFixture; +describe('CreateSubmissionDialogComponent', () => { + let component: CreateSubmissionDialogComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -23,12 +24,16 @@ describe('CreateApplicationDialogComponent', () => { provide: CodeService, useValue: {}, }, + { + provide: NoticeOfIntentSubmissionService, + useValue: {}, + }, ], - declarations: [CreateApplicationDialogComponent], + declarations: [CreateSubmissionDialogComponent], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(CreateApplicationDialogComponent); + fixture = TestBed.createComponent(CreateSubmissionDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.ts similarity index 55% rename from portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts rename to portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.ts index 126f5c57a9..7674c4110b 100644 --- a/portal-frontend/src/app/features/create-application-dialog/create-application-dialog.component.ts +++ b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.ts @@ -3,42 +3,46 @@ import { MatDialogRef } from '@angular/material/dialog'; import { MatRadioChange } from '@angular/material/radio'; import { Router } from '@angular/router'; import { ApplicationSubmissionService } from '../../services/application-submission/application-submission.service'; -import { ApplicationTypeDto, SubmissionTypeDto } from '../../services/code/code.dto'; +import { ApplicationTypeDto, NoticeOfIntentTypeDto, SubmissionTypeDto } from '../../services/code/code.dto'; import { CodeService } from '../../services/code/code.service'; +import { NoticeOfIntentSubmissionService } from '../../services/notice-of-intent-submission/notice-of-intent-submission.service'; import { scrollToElement } from '../../shared/utils/scroll-helper'; export enum ApplicationCreateDialogStepsEnum { submissionType = 0, applicationType = 1, prescribedBody = 2, + noticeOfIntentType = 3, } @Component({ selector: 'app-create-application-dialog', - templateUrl: './create-application-dialog.component.html', - styleUrls: ['./create-application-dialog.component.scss'], + templateUrl: './create-submission-dialog.component.html', + styleUrls: ['./create-submission-dialog.component.scss'], encapsulation: ViewEncapsulation.None, }) -export class CreateApplicationDialogComponent implements OnInit, AfterViewChecked { - submissionStep = ApplicationCreateDialogStepsEnum.submissionType; - applicationStep = ApplicationCreateDialogStepsEnum.applicationType; - prescribedBodyStep = ApplicationCreateDialogStepsEnum.prescribedBody; +export class CreateSubmissionDialogComponent implements OnInit, AfterViewChecked { + steps = ApplicationCreateDialogStepsEnum; + + selectedSubmissionType: SubmissionTypeDto | undefined = undefined; + submissionTypes: SubmissionTypeDto[] = []; applicationTypes: ApplicationTypeDto[] = []; selectedAppType: ApplicationTypeDto | undefined = undefined; - selectedSubmissionType: SubmissionTypeDto | undefined = undefined; - submissionTypes: SubmissionTypeDto[] = []; + noticeOfIntentTypes: NoticeOfIntentTypeDto[] = []; + selectedNoiType: NoticeOfIntentTypeDto | undefined = undefined; readMoreClicked: boolean = false; isReadMoreVisible: boolean = false; - currentStepIndex: number = 0; + currentStep: ApplicationCreateDialogStepsEnum = this.steps.submissionType; prescribedBody: string | undefined; constructor( - private dialogRef: MatDialogRef, + private dialogRef: MatDialogRef, private codeService: CodeService, - private applicationService: ApplicationSubmissionService, + private appSubmissionService: ApplicationSubmissionService, + private noiSubmissionService: NoticeOfIntentSubmissionService, private router: Router ) {} @@ -57,40 +61,58 @@ export class CreateApplicationDialogComponent implements OnInit, AfterViewChecke .filter((type) => !!type.portalLabel) .sort((a, b) => (a.portalLabel > b.portalLabel ? 1 : -1)); this.submissionTypes = codes.submissionTypes.sort((a, b) => (a.code > b.code ? 1 : -1)); + this.noticeOfIntentTypes = codes.noticeOfIntentTypes.sort((a, b) => (a.portalLabel > b.portalLabel ? 1 : -1)); } onCancel() { this.dialogRef.close(false); } - async onSubmit() { + async onSubmitApplication() { if (this.selectedAppType && this.selectedAppType.code === 'EXCL') { - this.currentStepIndex++; + this.currentStep++; } else { await this.createApplication(); } } + async onSubmitNoi() { + if (this.selectedAppType && this.selectedAppType.code === 'EXCL') { + this.currentStep++; + } else { + await this.createNoi(); + } + } + async onSubmitInlcExcl() { await this.createApplication(); } private async createApplication() { - const res = await this.applicationService.create(this.selectedAppType!.code, this.prescribedBody); + const res = await this.appSubmissionService.create(this.selectedAppType!.code, this.prescribedBody); if (res) { await this.router.navigateByUrl(`/application/${res.fileId}/edit`); this.dialogRef.close(true); } } - onStepChange(idx: number) { - this.currentStepIndex += idx; + private async createNoi() { + const res = await this.noiSubmissionService.create(this.selectedNoiType!.code); + if (res) { + await this.router.navigateByUrl(`/notice-of-intent/${res.fileId}/edit`); + this.dialogRef.close(true); + } + } + + onStepChange(step: ApplicationCreateDialogStepsEnum) { + this.currentStep = step; this.readMoreClicked = false; } onSubmissionTypeSelected(event: MatRadioChange) { this.selectedSubmissionType = this.submissionTypes.find((e) => e.code === event.value); this.readMoreClicked = false; + debugger; } onReadMoreClicked() { @@ -111,23 +133,35 @@ export class CreateApplicationDialogComponent implements OnInit, AfterViewChecke }, 300); } + onNoiTypeSelected(event: MatRadioChange) { + this.selectedNoiType = this.noticeOfIntentTypes.find((e) => e.code === event.value); + this.readMoreClicked = false; + setTimeout(() => { + scrollToElement({ id: 'warningBanner', center: true }); + }, 300); + } + isEllipsisActive(e: string): boolean { - const el = document.getElementById(e); - return el ? el.clientHeight < el.scrollHeight : false; + const el = document.getElementsByClassName(e); + if (el.length > 0) { + return el ? el[0].clientHeight < el[0].scrollHeight : false; + } + return true; } checkIfReadMoreVisible(): boolean { - switch (this.currentStepIndex) { - case this.applicationStep: - return this.readMoreClicked || this.isEllipsisActive('appTypeDescription'); - case this.submissionStep: - return this.readMoreClicked || this.isEllipsisActive('subTypeDescription'); - default: - return true; - } + return this.readMoreClicked || this.isEllipsisActive('typeDescription'); } onSelectPrescribedBody(name: string) { this.prescribedBody = name; } + + onConfirmSubmissionType() { + if (this.selectedSubmissionType && this.selectedSubmissionType.code === 'APP') { + this.currentStep = this.steps.applicationType; + } else { + this.currentStep = this.steps.noticeOfIntentType; + } + } } diff --git a/portal-frontend/src/app/features/home/home.component.ts b/portal-frontend/src/app/features/home/home.component.ts index c7c24c9dac..e9b2412b41 100644 --- a/portal-frontend/src/app/features/home/home.component.ts +++ b/portal-frontend/src/app/features/home/home.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { AuthenticationService } from '../../services/authentication/authentication.service'; import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; -import { CreateApplicationDialogComponent } from '../create-application-dialog/create-application-dialog.component'; +import { CreateSubmissionDialogComponent } from '../create-submission-dialog/create-submission-dialog.component'; @Component({ selector: 'app-home', @@ -27,7 +27,7 @@ export class HomeComponent implements OnInit { } async onCreateApplication() { - this.dialog.open(CreateApplicationDialogComponent, { + this.dialog.open(CreateSubmissionDialogComponent, { panelClass: 'no-padding', disableClose: true, autoFocus: false, diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html new file mode 100644 index 0000000000..2d6797f88a --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html @@ -0,0 +1 @@ +

edit-submission works!

diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts new file mode 100644 index 0000000000..74776e7f67 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditSubmissionComponent } from './edit-submission.component'; + +describe('EditSubmissionComponent', () => { + let component: EditSubmissionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EditSubmissionComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(EditSubmissionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts new file mode 100644 index 0000000000..2dcc57f5ab --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-edit-submission', + templateUrl: './edit-submission.component.html', + styleUrls: ['./edit-submission.component.scss'] +}) +export class EditSubmissionComponent { + +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts new file mode 100644 index 0000000000..547fc2c060 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { SharedModule } from '../../../shared/shared.module'; +import { EditSubmissionComponent } from './edit-submission.component'; + +const routes: Routes = [ + { + path: '', + component: EditSubmissionComponent, + }, +]; + +@NgModule({ + declarations: [EditSubmissionComponent], + imports: [CommonModule, SharedModule, RouterModule.forChild(routes)], +}) +export class EditSubmissionModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.html new file mode 100644 index 0000000000..6132555a09 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.html @@ -0,0 +1 @@ +

view-submission works!

diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.scss b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.spec.ts new file mode 100644 index 0000000000..67e0d001aa --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ViewNoticeOfIntentSubmissionComponent } from './view-notice-of-intent-submission.component'; + +describe('ViewSubmissionComponent', () => { + let component: ViewNoticeOfIntentSubmissionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ViewNoticeOfIntentSubmissionComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ViewNoticeOfIntentSubmissionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.ts new file mode 100644 index 0000000000..267cb8f84c --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-view-noi-submission', + templateUrl: './view-notice-of-intent-submission.component.html', + styleUrls: ['./view-notice-of-intent-submission.component.scss'], +}) +export class ViewNoticeOfIntentSubmissionComponent {} diff --git a/portal-frontend/src/app/services/code/code.dto.ts b/portal-frontend/src/app/services/code/code.dto.ts index e00e33c8ea..561e1c56e6 100644 --- a/portal-frontend/src/app/services/code/code.dto.ts +++ b/portal-frontend/src/app/services/code/code.dto.ts @@ -7,12 +7,18 @@ export interface LocalGovernmentDto { matchesUserGuid: boolean; } +export interface SubmissionTypeDto extends BaseCodeDto { + portalHtmlDescription: string; +} + export interface ApplicationTypeDto { code: string; portalLabel: string; htmlDescription: string; } -export interface SubmissionTypeDto extends BaseCodeDto { - portalHtmlDescription: string; +export interface NoticeOfIntentTypeDto { + code: string; + portalLabel: string; + htmlDescription: string; } diff --git a/portal-frontend/src/app/services/code/code.service.ts b/portal-frontend/src/app/services/code/code.service.ts index 1c241fdef0..9115600fd5 100644 --- a/portal-frontend/src/app/services/code/code.service.ts +++ b/portal-frontend/src/app/services/code/code.service.ts @@ -4,7 +4,7 @@ import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ApplicationDocumentTypeDto } from '../application-document/application-document.dto'; import { NaruSubtypeDto } from '../application-submission/application-submission.dto'; -import { ApplicationTypeDto, LocalGovernmentDto, SubmissionTypeDto } from './code.dto'; +import { ApplicationTypeDto, LocalGovernmentDto, NoticeOfIntentTypeDto, SubmissionTypeDto } from './code.dto'; @Injectable({ providedIn: 'root', @@ -19,6 +19,7 @@ export class CodeService { this.httpClient.get<{ localGovernments: LocalGovernmentDto[]; applicationTypes: ApplicationTypeDto[]; + noticeOfIntentTypes: NoticeOfIntentTypeDto[]; applicationDocumentTypes: ApplicationDocumentTypeDto[]; submissionTypes: SubmissionTypeDto[]; naruSubtypes: NaruSubtypeDto[]; diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/application-submission-draft.service.ts b/portal-frontend/src/app/services/notice-of-intent-submission/application-submission-draft.service.ts new file mode 100644 index 0000000000..6d8a45c483 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-submission/application-submission-draft.service.ts @@ -0,0 +1,79 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { + ApplicationSubmissionDetailedDto, + ApplicationSubmissionUpdateDto, +} from '../application-submission/application-submission.dto'; +import { ToastService } from '../toast/toast.service'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationSubmissionDraftService { + private serviceUrl = `${environment.apiUrl}/application-submission-draft`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private overlayService: OverlaySpinnerService + ) {} + + async getByFileId(fileId: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/${fileId}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Application, please try again later'); + return undefined; + } + } + + async updatePending(fileId: string, updateDto: ApplicationSubmissionUpdateDto) { + try { + this.overlayService.showSpinner(); + const result = await firstValueFrom( + this.httpClient.put(`${this.serviceUrl}/${fileId}`, updateDto) + ); + this.toastService.showSuccessToast('Application Saved'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Application, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + + return undefined; + } + + async publish(fileId: string) { + try { + this.overlayService.showSpinner(); + await firstValueFrom(this.httpClient.post(`${this.serviceUrl}/${fileId}/publish`, {})); + this.toastService.showSuccessToast('Application Submitted'); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Application, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + } + + async delete(fileId: string) { + try { + this.overlayService.showSpinner(); + await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}/${fileId}`)); + this.toastService.showSuccessToast('Draft Edit Deleted'); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to submit Application, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + } +} diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission-draft.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission-draft.service.spec.ts new file mode 100644 index 0000000000..2f9f7b164d --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission-draft.service.spec.ts @@ -0,0 +1,95 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../toast/toast.service'; +import { ApplicationSubmissionDraftService } from './application-submission-draft.service'; + +describe('ApplicationSubmissionDraftService', () => { + let service: ApplicationSubmissionDraftService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(ApplicationSubmissionDraftService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for loading a single application', async () => { + mockHttpClient.get.mockReturnValue(of({})); + let mockFileId = 'file-id'; + + await service.getByFileId(mockFileId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('application'); + expect(mockHttpClient.get.mock.calls[0][0]).toContain(mockFileId); + }); + + it('should show an error toast if getting a specific application fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.getByFileId('file-id'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a put request for update', async () => { + mockHttpClient.put.mockReturnValue(of({})); + let mockFileId = 'fileId'; + + await service.updatePending('fileId', {}); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockHttpClient.put.mock.calls[0][0]).toContain('application'); + expect(mockHttpClient.put.mock.calls[0][0]).toContain(mockFileId); + }); + + it('should show an error toast if updating an application fails', async () => { + mockHttpClient.put.mockReturnValue(throwError(() => ({}))); + + await service.updatePending('file-id', {}); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a delete request for delete', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.delete('fileId'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete.mock.calls[0][0]).toContain('application'); + }); + + it('should show an error toast if delete a draft fails', async () => { + mockHttpClient.delete.mockReturnValue(throwError(() => ({}))); + + await service.delete('fileId'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts new file mode 100644 index 0000000000..ba36fe6e58 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -0,0 +1,45 @@ +export interface NoticeOfIntentSubmissionDto { + fileNumber: string; + uuid: string; + createdAt: number; + updatedAt: number; + applicant: string; + localGovernmentUuid: string; + type: string; + canEdit: boolean; + canView: boolean; +} + +export interface NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmissionDto { + purpose: string | null; + parcelsAgricultureDescription: string; + parcelsAgricultureImprovementDescription: string; + parcelsNonAgricultureUseDescription: string; + northLandUseType: string; + northLandUseTypeDescription: string; + eastLandUseType: string; + eastLandUseTypeDescription: string; + southLandUseType: string; + southLandUseTypeDescription: string; + westLandUseType: string; + westLandUseTypeDescription: string; + primaryContactOwnerUuid?: string | null; +} + +export interface NoticeOfIntentSubmissionUpdateDto { + applicant?: string; + purpose?: string; + localGovernmentUuid?: string; + typeCode?: string; + parcelsAgricultureDescription?: string; + parcelsAgricultureImprovementDescription?: string; + parcelsNonAgricultureUseDescription?: string; + northLandUseType?: string; + northLandUseTypeDescription?: string; + eastLandUseType?: string; + eastLandUseTypeDescription?: string; + southLandUseType?: string; + southLandUseTypeDescription?: string; + westLandUseType?: string; + westLandUseTypeDescription?: string; +} diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts new file mode 100644 index 0000000000..99b4b804c8 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -0,0 +1,132 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../toast/toast.service'; + +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; + +describe('NoticeOfIntentSubmissionService', () => { + let service: NoticeOfIntentSubmissionService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentSubmissionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for loading notice of intents', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.getNoticeOfIntents(); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notice-of-intent'); + }); + + it('should show an error toast if getting notice of intents fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.getNoticeOfIntents(); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a get request for loading a single notice of intent', async () => { + mockHttpClient.get.mockReturnValue(of({})); + let mockFileId = 'file-id'; + + await service.getByFileId(mockFileId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notice-of-intent'); + expect(mockHttpClient.get.mock.calls[0][0]).toContain(mockFileId); + }); + + it('should show an error toast if getting a specific notice of intent fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.getByFileId('file-id'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for create', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.create('type'); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent'); + }); + + it('should show an error toast if creating an notice of intent fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.create('type'); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a put request for update', async () => { + mockHttpClient.put.mockReturnValue(of({})); + let mockFileId = 'fileId'; + + await service.updatePending('fileId', {}); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockHttpClient.put.mock.calls[0][0]).toContain('notice-of-intent'); + expect(mockHttpClient.put.mock.calls[0][0]).toContain(mockFileId); + }); + + it('should show an error toast if updating an notice of intent fails', async () => { + mockHttpClient.put.mockReturnValue(throwError(() => ({}))); + + await service.updatePending('file-id', {}); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for cancelling', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.cancel('fileId'); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent'); + }); + + it('should show an error toast if cancelling a file fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.cancel('fileId'); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.service.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.service.ts new file mode 100644 index 0000000000..d5663b6806 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -0,0 +1,123 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { ToastService } from '../toast/toast.service'; +import { + NoticeOfIntentSubmissionDetailedDto, + NoticeOfIntentSubmissionDto, + NoticeOfIntentSubmissionUpdateDto, +} from './notice-of-intent-submission.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentSubmissionService { + private serviceUrl = `${environment.apiUrl}/notice-of-intent-submission`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private overlayService: OverlaySpinnerService + ) {} + + async getNoticeOfIntents() { + try { + return await firstValueFrom(this.httpClient.get(`${this.serviceUrl}`)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Notice of Intents, please try again later'); + return []; + } + } + + async getByFileId(fileId: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/application/${fileId}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Notice of Intent, please try again later'); + return undefined; + } + } + + async getByUuid(uuid: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/${uuid}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Notice of Intent, please try again later'); + return undefined; + } + } + + async create(type: string) { + try { + this.overlayService.showSpinner(); + return await firstValueFrom( + this.httpClient.post<{ fileId: string }>(`${this.serviceUrl}`, { + type, + }) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to create Notice of Intent, please try again later'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } + + async updatePending(uuid: string, updateDto: NoticeOfIntentSubmissionUpdateDto) { + try { + this.overlayService.showSpinner(); + const result = await firstValueFrom( + this.httpClient.put(`${this.serviceUrl}/${uuid}`, updateDto) + ); + this.toastService.showSuccessToast('Notice of Intent Saved'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Notice of Intent, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + + return undefined; + } + + async cancel(uuid: string) { + try { + this.overlayService.showSpinner(); + return await firstValueFrom(this.httpClient.post<{ fileId: string }>(`${this.serviceUrl}/${uuid}/cancel`, {})); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to cancel Notice of Intent, please try again later'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } + + async submitToAlcs(uuid: string) { + let res; + try { + this.overlayService.showSpinner(); + res = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/alcs/submit/${uuid}`, {}) + ); + this.toastService.showSuccessToast('Notice of Intent Submitted'); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to submit Notice of Intent, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + return res; + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts index 862e199ffd..876e7e05d3 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts @@ -262,6 +262,10 @@ export class NoticeOfIntentService { return this.getByFileNumber(noticeOfIntent.fileNumber); } + async listTypes() { + return this.typeRepository.find(); + } + async listSubtypes() { return this.subtypeRepository.find({ where: { @@ -343,10 +347,6 @@ export class NoticeOfIntentService { return noticeOfIntent.fileNumber; } - async fetchTypes() { - return this.typeRepository.find(); - } - async submit(createDto: CreateNoticeOfIntentServiceDto) { const existingNoticeOfIntent = await this.repository.findOne({ where: { fileNumber: createDto.fileNumber }, diff --git a/services/apps/alcs/src/portal/code/code.controller.spec.ts b/services/apps/alcs/src/portal/code/code.controller.spec.ts index b97d56bbab..6c3a07e168 100644 --- a/services/apps/alcs/src/portal/code/code.controller.spec.ts +++ b/services/apps/alcs/src/portal/code/code.controller.spec.ts @@ -10,6 +10,7 @@ import { ApplicationDocumentService } from '../../alcs/application/application-d import { ApplicationService } from '../../alcs/application/application.service'; import { CardType } from '../../alcs/card/card-type/card-type.entity'; import { CardService } from '../../alcs/card/card.service'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; import { User } from '../../user/user.entity'; import { ApplicationSubmissionService } from '../application-submission/application-submission.service'; import { CodeController } from './code.controller'; @@ -21,6 +22,7 @@ describe('CodeController', () => { let mockCardService: DeepMocked; let mockAppDocService: DeepMocked; let mockAppSubmissionService: DeepMocked; + let mockNoiService: DeepMocked; beforeEach(async () => { mockLgService = createMock(); @@ -28,6 +30,7 @@ describe('CodeController', () => { mockCardService = createMock(); mockAppDocService = createMock(); mockAppSubmissionService = createMock(); + mockNoiService = createMock(); const app: TestingModule = await Test.createTestingModule({ imports: [ @@ -58,6 +61,10 @@ describe('CodeController', () => { provide: ApplicationSubmissionService, useValue: mockAppSubmissionService, }, + { + provide: NoticeOfIntentService, + useValue: mockNoiService, + }, { provide: ClsService, useValue: {}, @@ -87,6 +94,7 @@ describe('CodeController', () => { mockAppDocService.fetchTypes.mockResolvedValue([]); mockAppSubmissionService.listNaruSubtypes.mockResolvedValue([]); + mockNoiService.listTypes.mockResolvedValue([]); }); it('should call out to local government service for fetching codes', async () => { diff --git a/services/apps/alcs/src/portal/code/code.controller.ts b/services/apps/alcs/src/portal/code/code.controller.ts index 2766f79064..41fa24abe5 100644 --- a/services/apps/alcs/src/portal/code/code.controller.ts +++ b/services/apps/alcs/src/portal/code/code.controller.ts @@ -3,6 +3,7 @@ import { InjectMapper } from '@automapper/nestjs'; import { Controller, Get, Req, UseGuards } from '@nestjs/common'; import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; import { DocumentCode } from '../../document/document-code.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { ApplicationService } from '../../alcs/application/application.service'; @@ -11,6 +12,7 @@ import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.se import { DocumentTypeDto } from '../../document/document.dto'; import { User } from '../../user/user.entity'; import { ApplicationSubmissionService } from '../application-submission/application-submission.service'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission/notice-of-intent-submission.service'; export interface LocalGovernmentDto { uuid: string; @@ -29,6 +31,7 @@ export class CodeController { private applicationDocumentService: ApplicationDocumentService, private cardService: CardService, private applicationSubmissionService: ApplicationSubmissionService, + private noticeOfIntentService: NoticeOfIntentService, ) {} @Get() @@ -39,6 +42,7 @@ export class CodeController { const applicationDocumentTypes = await this.applicationDocumentService.fetchTypes(); const submissionTypes = await this.cardService.getPortalCardTypes(); + const noticeOfIntentTypes = await this.noticeOfIntentService.listTypes(); const naruSubtypes = await this.applicationSubmissionService.listNaruSubtypes(); @@ -54,6 +58,7 @@ export class CodeController { req.user.entity, ), applicationTypes, + noticeOfIntentTypes, submissionTypes, applicationDocumentTypes: mappedDocTypes, naruSubtypes, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts index f83d517ec9..f5355f2cc1 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -139,8 +139,4 @@ export class NoticeOfIntentSubmissionUpdateDto { @IsString() @IsOptional() westLandUseTypeDescription?: string; - - @IsBoolean() - @IsOptional() - hasOtherParcelsInCommunity?: boolean | null; } diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts index 51c346abe8..4fee705e4a 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -145,7 +145,7 @@ describe('NoticeOfIntentSubmissionService', () => { const applicant = 'Bruce Wayne'; const typeCode = 'fake-code'; - mockNoiService.fetchTypes.mockResolvedValue([ + mockNoiService.listTypes.mockResolvedValue([ new NoticeOfIntentType({ code: typeCode, portalLabel: 'portalLabel', @@ -162,7 +162,7 @@ describe('NoticeOfIntentSubmissionService', () => { mockRepository.findOne.mockResolvedValue(noiSubmission); const res = await service.mapToDTOs([noiSubmission]); - expect(mockNoiService.fetchTypes).toHaveBeenCalledTimes(1); + expect(mockNoiService.listTypes).toHaveBeenCalledTimes(1); expect(res[0].type).toEqual('label'); expect(res[0].applicant).toEqual(applicant); }); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index ca59616dbc..bb80d7bdbf 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -284,7 +284,7 @@ export class NoticeOfIntentSubmissionService { } async mapToDTOs(apps: NoticeOfIntentSubmission[]) { - const types = await this.noticeOfIntentService.fetchTypes(); + const types = await this.noticeOfIntentService.listTypes(); return apps.map((app) => { return { @@ -301,7 +301,7 @@ export class NoticeOfIntentSubmissionService { } async mapToDetailedDTO(noiSubmission: NoticeOfIntentSubmission) { - const types = await this.noticeOfIntentService.fetchTypes(); + const types = await this.noticeOfIntentService.listTypes(); const mappedApp = this.mapper.map( noiSubmission, NoticeOfIntentSubmission, diff --git a/services/apps/alcs/src/portal/portal.module.ts b/services/apps/alcs/src/portal/portal.module.ts index 78268ffeb2..4db394b272 100644 --- a/services/apps/alcs/src/portal/portal.module.ts +++ b/services/apps/alcs/src/portal/portal.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { RouterModule } from '@nestjs/core'; import { ApplicationModule } from '../alcs/application/application.module'; import { CardModule } from '../alcs/card/card.module'; +import { NoticeOfIntentModule } from '../alcs/notice-of-intent/notice-of-intent.module'; import { DocumentModule } from '../document/document.module'; import { PortalApplicationDecisionModule } from './application-decision/application-decision.module'; import { PortalApplicationDocumentModule } from './application-document/application-document.module'; @@ -11,6 +12,7 @@ import { ApplicationSubmissionReviewModule } from './application-submission-revi import { ApplicationSubmissionModule } from './application-submission/application-submission.module'; import { CodeController } from './code/code.controller'; import { PortalDocumentModule } from './document/document.module'; +import { NoticeOfIntentSubmissionModule } from './notice-of-intent-submission/notice-of-intent-submission.module'; import { ParcelModule } from './parcel/parcel.module'; import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; @@ -28,8 +30,11 @@ import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; ApplicationSubmissionDraftModule, PdfGenerationModule, PortalApplicationDecisionModule, + NoticeOfIntentModule, + NoticeOfIntentSubmissionModule, RouterModule.register([ { path: 'portal', module: ApplicationSubmissionModule }, + { path: 'portal', module: NoticeOfIntentSubmissionModule }, { path: 'portal', module: ParcelModule }, { path: 'portal', module: ApplicationSubmissionReviewModule }, { path: 'portal', module: PortalDocumentModule }, From cbc8c3d96eb9c531e24e57c67332a888dd40d9b0 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 9 Aug 2023 11:21:35 -0700 Subject: [PATCH 219/954] Code Review Feedback --- ...w-application-submission.component.spec.ts | 2 +- .../create-submission-dialog.component.html | 6 +- .../create-submission-dialog.component.ts | 3 +- ...ice-of-intent-submission.component.spec.ts | 2 +- .../application-submission-draft.service.ts | 79 --------------- ...of-intent-submission-draft.service.spec.ts | 95 ------------------- 6 files changed, 6 insertions(+), 181 deletions(-) delete mode 100644 portal-frontend/src/app/services/notice-of-intent-submission/application-submission-draft.service.ts delete mode 100644 portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission-draft.service.spec.ts diff --git a/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.spec.ts b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.spec.ts index 60ef872ed3..e9ef7369a7 100644 --- a/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.spec.ts +++ b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.spec.ts @@ -12,7 +12,7 @@ import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/c import { ViewApplicationSubmissionComponent } from './view-application-submission.component'; -describe('ViewSubmissionComponent', () => { +describe('ViewApplicationSubmissionComponent', () => { let component: ViewApplicationSubmissionComponent; let fixture: ComponentFixture; diff --git a/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html index b24b1f3975..c263c8a6cf 100644 --- a/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html +++ b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html @@ -80,8 +80,8 @@

color="primary" required > - - {{ appType.portalLabel }} + + {{ type.portalLabel }}

@@ -141,7 +141,7 @@

diff --git a/portal-frontend/src/app/features/home/noi-list/noi-list.component.html b/portal-frontend/src/app/features/home/noi-list/noi-list.component.html new file mode 100644 index 0000000000..e22f232d71 --- /dev/null +++ b/portal-frontend/src/app/features/home/noi-list/noi-list.component.html @@ -0,0 +1,66 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No NOIs Created
NOI ID{{ row.fileNumber }}Date Created{{ row.createdAt | date }}Owner Name{{ row.applicant || '(Unknown)' }}NOI Type{{ row.type }}Status +
+ {{ row.status.label }} +
+
Last Updated{{ row.updatedAt | date }}Actions + +
+ +
+
diff --git a/portal-frontend/src/app/features/home/noi-list/noi-list.component.scss b/portal-frontend/src/app/features/home/noi-list/noi-list.component.scss new file mode 100644 index 0000000000..5eb8d7a15e --- /dev/null +++ b/portal-frontend/src/app/features/home/noi-list/noi-list.component.scss @@ -0,0 +1,31 @@ +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; + +.label { + display: inline-block; + padding: rem(4) rem(16); + border-radius: rem(16); + font-weight: bold; +} + +.table-wrapper { + overflow-x: auto; + + .table-container { + display: table; + width: 100%; + + table { + width: 100%; + } + + ::ng-deep .mat-table { + ::ng-deep .mat-cell, + ::ng-deep .mat-header-cell, + ::ng-deep .mat-row, + ::ng-deep .mat-header-row { + min-width: rem(150); + } + } + } +} diff --git a/portal-frontend/src/app/features/home/noi-list/noi-list.component.spec.ts b/portal-frontend/src/app/features/home/noi-list/noi-list.component.spec.ts new file mode 100644 index 0000000000..05ba133ac5 --- /dev/null +++ b/portal-frontend/src/app/features/home/noi-list/noi-list.component.spec.ts @@ -0,0 +1,36 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { NoiListComponent } from './noi-list.component'; + +describe('NoiListComponent', () => { + let component: NoiListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [NoiListComponent], + providers: [ + { + provide: NoticeOfIntentSubmissionService, + useValue: {}, + }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(NoiListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/home/noi-list/noi-list.component.ts b/portal-frontend/src/app/features/home/noi-list/noi-list.component.ts new file mode 100644 index 0000000000..06f252fe20 --- /dev/null +++ b/portal-frontend/src/app/features/home/noi-list/noi-list.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatTableDataSource } from '@angular/material/table'; +import { NoticeOfIntentSubmissionDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; + +@Component({ + selector: 'app-noi-list', + templateUrl: './noi-list.component.html', + styleUrls: ['./noi-list.component.scss'], +}) +export class NoiListComponent implements OnInit { + dataSource: MatTableDataSource = new MatTableDataSource(); + displayedColumns: string[] = [ + 'fileNumber', + 'dateCreated', + 'applicant', + 'applicationType', + 'status', + 'lastUpdated', + 'actions', + ]; + + @ViewChild(MatPaginator) paginator!: MatPaginator; + + constructor(private noiSubmissionService: NoticeOfIntentSubmissionService) {} + + ngOnInit(): void { + this.loadNOIs(); + } + + async loadNOIs() { + const nois = await this.noiSubmissionService.getNoticeOfIntents(); + this.dataSource = new MatTableDataSource(nois); + this.dataSource.paginator = this.paginator; + } +} diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index 4449114f7c..c0d70aa686 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -74,9 +74,12 @@ export class ApplicationSubmissionReviewController { fileNumber, ); - if (applicationReview.createdBy) { + if ( + applicationReview.createdBy && + applicationReview.createdBy.bceidBusinessGuid + ) { const reviewGovernment = await this.localGovernmentService.getByGuid( - applicationReview.createdBy?.bceidBusinessGuid, + applicationReview.createdBy.bceidBusinessGuid, ); if (reviewGovernment) { @@ -206,7 +209,7 @@ export class ApplicationSubmissionReviewController { const creatingGuid = applicationSubmission.createdBy.bceidBusinessGuid; const creatingGovernment = await this.localGovernmentService.getByGuid( - creatingGuid, + creatingGuid!, ); if ( diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts index 3e33476d3c..a0052602b0 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts @@ -7,6 +7,7 @@ import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.entity'; import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; @@ -269,11 +270,11 @@ describe('NoticeOfIntentSubmissionController', () => { new NoticeOfIntent(), ); mockNoiSubmissionService.getIfCreatorByUuid.mockResolvedValue( - new NoticeOfIntentSubmission({ - typeCode: 'TURP', - }), + new NoticeOfIntentSubmission(), ); + mockNoiSubmissionService.updateStatus.mockResolvedValue(); + await controller.submitAsApplicant(mockFileId, { user: { entity: new User(), @@ -284,5 +285,10 @@ describe('NoticeOfIntentSubmissionController', () => { 1, ); expect(mockNoiSubmissionService.submitToAlcs).toHaveBeenCalledTimes(1); + expect(mockNoiSubmissionService.updateStatus).toHaveBeenCalledTimes(1); + expect(mockNoiSubmissionService.updateStatus).toHaveBeenCalledWith( + undefined, + NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, + ); }); }); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts index 3aa22ec08e..82e569cfa9 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts @@ -9,6 +9,8 @@ import { Req, UseGuards, } from '@nestjs/common'; +import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; +import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; import { User } from '../../user/user.entity'; import { @@ -136,11 +138,10 @@ export class NoticeOfIntentSubmissionController { await this.noticeOfIntentSubmissionService.submitToAlcs( validatedApplicationSubmission, ); - //TODO: Uncomment when we add status - // return await this.noticeOfIntentSubmissionService.updateStatus( - // noticeOfIntentSubmission, - // SUBMISSION_STATUS.SUBMITTED_TO_ALC, - // ); + return await this.noticeOfIntentSubmissionService.updateStatus( + noticeOfIntentSubmission.uuid, + NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, + ); } else { //TODO: Uncomment when we add validation //this.logger.debug(validationResult.errors); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts index 4ae134c0b3..2a7378251c 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts @@ -5,6 +5,7 @@ import { LocalGovernmentModule } from '../../alcs/local-government/local-governm import { NoticeOfIntentSubmissionStatusModule } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; import { AuthorizationModule } from '../../common/authorization/authorization.module'; +import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; @@ -23,6 +24,6 @@ import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.s FileNumberModule, ], controllers: [NoticeOfIntentSubmissionController], - providers: [NoticeOfIntentSubmissionService], + providers: [NoticeOfIntentSubmissionService, NoticeOfIntentSubmissionProfile], }) export class NoticeOfIntentSubmissionModule {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index d829130932..abf85de386 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -6,7 +6,13 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { FindOptionsRelations, Repository } from 'typeorm'; +import { + FindOptionsRelations, + FindOptionsWhere, + IsNull, + Not, + Repository, +} from 'typeorm'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; @@ -196,14 +202,41 @@ export class NoticeOfIntentSubmissionService { // } // } - getByUser(user: User) { - return this.noticeOfIntentSubmissionRepository.find({ - where: { + async getByUser(user: User) { + const searchQueries: FindOptionsWhere[] = []; + + searchQueries.push({ + createdBy: { + uuid: user.uuid, + }, + isDraft: false, + }); + + if (user.bceidBusinessGuid) { + searchQueries.push({ createdBy: { - uuid: user.uuid, + bceidBusinessGuid: user.bceidBusinessGuid, }, isDraft: false, - }, + }); + + const matchingLocalGovernment = + await this.localGovernmentService.getByGuid(user.bceidBusinessGuid); + if (matchingLocalGovernment) { + searchQueries.push({ + localGovernmentUuid: matchingLocalGovernment.uuid, + isDraft: false, + //TODO: Test this once we can submit NOIs + submissionStatuses: { + effectiveDate: Not(IsNull()), + statusTypeCode: Not(NOI_SUBMISSION_STATUS.IN_PROGRESS), + }, + }); + } + } + + return this.noticeOfIntentSubmissionRepository.find({ + where: searchQueries, order: { auditUpdatedAt: 'DESC', }, @@ -348,11 +381,11 @@ export class NoticeOfIntentSubmissionService { } async updateStatus( - fileNumber: string, + uuid: string, statusCode: NOI_SUBMISSION_STATUS, effectiveDate?: Date | null, ) { - const submission = await this.loadBarebonesSubmission(fileNumber); + const submission = await this.loadBarebonesSubmission(uuid); await this.noticeOfIntentSubmissionStatusService.setStatusDate( submission.uuid, statusCode, @@ -367,11 +400,11 @@ export class NoticeOfIntentSubmissionService { ); } - private loadBarebonesSubmission(fileNumber: string) { + private loadBarebonesSubmission(uuid: string) { //Load submission without relations to prevent save from crazy cascading return this.noticeOfIntentSubmissionRepository.findOneOrFail({ where: { - fileNumber, + uuid, }, }); } diff --git a/services/apps/alcs/src/user/user.dto.ts b/services/apps/alcs/src/user/user.dto.ts index b14b6b22d0..6efee456af 100644 --- a/services/apps/alcs/src/user/user.dto.ts +++ b/services/apps/alcs/src/user/user.dto.ts @@ -57,7 +57,7 @@ export class CreateUserDto { bceidUserName?: string; idirUserGuid?: string; bceidGuid?: string; - bceidBusinessGuid?: string; + bceidBusinessGuid?: string | null; } export class AssigneeDto { diff --git a/services/apps/alcs/src/user/user.entity.ts b/services/apps/alcs/src/user/user.entity.ts index 4711c62f9b..1016ef31d2 100644 --- a/services/apps/alcs/src/user/user.entity.ts +++ b/services/apps/alcs/src/user/user.entity.ts @@ -64,8 +64,8 @@ export class User extends Base { @Column({ nullable: true }) bceidUserName: string; - @Column({ nullable: true }) - bceidBusinessGuid: string; + @Column({ nullable: true, type: 'varchar' }) + bceidBusinessGuid: string | null; @AutoMap() @Column({ default: [], array: true, type: 'text' }) From 911bfa56669e7505f23c0537fe93e663e654ad13 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 10 Aug 2023 15:35:57 -0700 Subject: [PATCH 224/954] Add NOI Stepper and Step 1 * Add Parcels and Owners * Change ApplicationOwnerType to OwnerType and re-use across both Apps and NOIs * Add NOI Document controller and service for portal --- .../application-details.component.html | 10 +- .../application-details.component.ts | 17 +- .../excl-details/excl-details.component.ts | 3 +- .../incl-details/incl-details.component.ts | 3 +- .../naru-details/naru-details.component.ts | 3 +- .../pfrs-details/pfrs-details.component.ts | 3 +- .../pofo-details/pofo-details.component.ts | 3 +- .../roso-details/roso-details.component.ts | 3 +- .../subd-details/subd-details.component.ts | 3 +- .../tur-details/tur-details.component.ts | 3 +- .../edit-submission/files-step.partial.ts | 4 +- .../other-attachments.component.ts | 10 +- .../other-parcels/other-parcels.component.ts | 7 +- ...pplication-crown-owner-dialog.component.ts | 6 +- .../application-owner-dialog.component.ts | 20 +- .../application-owners-dialog.component.ts | 5 +- .../parcel-details.component.ts | 9 +- .../parcel-entry/parcel-entry.component.ts | 13 +- .../parcel-owners/parcel-owners.component.ts | 5 +- .../primary-contact.component.ts | 25 +- .../excl-proposal/excl-proposal.component.ts | 6 +- .../incl-proposal/incl-proposal.component.ts | 6 +- .../naru-proposal/naru-proposal.component.ts | 6 +- .../pfrs-proposal/pfrs-proposal.component.ts | 6 +- .../pofo-proposal/pofo-proposal.component.ts | 6 +- .../roso-proposal/roso-proposal.component.ts | 6 +- .../subd-proposal/subd-proposal.component.ts | 6 +- .../tur-proposal/tur-proposal.component.ts | 6 +- .../review-attachments.component.ts | 7 +- .../review-submit-fng.component.ts | 3 +- .../review-submit/review-submit.component.ts | 3 +- .../lfng-review/lfng-review.component.ts | 7 +- .../edit-submission-base.module.ts | 48 ++ .../edit-submission.component.html | 71 ++- .../edit-submission.component.scss | 378 +++++++++++++ .../edit-submission.component.spec.ts | 27 +- .../edit-submission.component.ts | 150 +++++- .../edit-submission/edit-submission.module.ts | 12 +- .../delete-parcel-dialog.component.html | 36 ++ .../delete-parcel-dialog.component.scss | 24 + .../delete-parcel-dialog.component.spec.ts | 48 ++ .../delete-parcel-dialog.component.ts | 52 ++ .../parcels/parcel-details.component.html | 68 +++ .../parcels/parcel-details.component.scss | 0 .../parcels/parcel-details.component.spec.ts | 70 +++ .../parcels/parcel-details.component.ts | 173 ++++++ ...l-entry-confirmation-dialog.component.html | 35 ++ ...l-entry-confirmation-dialog.component.scss | 24 + ...ntry-confirmation-dialog.component.spec.ts | 35 ++ ...cel-entry-confirmation-dialog.component.ts | 33 ++ .../parcel-entry/parcel-entry.component.html | 372 +++++++++++++ .../parcel-entry/parcel-entry.component.scss | 96 ++++ .../parcel-entry.component.spec.ts | 81 +++ .../parcel-entry/parcel-entry.component.ts | 501 ++++++++++++++++++ .../parcel-owners.component.html | 73 +++ .../parcel-owners.component.scss | 14 + .../parcel-owners.component.spec.ts | 41 ++ .../parcel-owners/parcel-owners.component.ts | 91 ++++ .../edit-submission/step.partial.ts | 34 ++ .../application-document.dto.ts | 44 +- .../application-document.service.spec.ts | 2 +- .../application-document.service.ts | 8 +- .../application-owner.dto.ts | 17 +- .../application-owner.service.ts | 2 +- .../application-parcel.service.ts | 12 +- .../src/app/services/code/code.service.ts | 4 +- .../app/services/document/document.service.ts | 2 +- .../notice-of-intent-document.dto.ts | 18 + .../notice-of-intent-document.service.spec.ts | 91 ++++ .../notice-of-intent-document.service.ts | 91 ++++ .../notice-of-intent-owner.dto.ts | 57 ++ .../notice-of-intent-owner.service.spec.ts | 189 +++++++ .../notice-of-intent-owner.service.ts | 144 +++++ .../notice-of-intent-parcel.dto.ts | 29 + .../notice-of-intent-parcel.service.spec.ts | 102 ++++ .../notice-of-intent-parcel.service.ts | 107 ++++ .../notice-of-intent-submission.dto.ts | 2 + .../src/app/shared/dto/document.dto.ts | 41 ++ .../src/app/shared/dto/owner.dto.ts | 13 + .../application-document.service.ts | 4 +- ...tice-of-intent-document.controller.spec.ts | 2 - .../notice-of-intent-document.service.ts | 68 +-- .../notice-of-intent.service.ts | 4 +- .../application-owner.automapper.profile.ts | 5 +- ...tice-of-intent-owner.automapper.profile.ts | 82 +++ ...ice-of-intent-parcel.automapper.profile.ts | 85 +++ .../common/owner-type/owner-type.entity.ts | 23 + .../application-submission-draft.module.ts | 4 +- ...ation-submission-review.controller.spec.ts | 4 +- ...pplication-submission-review.controller.ts | 10 +- .../application-owner-type.entity.ts | 12 - .../application-owner.controller.spec.ts | 14 +- .../application-owner.controller.ts | 15 +- .../application-owner.dto.ts | 18 +- .../application-owner.entity.ts | 6 +- .../application-owner.service.spec.ts | 8 +- .../application-owner.service.ts | 14 +- ...ation-submission-validator.service.spec.ts | 34 +- ...pplication-submission-validator.service.ts | 16 +- .../application-submission.module.ts | 4 +- .../alcs/src/portal/code/code.controller.ts | 7 +- ...tice-of-intent-document.controller.spec.ts | 179 +++++++ .../notice-of-intent-document.controller.ts | 162 ++++++ .../notice-of-intent-document.dto.ts | 30 ++ .../notice-of-intent-document.module.ts | 17 + .../notice-of-intent-owner.controller.spec.ts | 345 ++++++++++++ .../notice-of-intent-owner.controller.ts | 259 +++++++++ .../notice-of-intent-owner.dto.ts | 138 +++++ .../notice-of-intent-owner.entity.ts | 77 +++ .../notice-of-intent-owner.service.spec.ts | 341 ++++++++++++ .../notice-of-intent-owner.service.ts | 294 ++++++++++ ...-of-intent-parcel-ownership-type.entity.ts | 5 + ...notice-of-intent-parcel.controller.spec.ts | 165 ++++++ .../notice-of-intent-parcel.controller.ts | 158 ++++++ .../notice-of-intent-parcel.dto.ts | 147 +++++ .../notice-of-intent-parcel.entity.ts | 147 +++++ .../notice-of-intent-parcel.service.spec.ts | 248 +++++++++ .../notice-of-intent-parcel.service.ts | 140 +++++ .../notice-of-intent-submission.entity.ts | 7 + .../notice-of-intent-submission.module.ts | 34 +- .../notice-of-intent-submission.service.ts | 4 + .../generate-submission-document.service.ts | 4 +- .../apps/alcs/src/portal/portal.module.ts | 3 + ...5-add_noi_submission_parcels_and_owners.ts | 111 ++++ ...691705084117-seed_noi_parcel_owner_type.ts | 14 + 125 files changed, 6910 insertions(+), 343 deletions(-) create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/step.partial.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.dto.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts create mode 100644 portal-frontend/src/app/shared/dto/document.dto.ts create mode 100644 portal-frontend/src/app/shared/dto/owner.dto.ts create mode 100644 services/apps/alcs/src/common/automapper/notice-of-intent-owner.automapper.profile.ts create mode 100644 services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts create mode 100644 services/apps/alcs/src/common/owner-type/owner-type.entity.ts delete mode 100644 services/apps/alcs/src/portal/application-submission/application-owner/application-owner-type/application-owner-type.entity.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.dto.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.module.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691692339215-add_noi_submission_parcels_and_owners.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691705084117-seed_noi_parcel_owner_type.ts diff --git a/portal-frontend/src/app/features/applications/application-details/application-details.component.html b/portal-frontend/src/app/features/applications/application-details/application-details.component.html index 8c1e4e2548..de1ad9841b 100644 --- a/portal-frontend/src/app/features/applications/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/applications/application-details/application-details.component.html @@ -42,18 +42,18 @@

3. Primary Contact

Organization (optional) - Ministry/Department Responsible - Department + Ministry/Department Responsible + Department
{{ primaryContact?.organizationName }}
diff --git a/portal-frontend/src/app/features/applications/application-details/application-details.component.ts b/portal-frontend/src/app/features/applications/application-details/application-details.component.ts index c8ac8740c3..da184ade67 100644 --- a/portal-frontend/src/app/features/applications/application-details/application-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/application-details.component.ts @@ -1,17 +1,15 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; -import { APPLICATION_OWNER, ApplicationOwnerDetailedDto } from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDetailedDto } from '../../../services/application-owner/application-owner.dto'; import { PARCEL_TYPE } from '../../../services/application-parcel/application-parcel.dto'; import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; import { LocalGovernmentDto } from '../../../services/code/code.dto'; import { CodeService } from '../../../services/code/code.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../shared/dto/owner.dto'; @Component({ selector: 'app-application-details', @@ -37,7 +35,7 @@ export class ApplicationDetailsComponent implements OnInit, OnDestroy { otherFiles: ApplicationDocumentDto[] = []; needsAuthorizationLetter = true; appDocuments: ApplicationDocumentDto[] = []; - APPLICATION_OWNER = APPLICATION_OWNER; + OWNER_TYPE = OWNER_TYPE; private localGovernments: LocalGovernmentDto[] = []; private otherFileTypes = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; @@ -57,11 +55,10 @@ export class ApplicationDetailsComponent implements OnInit, OnDestroy { this.populateLocalGovernment(app.localGovernmentUuid); this.needsAuthorizationLetter = - !(this.primaryContact?.type.code === APPLICATION_OWNER.GOVERNMENT) && + !(this.primaryContact?.type.code === OWNER_TYPE.GOVERNMENT) && !( app.owners.length === 1 && - (app.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL || - app.owners[0].type.code === APPLICATION_OWNER.GOVERNMENT) + (app.owners[0].type.code === OWNER_TYPE.INDIVIDUAL || app.owners[0].type.code === OWNER_TYPE.GOVERNMENT) ); } }); diff --git a/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts index 0d9ce89d03..cb7cd34589 100644 --- a/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/excl-details/excl-details.component.ts @@ -2,7 +2,8 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-excl-details', diff --git a/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts index b81fd9271c..758c5d32fd 100644 --- a/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/incl-details/incl-details.component.ts @@ -3,8 +3,9 @@ import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-incl-details', diff --git a/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts index bcbb2644a9..50b6c278e4 100644 --- a/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/naru-details/naru-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-naru-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts index d719c83504..934048d407 100644 --- a/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/pfrs-details/pfrs-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-pfrs-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts index c412d258e5..f2f1576b33 100644 --- a/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/pofo-details/pofo-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-pofo-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts index 7b4d0d5c5e..1d7f7db422 100644 --- a/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/roso-details/roso-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-roso-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts index 61ee6278fe..295e9ab999 100644 --- a/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/subd-details/subd-details.component.ts @@ -1,10 +1,11 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { PARCEL_TYPE } from '../../../../services/application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-subd-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts index dc196ca276..6444c3b03d 100644 --- a/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/tur-details/tur-details.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-tur-details[applicationSubmission]', diff --git a/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts index 2840e92774..c583e6b74d 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; import { FileHandle } from '../../../shared/file-drag-drop/drag-drop.directive'; import { RemoveFileConfirmationDialogComponent } from '../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; import { StepComponent } from './step.partial'; @@ -18,6 +19,7 @@ export abstract class FilesStepComponent extends StepComponent { DOCUMENT_TYPE = DOCUMENT_TYPE; protected fileId = ''; + protected abstract save(): Promise; protected constructor(protected applicationDocumentService: ApplicationDocumentService, protected dialog: MatDialog) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts index 2984985c68..8e26372636 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts @@ -5,14 +5,12 @@ import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; import { ApplicationDocumentDto, - ApplicationDocumentTypeDto, ApplicationDocumentUpdateDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { CodeService } from '../../../../services/code/code.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../shared/dto/document.dto'; import { EditApplicationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; @@ -27,13 +25,13 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI currentStep = EditApplicationSteps.Attachments; displayedColumns = ['type', 'description', 'fileName', 'actions']; - selectableTypes: ApplicationDocumentTypeDto[] = []; + selectableTypes: DocumentTypeDto[] = []; otherFiles: ApplicationDocumentDto[] = []; private isDirty = false; form = new FormGroup({} as any); - private documentCodes: ApplicationDocumentTypeDto[] = []; + private documentCodes: DocumentTypeDto[] = []; constructor( private router: Router, @@ -117,7 +115,7 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI private async loadDocumentCodes() { const codes = await this.codeService.loadCodes(); - this.documentCodes = codes.applicationDocumentTypes; + this.documentCodes = codes.documentTypes; this.selectableTypes = this.documentCodes.filter((code) => USER_CONTROLLED_TYPES.includes(code.code)); } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts index d15c92e1f3..e9476cdbc2 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-parcels/other-parcels.component.ts @@ -1,11 +1,9 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MatDialog } from '@angular/material/dialog'; -import { Router } from '@angular/router'; import { BehaviorSubject, takeUntil } from 'rxjs'; import { - APPLICATION_OWNER, ApplicationOwnerDetailedDto, ApplicationOwnerDto, } from '../../../../services/application-owner/application-owner.dto'; @@ -18,6 +16,7 @@ import { ApplicationParcelService } from '../../../../services/application-parce import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { ToastService } from '../../../../services/toast/toast.service'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; import { getLetterCombinations } from '../../../../shared/utils/number-to-letter-helper'; import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; @@ -71,7 +70,7 @@ export class OtherParcelsComponent extends StepComponent implements OnInit, OnDe this.fileId = application.fileNumber; this.submissionUuid = application.uuid; const nonAgentOwners = application.owners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.owners = nonAgentOwners.map((o) => ({ ...o, diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts index fc2981a6fe..8aba0af222 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts @@ -2,12 +2,12 @@ import { Component, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { - APPLICATION_OWNER, ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; @Component({ selector: 'app-application-crown-owner-dialog', @@ -61,7 +61,7 @@ export class ApplicationCrownOwnerDialogComponent { lastName: this.lastName.getRawValue() || undefined, email: this.email.getRawValue()!, phoneNumber: this.phoneNumber.getRawValue()!, - typeCode: APPLICATION_OWNER.CROWN, + typeCode: OWNER_TYPE.CROWN, applicationSubmissionUuid: this.data.submissionUuid, }; @@ -80,7 +80,7 @@ export class ApplicationCrownOwnerDialogComponent { lastName: this.lastName.getRawValue(), email: this.email.getRawValue()!, phoneNumber: this.phoneNumber.getRawValue()!, - typeCode: APPLICATION_OWNER.CROWN, + typeCode: OWNER_TYPE.CROWN, }; if (this.existingUuid) { const res = await this.appOwnerService.update(this.existingUuid, updateDto); diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts index 37fe30f179..0826020249 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts @@ -2,21 +2,17 @@ import { Component, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { - ApplicationDocumentDto, - ApplicationDocumentTypeDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { - APPLICATION_OWNER, ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; import { CodeService } from '../../../../../services/code/code.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; @@ -26,8 +22,8 @@ import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submis styleUrls: ['./application-owner-dialog.component.scss'], }) export class ApplicationOwnerDialogComponent { - OWNER_TYPE = APPLICATION_OWNER; - type = new FormControl(APPLICATION_OWNER.INDIVIDUAL); + OWNER_TYPE = OWNER_TYPE; + type = new FormControl(OWNER_TYPE.INDIVIDUAL); firstName = new FormControl('', [Validators.required]); lastName = new FormControl('', [Validators.required]); organizationName = new FormControl(''); @@ -49,7 +45,7 @@ export class ApplicationOwnerDialogComponent { corporateSummary: this.corporateSummary, }); private pendingFile: File | undefined; - private documentCodes: ApplicationDocumentTypeDto[] = []; + private documentCodes: DocumentTypeDto[] = []; constructor( private dialogRef: MatDialogRef, @@ -87,7 +83,7 @@ export class ApplicationOwnerDialogComponent { } onChangeType($event: MatButtonToggleChange) { - if ($event.value === APPLICATION_OWNER.ORGANIZATION) { + if ($event.value === OWNER_TYPE.ORGANIZATION) { this.organizationName.setValidators([Validators.required]); this.corporateSummary.setValidators([Validators.required]); } else { @@ -210,6 +206,6 @@ export class ApplicationOwnerDialogComponent { private async loadDocumentCodes() { const codes = await this.codeService.loadCodes(); - this.documentCodes = codes.applicationDocumentTypes; + this.documentCodes = codes.documentTypes; } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts index 83c7335b90..8a884734aa 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts @@ -1,7 +1,8 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; @Component({ selector: 'app-application-owner-dialog', @@ -32,7 +33,7 @@ export class ApplicationOwnersDialogComponent { const updatedOwners = await this.applicationOwnerService.fetchBySubmissionId(this.submissionUuid); if (updatedOwners) { this.owners = updatedOwners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.isDirty = true; } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts index 6c2f6e4cee..3cc45bef0e 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-details.component.ts @@ -1,8 +1,8 @@ -import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { BehaviorSubject, takeUntil } from 'rxjs'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; import { ApplicationParcelDto, @@ -11,6 +11,7 @@ import { } from '../../../../services/application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; import { ToastService } from '../../../../services/toast/toast.service'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../edit-submission.component'; import { StepComponent } from '../step.partial'; @@ -50,7 +51,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft this.submissionUuid = applicationSubmission.uuid; this.loadParcels(); const parcelOwners = applicationSubmission.owners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.$owners.next(parcelOwners); } @@ -164,7 +165,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft const owners = await this.applicationOwnerService.fetchBySubmissionId(this.submissionUuid); if (owners) { const parcelOwners = owners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.$owners.next(parcelOwners); } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts index b6567a7ea4..072389f93b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts @@ -3,12 +3,9 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; import { ApplicationParcelDto, @@ -16,6 +13,8 @@ import { } from '../../../../../services/application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; import { ParcelService } from '../../../../../services/parcel/parcel.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; @@ -334,9 +333,9 @@ export class ParcelEntryComponent implements OnInit { return owners .filter((owner) => { if (this.isCrownLand) { - return [APPLICATION_OWNER.CROWN].includes(owner.type.code); + return [OWNER_TYPE.CROWN].includes(owner.type.code); } else { - return [APPLICATION_OWNER.INDIVIDUAL, APPLICATION_OWNER.ORGANIZATION].includes(owner.type.code); + return [OWNER_TYPE.INDIVIDUAL, OWNER_TYPE.ORGANIZATION].includes(owner.type.code); } }) .map((owner) => { diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts index 760e03f11a..e0f04f417f 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts @@ -1,8 +1,9 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { ApplicationOwnerDto, APPLICATION_OWNER } from '../../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { ApplicationCrownOwnerDialogComponent } from '../application-crown-owner-dialog/application-crown-owner-dialog.component'; import { ApplicationOwnerDialogComponent } from '../application-owner-dialog/application-owner-dialog.component'; @@ -44,7 +45,7 @@ export class ParcelOwnersComponent { onEdit(owner: ApplicationOwnerDto) { let dialog; - if (owner.type.code === APPLICATION_OWNER.CROWN) { + if (owner.type.code === OWNER_TYPE.CROWN) { dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { data: { isDraft: this.isDraft, diff --git a/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts index cc5f41607d..e42b200f13 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts @@ -3,12 +3,14 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; -import { APPLICATION_OWNER, ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerDto } from '../../../../services/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; import { EditApplicationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; @@ -102,10 +104,9 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni ...owner, isSelected: owner.uuid === uuid, })); - this.selectedThirdPartyAgent = - (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.AGENT) || uuid == 'agent'; + this.selectedThirdPartyAgent = (selectedOwner && selectedOwner.type.code === OWNER_TYPE.AGENT) || uuid == 'agent'; this.selectedLocalGovernment = - (selectedOwner && selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT) || uuid == 'government'; + (selectedOwner && selectedOwner.type.code === OWNER_TYPE.GOVERNMENT) || uuid == 'government'; this.form.reset(); if (this.selectedLocalGovernment) { @@ -136,7 +137,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni phoneNumber: selectedOwner.phoneNumber, email: selectedOwner.email, }); - this.isCrownOwner = selectedOwner.type.code === APPLICATION_OWNER.CROWN; + this.isCrownOwner = selectedOwner.type.code === OWNER_TYPE.CROWN; } } this.calculateLetterRequired(); @@ -146,14 +147,14 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni if (this.selectedLocalGovernment) { this.needsAuthorizationLetter = false; } else { - const isSelfApplicant = this.owners.length > 0 && this.owners[0].type.code === APPLICATION_OWNER.INDIVIDUAL; + const isSelfApplicant = this.owners.length > 0 && this.owners[0].type.code === OWNER_TYPE.INDIVIDUAL; this.needsAuthorizationLetter = this.selectedThirdPartyAgent || !( isSelfApplicant && (this.owners.length === 1 || (this.owners.length === 2 && - this.owners[1].type.code === APPLICATION_OWNER.AGENT && + this.owners[1].type.code === OWNER_TYPE.AGENT && !this.selectedThirdPartyAgent)) ); } @@ -181,7 +182,7 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni email: this.email.getRawValue() ?? '', phoneNumber: this.phoneNumber.getRawValue() ?? '', ownerUuid: selectedOwner?.uuid, - type: this.selectedThirdPartyAgent ? APPLICATION_OWNER.AGENT : APPLICATION_OWNER.GOVERNMENT, + type: this.selectedThirdPartyAgent ? OWNER_TYPE.AGENT : OWNER_TYPE.GOVERNMENT, }); } else if (selectedOwner) { await this.applicationOwnerService.setPrimaryContact({ @@ -203,13 +204,13 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni if (owners) { const selectedOwner = owners.find((owner) => owner.uuid === primaryContactOwnerUuid); this.parcelOwners = owners.filter( - (owner) => ![APPLICATION_OWNER.AGENT, APPLICATION_OWNER.GOVERNMENT].includes(owner.type.code) + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) ); this.owners = owners; if (selectedOwner) { - this.selectedThirdPartyAgent = selectedOwner.type.code === APPLICATION_OWNER.AGENT; - this.selectedLocalGovernment = selectedOwner.type.code === APPLICATION_OWNER.GOVERNMENT; + this.selectedThirdPartyAgent = selectedOwner.type.code === OWNER_TYPE.AGENT; + this.selectedLocalGovernment = selectedOwner.type.code === OWNER_TYPE.GOVERNMENT; } if (this.selectedLocalGovernment) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts index 638e8569b5..6f50c7aa0b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts index f56da91fb4..c4b34405ef 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts @@ -1,6 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { AuthenticationService } from '../../../../../services/authentication/authentication.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { FilesStepComponent } from '../../files-step.partial'; @@ -8,10 +9,7 @@ import { ApplicationSubmissionService } from '../../../../../services/applicatio import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { MatDialog } from '@angular/material/dialog'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { EditApplicationSteps } from '../../edit-submission.component'; import { takeUntil } from 'rxjs'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts index 7a5a8ff95b..7c9c17c3a4 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts @@ -4,10 +4,7 @@ import { MatDialog } from '@angular/material/dialog'; import { MatRadioChange } from '@angular/material/radio'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto, @@ -15,6 +12,7 @@ import { } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { CodeService } from '../../../../../services/code/code.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToYesNoString } from '../../../../../shared/utils/boolean-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts index 07a2f4552d..f657a6f1aa 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts index b3ccbb4b96..6f06250f2c 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts index 83812714ac..3528028be2 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts index e1bfec8990..9410136d51 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts @@ -4,15 +4,13 @@ import { MatDialog } from '@angular/material/dialog'; import { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { PARCEL_TYPE } from '../../../../../services/application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts index 0c85764f3e..4a845b4dc0 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts @@ -3,13 +3,11 @@ import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_TYPE, -} from '../../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; diff --git a/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts index 8e8b0f4f06..2ab11d4d1d 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts @@ -1,13 +1,10 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { FileHandle } from '../../../../shared/file-drag-drop/drag-drop.directive'; import { ReviewApplicationFngSteps, ReviewApplicationSteps } from '../review-submission.component'; diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts index b729afc8d9..fc165fe3c2 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts @@ -2,13 +2,14 @@ import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output import { MatExpansionPanel } from '@angular/material/expansion'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionDto } from '../../../../services/application-submission/application-submission.dto'; import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; import { CustomStepperComponent } from '../../../../shared/custom-stepper/custom-stepper.component'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; import { ReviewApplicationFngSteps } from '../review-submission.component'; diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts index b14ca7b5f0..0ee3796c66 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts @@ -2,13 +2,14 @@ import { Component, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output import { MatExpansionPanel } from '@angular/material/expansion'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { ApplicationDocumentDto, DOCUMENT_TYPE } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; import { ApplicationSubmissionDto } from '../../../../services/application-submission/application-submission.dto'; import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; import { CustomStepperComponent } from '../../../../shared/custom-stepper/custom-stepper.component'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; import { ReviewApplicationSteps } from '../review-submission.component'; diff --git a/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts index 91d1d69272..e80935380e 100644 --- a/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.ts @@ -1,11 +1,7 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { - ApplicationDocumentDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionReviewDto } from '../../../../services/application-submission-review/application-submission-review.dto'; import { ApplicationSubmissionReviewService } from '../../../../services/application-submission-review/application-submission-review.service'; @@ -14,6 +10,7 @@ import { SUBMISSION_STATUS, } from '../../../../services/application-submission/application-submission.dto'; import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-lfng-review', diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts new file mode 100644 index 0000000000..744f8fb3ee --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts @@ -0,0 +1,48 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatButtonToggleModule } from '@angular/material/button-toggle'; +import { MatOptionModule } from '@angular/material/core'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTableModule } from '@angular/material/table'; +import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; +import { SharedModule } from '../../../shared/shared.module'; +import { EditSubmissionComponent } from './edit-submission.component'; +import { DeleteParcelDialogComponent } from './parcels/delete-parcel/delete-parcel-dialog.component'; +import { ParcelDetailsComponent } from './parcels/parcel-details.component'; +import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; +import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; +import { ParcelOwnersComponent } from './parcels/parcel-owners/parcel-owners.component'; + +@NgModule({ + declarations: [ + EditSubmissionComponent, + ParcelDetailsComponent, + ParcelEntryComponent, + ParcelEntryConfirmationDialogComponent, + DeleteParcelDialogComponent, + ParcelOwnersComponent, + ], + imports: [ + CommonModule, + SharedModule, + NgxMaskDirective, + NgxMaskPipe, + MatInputModule, + MatButtonToggleModule, + MatFormFieldModule, + MatOptionModule, + MatSelectModule, + MatTableModule, + ], + exports: [ + EditSubmissionComponent, + ParcelDetailsComponent, + ParcelEntryComponent, + ParcelEntryConfirmationDialogComponent, + DeleteParcelDialogComponent, + ParcelOwnersComponent, + ], +}) +export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html index 2d6797f88a..ab2679138c 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html @@ -1 +1,70 @@ -

edit-submission works!

+ +
+
Notice of Intent ID: {{ noiSubmission.fileNumber }} | {{ noiSubmission.type }}
+
+ +
+ + + help_outline + +
+
+
+
+ + + + +
+ +
+
+ + +
Primary Contact
+
+ + +
Select Government
+
+ + +
Land Use
+
+ + +
Proposal
+
+ + +
Other attachments
+
+ + +
Review and Submit
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss index e69de29bb2..e745545965 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.scss @@ -0,0 +1,378 @@ +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; + +.header { + margin: rem(24) 0; + display: flex; + justify-content: space-between; + flex-flow: row wrap; + + h6 { + display: flex; + align-items: center; + justify-content: center; + } + .header-btn-wrapper { + display: flex; + align-items: center; + flex-wrap: wrap; + margin-top: rem(16); + width: 100%; + + button { + width: 100%; + } + } + + .change-app-type-btn-wrapper { + display: flex; + align-items: center; + margin-top: rem(16); + margin-left: 0; + width: 100%; + + button { + width: 100%; + } + + .mat-icon { + margin-left: rem(8); + width: rem(22); + height: rem(22); + font-size: rem(22); + } + } + + @media screen and (min-width: $tabletBreakpoint) { + .header-btn-wrapper { + margin-top: 0; + width: unset; + + button { + width: auto; + } + + .change-app-type-btn-wrapper { + width: auto; + display: flex; + align-items: center; + margin-top: 0; + margin-left: rem(16); + + button { + width: auto; + } + + .mat-icon { + margin-left: rem(16); + font-size: rem(24); + } + } + } + } +} + +:host::ng-deep { + .step-description { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: rem(24); + margin-bottom: rem(32); + } + + .step-documents { + border: rem(1) solid colors.$secondary-color; + border-radius: rem(4); + padding: rem(8); + + h6 { + color: colors.$secondary-color; + margin-top: unset !important; + } + } + + .mat-icon.mat-icon-inline { + line-height: initial; + } + + .mat-horizontal-content-container { + padding: 0; + min-height: 100%; + } + + .mat-horizontal-stepper-wrapper { + min-height: 100%; + } + + .subtext { + margin-bottom: rem(8) !important; + } + + .mat-step-header .mat-step-icon-selected { + color: #fff; + } + + .mat-step-header .mat-step-label.mat-step-label-active { + display: none; + } + + .mat-step-label { + display: none; + } + + .section { + margin: rem(24) 0; + } + + .date-picker { + ::ng-deep.mat-form-field-infix { + display: flex; + padding: rem(3) 0; + } + } + + .mat-button-wrapper { + white-space: break-spaces; + } + + .parcel-buttons-wrappers button { + margin-top: 1.2rem !important; + } + + @media screen and (min-width: $tabletBreakpoint) { + .mat-step-header .mat-step-label.mat-step-label-active { + display: inherit; + } + + .mat-step-label { + display: inherit; + } + } + + .review-step { + min-height: 100%; + } + + .mobile-hidden { + display: none !important; + + @media screen and (min-width: $tabletBreakpoint) { + display: initial !important; + } + } + + .tablet-hidden { + display: initial !important; + + @media screen and (min-width: $tabletBreakpoint) { + display: none !important; + } + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: rem(33); + } + + .mat-button-toggle-checked.mat-button-toggle-appearance-standard { + background-color: colors.$primary-color-light; + } + + .edit-application { + .description { + margin-top: rem(12); + margin-bottom: rem(20); + display: flex; + flex-direction: column; + + div { + display: flex; + align-items: center; + } + + .save-button { + margin-top: rem(16) !important; + width: 100%; + } + + @media screen and (min-width: $tabletBreakpoint) { + flex-direction: row; + justify-content: space-between; + + .save-button { + margin-left: rem(12) !important; + margin-top: unset !important; + width: unset; + } + } + } + + .button-container { + margin-top: rem(40); + margin-bottom: rem(24); + display: flex; + flex-direction: column-reverse; + justify-content: space-between; + + @media screen and (min-width: $tabletBreakpoint) { + flex-direction: row; + justify-content: space-between; + } + + div { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: rem(8); + + @media screen and (min-width: $tabletBreakpoint) { + display: flex; + justify-content: space-between; + } + + button { + margin-bottom: rem(24) !important; + + @media screen and (min-width: $tabletBreakpoint) { + margin-left: rem(24) !important; + margin-bottom: 0 !important; + } + } + } + } + } + + // parcel entry details + .float-right { + float: right; + } + + .container { + margin: rem(20) 0 rem(20) 0; + } + + .parcel-checkbox { + margin: rem(20) 0 0 0; + } + + .type { + margin-bottom: rem(30); + } + + .pmbc-search { + border: 1px solid colors.$primary-color-dark; + border-radius: rem(4); + background-color: rgba(colors.$accent-color-light, 0.2); + padding: rem(16); + } + + .field-error { + color: colors.$error-color; + font-size: rem(15); + font-weight: 700; + display: flex; + align-items: center; + margin-top: rem(4); + } + + .form-row { + margin-top: rem(16); + display: grid; + grid-template-columns: 1fr; + grid-column-gap: rem(30); + + @media screen and (min-width: $desktopBreakpoint) { + & { + grid-template-columns: 1fr 1fr; + } + + .full-row { + grid-column: 1/3; + } + } + } + + .radio-row { + margin: rem(10) 0; + display: block; + + .mat-radio-button { + margin-right: rem(12); + } + } + + .full-width-input { + width: 100%; + } + + .mat-expansion-panel { + margin-bottom: rem(16); + } + + .mat-checkbox { + display: flex; + justify-content: center; + .mat-checkbox-layout { + white-space: break-spaces; + } + } + + .flex-center-wrap { + display: flex; + justify-content: center; + flex-wrap: wrap; + } + + .flex-evenly-wrap { + display: flex; + justify-content: space-evenly; + flex-wrap: wrap; + } + + .flex-space-between-wrap { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + } + + .other-parcels-form { + width: 100%; + display: flex; + flex-direction: column; + row-gap: rem(24); + } + + .owner-table-wrapper { + display: block; + overflow: auto; + width: 100%; + } + + .land-use-form { + display: grid; + grid-template-columns: 1fr; + + .land-use-type-wrapper { + display: flex; + gap: rem(24); + flex-wrap: wrap; + width: 100%; + + .land-use-type { + width: 100%; + } + } + + @media screen and (min-width: $tabletBreakpoint) { + .land-use-type-wrapper { + flex-wrap: nowrap; + + .land-use-type { + width: 64%; + } + } + } + } + + .display-block { + display: block; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts index 74776e7f67..b77ae01080 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts @@ -1,4 +1,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute } from '@angular/router'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { ToastService } from '../../../services/toast/toast.service'; import { EditSubmissionComponent } from './edit-submission.component'; @@ -8,9 +12,26 @@ describe('EditSubmissionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ EditSubmissionComponent ] - }) - .compileComponents(); + declarations: [EditSubmissionComponent], + providers: [ + { + provide: NoticeOfIntentSubmissionService, + useValue: {}, + }, + { + provide: ToastService, + useValue: {}, + }, + { + provide: ActivatedRoute, + useValue: {}, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + }).compileComponents(); fixture = TestBed.createComponent(EditSubmissionComponent); component = fixture.componentInstance; diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts index 2dcc57f5ab..0f183800a7 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts @@ -1,10 +1,154 @@ -import { Component } from '@angular/core'; +import { StepperSelectionEvent } from '@angular/cdk/stepper'; +import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { ToastService } from '../../../services/toast/toast.service'; +import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; +import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; +import { scrollToElement } from '../../../shared/utils/scroll-helper'; +import { EditApplicationSteps } from '../../applications/edit-submission/edit-submission.component'; +import { ParcelDetailsComponent } from './parcels/parcel-details.component'; + +export enum EditNoiSteps { + Parcel = 0, + PrimaryContact = 1, + Government = 2, + LandUse = 3, + Proposal = 4, + ExtraInfo = 5, + Attachments = 6, + ReviewAndSubmit = 7, +} @Component({ selector: 'app-edit-submission', templateUrl: './edit-submission.component.html', - styleUrls: ['./edit-submission.component.scss'] + styleUrls: ['./edit-submission.component.scss'], }) -export class EditSubmissionComponent { +export class EditSubmissionComponent implements OnDestroy, AfterViewInit { + fileId = ''; + + $destroy = new Subject(); + $noiSubmission = new BehaviorSubject(undefined); + noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + + steps = EditNoiSteps; + expandedParcelUuid?: string; + showValidationErrors = false; + + @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; + + @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; + + constructor( + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private activatedRoute: ActivatedRoute, + private dialog: MatDialog, + private toastService: ToastService, + private overlayService: OverlaySpinnerService, + private router: Router + ) { + this.expandedParcelUuid = undefined; + + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + this.noiSubmission = submission; + }); + } + + ngAfterViewInit(): void { + combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.paramMap]) + .pipe(takeUntil(this.$destroy)) + .subscribe(([queryParamMap, paramMap]) => { + const fileId = paramMap.get('fileId'); + if (fileId) { + this.loadSubmission(fileId).then(() => { + const stepInd = paramMap.get('stepInd'); + const parcelUuid = queryParamMap.get('parcelUuid'); + const showErrors = queryParamMap.get('errors'); + if (showErrors) { + this.showValidationErrors = showErrors === 't'; + } + + if (stepInd) { + // setTimeout is required for stepper to be initialized + setTimeout(() => { + this.customStepper.navigateToStep(parseInt(stepInd), true); + + if (parcelUuid) { + this.expandedParcelUuid = parcelUuid; + } + }); + } + }); + } + }); + } + + async onExit() { + await this.router.navigateByUrl(`/notice-of-intent/${this.fileId}`); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + // this gets fired whenever applicant navigates away from edit page + async canDeactivate(): Promise> { + await this.saveSubmission(this.customStepper.selectedIndex); + + return of(true); + } + + async onStepChange($event: StepperSelectionEvent) { + // scrolls to step if step selected programmatically + scrollToElement({ id: `stepWrapper_${$event.selectedIndex}`, center: false }); + } + + async onBeforeSwitchStep(index: number) { + // navigation to url will cause step change based on the index (index starts from 0) + // The save will be triggered using canDeactivate guard + this.showValidationErrors = this.customStepper.selectedIndex === EditNoiSteps.ReviewAndSubmit; + await this.router.navigateByUrl(`notice-of-intent/${this.fileId}/edit/${index}`); + } + + async saveSubmission(step: number) { + switch (step) { + case EditApplicationSteps.AppParcel: + await this.parcelDetailsComponent.onSave(); + break; + default: + this.toastService.showErrorToast('Error updating notice of intent.'); + } + } + + async onDownloadPdf(fileNumber: string | undefined) { + if (fileNumber) { + //TODO: Hook this up later + } + } + + onChangeSubmissionType() { + //TODO: Hook this up later + } + + private async loadSubmission(fileId: string, reload = false) { + if (!this.noiSubmission || reload) { + this.overlayService.showSpinner(); + this.noiSubmission = await this.noticeOfIntentSubmissionService.getByFileId(fileId); + this.fileId = fileId; + this.$noiSubmission.next(this.noiSubmission); + this.overlayService.hideSpinner(); + } + } + onParcelDetailsInitialized() { + if (this.expandedParcelUuid && this.parcelDetailsComponent) { + this.parcelDetailsComponent.openParcel(this.expandedParcelUuid); + this.expandedParcelUuid = undefined; + } + } } diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts index 547fc2c060..ba969b4f33 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts @@ -1,18 +1,26 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule, Routes } from '@angular/router'; +import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; import { SharedModule } from '../../../shared/shared.module'; +import { EditSubmissionBaseModule } from './edit-submission-base.module'; import { EditSubmissionComponent } from './edit-submission.component'; +import { StepComponent } from './step.partial'; const routes: Routes = [ { path: '', component: EditSubmissionComponent, }, + { + path: ':stepInd', + component: EditSubmissionComponent, + canDeactivate: [CanDeactivateGuard], + }, ]; @NgModule({ - declarations: [EditSubmissionComponent], - imports: [CommonModule, SharedModule, RouterModule.forChild(routes)], + declarations: [StepComponent], + imports: [CommonModule, SharedModule, RouterModule.forChild(routes), EditSubmissionBaseModule], }) export class EditSubmissionModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html new file mode 100644 index 0000000000..77c2809519 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + +
+

Delete Parcel #{{ parcelNumber }}

+
+ +
+
+ + Warning: All information relevant to this parcel, including information added in subsequent steps, will be + deleted. + + +
+ +
+
+ +
+
+
Are you sure you want to delete this? This action cannot be undone.
+
+ +
+ +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss new file mode 100644 index 0000000000..1f78db7f1e --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss @@ -0,0 +1,24 @@ +@use '../../../../../../styles/functions' as *; + +.margin-bottom-1 { + margin-bottom: rem(16); +} + +.step-controls { + display: flex; + justify-content: space-between; +} + +.confirm-content { + margin: rem(24) 0; +} + +@media screen and (min-width: $desktopBreakpoint) { + .step-controls { + justify-content: flex-end; + + button { + margin-left: rem(25) !important; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts new file mode 100644 index 0000000000..9beff5eaf0 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HttpClient } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { DeleteParcelDialogComponent } from './delete-parcel-dialog.component'; + +describe('DeleteParcelDialogComponent', () => { + let component: DeleteParcelDialogComponent; + let fixture: ComponentFixture; + let mockHttpClient: DeepMocked; + let mockApplicationParcelService: DeepMocked; + + beforeEach(async () => { + mockHttpClient = createMock(); + mockApplicationParcelService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [DeleteParcelDialogComponent], + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ApplicationParcelService, + useValue: mockApplicationParcelService, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DeleteParcelDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts new file mode 100644 index 0000000000..f86e0979ab --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts @@ -0,0 +1,52 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; + +export enum ApplicationParcelDeleteStepsEnum { + warning = 0, + confirmation = 1, +} + +@Component({ + selector: 'app-delete-parcel-dialog', + templateUrl: './delete-parcel-dialog.component.html', + styleUrls: ['./delete-parcel-dialog.component.scss'], +}) +export class DeleteParcelDialogComponent { + parcelUuid!: string; + parcelNumber!: string; + + stepIdx = 0; + + warningStep = ApplicationParcelDeleteStepsEnum.warning; + confirmationStep = ApplicationParcelDeleteStepsEnum.confirmation; + + constructor( + private applicationParcelService: ApplicationParcelService, + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DeleteParcelDialogComponent + ) { + this.parcelUuid = data.parcelUuid; + this.parcelNumber = data.parcelNumber; + } + + async next() { + this.stepIdx += 1; + } + + async back() { + this.stepIdx -= 1; + } + + async onCancel(dialogResult: boolean = false) { + this.dialogRef.close(dialogResult); + } + + async onDelete() { + const result = await this.applicationParcelService.deleteMany([this.parcelUuid]); + + if (result) { + this.onCancel(true); + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html new file mode 100644 index 0000000000..0a42627959 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html @@ -0,0 +1,68 @@ +
+
+

Identify Parcels Under Application

+

Provide parcel identification and registered ownership information for each parcel under application.

+

*All fields are required unless stated optional.

+
+
Documents needed for this step:
+
    +
  • Certificate of Title
  • +
  • Corporate Summary (if applicable)
  • +
+
+
+ + + Parcel #{{ parcelInd + 1 }} Details & Owner Information + + + + +
+
+ + +
+
+
+ +
+ +
+ +
+ + +
+
+ +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.spec.ts new file mode 100644 index 0000000000..ef3dea17b9 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.spec.ts @@ -0,0 +1,70 @@ +import { HttpClient } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { ApplicationParcelService } from '../../../../services/application-parcel/application-parcel.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { ParcelDetailsComponent } from './parcel-details.component'; + +describe('ParcelDetailsComponent', () => { + let component: ParcelDetailsComponent; + let fixture: ComponentFixture; + let mockHttpClient: DeepMocked; + let mockNOIParcelService: DeepMocked; + let mockNOIOwnerService: DeepMocked; + let mockToastService: DeepMocked; + let mockMatDialog: DeepMocked; + let noiPipe = new BehaviorSubject(undefined); + + beforeEach(async () => { + mockHttpClient = createMock(); + mockNOIParcelService = createMock(); + mockToastService = createMock(); + mockMatDialog = createMock(); + mockNOIOwnerService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ParcelDetailsComponent], + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNOIParcelService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockNOIOwnerService, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: MatDialog, + useValue: mockMatDialog, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelDetailsComponent); + component = fixture.componentInstance; + component.$noiSubmission = noiPipe; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts new file mode 100644 index 0000000000..d0352c217a --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts @@ -0,0 +1,173 @@ +import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { BehaviorSubject, takeUntil } from 'rxjs'; +import { ApplicationParcelUpdateDto } from '../../../../services/application-parcel/application-parcel.dto'; +import { NoticeOfIntentOwnerDto } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelDto } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; +import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +import { EditNoiSteps } from '../edit-submission.component'; +import { StepComponent } from '../step.partial'; +import { DeleteParcelDialogComponent } from './delete-parcel/delete-parcel-dialog.component'; +import { ParcelEntryFormData } from './parcel-entry/parcel-entry.component'; + +@Component({ + selector: 'app-noi-parcel-details', + templateUrl: './parcel-details.component.html', + styleUrls: ['./parcel-details.component.scss'], +}) +export class ParcelDetailsComponent extends StepComponent implements OnInit, AfterViewInit { + @Output() componentInitialized = new EventEmitter(); + + currentStep = EditNoiSteps.Parcel; + fileId = ''; + submissionUuid = ''; + parcels: NoticeOfIntentParcelDto[] = []; + $owners = new BehaviorSubject([]); + newParcelAdded = false; + isDirty = false; + + constructor( + private router: Router, + private noiParcelService: NoticeOfIntentParcelService, + private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + private toastService: ToastService, + private dialog: MatDialog + ) { + super(); + } + + ngOnInit(): void { + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.fileId = noiSubmission.fileNumber; + this.submissionUuid = noiSubmission.uuid; + this.loadParcels(); + const parcelOwners = noiSubmission.owners.filter( + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) + ); + this.$owners.next(parcelOwners); + } + }); + + this.newParcelAdded = false; + } + + ngAfterViewInit(): void { + setTimeout((_) => this.componentInitialized.emit(true)); + } + + async loadParcels() { + this.parcels = (await this.noiParcelService.fetchBySubmissionUuid(this.submissionUuid)) || []; + if (!this.parcels || this.parcels.length === 0) { + await this.onAddParcel(); + } + } + + async onAddParcel() { + const parcel = await this.noiParcelService.create(this.submissionUuid); + + if (parcel) { + this.parcels.push({ + uuid: parcel!.uuid, + owners: [], + isConfirmedByApplicant: false, + }); + this.newParcelAdded = true; + } else { + this.toastService.showErrorToast('Error adding new parcel. Please refresh page and try again.'); + } + } + + async onParcelFormChange(formData: Partial) { + const parcel = this.parcels.find((e) => e.uuid === formData.uuid); + if (!parcel) { + this.toastService.showErrorToast('Error updating the parcel. Please refresh page and try again.'); + return; + } + + this.isDirty = true; + parcel.pid = formData.pid !== undefined ? formData.pid : parcel.pid; + parcel.pin = formData.pid !== undefined ? formData.pin : parcel.pin; + parcel.civicAddress = formData.civicAddress !== undefined ? formData.civicAddress : parcel.civicAddress; + parcel.legalDescription = + formData.legalDescription !== undefined ? formData.legalDescription : parcel.legalDescription; + + parcel.mapAreaHectares = formData.mapArea !== undefined ? formData.mapArea : parcel.mapAreaHectares; + parcel.ownershipTypeCode = formData.parcelType !== undefined ? formData.parcelType : parcel.ownershipTypeCode; + parcel.isFarm = formData.isFarm !== undefined ? parseStringToBoolean(formData.isFarm) : parcel.isFarm; + parcel.purchasedDate = + formData.purchaseDate !== undefined ? formData.purchaseDate?.getTime() : parcel.purchasedDate; + parcel.isConfirmedByApplicant = formData.isConfirmedByApplicant || false; + parcel.crownLandOwnerType = + formData.crownLandOwnerType !== undefined ? formData.crownLandOwnerType : parcel.crownLandOwnerType; + if (formData.owners) { + //parcel.owners = formData.owners; + } + } + + private async saveProgress() { + if (this.isDirty || this.newParcelAdded) { + const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; + for (const parcel of this.parcels) { + parcelsToUpdate.push({ + uuid: parcel.uuid, + pid: parcel.pid?.toString() || null, + pin: parcel.pin?.toString() || null, + civicAddress: parcel.civicAddress ?? null, + legalDescription: parcel.legalDescription, + isFarm: parcel.isFarm, + purchasedDate: parcel.purchasedDate, + mapAreaHectares: parcel.mapAreaHectares, + ownershipTypeCode: parcel.ownershipTypeCode, + isConfirmedByApplicant: parcel.isConfirmedByApplicant, + crownLandOwnerType: parcel.crownLandOwnerType, + ownerUuids: [], + }); + } + await this.noiParcelService.update(parcelsToUpdate); + } + } + + async onSave() { + await this.saveProgress(); + } + + async onDelete(parcelUuid: string, parcelNumber: number) { + this.dialog + .open(DeleteParcelDialogComponent, { + panelClass: 'no-padding', + disableClose: true, + data: { + parcelUuid, + parcelNumber, + }, + }) + .beforeClosed() + .subscribe((result) => { + if (result) { + this.loadParcels(); + } + }); + } + + async onOwnersUpdated() { + const owners = await this.noticeOfIntentOwnerService.fetchBySubmissionId(this.submissionUuid); + if (owners) { + const parcelOwners = owners.filter( + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) + ); + this.$owners.next(parcelOwners); + } + } + + expandedParcel: string = ''; + + openParcel(index: string) { + this.expandedParcel = index; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html new file mode 100644 index 0000000000..e7e96cb4c5 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html @@ -0,0 +1,35 @@ + + + + + + + + + + +
+

Change Parcel Ownership Type

+
+ +
+
+ + Warning: Changing parcel ownership type will remove some inputs relevant to the current parcel. + + +
+ +
+
+ +
+
+
Are you sure you want to change parcel ownership type? This action cannot be undone.
+
+ +
+ +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss new file mode 100644 index 0000000000..c4e03e5c59 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss @@ -0,0 +1,24 @@ +@use '../../../../../../../styles/functions' as *; + +.margin-bottom-1 { + margin-bottom: rem(16); +} + +.step-controls { + display: flex; + justify-content: space-between; +} + +.confirm-content { + margin: rem(24) 0; +} + +@media screen and (min-width: $desktopBreakpoint) { + .step-controls { + justify-content: flex-end; + + button { + margin-left: rem(25) !important; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts new file mode 100644 index 0000000000..705f0c459e --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog.component'; + +describe('ParcelEntryConfirmationDialogComponent', () => { + let component: ParcelEntryConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ParcelEntryConfirmationDialogComponent], + providers: [ + { + provide: MatDialog, + useValue: {}, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelEntryConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts new file mode 100644 index 0000000000..dfe4bb5e16 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { ApplicationParcelDeleteStepsEnum } from '../../delete-parcel/delete-parcel-dialog.component'; + +@Component({ + selector: 'app-parcel-entry-confirmation-dialog', + templateUrl: './parcel-entry-confirmation-dialog.component.html', + styleUrls: ['./parcel-entry-confirmation-dialog.component.scss'], +}) +export class ParcelEntryConfirmationDialogComponent { + stepIdx = 0; + + warningStep = ApplicationParcelDeleteStepsEnum.warning; + confirmationStep = ApplicationParcelDeleteStepsEnum.confirmation; + + constructor(private dialogRef: MatDialogRef) {} + + async next() { + this.stepIdx += 1; + } + + async back() { + this.stepIdx -= 1; + } + + async onCancel(dialogResult: boolean = false) { + this.dialogRef.close(dialogResult); + } + + async onDelete() { + this.onCancel(true); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html new file mode 100644 index 0000000000..ad624d34bd --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html @@ -0,0 +1,372 @@ +
+
+ +
+ The answer to the following question will change the rest of the application form. Do not change this answer once + selected. +
+ + Fee Simple + Crown + +
+ warning +
This field is required
+
+
+ +
Parcel Lookup
+
+ + +
+ +
Can be found on the parcel's Certificate of Title
+ + + +
+ warning +
This field is required
+
+
+ +
+ +
The area of the entire parcel in hectares, not just the area under application.
+ + + +
+ warning +
This field is required
+
+
+ +
+ + + + +
+ warning +
This field is required
+
Invalid format
+
+
+ +
+ + + + +
+ +
+ + + + + + +
+ warning +
This field is required
+
+
+ Example: 2020-Mar-01 +
+
+ +
+ +
+ As determined by + BC Assessment +
+ + Yes + No + +
+ warning +
This field is required
+
+
+ +
+ + + + +
+ warning +
This field is required
+
+
+
+ +
+ + +
+ +
+
Owner Information
+ +

Provide the following information for all owners listed on the parcel's Certificate of Title

+ + + + + +
+
{{ option.displayName }}
+ +
+
+ +
+
No owner matching search
+ +
+
+ + See all Owners + +
+
+
+ warning +
This field is required
+
+ +
+ +
+
+ +
+ +
+ + Provincial Crown + Federal Crown +
+ warning +
This field is required
+
+
+ + + + +
+
{{ option.displayName }}
+ +
+
+ +
+
No owner matching search
+ +
+
+ + See all Owners + +
+
+
+ warning +
This field is required
+
+ +
+ + + + + + + + + + +
+
+
+ I confirm that the owner information provided above matches the current Certificate of Title. Mismatched + information can cause significant delays to processing time. + +
+ warning +
This field is required
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.scss new file mode 100644 index 0000000000..223b120fa2 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.scss @@ -0,0 +1,96 @@ +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; + +.owner-option { + display: flex; + justify-content: space-between; + align-items: center; +} + +.new-owner { + margin: rem(8) 0 !important; +} + +.link-text { + color: colors.$link-color; + text-decoration: underline; +} + +.lookup-pid-fields { + display: flex; + align-items: center; + flex-direction: column; + + @media screen and (min-width: $desktopBreakpoint) { + flex-direction: row; + } +} + +.lookup-search-by { + margin-top: rem(8); + + @media screen and (min-width: $desktopBreakpoint) { + width: unset !important; + flex-grow: 1; + } +} + +.lookup-input { + margin-top: rem(8); + + @media screen and (min-width: $desktopBreakpoint) { + width: unset !important; + flex-grow: 5; + } +} + +.lookup-search-button { + width: 100%; + margin-top: rem(8) !important; + + @media screen and (min-width: $desktopBreakpoint) { + height: rem(55); + margin-top: rem(7) !important; + width: rem(150); + } +} + +.lookup-bottom-row { + display: flex; + align-items: center; + flex-direction: column; + margin-top: rem(24); + + .reset-button { + width: 100%; + margin-bottom: rem(8); + } + + @media screen and (min-width: $desktopBreakpoint) { + flex-direction: row; + justify-content: space-between; + + .reset-button { + width: unset; + } + } +} + +.crown-owner-type { + display: block; + margin-bottom: rem(24); + + .mat-mdc-radio-button ~ .mat-mdc-radio-button { + margin-left: rem(16); + } +} + +mat-button-toggle-group#isFarm { + height: rem(55); + + .mat-button-toggle { + display: flex; + align-items: center; + height: 100%; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts new file mode 100644 index 0000000000..9bce91efb1 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts @@ -0,0 +1,81 @@ +import { HttpClient } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelDto } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { ParcelService } from '../../../../../services/parcel/parcel.service'; +import { ParcelEntryComponent } from './parcel-entry.component'; + +describe('ParcelEntryComponent', () => { + let component: ParcelEntryComponent; + let fixture: ComponentFixture; + let mockParcelService: DeepMocked; + let mockHttpClient: DeepMocked; + let mockNOIParcelService: DeepMocked; + let mockNOIOwnerService: DeepMocked; + let mockNOIDocumentService: DeepMocked; + + let mockParcel: NoticeOfIntentParcelDto = { + isConfirmedByApplicant: false, + uuid: '', + owners: [], + }; + + beforeEach(async () => { + mockParcelService = createMock(); + mockHttpClient = createMock(); + mockNOIParcelService = createMock(); + mockNOIOwnerService = createMock(); + mockNOIDocumentService = createMock(); + + await TestBed.configureTestingModule({ + imports: [MatAutocompleteModule], + declarations: [ParcelEntryComponent], + providers: [ + { + provide: ParcelService, + useValue: mockParcelService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNOIParcelService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockNOIOwnerService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNOIDocumentService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelEntryComponent); + component = fixture.componentInstance; + component.$owners = new BehaviorSubject([]); + component.parcel = mockParcel; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts new file mode 100644 index 0000000000..bb0f985ece --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts @@ -0,0 +1,501 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject } from 'rxjs'; +import { PARCEL_OWNERSHIP_TYPE } from '../../../../../services/application-parcel/application-parcel.dto'; +import { NoticeOfIntentDocumentDto } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelDto } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { ParcelService } from '../../../../../services/parcel/parcel.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; +import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; +import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; +import { RemoveFileConfirmationDialogComponent } from '../../../../applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; +import { ApplicationCrownOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; +import { ApplicationOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component'; +import { ApplicationOwnersDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component'; +import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; + +export interface ParcelEntryFormData { + uuid: string; + legalDescription: string | undefined | null; + mapArea: string | undefined | null; + pin: string | undefined | null; + pid: string | undefined | null; + civicAddress: string | undefined | null; + parcelType: string | undefined | null; + isFarm: string | undefined | null; + purchaseDate?: Date | null; + crownLandOwnerType?: string | null; + isConfirmedByApplicant: boolean; + owners: NoticeOfIntentOwnerDto[]; +} + +@Component({ + selector: 'app-noi-parcel-entry[parcel][fileId][submissionUuid]', + templateUrl: './parcel-entry.component.html', + styleUrls: ['./parcel-entry.component.scss'], +}) +export class ParcelEntryComponent implements OnInit { + @Input() parcel!: NoticeOfIntentParcelDto; + @Input() fileId!: string; + @Input() submissionUuid!: string; + @Input() $owners: BehaviorSubject = new BehaviorSubject([]); + + @Input() enableOwners = true; + @Input() enableCertificateOfTitleUpload = true; + @Input() enableUserSignOff = true; + @Input() enableAddNewOwner = true; + @Input() showErrors = false; + @Input() _disabled = false; + @Input() isDraft = false; + + @Input() + public set disabled(disabled: boolean) { + this._disabled = disabled; + this.onFormDisabled(); + } + + @Output() private onFormGroupChange = new EventEmitter>(); + @Output() private onSaveProgress = new EventEmitter(); + @Output() onOwnersUpdated = new EventEmitter(); + + owners: NoticeOfIntentOwnerDto[] = []; + filteredOwners: (NoticeOfIntentOwnerDto & { isSelected: boolean })[] = []; + + searchBy = new FormControl(null); + isCrownLand: boolean | null = null; + isCertificateOfTitleRequired = true; + + pidPin = new FormControl(''); + legalDescription = new FormControl(null, [Validators.required]); + mapArea = new FormControl(null, [Validators.required]); + pid = new FormControl(null, [Validators.required]); + pin = new FormControl(null); + civicAddress = new FormControl(null, [Validators.required]); + parcelType = new FormControl(null, [Validators.required]); + isFarm = new FormControl(null, [Validators.required]); + purchaseDate = new FormControl(null, [Validators.required]); + crownLandOwnerType = new FormControl(null); + isConfirmedByApplicant = new FormControl(false, [Validators.requiredTrue]); + parcelForm = new FormGroup({ + pidPin: this.pidPin, + legalDescription: this.legalDescription, + mapArea: this.mapArea, + pin: this.pin, + pid: this.pid, + civicAddress: this.civicAddress, + parcelType: this.parcelType, + isFarm: this.isFarm, + purchaseDate: this.purchaseDate, + crownLandOwnerType: this.crownLandOwnerType, + isConfirmedByApplicant: this.isConfirmedByApplicant, + searchBy: this.searchBy, + }); + pidPinPlaceholder = ''; + + ownerInput = new FormControl(null); + + DOCUMENT_TYPES = DOCUMENT_TYPE; + PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; + maxPurchasedDate = new Date(); + + constructor( + private parcelService: ParcelService, + private noticeOfIntentParcelService: NoticeOfIntentParcelService, + private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private dialog: MatDialog + ) {} + + ngOnInit(): void { + this.setupForm(); + + this.$owners.subscribe((owners) => { + this.owners = owners; + this.filteredOwners = this.mapOwners(owners); + this.parcel.owners = this.parcel.owners.map((owner) => { + const updatedOwner = owners.find((updatedOwner) => updatedOwner.uuid === owner.uuid)!; + if (!updatedOwner) { + console.warn('Failed to find user in array'); + return owner; + } + return updatedOwner; + }); + }); + } + + async onSearch() { + let result; + if (this.searchBy.getRawValue() === 'pin') { + result = await this.parcelService.getByPin(this.pidPin.getRawValue()!); + } else { + result = await this.parcelService.getByPid(this.pidPin.getRawValue()!); + } + + this.onReset(); + if (result) { + this.legalDescription.setValue(result.legalDescription); + this.mapArea.setValue(result.mapArea); + + if (result.pin) { + this.pin.setValue(result.pin); + } + + if (result.pid) { + this.pid.setValue(result.pid); + } + + this.emitFormChangeOnSearchActions(); + } + } + + onReset() { + this.parcelForm.controls.pidPin.reset(); + this.parcelForm.controls.pid.reset(); + this.parcelForm.controls.pin.reset(); + this.parcelForm.controls.legalDescription.reset(); + this.parcelForm.controls.mapArea.reset(); + this.parcelForm.controls.purchaseDate.reset(); + this.parcelForm.controls.isFarm.reset(); + this.parcelForm.controls.civicAddress.reset(); + + this.emitFormChangeOnSearchActions(); + + if (this.showErrors) { + this.parcelForm.markAllAsTouched(); + } else { + this.parcelForm.controls.isFarm.markAsTouched(); + } + } + + private emitFormChangeOnSearchActions() { + this.onFormGroupChange.emit({ + uuid: this.parcel.uuid, + legalDescription: this.legalDescription.getRawValue(), + mapArea: this.mapArea.getRawValue(), + pin: this.pin.getRawValue(), + pid: this.pid.getRawValue(), + }); + } + + onChangeParcelType($event: MatButtonToggleChange) { + const dirtyForm = + this.legalDescription.value || + this.mapArea.value || + this.pid.value || + this.pin.value || + this.purchaseDate.value || + this.isFarm.value || + this.civicAddress.value; + + const changeParcelType = () => { + if ($event.value === this.PARCEL_OWNERSHIP_TYPES.CROWN) { + this.searchBy.setValue(null); + this.pidPinPlaceholder = ''; + this.isCrownLand = true; + this.pid.setValidators([]); + this.purchaseDate.disable(); + } else { + this.searchBy.setValue('pid'); + this.pidPinPlaceholder = 'Type 9 digit PID'; + this.isCrownLand = false; + this.pid.setValidators([Validators.required]); + this.crownLandOwnerType.setValue(null); + this.purchaseDate.enable(); + } + + this.updateParcelOwners([]); + this.filteredOwners = this.mapOwners(this.owners); + this.pid.updateValueAndValidity(); + }; + + if (dirtyForm && this.isCrownLand !== null) { + this.dialog + .open(ParcelEntryConfirmationDialogComponent, { + panelClass: 'no-padding', + disableClose: true, + }) + .beforeClosed() + .subscribe(async (result) => { + if (result) { + this.onReset(); + return changeParcelType(); + } else { + const newParcelType = this.parcelType.getRawValue(); + + const prevParcelType = + newParcelType === this.PARCEL_OWNERSHIP_TYPES.CROWN + ? this.PARCEL_OWNERSHIP_TYPES.FEE_SIMPLE + : this.PARCEL_OWNERSHIP_TYPES.CROWN; + + this.parcelType.setValue(prevParcelType); + } + }); + } else { + return changeParcelType(); + } + } + + async attachFile(file: FileHandle, parcelUuid: string) { + if (parcelUuid) { + const mappedFiles = file.file; + this.parcel.certificateOfTitle = await this.noticeOfIntentParcelService.attachCertificateOfTitle( + this.fileId, + parcelUuid, + mappedFiles + ); + } + } + + async deleteFile($event: NoticeOfIntentDocumentDto) { + if (this.isDraft) { + this.dialog + .open(RemoveFileConfirmationDialogComponent) + .beforeClosed() + .subscribe(async (didConfirm) => { + if (didConfirm) { + await this.noticeOfIntentDocumentService.deleteExternalFile($event.uuid); + this.parcel.certificateOfTitle = undefined; + } + }); + } else { + await this.noticeOfIntentDocumentService.deleteExternalFile($event.uuid); + this.parcel.certificateOfTitle = undefined; + } + } + + async openFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + if (res) { + window.open(res.url, '_blank'); + } + } + + async beforeFileUploadOpened() { + this.onSaveProgress.emit(); + } + + onAddNewOwner() { + const dialog = this.dialog.open(ApplicationOwnerDialogComponent, { + data: { + fileId: this.fileId, + submissionUuid: this.submissionUuid, + parcelUuid: this.parcel.uuid, + }, + }); + dialog.beforeClosed().subscribe((createdDto) => { + if (createdDto) { + this.onOwnersUpdated.emit(); + const updatedArray = [...this.parcel.owners, createdDto]; + this.updateParcelOwners(updatedArray); + } + }); + } + + onAddNewGovernmentContact() { + const dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { + data: { + fileId: this.fileId, + submissionUuid: this.submissionUuid, + parcelUuid: this.parcel.uuid, + }, + }); + dialog.beforeClosed().subscribe((createdDto) => { + if (createdDto) { + this.onOwnersUpdated.emit(); + const updatedArray = [...this.parcel.owners, createdDto]; + this.updateParcelOwners(updatedArray); + } + }); + } + + async onSelectOwner(owner: NoticeOfIntentOwnerDto, isSelected: boolean) { + if (!isSelected) { + const selectedOwners = [...this.parcel.owners, owner]; + this.updateParcelOwners(selectedOwners); + } + } + + async onOwnerRemoved(uuid: string) { + const updatedArray = this.parcel.owners.filter((existingOwner) => existingOwner.uuid !== uuid); + this.updateParcelOwners(updatedArray); + } + + mapOwners(owners: NoticeOfIntentOwnerDto[]) { + return owners + .filter((owner) => { + if (this.isCrownLand) { + return [OWNER_TYPE.CROWN].includes(owner.type.code); + } else { + return [OWNER_TYPE.INDIVIDUAL, OWNER_TYPE.ORGANIZATION].includes(owner.type.code); + } + }) + .map((owner) => { + const isSelected = this.parcel.owners.some((parcelOwner) => parcelOwner.uuid === owner.uuid); + return { + ...owner, + isSelected, + }; + }) + .sort(this.noticeOfIntentOwnerService.sortOwners); + } + + onTypeOwner($event: Event) { + const element = $event.target as HTMLInputElement; + this.filteredOwners = this.mapOwners(this.owners).filter((option) => { + return option.displayName.toLowerCase().includes(element.value.toLowerCase()); + }); + } + + onSeeAllOwners() { + this.dialog + .open(ApplicationOwnersDialogComponent, { + data: { + owners: this.owners, + fileId: this.fileId, + submissionUuid: this.submissionUuid, + }, + }) + .beforeClosed() + .subscribe((isDirty) => { + if (isDirty) { + this.onOwnersUpdated.emit(); + } + }); + } + + private setupForm() { + this.parcelForm.patchValue({ + legalDescription: this.parcel.legalDescription, + mapArea: this.parcel.mapAreaHectares, + pid: this.parcel.pid, + pin: this.parcel.pin, + civicAddress: this.parcel.civicAddress, + parcelType: this.parcel.ownershipTypeCode, + isFarm: formatBooleanToString(this.parcel.isFarm), + purchaseDate: this.parcel.purchasedDate ? new Date(this.parcel.purchasedDate) : null, + crownLandOwnerType: this.parcel.crownLandOwnerType, + isConfirmedByApplicant: this.enableUserSignOff ? this.parcel.isConfirmedByApplicant : false, + }); + + this.isCrownLand = this.parcelType.value + ? this.parcelType.getRawValue() === this.PARCEL_OWNERSHIP_TYPES.CROWN + : null; + + if (this.isCrownLand) { + this.pidPin.disable(); + this.purchaseDate.disable(); + this.pid.setValidators([]); + const pidValue = this.pid.getRawValue(); + this.isCertificateOfTitleRequired = !!pidValue && pidValue.length > 0; + this.pidPinPlaceholder = ''; + } else { + this.pidPinPlaceholder = 'Type 9 digit PID'; + this.isCertificateOfTitleRequired = true; + } + + if (this.parcel.owners.length > 0 && this.isCrownLand) { + this.ownerInput.disable(); + } + + if (this.showErrors) { + this.parcelForm.markAllAsTouched(); + + if (this.parcel.owners.length === 0) { + this.ownerInput.setValidators([Validators.required]); + this.ownerInput.setErrors({ required: true }); + this.ownerInput.markAllAsTouched(); + } + } + + this.parcelForm.valueChanges.subscribe((formData) => { + if (!this.parcelForm.dirty) { + return; + } + + if ((this.isCrownLand && !this.searchBy.getRawValue()) || this.disabled) { + this.pidPin.disable({ + emitEvent: false, + }); + } else { + this.pidPin.enable({ + emitEvent: false, + }); + } + + const pidValue = this.pid.getRawValue(); + if (this.isCrownLand) { + this.isCertificateOfTitleRequired = !!pidValue && pidValue.length > 0; + } else { + this.isCertificateOfTitleRequired = true; + } + + if (this.parcelForm.dirty && formData.isConfirmedByApplicant === this.parcel.isConfirmedByApplicant) { + this.parcel.isConfirmedByApplicant = false; + formData.isConfirmedByApplicant = false; + + this.parcelForm.patchValue( + { + isConfirmedByApplicant: false, + }, + { emitEvent: false } + ); + } + + return this.onFormGroupChange.emit({ + ...formData, + uuid: this.parcel.uuid, + isConfirmedByApplicant: formData.isConfirmedByApplicant!, + purchaseDate: new Date(formData.purchaseDate?.valueOf()), + }); + }); + } + + private updateParcelOwners(updatedArray: NoticeOfIntentOwnerDto[]) { + if (updatedArray.length > 0) { + this.ownerInput.clearValidators(); + this.ownerInput.updateValueAndValidity(); + } else if (updatedArray.length === 0 && this.showErrors) { + this.ownerInput.markAllAsTouched(); + this.ownerInput.setValidators([Validators.required]); + this.ownerInput.setErrors({ required: true }); + } + + if (this.isCrownLand && updatedArray.length > 0) { + this.ownerInput.disable(); + } else { + this.ownerInput.enable(); + } + + this.parcel.owners = updatedArray; + this.filteredOwners = this.mapOwners(this.owners); + this.onFormGroupChange.emit({ + uuid: this.parcel.uuid, + owners: updatedArray, + }); + } + + private onFormDisabled() { + if (this._disabled) { + this.parcelForm.disable(); + this.ownerInput.disable(); + } else { + this.parcelForm.enable(); + this.ownerInput.enable(); + } + } + + onChangeSearchBy(value: string) { + if (value === 'pid') { + this.pidPinPlaceholder = 'Type 9 digit PID'; + } else { + this.pidPinPlaceholder = 'Type PIN'; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html new file mode 100644 index 0000000000..51ea96511d --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html @@ -0,0 +1,73 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type{{ element.type.label }}{{ element.position }}Full Name{{ element.displayName }}Organization NameOrganization Name / Ministry / Department + + {{ element.organizationName }} + Ministry/ Department{{ element.organizationName }}Phone{{ element.phoneNumber | mask : '(000) 000-0000' }}Email{{ element.email }}Actions + + + +
No owner information
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss new file mode 100644 index 0000000000..fb105e09eb --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss @@ -0,0 +1,14 @@ +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; + +.actions { + button:not(:last-child) { + margin-right: rem(8) !important; + } +} + +.no-data-text { + text-align: center; + color: colors.$grey; + padding-top: rem(12); +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts new file mode 100644 index 0000000000..b36089cde8 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts @@ -0,0 +1,41 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { ParcelOwnersComponent } from './parcel-owners.component'; + +describe('ParcelOwnersComponent', () => { + let component: ParcelOwnersComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + { + provide: MatDialog, + useValue: {}, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: {}, + }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, + ], + declarations: [ParcelOwnersComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelOwnersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts new file mode 100644 index 0000000000..ebfcd50c62 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts @@ -0,0 +1,91 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { NoticeOfIntentOwnerDto } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; +import { ApplicationCrownOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; +import { ApplicationOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component'; + +@Component({ + selector: 'app-parcel-owners[owners][fileId][submissionUuid]', + templateUrl: './parcel-owners.component.html', + styleUrls: ['./parcel-owners.component.scss'], +}) +export class ParcelOwnersComponent { + @Output() onOwnersUpdated = new EventEmitter(); + @Output() onOwnerRemoved = new EventEmitter(); + + @Input() + public set owners(owners: NoticeOfIntentOwnerDto[]) { + this._owners = owners.sort(this.noticeOfIntentOwnerService.sortOwners); + } + + @Input() + public set disabled(disabled: boolean) { + this._disabled = disabled; + } + + @Input() submissionUuid!: string; + @Input() fileId!: string; + @Input() parcelUuid?: string | undefined; + @Input() isCrown = false; + @Input() isDraft = false; + @Input() isShowAllOwners = false; + + _owners: NoticeOfIntentOwnerDto[] = []; + _disabled = false; + displayedColumns = ['type', 'position', 'displayName', 'organizationName', 'phone', 'email', 'actions']; + + constructor( + private dialog: MatDialog, + private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + private confDialogService: ConfirmationDialogService + ) {} + + onEdit(owner: NoticeOfIntentOwnerDto) { + let dialog; + if (owner.type.code === OWNER_TYPE.CROWN) { + dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { + data: { + isDraft: this.isDraft, + parcelUuid: this.parcelUuid, + existingOwner: owner, + submissionUuid: this.submissionUuid, + }, + }); + } else { + dialog = this.dialog.open(ApplicationOwnerDialogComponent, { + data: { + isDraft: this.isDraft, + fileId: this.fileId, + submissionUuid: this.submissionUuid, + parcelUuid: this.parcelUuid, + existingOwner: owner, + }, + }); + } + dialog.beforeClosed().subscribe((updatedUuid) => { + if (updatedUuid) { + this.onOwnersUpdated.emit(); + } + }); + } + + async onRemove(uuid: string) { + this.onOwnerRemoved.emit(uuid); + } + + async onDelete(owner: NoticeOfIntentOwnerDto) { + this.confDialogService + .openDialog({ + body: `This action will remove ${owner.displayName} and its usage from the entire application. Are you sure you want to remove this owner? `, + }) + .subscribe(async (didConfirm) => { + if (didConfirm) { + await this.noticeOfIntentOwnerService.delete(owner.uuid); + this.onOwnersUpdated.emit(); + } + }); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/step.partial.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/step.partial.ts new file mode 100644 index 0000000000..6ac94b8e94 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/step.partial.ts @@ -0,0 +1,34 @@ +import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; + +@Component({ + selector: 'app-step', + template: '

', + styleUrls: [], +}) +export class StepComponent implements OnDestroy { + protected $destroy = new Subject(); + + @Input() $noiSubmission!: BehaviorSubject; + + @Input() showErrors = false; + @Input() draftMode = false; + + @Output() navigateToStep = new EventEmitter(); + @Output() exit = new EventEmitter(); + + async onSaveExit() { + this.exit.emit(); + } + + onNavigateToStep(step: number) { + this.navigateToStep.emit(step); + } + + async ngOnDestroy() { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/portal-frontend/src/app/services/application-document/application-document.dto.ts b/portal-frontend/src/app/services/application-document/application-document.dto.ts index f5762d92fe..24a998e007 100644 --- a/portal-frontend/src/app/services/application-document/application-document.dto.ts +++ b/portal-frontend/src/app/services/application-document/application-document.dto.ts @@ -1,47 +1,7 @@ -import { BaseCodeDto } from '../../shared/dto/base.dto'; - -export enum DOCUMENT_TYPE { - //ALCS - DECISION_DOCUMENT = 'DPAC', - OTHER = 'OTHR', - - //Government Review - RESOLUTION_DOCUMENT = 'RESO', - STAFF_REPORT = 'STFF', - - //Applicant Uploaded - CORPORATE_SUMMARY = 'CORS', - PROFESSIONAL_REPORT = 'PROR', - PHOTOGRAPH = 'PHTO', - AUTHORIZATION_LETTER = 'AAGR', - CERTIFICATE_OF_TITLE = 'CERT', - - //App Documents - SERVING_NOTICE = 'POSN', - PROPOSAL_MAP = 'PRSK', - HOMESITE_SEVERANCE = 'HOME', - CROSS_SECTIONS = 'SPCS', - RECLAMATION_PLAN = 'RECP', - NOTICE_OF_WORK = 'NOWE', - PROOF_OF_SIGNAGE = 'POSA', - REPORT_OF_PUBLIC_HEARING = 'ROPH', - PROOF_OF_ADVERTISING = 'POAA', -} - -export enum DOCUMENT_SOURCE { - APPLICANT = 'Applicant', - ALC = 'ALC', - LFNG = 'L/FNG', - AFFECTED_PARTY = 'Affected Party', - PUBLIC = 'Public', -} - -export interface ApplicationDocumentTypeDto extends BaseCodeDto { - code: DOCUMENT_TYPE; -} +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../shared/dto/document.dto'; export interface ApplicationDocumentDto { - type: ApplicationDocumentTypeDto | null; + type: DocumentTypeDto | null; description?: string | null; uuid: string; fileName: string; diff --git a/portal-frontend/src/app/services/application-document/application-document.service.spec.ts b/portal-frontend/src/app/services/application-document/application-document.service.spec.ts index 44f8eaa138..285edba50b 100644 --- a/portal-frontend/src/app/services/application-document/application-document.service.spec.ts +++ b/portal-frontend/src/app/services/application-document/application-document.service.spec.ts @@ -71,7 +71,7 @@ describe('ApplicationDocumentService', () => { expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); }); - it('should make a delete request for update file', async () => { + it('should make a patch request for update file', async () => { mockHttpClient.patch.mockReturnValue(of({})); await service.update('fileId', []); diff --git a/portal-frontend/src/app/services/application-document/application-document.service.ts b/portal-frontend/src/app/services/application-document/application-document.service.ts index 34020c3ebe..41067bf26d 100644 --- a/portal-frontend/src/app/services/application-document/application-document.service.ts +++ b/portal-frontend/src/app/services/application-document/application-document.service.ts @@ -2,15 +2,11 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; import { DocumentService } from '../document/document.service'; import { ToastService } from '../toast/toast.service'; -import { - ApplicationDocumentDto, - ApplicationDocumentUpdateDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from './application-document.dto'; +import { ApplicationDocumentDto, ApplicationDocumentUpdateDto } from './application-document.dto'; @Injectable({ providedIn: 'root', diff --git a/portal-frontend/src/app/services/application-owner/application-owner.dto.ts b/portal-frontend/src/app/services/application-owner/application-owner.dto.ts index 090698c4d0..c37d003247 100644 --- a/portal-frontend/src/app/services/application-owner/application-owner.dto.ts +++ b/portal-frontend/src/app/services/application-owner/application-owner.dto.ts @@ -1,19 +1,8 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { OWNER_TYPE, OwnerTypeDto } from '../../shared/dto/owner.dto'; import { ApplicationDocumentDto } from '../application-document/application-document.dto'; import { ApplicationParcelDto } from '../application-parcel/application-parcel.dto'; -export enum APPLICATION_OWNER { - INDIVIDUAL = 'INDV', - ORGANIZATION = 'ORGZ', - AGENT = 'AGEN', - CROWN = 'CRWN', - GOVERNMENT = 'GOVR', -} - -export interface ApplicationOwnerTypeDto extends BaseCodeDto { - code: APPLICATION_OWNER; -} - export interface ApplicationOwnerDto { uuid: string; applicationSubmissionUuid: string; @@ -23,7 +12,7 @@ export interface ApplicationOwnerDto { organizationName: string | null; phoneNumber: string | null; email: string | null; - type: ApplicationOwnerTypeDto; + type: OwnerTypeDto; corporateSummary?: ApplicationDocumentDto; } @@ -51,7 +40,7 @@ export interface SetPrimaryContactDto { organization?: string; phoneNumber?: string; email?: string; - type?: APPLICATION_OWNER; + type?: OWNER_TYPE; ownerUuid?: string; applicationSubmissionUuid: string; } diff --git a/portal-frontend/src/app/services/application-owner/application-owner.service.ts b/portal-frontend/src/app/services/application-owner/application-owner.service.ts index debb0ae2c4..290e3a83db 100644 --- a/portal-frontend/src/app/services/application-owner/application-owner.service.ts +++ b/portal-frontend/src/app/services/application-owner/application-owner.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../application-document/application-document.dto'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; import { DocumentService } from '../document/document.service'; import { ToastService } from '../toast/toast.service'; import { diff --git a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts index 813a830003..1112a20315 100644 --- a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts +++ b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts @@ -2,15 +2,12 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; -import { - ApplicationDocumentDto, - DOCUMENT_SOURCE, - DOCUMENT_TYPE, -} from '../application-document/application-document.dto'; +import { ApplicationDocumentDto } from '../application-document/application-document.dto'; import { DocumentService } from '../document/document.service'; import { ToastService } from '../toast/toast.service'; -import { ApplicationParcelDto, ApplicationParcelUpdateDto, PARCEL_TYPE } from './application-parcel.dto'; +import { ApplicationParcelDto, ApplicationParcelUpdateDto } from './application-parcel.dto'; @Injectable({ providedIn: 'root', @@ -37,12 +34,11 @@ export class ApplicationParcelService { return undefined; } - async create(applicationSubmissionUuid: string, parcelType?: PARCEL_TYPE, ownerUuid?: string) { + async create(applicationSubmissionUuid: string, ownerUuid?: string) { try { return await firstValueFrom( this.httpClient.post(`${this.serviceUrl}`, { applicationSubmissionUuid, - parcelType, ownerUuid, }) ); diff --git a/portal-frontend/src/app/services/code/code.service.ts b/portal-frontend/src/app/services/code/code.service.ts index 9115600fd5..8dc58f9d8c 100644 --- a/portal-frontend/src/app/services/code/code.service.ts +++ b/portal-frontend/src/app/services/code/code.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { ApplicationDocumentTypeDto } from '../application-document/application-document.dto'; +import { DocumentTypeDto } from '../../shared/dto/document.dto'; import { NaruSubtypeDto } from '../application-submission/application-submission.dto'; import { ApplicationTypeDto, LocalGovernmentDto, NoticeOfIntentTypeDto, SubmissionTypeDto } from './code.dto'; @@ -20,7 +20,7 @@ export class CodeService { localGovernments: LocalGovernmentDto[]; applicationTypes: ApplicationTypeDto[]; noticeOfIntentTypes: NoticeOfIntentTypeDto[]; - applicationDocumentTypes: ApplicationDocumentTypeDto[]; + documentTypes: DocumentTypeDto[]; submissionTypes: SubmissionTypeDto[]; naruSubtypes: NaruSubtypeDto[]; }>(`${this.baseUrl}`) diff --git a/portal-frontend/src/app/services/document/document.service.ts b/portal-frontend/src/app/services/document/document.service.ts index e4e82374e8..899968fcbc 100644 --- a/portal-frontend/src/app/services/document/document.service.ts +++ b/portal-frontend/src/app/services/document/document.service.ts @@ -2,8 +2,8 @@ import { HttpBackend, HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../application-document/application-document.dto'; import { ToastService } from '../toast/toast.service'; import { UploadDocumentUrlDto } from './document.dto'; diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.dto.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.dto.ts new file mode 100644 index 0000000000..cd724b639a --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.dto.ts @@ -0,0 +1,18 @@ +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../shared/dto/document.dto'; + +export interface NoticeOfIntentDocumentDto { + type: DocumentTypeDto | null; + description?: string | null; + uuid: string; + fileName: string; + fileSize: number; + uploadedBy: string; + uploadedAt: number; + source: DOCUMENT_SOURCE; +} + +export interface NoticeOfIntentDocumentUpdateDto { + uuid: string; + type: DOCUMENT_TYPE | null; + description?: string | null; +} diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts new file mode 100644 index 0000000000..5c07e25903 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts @@ -0,0 +1,91 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../toast/toast.service'; +import { NoticeOfIntentDocumentService } from './notice-of-intent-document.service'; + +describe('NoticeOfIntentDocumentService', () => { + let service: NoticeOfIntentDocumentService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentDocumentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for open file', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.openFile('fileId'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notice-of-intent-document'); + }); + + it('should show an error toast if opening a file fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.openFile('fileId'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a delete request for delete file', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.deleteExternalFile('fileId'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete.mock.calls[0][0]).toContain('notice-of-intent-document'); + }); + + it('should show an error toast if deleting a file fails', async () => { + mockHttpClient.delete.mockReturnValue(throwError(() => ({}))); + + await service.deleteExternalFile('fileId'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a patch request for update file', async () => { + mockHttpClient.patch.mockReturnValue(of({})); + + await service.update('fileId', []); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch.mock.calls[0][0]).toContain('notice-of-intent-document'); + }); + + it('should show an error toast if updating a file fails', async () => { + mockHttpClient.patch.mockReturnValue(throwError(() => ({}))); + + await service.update('fileId', []); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts new file mode 100644 index 0000000000..6b51562e5e --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts @@ -0,0 +1,91 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; +import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; +import { NoticeOfIntentDocumentDto, NoticeOfIntentDocumentUpdateDto } from './notice-of-intent-document.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentDocumentService { + private serviceUrl = `${environment.apiUrl}/notice-of-intent-document`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private documentService: DocumentService, + private overlayService: OverlaySpinnerService + ) {} + + async attachExternalFile( + fileNumber: string, + file: File, + documentType: DOCUMENT_TYPE | null, + source = DOCUMENT_SOURCE.APPLICANT + ) { + try { + const res = await this.documentService.uploadFile( + fileNumber, + file, + documentType, + source, + `${this.serviceUrl}/application/${fileNumber}/attachExternal` + ); + this.toastService.showSuccessToast('Document uploaded'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to attach document to Application, please try again'); + } + return undefined; + } + + async openFile(fileUuid: string) { + try { + return await firstValueFrom(this.httpClient.get<{ url: string }>(`${this.serviceUrl}/${fileUuid}/open`)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to open the document, please try again'); + } + return undefined; + } + + async deleteExternalFile(fileUuid: string) { + try { + this.overlayService.showSpinner(); + await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}/${fileUuid}`)); + this.toastService.showSuccessToast('Document deleted'); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete document, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + } + + async update(fileNumber: string | undefined, updateDtos: NoticeOfIntentDocumentUpdateDto[]) { + try { + await firstValueFrom(this.httpClient.patch(`${this.serviceUrl}/application/${fileNumber}`, updateDtos)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update documents, please try again'); + } + return undefined; + } + + async getByFileId(fileNumber: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/application/${fileNumber}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to fetch documents, please try again'); + } + return undefined; + } +} diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts new file mode 100644 index 0000000000..21b23da93c --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts @@ -0,0 +1,57 @@ +import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { ApplicationDocumentDto } from '../application-document/application-document.dto'; +import { ApplicationParcelDto } from '../application-parcel/application-parcel.dto'; + +export enum OWNER_TYPE { + INDIVIDUAL = 'INDV', + ORGANIZATION = 'ORGZ', + AGENT = 'AGEN', + CROWN = 'CRWN', + GOVERNMENT = 'GOVR', +} + +export interface OwnerTypeDto extends BaseCodeDto { + code: OWNER_TYPE; +} + +export interface NoticeOfIntentOwnerDto { + uuid: string; + noticeOfIntentSubmissionUuid: string; + displayName: string; + firstName: string | null; + lastName: string | null; + organizationName: string | null; + phoneNumber: string | null; + email: string | null; + type: OwnerTypeDto; + corporateSummary?: ApplicationDocumentDto; +} + +export interface NoticeOfIntentOwnerDetailedDto extends NoticeOfIntentOwnerDto { + parcels: ApplicationParcelDto[]; +} + +export interface NoticeOfIntentOwnerUpdateDto { + firstName?: string | null; + lastName?: string | null; + organizationName?: string | null; + phoneNumber: string; + email: string; + typeCode: string; + corporateSummaryUuid?: string | null; +} + +export interface NoticeOfIntentOwnerCreateDto extends NoticeOfIntentOwnerUpdateDto { + noticeOfIntentSubmissionUuid: string; +} + +export interface SetPrimaryContactDto { + firstName?: string; + lastName?: string; + organization?: string; + phoneNumber?: string; + email?: string; + type?: OWNER_TYPE; + ownerUuid?: string; + noticeOfIntentSubmissionUuid: string; +} diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts new file mode 100644 index 0000000000..236396e06e --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts @@ -0,0 +1,189 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; + +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner.service'; + +describe('NoticeOfIntentOwnerService', () => { + let service: NoticeOfIntentOwnerService; + let mockHttpClient: DeepMocked; + let mockToastService: DeepMocked; + let mockDocumentService: DeepMocked; + + let fileId = '123'; + + beforeEach(() => { + mockHttpClient = createMock(); + mockToastService = createMock(); + mockDocumentService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentOwnerService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for loading owners', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.fetchBySubmissionId(fileId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if getting owners fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.fetchBySubmissionId(fileId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for create', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.create({ + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if creating owner fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.create({ + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a patch request for update', async () => { + mockHttpClient.patch.mockReturnValue(of({})); + + await service.update('', { + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if updating owner fails', async () => { + mockHttpClient.patch.mockReturnValue(throwError(() => ({}))); + + await service.update('', { + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a delete request for delete', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.delete(''); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if delete owner fails', async () => { + mockHttpClient.delete.mockReturnValue(throwError(() => ({}))); + + await service.delete(''); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for removeFromParcel', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.removeFromParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if removeFromParcel', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.removeFromParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for linkToParcel', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.linkToParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if linkToParcel', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.linkToParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for setPrimaryContact', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.setPrimaryContact({ noticeOfIntentSubmissionUuid: '' }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-owner'); + }); + + it('should show an error toast if setPrimaryContact', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.setPrimaryContact({ noticeOfIntentSubmissionUuid: '' }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts new file mode 100644 index 0000000000..92abf5b9e6 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts @@ -0,0 +1,144 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; +import { + NoticeOfIntentOwnerCreateDto, + NoticeOfIntentOwnerDto, + NoticeOfIntentOwnerUpdateDto, + SetPrimaryContactDto, +} from './notice-of-intent-owner.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentOwnerService { + private serviceUrl = `${environment.apiUrl}/notice-of-intent-owner`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private documentService: DocumentService + ) {} + + async fetchBySubmissionId(submissionUuid: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/submission/${submissionUuid}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Owners, please try again later'); + } + return undefined; + } + + async create(dto: NoticeOfIntentOwnerCreateDto) { + try { + const res = await firstValueFrom(this.httpClient.post(`${this.serviceUrl}`, dto)); + this.toastService.showSuccessToast('Owner created'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to create Owner, please try again later'); + return undefined; + } + } + + async update(uuid: string, updateDto: NoticeOfIntentOwnerUpdateDto) { + try { + const res = await firstValueFrom( + this.httpClient.patch(`${this.serviceUrl}/${uuid}`, updateDto) + ); + this.toastService.showSuccessToast('Owner saved'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Owner, please try again later'); + return undefined; + } + } + + async setPrimaryContact(updateDto: SetPrimaryContactDto) { + try { + const res = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/setPrimaryContact`, updateDto) + ); + this.toastService.showSuccessToast('Application saved'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Application, please try again later'); + return undefined; + } + } + + async delete(uuid: string) { + try { + const result = await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}/${uuid}`)); + this.toastService.showSuccessToast('Owner deleted'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete Owner, please try again'); + } + return undefined; + } + + async removeFromParcel(ownerUuid: string, parcelUuid: string) { + try { + const result = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/${ownerUuid}/unlink/${parcelUuid}`, {}) + ); + this.toastService.showSuccessToast('Owner removed from parcel'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to remove Owner, please try again'); + } + return undefined; + } + + async linkToParcel(ownerUuid: any, parcelUuid: string) { + try { + const result = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/${ownerUuid}/link/${parcelUuid}`, {}) + ); + this.toastService.showSuccessToast('Owner linked to parcel'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to link Owner, please try again'); + } + return undefined; + } + + sortOwners(a: NoticeOfIntentOwnerDto, b: NoticeOfIntentOwnerDto) { + if (a.displayName < b.displayName) { + return -1; + } + if (a.displayName > b.displayName) { + return 1; + } + return 0; + } + + async uploadCorporateSummary(applicationFileId: string, file: File) { + try { + return await this.documentService.uploadFile<{ uuid: string }>( + applicationFileId, + file, + DOCUMENT_TYPE.CORPORATE_SUMMARY, + DOCUMENT_SOURCE.APPLICANT, + `${this.serviceUrl}/attachCorporateSummary` + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to attach document to Owner, please try again'); + } + return undefined; + } +} diff --git a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts new file mode 100644 index 0000000000..7b5e10b9cd --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts @@ -0,0 +1,29 @@ +import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { ApplicationDocumentDto } from '../application-document/application-document.dto'; +import { NoticeOfIntentOwnerDto } from '../notice-of-intent-owner/notice-of-intent-owner.dto'; + +export interface NoticeOfIntentParcelUpdateDto { + uuid: string; + pid?: string | null; + pin?: string | null; + civicAddress?: string | null; + legalDescription?: string | null; + mapAreaHectares?: string | null; + purchasedDate?: number | null; + isFarm?: boolean | null; + ownershipTypeCode?: string | null; + crownLandOwnerType?: string | null; + isConfirmedByApplicant: boolean; + ownerUuids: string[] | null; +} + +export interface NoticeOfIntentParcelDto extends Omit { + ownershipType?: BaseCodeDto; + owners: NoticeOfIntentOwnerDto[]; + certificateOfTitle?: ApplicationDocumentDto; +} + +export enum PARCEL_OWNERSHIP_TYPE { + FEE_SIMPLE = 'SMPL', + CROWN = 'CRWN', +} diff --git a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts new file mode 100644 index 0000000000..735991e006 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts @@ -0,0 +1,102 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ToastService } from '../toast/toast.service'; + +import { of, throwError } from 'rxjs'; +import { DocumentService } from '../document/document.service'; +import { NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel.service'; + +describe('NoticeOfIntentParcelService', () => { + let service: NoticeOfIntentParcelService; + let mockHttpClient: DeepMocked; + let mockDocumentService: DeepMocked; + let mockToastService: DeepMocked; + + const mockUuid = 'fake_uuid'; + + beforeEach(() => { + mockHttpClient = createMock(); + mockToastService = createMock(); + + TestBed.configureTestingModule({ + imports: [], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentParcelService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for loading parcels', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.fetchBySubmissionUuid(mockUuid); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notice-of-intent-parcel'); + }); + + it('should show an error toast if getting parcel fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.fetchBySubmissionUuid(mockUuid); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for create', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.create(mockUuid); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-parcel'); + }); + + it('should show an error toast if creating a parcel fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.create(mockUuid); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a put request for update', async () => { + mockHttpClient.put.mockReturnValue(of({})); + let mockUuid = 'fake'; + + await service.update([{ uuid: mockUuid }] as NoticeOfIntentParcelUpdateDto[]); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockToastService.showSuccessToast).toHaveBeenCalledTimes(1); + expect(mockHttpClient.put.mock.calls[0][0]).toContain('notice-of-intent-parcel'); + }); + + it('should show an error toast if updating a parcel fails', async () => { + mockHttpClient.put.mockReturnValue(throwError(() => ({}))); + + await service.update([{}] as NoticeOfIntentParcelUpdateDto[]); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts new file mode 100644 index 0000000000..b468fd8cf7 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts @@ -0,0 +1,107 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; +import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { DocumentService } from '../document/document.service'; +import { NoticeOfIntentDocumentDto } from '../notice-of-intent-document/notice-of-intent-document.dto'; +import { ToastService } from '../toast/toast.service'; +import { NoticeOfIntentParcelDto, NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentParcelService { + private serviceUrl = `${environment.apiUrl}/notice-of-intent-parcel`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private documentService: DocumentService, + private overlayService: OverlaySpinnerService + ) {} + + async fetchBySubmissionUuid(submissionUuid: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/submission/${submissionUuid}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Parcel, please try again later'); + } + return undefined; + } + + async create(noticeOfIntentSubmissionUuid: string, ownerUuid?: string) { + try { + return await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}`, { + noticeOfIntentSubmissionUuid, + ownerUuid, + }) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to create Parcel, please try again later'); + return undefined; + } + } + + async update(updateDtos: NoticeOfIntentParcelUpdateDto[]) { + try { + this.overlayService.showSpinner(); + const formattedDtos = updateDtos.map((e) => ({ + ...e, + mapAreaHectares: e.mapAreaHectares ? parseFloat(e.mapAreaHectares) : e.mapAreaHectares, + })); + + const result = await firstValueFrom( + this.httpClient.put(`${this.serviceUrl}`, formattedDtos) + ); + + this.toastService.showSuccessToast('Parcel saved'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Parcel, please try again later'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } + + async attachCertificateOfTitle(fileId: string, parcelUuid: string, file: File) { + try { + const document = await this.documentService.uploadFile( + fileId, + file, + DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + DOCUMENT_SOURCE.APPLICANT, + `${this.serviceUrl}/${parcelUuid}/attachCertificateOfTitle` + ); + this.toastService.showSuccessToast('Document uploaded'); + return document; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to attach document to Parcel, please try again'); + } + return undefined; + } + + async deleteMany(parcelUuids: string[]) { + try { + this.overlayService.showSpinner(); + const result = await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}`, { body: parcelUuids })); + this.toastService.showSuccessToast('Parcel deleted'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete Parcel, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } +} diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts index cc747f826b..242403d17c 100644 --- a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -1,4 +1,5 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { NoticeOfIntentOwnerDto } from '../notice-of-intent-owner/notice-of-intent-owner.dto'; export enum NOI_SUBMISSION_STATUS { IN_PROGRESS = 'PROG', @@ -32,6 +33,7 @@ export interface NoticeOfIntentSubmissionDto { type: string; status: NoticeOfIntentSubmissionStatusDto; submissionStatuses: NoticeOfIntentSubmissionToSubmissionStatusDto[]; + owners: NoticeOfIntentOwnerDto[]; canEdit: boolean; canView: boolean; } diff --git a/portal-frontend/src/app/shared/dto/document.dto.ts b/portal-frontend/src/app/shared/dto/document.dto.ts new file mode 100644 index 0000000000..e2dfebeef6 --- /dev/null +++ b/portal-frontend/src/app/shared/dto/document.dto.ts @@ -0,0 +1,41 @@ +import { BaseCodeDto } from './base.dto'; + +export enum DOCUMENT_TYPE { + //ALCS + DECISION_DOCUMENT = 'DPAC', + OTHER = 'OTHR', + + //Government Review + RESOLUTION_DOCUMENT = 'RESO', + STAFF_REPORT = 'STFF', + + //Applicant Uploaded + CORPORATE_SUMMARY = 'CORS', + PROFESSIONAL_REPORT = 'PROR', + PHOTOGRAPH = 'PHTO', + AUTHORIZATION_LETTER = 'AAGR', + CERTIFICATE_OF_TITLE = 'CERT', + + //App Documents + SERVING_NOTICE = 'POSN', + PROPOSAL_MAP = 'PRSK', + HOMESITE_SEVERANCE = 'HOME', + CROSS_SECTIONS = 'SPCS', + RECLAMATION_PLAN = 'RECP', + NOTICE_OF_WORK = 'NOWE', + PROOF_OF_SIGNAGE = 'POSA', + REPORT_OF_PUBLIC_HEARING = 'ROPH', + PROOF_OF_ADVERTISING = 'POAA', +} + +export enum DOCUMENT_SOURCE { + APPLICANT = 'Applicant', + ALC = 'ALC', + LFNG = 'L/FNG', + AFFECTED_PARTY = 'Affected Party', + PUBLIC = 'Public', +} + +export interface DocumentTypeDto extends BaseCodeDto { + code: DOCUMENT_TYPE; +} diff --git a/portal-frontend/src/app/shared/dto/owner.dto.ts b/portal-frontend/src/app/shared/dto/owner.dto.ts new file mode 100644 index 0000000000..9e1d82879a --- /dev/null +++ b/portal-frontend/src/app/shared/dto/owner.dto.ts @@ -0,0 +1,13 @@ +import { BaseCodeDto } from './base.dto'; + +export enum OWNER_TYPE { + INDIVIDUAL = 'INDV', + ORGANIZATION = 'ORGZ', + AGENT = 'AGEN', + CROWN = 'CRWN', + GOVERNMENT = 'GOVR', +} + +export interface OwnerTypeDto extends BaseCodeDto { + code: OWNER_TYPE; +} diff --git a/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts b/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts index c91b4a0d59..f93e8d68c2 100644 --- a/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts +++ b/services/apps/alcs/src/alcs/application/application-document/application-document.service.ts @@ -42,7 +42,7 @@ export class ApplicationDocumentService { @InjectRepository(ApplicationDocument) private applicationDocumentRepository: Repository, @InjectRepository(DocumentCode) - private applicationDocumentCodeRepository: Repository, + private documentCodeRepository: Repository, ) {} async attachDocument({ @@ -253,7 +253,7 @@ export class ApplicationDocumentService { } async fetchTypes() { - return await this.applicationDocumentCodeRepository.find(); + return await this.documentCodeRepository.find(); } async update({ diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.controller.spec.ts index 84513e6ff9..3891462cba 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.controller.spec.ts @@ -9,9 +9,7 @@ import { NoticeOfIntentProfile } from '../../../common/automapper/notice-of-inte import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; import { DOCUMENT_SOURCE } from '../../../document/document.dto'; import { Document } from '../../../document/document.entity'; -import { ApplicationOwner } from '../../../portal/application-submission/application-owner/application-owner.entity'; import { ApplicationOwnerService } from '../../../portal/application-submission/application-owner/application-owner.service'; -import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; import { ApplicationParcelService } from '../../../portal/application-submission/application-parcel/application-parcel.service'; import { User } from '../../../user/user.entity'; import { CodeService } from '../../code/code.service'; diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts index bd0039567e..91509763b4 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts @@ -1,5 +1,9 @@ import { MultipartFile } from '@fastify/multipart'; -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ArrayOverlap, @@ -16,6 +20,7 @@ import { DOCUMENT_SYSTEM, } from '../../../document/document.dto'; import { DocumentService } from '../../../document/document.service'; +import { PortalNoticeOfIntentDocumentUpdateDto } from '../../../portal/notice-of-intent-document/notice-of-intent-document.dto'; import { User } from '../../../user/user.entity'; import { NoticeOfIntentService } from '../notice-of-intent.service'; import { @@ -197,37 +202,36 @@ export class NoticeOfIntentDocumentService { ); return this.get(savedDocument.uuid); } - // TODO: Re-enable as part of creating Step 7 - // async updateDescriptionAndType( - // updates: PortalNoticeOfIntentDocumentUpdateDto[], - // noticeOfIntentUuid: string, - // ) { - // const results: NoticeOfIntentDocument[] = []; - // for (const update of updates) { - // const file = await this.noticeOfIntentDocumentRepository.findOne({ - // where: { - // uuid: update.uuid, - // noticeOfIntentUuid, - // }, - // relations: { - // document: true, - // }, - // }); - // if (!file) { - // throw new BadRequestException( - // 'Failed to find file linked to provided noticeOfIntent', - // ); - // } - // - // file.typeCode = update.type; - // file.description = update.description; - // const updatedFile = await this.noticeOfIntentDocumentRepository.save( - // file, - // ); - // results.push(updatedFile); - // } - // return results; - // } + async updateDescriptionAndType( + updates: PortalNoticeOfIntentDocumentUpdateDto[], + noticeOfIntentUuid: string, + ) { + const results: NoticeOfIntentDocument[] = []; + for (const update of updates) { + const file = await this.noticeOfIntentDocumentRepository.findOne({ + where: { + uuid: update.uuid, + noticeOfIntentUuid, + }, + relations: { + document: true, + }, + }); + if (!file) { + throw new BadRequestException( + 'Failed to find file linked to provided noticeOfIntent', + ); + } + + file.typeCode = update.type; + file.description = update.description; + const updatedFile = await this.noticeOfIntentDocumentRepository.save( + file, + ); + results.push(updatedFile); + } + return results; + } async deleteByType(documentType: DOCUMENT_TYPE, noticeOfIntentUuid: string) { const documents = await this.noticeOfIntentDocumentRepository.find({ diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts index 876e7e05d3..a33b7ba440 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts @@ -341,10 +341,10 @@ export class NoticeOfIntentService { fileNumber, }, select: { - fileNumber: true, + uuid: true, }, }); - return noticeOfIntent.fileNumber; + return noticeOfIntent.uuid; } async submit(createDto: CreateNoticeOfIntentServiceDto) { diff --git a/services/apps/alcs/src/common/automapper/application-owner.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-owner.automapper.profile.ts index f980f99fb6..5b357761a8 100644 --- a/services/apps/alcs/src/common/automapper/application-owner.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-owner.automapper.profile.ts @@ -3,11 +3,10 @@ import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { ApplicationDocumentDto } from '../../alcs/application/application-document/application-document.dto'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; -import { ApplicationOwnerType } from '../../portal/application-submission/application-owner/application-owner-type/application-owner-type.entity'; +import { OwnerType, OwnerTypeDto } from '../owner-type/owner-type.entity'; import { ApplicationOwnerDetailedDto, ApplicationOwnerDto, - ApplicationOwnerTypeDto, } from '../../portal/application-submission/application-owner/application-owner.dto'; import { ApplicationOwner } from '../../portal/application-submission/application-owner/application-owner.entity'; import { ApplicationParcelDto } from '../../portal/application-submission/application-parcel/application-parcel.dto'; @@ -77,7 +76,7 @@ export class ApplicationOwnerProfile extends AutomapperProfile { ), ); - createMap(mapper, ApplicationOwnerType, ApplicationOwnerTypeDto); + createMap(mapper, OwnerType, OwnerTypeDto); }; } } diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-owner.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-owner.automapper.profile.ts new file mode 100644 index 0000000000..ad4734b3fe --- /dev/null +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-owner.automapper.profile.ts @@ -0,0 +1,82 @@ +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { + NoticeOfIntentOwnerDetailedDto, + NoticeOfIntentOwnerDto, +} from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentParcelDto } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { OwnerType, OwnerTypeDto } from '../owner-type/owner-type.entity'; + +@Injectable() +export class NoticeOfIntentOwnerProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap( + mapper, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + forMember( + (pd) => pd.displayName, + mapFrom((p) => `${p.firstName} ${p.lastName}`), + ), + forMember( + (pd) => pd.corporateSummary, + mapFrom((p) => + p.corporateSummary + ? this.mapper.map( + p.corporateSummary, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ) + : undefined, + ), + ), + ); + + createMap( + mapper, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDetailedDto, + forMember( + (pd) => pd.displayName, + mapFrom((p) => `${p.firstName} ${p.lastName}`), + ), + forMember( + (ad) => ad.parcels, + mapFrom((a) => { + if (a.parcels) { + return this.mapper.mapArray( + a.parcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + }), + ), + forMember( + (pd) => pd.corporateSummary, + mapFrom((p) => + p.corporateSummary + ? this.mapper.map( + p.corporateSummary, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ) + : undefined, + ), + ), + ); + + createMap(mapper, OwnerType, OwnerTypeDto); + }; + } +} diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts new file mode 100644 index 0000000000..eb57ac85aa --- /dev/null +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts @@ -0,0 +1,85 @@ +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { NoticeOfIntentOwnerDto } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentParcelOwnershipType } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity'; +import { + NoticeOfIntentParcelDto, + NoticeOfIntentParcelOwnershipTypeDto, +} from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; + +@Injectable() +export class NoticeOfIntentParcelProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap( + mapper, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + forMember( + (pd) => pd.ownershipTypeCode, + mapFrom((p) => p.ownershipTypeCode), + ), + forMember( + (pd) => pd.purchasedDate, + mapFrom((p) => p.purchasedDate?.getTime()), + ), + forMember( + (p) => p.certificateOfTitle, + mapFrom((pd) => { + if (pd.certificateOfTitle) { + return this.mapper.map( + pd.certificateOfTitle, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ); + } + return; + }), + ), + forMember( + (p) => p.owners, + mapFrom((pd) => { + if (pd.owners) { + return this.mapper.mapArray( + pd.owners, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } else { + return []; + } + }), + ), + forMember( + (p) => p.ownershipType, + mapFrom((pd) => { + if (pd.ownershipType) { + return this.mapper.map( + pd.ownershipType, + NoticeOfIntentParcelOwnershipType, + NoticeOfIntentParcelOwnershipTypeDto, + ); + } else { + return undefined; + } + }), + ), + ); + + createMap( + mapper, + NoticeOfIntentParcelOwnershipType, + NoticeOfIntentParcelOwnershipTypeDto, + ); + }; + } +} diff --git a/services/apps/alcs/src/common/owner-type/owner-type.entity.ts b/services/apps/alcs/src/common/owner-type/owner-type.entity.ts new file mode 100644 index 0000000000..558d3e31ca --- /dev/null +++ b/services/apps/alcs/src/common/owner-type/owner-type.entity.ts @@ -0,0 +1,23 @@ +import { Entity } from 'typeorm'; +import { BaseCodeDto } from '../dtos/base.dto'; +import { BaseCodeEntity } from '../entities/base.code.entity'; + +export enum OWNER_TYPE { + INDIVIDUAL = 'INDV', + ORGANIZATION = 'ORGZ', + AGENT = 'AGEN', + CROWN = 'CRWN', + GOVERNMENT = 'GOVR', +} + +export class OwnerTypeDto extends BaseCodeDto {} + +@Entity() +export class OwnerType extends BaseCodeEntity { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } +} diff --git a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts index 53cdc35914..fc9d96e259 100644 --- a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts +++ b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationSubmissionStatusModule } from '../../alcs/application/application-submission-status/application-submission-status.module'; import { ApplicationSubmissionStatusType } from '../../alcs/application/application-submission-status/submission-status-type.entity'; -import { ApplicationOwnerType } from '../application-submission/application-owner/application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; import { ApplicationParcelOwnershipType } from '../application-submission/application-parcel/application-parcel-ownership-type/application-parcel-ownership-type.entity'; import { ApplicationParcel } from '../application-submission/application-parcel/application-parcel.entity'; @@ -20,7 +20,7 @@ import { ApplicationSubmissionDraftService } from './application-submission-draf ApplicationParcel, ApplicationParcelOwnershipType, ApplicationOwner, - ApplicationOwnerType, + OwnerType, ]), ApplicationSubmissionModule, PdfGenerationModule, diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts index cb59406379..370f48ebbd 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts @@ -21,7 +21,7 @@ import { DOCUMENT_SOURCE } from '../../document/document.dto'; import { Document } from '../../document/document.entity'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; -import { ApplicationOwnerType } from '../application-submission/application-owner/application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; import { ApplicationSubmissionValidatorService, @@ -253,7 +253,7 @@ describe('ApplicationSubmissionReviewController', () => { new ApplicationOwner({ uuid: 'uuid', email: 'fake-email', - type: new ApplicationOwnerType(), + type: new OwnerType(), }), ], primaryContactOwnerUuid: 'uuid', diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index c0d70aa686..ebec8444d1 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -14,17 +14,17 @@ import { UseGuards, } from '@nestjs/common'; import { generateStatusHtml } from '../../../../../templates/emails/submission-status.template'; -import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; -import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; -import { ApplicationService } from '../../alcs/application/application.service'; import { ApplicationSubmissionStatusService } from '../../alcs/application/application-submission-status/application-submission-status.service'; import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; +import { ApplicationService } from '../../alcs/application/application.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; +import { OWNER_TYPE } from '../../common/owner-type/owner-type.entity'; import { DOCUMENT_SOURCE } from '../../document/document.dto'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; -import { APPLICATION_OWNER } from '../application-submission/application-owner/application-owner.dto'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; import { ApplicationSubmissionValidatorService } from '../application-submission/application-submission-validator.service'; import { ApplicationSubmission } from '../application-submission/application-submission.entity'; @@ -215,7 +215,7 @@ export class ApplicationSubmissionReviewController { if ( creatingGovernment?.uuid === applicationSubmission.localGovernmentUuid && primaryContact && - primaryContact.type.code === APPLICATION_OWNER.GOVERNMENT + primaryContact.type.code === OWNER_TYPE.GOVERNMENT ) { //Copy contact details over to government form when applying to self await this.applicationSubmissionReviewService.update( diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner-type/application-owner-type.entity.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner-type/application-owner-type.entity.ts deleted file mode 100644 index ecbfbad3a2..0000000000 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner-type/application-owner-type.entity.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Entity } from 'typeorm'; -import { BaseCodeEntity } from '../../../../common/entities/base.code.entity'; - -@Entity() -export class ApplicationOwnerType extends BaseCodeEntity { - constructor(data?: Partial) { - super(); - if (data) { - Object.assign(this, data); - } - } -} diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts index 8aa0a7ddcb..d4decd8739 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.spec.ts @@ -7,12 +7,14 @@ import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; import { ApplicationDocumentService } from '../../../alcs/application/application-document/application-document.service'; import { ApplicationOwnerProfile } from '../../../common/automapper/application-owner.automapper.profile'; +import { + OWNER_TYPE, + OwnerType, +} from '../../../common/owner-type/owner-type.entity'; import { DocumentService } from '../../../document/document.service'; import { ApplicationSubmission } from '../application-submission.entity'; import { ApplicationSubmissionService } from '../application-submission.service'; -import { ApplicationOwnerType } from './application-owner-type/application-owner-type.entity'; import { ApplicationOwnerController } from './application-owner.controller'; -import { APPLICATION_OWNER } from './application-owner.dto'; import { ApplicationOwner } from './application-owner.entity'; import { ApplicationOwnerService } from './application-owner.service'; @@ -282,8 +284,8 @@ describe('ApplicationOwnerController', () => { it('should set the owner and delete agents when using a non-agent owner', async () => { mockAppOwnerService.getOwner.mockResolvedValue( new ApplicationOwner({ - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.INDIVIDUAL, + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, }), }), ); @@ -312,8 +314,8 @@ describe('ApplicationOwnerController', () => { it('should update the agent owner when calling set primary contact', async () => { mockAppOwnerService.getOwner.mockResolvedValue( new ApplicationOwner({ - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.AGENT, + type: new OwnerType({ + code: OWNER_TYPE.AGENT, }), }), ); diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts index 7a0cbe42fe..508340d81a 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.controller.ts @@ -12,10 +12,11 @@ import { Req, UseGuards, } from '@nestjs/common'; -import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; import { VISIBILITY_FLAG } from '../../../alcs/application/application-document/application-document.entity'; import { ApplicationDocumentService } from '../../../alcs/application/application-document/application-document.service'; import { PortalAuthGuard } from '../../../common/authorization/portal-auth-guard.service'; +import { OWNER_TYPE } from '../../../common/owner-type/owner-type.entity'; +import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, @@ -23,7 +24,6 @@ import { import { DocumentService } from '../../../document/document.service'; import { ApplicationSubmissionService } from '../application-submission.service'; import { - APPLICATION_OWNER, ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, @@ -133,7 +133,7 @@ export class ApplicationOwnerController { dto: ApplicationOwnerUpdateDto | ApplicationOwnerCreateDto, ) { if ( - dto.typeCode === APPLICATION_OWNER.INDIVIDUAL && + dto.typeCode === OWNER_TYPE.INDIVIDUAL && (!dto.firstName || !dto.lastName) ) { throw new BadRequestException( @@ -141,10 +141,7 @@ export class ApplicationOwnerController { ); } - if ( - dto.typeCode === APPLICATION_OWNER.ORGANIZATION && - !dto.organizationName - ) { + if (dto.typeCode === OWNER_TYPE.ORGANIZATION && !dto.organizationName) { throw new BadRequestException( 'Organizations must have an organizationName', ); @@ -184,8 +181,8 @@ export class ApplicationOwnerController { ); if ( - primaryContactOwner.type.code === APPLICATION_OWNER.AGENT || - primaryContactOwner.type.code === APPLICATION_OWNER.GOVERNMENT + primaryContactOwner.type.code === OWNER_TYPE.AGENT || + primaryContactOwner.type.code === OWNER_TYPE.GOVERNMENT ) { //Update Fields for non parcel owners await this.ownerService.update(primaryContactOwner.uuid, { diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts index 10ec1bdac7..43b3bbf60a 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.dto.ts @@ -8,19 +8,13 @@ import { } from 'class-validator'; import { ApplicationDocumentDto } from '../../../alcs/application/application-document/application-document.dto'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; +import { + OWNER_TYPE, + OwnerTypeDto, +} from '../../../common/owner-type/owner-type.entity'; import { emailRegex } from '../../../utils/email.helper'; import { ApplicationParcelDto } from '../application-parcel/application-parcel.dto'; -export enum APPLICATION_OWNER { - INDIVIDUAL = 'INDV', - ORGANIZATION = 'ORGZ', - AGENT = 'AGEN', - CROWN = 'CRWN', - GOVERNMENT = 'GOVR', -} - -export class ApplicationOwnerTypeDto extends BaseCodeDto {} - export class ApplicationOwnerDto { @AutoMap() uuid: string; @@ -49,7 +43,7 @@ export class ApplicationOwnerDto { email?: string | null; @AutoMap() - type: ApplicationOwnerTypeDto; + type: OwnerTypeDto; @AutoMap(() => ApplicationDocumentDto) corporateSummary?: ApplicationDocumentDto; @@ -117,7 +111,7 @@ export class SetPrimaryContactDto { @IsString() @IsOptional() - type?: APPLICATION_OWNER; + type?: OWNER_TYPE; @IsUUID() @IsOptional() diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.entity.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.entity.ts index 8f830e32ef..a24fec78da 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.entity.ts @@ -5,7 +5,7 @@ import { ApplicationDocument } from '../../../alcs/application/application-docum import { Base } from '../../../common/entities/base.entity'; import { ApplicationParcel } from '../application-parcel/application-parcel.entity'; import { ApplicationSubmission } from '../application-submission.entity'; -import { ApplicationOwnerType } from './application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; @Entity() export class ApplicationOwner extends Base { @@ -62,8 +62,8 @@ export class ApplicationOwner extends Base { corporateSummaryUuid: string | null; @AutoMap() - @ManyToOne(() => ApplicationOwnerType, { nullable: false }) - type: ApplicationOwnerType; + @ManyToOne(() => OwnerType, { nullable: false }) + type: OwnerType; @ManyToOne(() => ApplicationSubmission, { nullable: false }) applicationSubmission: ApplicationSubmission; diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts index b78ca5feb0..4fc9af391b 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.spec.ts @@ -9,7 +9,7 @@ import { ApplicationParcel } from '../application-parcel/application-parcel.enti import { ApplicationParcelService } from '../application-parcel/application-parcel.service'; import { ApplicationSubmission } from '../application-submission.entity'; import { ApplicationSubmissionService } from '../application-submission.service'; -import { ApplicationOwnerType } from './application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from './application-owner.entity'; import { ApplicationOwnerService } from './application-owner.service'; @@ -17,7 +17,7 @@ describe('ApplicationOwnerService', () => { let service: ApplicationOwnerService; let mockParcelService: DeepMocked; let mockRepo: DeepMocked>; - let mockTypeRepo: DeepMocked>; + let mockTypeRepo: DeepMocked>; let mockAppDocumentService: DeepMocked; let mockApplicationservice: DeepMocked; @@ -40,7 +40,7 @@ describe('ApplicationOwnerService', () => { useValue: mockRepo, }, { - provide: getRepositoryToken(ApplicationOwnerType), + provide: getRepositoryToken(OwnerType), useValue: mockTypeRepo, }, { @@ -80,7 +80,7 @@ describe('ApplicationOwnerService', () => { it('should load the type and then call save for create', async () => { mockRepo.save.mockResolvedValue(new ApplicationOwner()); - mockTypeRepo.findOneOrFail.mockResolvedValue(new ApplicationOwnerType()); + mockTypeRepo.findOneOrFail.mockResolvedValue(new OwnerType()); await service.create( { diff --git a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts index ddc745c66a..81e5dde54b 100644 --- a/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-owner/application-owner.service.ts @@ -2,13 +2,15 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Any, Repository } from 'typeorm'; import { ApplicationDocumentService } from '../../../alcs/application/application-document/application-document.service'; +import { + OWNER_TYPE, + OwnerType, +} from '../../../common/owner-type/owner-type.entity'; import { PARCEL_TYPE } from '../application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../application-parcel/application-parcel.service'; import { ApplicationSubmission } from '../application-submission.entity'; import { ApplicationSubmissionService } from '../application-submission.service'; -import { ApplicationOwnerType } from './application-owner-type/application-owner-type.entity'; import { - APPLICATION_OWNER, ApplicationOwnerCreateDto, ApplicationOwnerUpdateDto, } from './application-owner.dto'; @@ -19,8 +21,8 @@ export class ApplicationOwnerService { constructor( @InjectRepository(ApplicationOwner) private repository: Repository, - @InjectRepository(ApplicationOwnerType) - private typeRepository: Repository, + @InjectRepository(OwnerType) + private typeRepository: Repository, @Inject(forwardRef(() => ApplicationParcelService)) private applicationParcelService: ApplicationParcelService, @Inject(forwardRef(() => ApplicationSubmissionService)) @@ -222,7 +224,7 @@ export class ApplicationOwnerService { uuid: submissionUuid, }, type: { - code: APPLICATION_OWNER.AGENT, + code: OWNER_TYPE.AGENT, }, }, { @@ -230,7 +232,7 @@ export class ApplicationOwnerService { uuid: submissionUuid, }, type: { - code: APPLICATION_OWNER.GOVERNMENT, + code: OWNER_TYPE.GOVERNMENT, }, }, ], diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts index 1e6e28cb0b..4c11f99ea3 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts @@ -11,8 +11,10 @@ import { ApplicationDocument } from '../../alcs/application/application-document import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; import { DOCUMENT_SOURCE } from '../../document/document.dto'; import { Document } from '../../document/document.entity'; -import { ApplicationOwnerType } from './application-owner/application-owner-type/application-owner-type.entity'; -import { APPLICATION_OWNER } from './application-owner/application-owner.dto'; +import { + OWNER_TYPE, + OwnerType, +} from '../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from './application-owner/application-owner.entity'; import { PARCEL_TYPE } from './application-parcel/application-parcel.dto'; import { ApplicationParcel } from './application-parcel/application-parcel.entity'; @@ -174,8 +176,8 @@ describe('ApplicationSubmissionValidatorService', () => { const applicationSubmission = new ApplicationSubmission({ owners: [ new ApplicationOwner({ - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.CROWN, + type: new OwnerType({ + code: OWNER_TYPE.CROWN, }), }), ], @@ -255,8 +257,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should return errors for an invalid third party agent', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.AGENT, + type: new OwnerType({ + code: OWNER_TYPE.AGENT, }), firstName: 'Bruce', lastName: 'Wayne', @@ -279,8 +281,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should require an authorization letter for more than one owner', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.AGENT, + type: new OwnerType({ + code: OWNER_TYPE.AGENT, }), firstName: 'Bruce', lastName: 'Wayne', @@ -303,8 +305,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should not require an authorization letter for a single owner', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.INDIVIDUAL, + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, }), firstName: 'Bruce', lastName: 'Wayne', @@ -327,8 +329,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should not require an authorization letter when contact is goverment', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.INDIVIDUAL, + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, }), firstName: 'Bruce', lastName: 'Wayne', @@ -336,8 +338,8 @@ describe('ApplicationSubmissionValidatorService', () => { const governmentOwner = new ApplicationOwner({ uuid: 'government-owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.GOVERNMENT, + type: new OwnerType({ + code: OWNER_TYPE.GOVERNMENT, }), firstName: 'Govern', lastName: 'Ment', @@ -361,8 +363,8 @@ describe('ApplicationSubmissionValidatorService', () => { it('should not have an authorization letter error when one is provided', async () => { const mockOwner = new ApplicationOwner({ uuid: 'owner-uuid', - type: new ApplicationOwnerType({ - code: APPLICATION_OWNER.INDIVIDUAL, + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, }), firstName: 'Bruce', lastName: 'Wayne', diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts index aed07bd264..7985577d18 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts @@ -1,10 +1,10 @@ import { ServiceValidationException } from '@app/common/exceptions/base.exception'; import { Injectable, Logger } from '@nestjs/common'; -import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; -import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; -import { APPLICATION_OWNER } from './application-owner/application-owner.dto'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { OWNER_TYPE } from '../../common/owner-type/owner-type.entity'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationOwner } from './application-owner/application-owner.entity'; import { PARCEL_TYPE } from './application-parcel/application-parcel.dto'; import { ApplicationParcel } from './application-parcel/application-parcel.entity'; @@ -260,11 +260,10 @@ export class ApplicationSubmissionValidatorService { const onlyHasIndividualOwner = applicationSubmission.owners.length === 1 && - applicationSubmission.owners[0].type.code === - APPLICATION_OWNER.INDIVIDUAL; + applicationSubmission.owners[0].type.code === OWNER_TYPE.INDIVIDUAL; const isGovernmentContact = - primaryOwner.type.code === APPLICATION_OWNER.GOVERNMENT; + primaryOwner.type.code === OWNER_TYPE.GOVERNMENT; if (!onlyHasIndividualOwner && !isGovernmentContact) { const authorizationLetters = documents.filter( @@ -280,10 +279,7 @@ export class ApplicationSubmissionValidatorService { } } - if ( - primaryOwner.type.code === APPLICATION_OWNER.AGENT || - isGovernmentContact - ) { + if (primaryOwner.type.code === OWNER_TYPE.AGENT || isGovernmentContact) { if ( !primaryOwner.firstName || !primaryOwner.lastName || diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts index 2ffb29d04c..8960faeeae 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts @@ -11,7 +11,7 @@ import { ApplicationSubmissionProfile } from '../../common/automapper/applicatio import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; import { PdfGenerationModule } from '../pdf-generation/pdf-generation.module'; -import { ApplicationOwnerType } from './application-owner/application-owner-type/application-owner-type.entity'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { ApplicationOwnerController } from './application-owner/application-owner.controller'; import { ApplicationOwner } from './application-owner/application-owner.entity'; import { ApplicationOwnerService } from './application-owner/application-owner.service'; @@ -33,7 +33,7 @@ import { NaruSubtype } from './naru-subtype/naru-subtype.entity'; ApplicationParcel, ApplicationParcelOwnershipType, ApplicationOwner, - ApplicationOwnerType, + OwnerType, NaruSubtype, ApplicationSubmissionToSubmissionStatus, ]), diff --git a/services/apps/alcs/src/portal/code/code.controller.ts b/services/apps/alcs/src/portal/code/code.controller.ts index 41fa24abe5..8de46fd499 100644 --- a/services/apps/alcs/src/portal/code/code.controller.ts +++ b/services/apps/alcs/src/portal/code/code.controller.ts @@ -39,14 +39,13 @@ export class CodeController { const localGovernments = await this.localGovernmentService.listActive(); const applicationTypes = await this.applicationService.fetchApplicationTypes(); - const applicationDocumentTypes = - await this.applicationDocumentService.fetchTypes(); + const documentTypes = await this.applicationDocumentService.fetchTypes(); const submissionTypes = await this.cardService.getPortalCardTypes(); const noticeOfIntentTypes = await this.noticeOfIntentService.listTypes(); const naruSubtypes = await this.applicationSubmissionService.listNaruSubtypes(); - const mappedDocTypes = applicationDocumentTypes.map((docType) => { + const mappedDocTypes = documentTypes.map((docType) => { if (docType.portalLabel) { docType.label = docType.portalLabel; } @@ -60,7 +59,7 @@ export class CodeController { applicationTypes, noticeOfIntentTypes, submissionTypes, - applicationDocumentTypes: mappedDocTypes, + documentTypes: mappedDocTypes, naruSubtypes, }; } diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts new file mode 100644 index 0000000000..920aa7d08b --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts @@ -0,0 +1,179 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; +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 { NoticeOfIntentProfile } from '../../common/automapper/notice-of-intent.automapper.profile'; +import { DocumentCode } from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM } from '../../document/document.dto'; +import { Document } from '../../document/document.entity'; +import { DocumentService } from '../../document/document.service'; +import { User } from '../../user/user.entity'; +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 { NoticeOfIntentDocumentController } from './notice-of-intent-document.controller'; +import { AttachExternalDocumentDto } from './notice-of-intent-document.dto'; + +describe('NoticeOfIntentDocumentController', () => { + let controller: NoticeOfIntentDocumentController; + let noiDocumentService: DeepMocked; + let mockNoiSubmissionService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockNoticeOfIntentService: DeepMocked; + + const mockDocument = new NoticeOfIntentDocument({ + document: new Document({ + fileName: 'fileName', + uploadedAt: new Date(), + uploadedBy: new User(), + }), + }); + + beforeEach(async () => { + noiDocumentService = createMock(); + mockDocumentService = createMock(); + mockNoiSubmissionService = createMock(); + mockNoticeOfIntentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentDocumentController], + providers: [ + NoticeOfIntentProfile, + { + provide: NoticeOfIntentDocumentService, + useValue: noiDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoiSubmissionService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: NoticeOfIntentService, + useValue: mockNoticeOfIntentService, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + controller = module.get( + NoticeOfIntentDocumentController, + ); + + mockNoiSubmissionService.verifyAccessByFileId.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + mockNoticeOfIntentService.getUuid.mockResolvedValue('uuid'); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call through to delete documents', async () => { + noiDocumentService.delete.mockResolvedValue(mockDocument); + noiDocumentService.get.mockResolvedValue(mockDocument); + + await controller.delete('fake-uuid', { + user: { + entity: {}, + }, + }); + + expect(noiDocumentService.get).toHaveBeenCalledTimes(1); + expect(noiDocumentService.delete).toHaveBeenCalledTimes(1); + }); + + it('should call through to update documents', async () => { + noiDocumentService.updateDescriptionAndType.mockResolvedValue([]); + + await controller.update( + 'file-number', + { + user: { + entity: {}, + }, + }, + [], + ); + + expect(noiDocumentService.updateDescriptionAndType).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should call through for download', async () => { + const fakeUrl = 'fake-url'; + noiDocumentService.getInlineUrl.mockResolvedValue(fakeUrl); + noiDocumentService.get.mockResolvedValue(mockDocument); + + const res = await controller.open('fake-uuid', { + user: { + entity: {}, + }, + }); + + expect(res.url).toEqual(fakeUrl); + }); + + it('should call out to service to attach external document', async () => { + const user = { user: { entity: 'Bruce' } }; + const fakeUuid = 'fakeUuid'; + const docObj = new Document({ uuid: 'fake-uuid' }); + const userEntity = new User({ + name: user.user.entity, + }); + + const docDto: AttachExternalDocumentDto = { + fileSize: 0, + mimeType: 'mimeType', + fileName: 'fileName', + fileKey: 'fileKey', + source: DOCUMENT_SOURCE.APPLICANT, + }; + + mockDocumentService.createDocumentRecord.mockResolvedValue(docObj); + + noiDocumentService.attachExternalDocument.mockResolvedValue( + new NoticeOfIntentDocument({ + noticeOfIntent: undefined, + type: new DocumentCode(), + uuid: fakeUuid, + document: new Document({ + uploadedAt: new Date(), + uploadedBy: userEntity, + }), + }), + ); + + const res = await controller.attachExternalDocument( + 'fake-number', + docDto, + user, + ); + + expect(mockDocumentService.createDocumentRecord).toBeCalledTimes(1); + expect(noiDocumentService.attachExternalDocument).toBeCalledTimes(1); + expect(mockDocumentService.createDocumentRecord).toBeCalledWith({ + ...docDto, + system: DOCUMENT_SYSTEM.PORTAL, + }); + expect(res.uploadedBy).toEqual(user.user.entity); + expect(res.uuid).toEqual(fakeUuid); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts new file mode 100644 index 0000000000..fb61df26e8 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts @@ -0,0 +1,162 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { ApplicationDocumentDto } from '../../alcs/application/application-document/application-document.dto'; +import { VISIBILITY_FLAG } from '../../alcs/application/application-document/application-document.entity'; +import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +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 { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { DOCUMENT_SYSTEM } from '../../document/document.dto'; +import { DocumentService } from '../../document/document.service'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission/notice-of-intent-submission.service'; +import { + AttachExternalDocumentDto, + PortalNoticeOfIntentDocumentUpdateDto, +} from './notice-of-intent-document.dto'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(PortalAuthGuard) +@Controller('notice-of-intent-document') +export class NoticeOfIntentDocumentController { + constructor( + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private noticeOfIntentService: NoticeOfIntentService, + private documentService: DocumentService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('/application/:fileNumber') + async listApplicantDocuments( + @Param('fileNumber') fileNumber: string, + @Param('documentType') documentType: DOCUMENT_TYPE | null, + @Req() req, + ): Promise { + await this.noticeOfIntentSubmissionService.verifyAccessByFileId( + fileNumber, + req.user.entity, + ); + + const documents = await this.noticeOfIntentDocumentService.list( + fileNumber, + [VISIBILITY_FLAG.APPLICANT], + ); + return this.mapPortalDocuments(documents); + } + + @Get('/:uuid/open') + async open(@Param('uuid') fileUuid: string, @Req() req) { + const document = await this.noticeOfIntentDocumentService.get(fileUuid); + + //TODO: How do we know which documents applicant can access? + // await this.applicationSubmissionService.verifyAccess( + // document.applicationUuid, + // req.user.entity, + // ); + + const url = await this.noticeOfIntentDocumentService.getInlineUrl(document); + return { url }; + } + + @Patch('/application/:fileNumber') + async update( + @Param('fileNumber') fileNumber: string, + @Req() req, + @Body() body: PortalNoticeOfIntentDocumentUpdateDto[], + ) { + await this.noticeOfIntentSubmissionService.verifyAccessByFileId( + fileNumber, + req.user.entity, + ); + + //Map from file number to uuid + const noticeOfIntentUuid = await this.noticeOfIntentService.getUuid( + fileNumber, + ); + + const res = + await this.noticeOfIntentDocumentService.updateDescriptionAndType( + body, + noticeOfIntentUuid, + ); + return this.mapPortalDocuments(res); + } + + @Delete('/:uuid') + async delete(@Param('uuid') fileUuid: string, @Req() req) { + const document = await this.noticeOfIntentDocumentService.get(fileUuid); + + //TODO: How do we know which documents applicant can delete? + // await this.applicationSubmissionService.verifyAccess( + // document.applicationUuid, + // req.user.entity, + // ); + + await this.noticeOfIntentDocumentService.delete(document); + return {}; + } + + @Post('/application/:uuid/attachExternal') + async attachExternalDocument( + @Param('uuid') fileNumber: string, + @Body() data: AttachExternalDocumentDto, + @Req() req, + ): Promise { + const submission = + await this.noticeOfIntentSubmissionService.verifyAccessByFileId( + fileNumber, + req.user.entity, + ); + + const document = await this.documentService.createDocumentRecord({ + ...data, + system: DOCUMENT_SYSTEM.PORTAL, + }); + + const savedDocument = + await this.noticeOfIntentDocumentService.attachExternalDocument( + submission.fileNumber, + { + documentUuid: document.uuid, + type: data.documentType, + }, + [ + VISIBILITY_FLAG.APPLICANT, + VISIBILITY_FLAG.GOVERNMENT, + VISIBILITY_FLAG.COMMISSIONER, + ], + ); + + const mappedDocs = this.mapPortalDocuments([savedDocument]); + return mappedDocs[0]; + } + + private mapPortalDocuments(documents: NoticeOfIntentDocument[]) { + const labeledDocuments = documents.map((document) => { + if (document.type?.portalLabel) { + document.type.label = document.type.portalLabel; + } + return document; + }); + return this.mapper.mapArray( + labeledDocuments, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ); + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.dto.ts new file mode 100644 index 0000000000..3cbf7d4a0e --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.dto.ts @@ -0,0 +1,30 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE } from '../../document/document.dto'; + +export class AttachExternalDocumentDto { + @IsString() + mimeType: string; + + @IsString() + fileName: string; + + @IsNumber() + fileSize: number; + + @IsString() + fileKey: string; + + @IsString() + source: DOCUMENT_SOURCE.APPLICANT; + + @IsString() + @IsOptional() + documentType?: DOCUMENT_TYPE; +} + +export class PortalNoticeOfIntentDocumentUpdateDto { + uuid: string; + type: DOCUMENT_TYPE | null; + description: string | null; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.module.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.module.ts new file mode 100644 index 0000000000..ae4a6622a8 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; +import { DocumentModule } from '../../document/document.module'; +import { NoticeOfIntentSubmissionModule } from '../notice-of-intent-submission/notice-of-intent-submission.module'; +import { NoticeOfIntentDocumentController } from './notice-of-intent-document.controller'; + +@Module({ + imports: [ + DocumentModule, + NoticeOfIntentModule, + NoticeOfIntentSubmissionModule, + ], + controllers: [NoticeOfIntentDocumentController], + providers: [], + exports: [], +}) +export class PortalNoticeOfIntentDocumentModule {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.spec.ts new file mode 100644 index 0000000000..ee57a26100 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.spec.ts @@ -0,0 +1,345 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { BadRequestException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerProfile } from '../../../common/automapper/notice-of-intent-owner.automapper.profile'; +import { + OWNER_TYPE, + OwnerType, +} from '../../../common/owner-type/owner-type.entity'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { NoticeOfIntentOwnerController } from './notice-of-intent-owner.controller'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner.entity'; +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner.service'; + +describe('NoticeOfIntentOwnerController', () => { + let controller: NoticeOfIntentOwnerController; + let mockNOISubmissionService: DeepMocked; + let mockOwnerService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockNOIDocumentService: DeepMocked; + + beforeEach(async () => { + mockNOISubmissionService = createMock(); + mockOwnerService = createMock(); + mockDocumentService = createMock(); + mockNOIDocumentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentOwnerController], + providers: [ + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNOISubmissionService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockOwnerService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNOIDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + NoticeOfIntentOwnerProfile, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NoticeOfIntentOwnerController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should verify access before fetching applications and map displayName', async () => { + const owner = new NoticeOfIntentOwner({ + firstName: 'Bruce', + lastName: 'Wayne', + }); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission({ + owners: [owner], + }), + ); + + const owners = await controller.fetchByFileId('', { + user: { + entity: {}, + }, + }); + + expect(owners.length).toEqual(1); + expect(owners[0].displayName).toBe('Bruce Wayne'); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should verify the dto and file access then create', async () => { + const owner = new NoticeOfIntentOwner({ + firstName: 'Bruce', + lastName: 'Wayne', + }); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + mockOwnerService.create.mockResolvedValue(owner); + + const createdOwner = await controller.create( + { + firstName: 'B', + lastName: 'W', + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: 'INDV', + }, + { + user: { + entity: {}, + }, + }, + ); + + expect(createdOwner).toBeDefined(); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.create).toHaveBeenCalledTimes(1); + }); + + it('should throw an exception when creating an individual owner without first name', async () => { + const promise = controller.create( + { + lastName: 'W', + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: 'INDV', + }, + { + user: { + entity: {}, + }, + }, + ); + await expect(promise).rejects.toMatchObject( + new BadRequestException('Individuals require both first and last name'), + ); + }); + + it('should throw an exception when creating an organization an org name', async () => { + const promise = controller.create( + { + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: 'ORGZ', + }, + { + user: { + entity: {}, + }, + }, + ); + await expect(promise).rejects.toMatchObject( + new BadRequestException('Organizations must have an organizationName'), + ); + }); + + it('should call through for update', async () => { + mockOwnerService.update.mockResolvedValue(new NoticeOfIntentOwner()); + mockOwnerService.getOwner.mockResolvedValue(new NoticeOfIntentOwner()); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + const res = await controller.update( + '', + { + organizationName: 'orgName', + email: '', + phoneNumber: '', + typeCode: 'ORGZ', + }, + { + user: { + entity: {}, + }, + }, + ); + + expect(mockOwnerService.update).toHaveBeenCalledTimes(1); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + }); + + it('should call through for delete', async () => { + mockOwnerService.delete.mockResolvedValue({} as any); + mockOwnerService.getOwner.mockResolvedValue(new NoticeOfIntentOwner()); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.delete('', { + user: { + entity: {}, + }, + }); + + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.delete).toHaveBeenCalledTimes(1); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + }); + + it('should call through for attachToParcel', async () => { + mockOwnerService.attachToParcel.mockResolvedValue({} as any); + mockOwnerService.getOwner.mockResolvedValue(new NoticeOfIntentOwner()); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.linkToParcel('', '', { + user: { + entity: {}, + }, + }); + + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.attachToParcel).toHaveBeenCalledTimes(1); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + }); + + it('should call through for removeFromParcel', async () => { + mockOwnerService.removeFromParcel.mockResolvedValue({} as any); + mockOwnerService.getOwner.mockResolvedValue(new NoticeOfIntentOwner()); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.removeFromParcel('', '', { + user: { + entity: {}, + }, + }); + + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.removeFromParcel).toHaveBeenCalledTimes(1); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + }); + + it('should create a new owner when setting primary contact to third party agent that doesnt exist', async () => { + mockOwnerService.deleteNonParcelOwners.mockResolvedValue([]); + mockOwnerService.create.mockResolvedValue(new NoticeOfIntentOwner()); + mockOwnerService.setPrimaryContact.mockResolvedValue(); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.setPrimaryContact( + { noticeOfIntentSubmissionUuid: '' }, + { + user: { + entity: {}, + }, + }, + ); + + expect(mockOwnerService.deleteNonParcelOwners).toHaveBeenCalledTimes(1); + expect(mockOwnerService.create).toHaveBeenCalledTimes(1); + expect(mockOwnerService.setPrimaryContact).toHaveBeenCalledTimes(1); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should set the owner and delete agents when using a non-agent owner', async () => { + mockOwnerService.getOwner.mockResolvedValue( + new NoticeOfIntentOwner({ + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, + }), + }), + ); + mockOwnerService.setPrimaryContact.mockResolvedValue(); + mockOwnerService.deleteNonParcelOwners.mockResolvedValue({} as any); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.setPrimaryContact( + { noticeOfIntentSubmissionUuid: '', ownerUuid: 'fake-uuid' }, + { + user: { + entity: {}, + }, + }, + ); + + expect(mockOwnerService.setPrimaryContact).toHaveBeenCalledTimes(1); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.deleteNonParcelOwners).toHaveBeenCalledTimes(1); + }); + + it('should update the agent owner when calling set primary contact', async () => { + mockOwnerService.getOwner.mockResolvedValue( + new NoticeOfIntentOwner({ + type: new OwnerType({ + code: OWNER_TYPE.AGENT, + }), + }), + ); + mockOwnerService.update.mockResolvedValue(new NoticeOfIntentOwner()); + mockOwnerService.setPrimaryContact.mockResolvedValue(); + mockNOISubmissionService.verifyAccessByUuid.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + + await controller.setPrimaryContact( + { noticeOfIntentSubmissionUuid: '', ownerUuid: 'fake-uuid' }, + { + user: { + entity: {}, + }, + }, + ); + + expect(mockOwnerService.setPrimaryContact).toHaveBeenCalledTimes(1); + expect(mockNOISubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + expect(mockOwnerService.update).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.ts new file mode 100644 index 0000000000..981c11489f --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.controller.ts @@ -0,0 +1,259 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { VISIBILITY_FLAG } from '../../../alcs/application/application-document/application-document.entity'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { PortalAuthGuard } from '../../../common/authorization/portal-auth-guard.service'; +import { OWNER_TYPE } from '../../../common/owner-type/owner-type.entity'; +import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; +import { + DOCUMENT_SOURCE, + DOCUMENT_SYSTEM, +} from '../../../document/document.dto'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { + AttachCorporateSummaryDto, + NoticeOfIntentOwnerCreateDto, + NoticeOfIntentOwnerDto, + NoticeOfIntentOwnerUpdateDto, + SetPrimaryContactDto, +} from './notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner.entity'; +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner.service'; + +@Controller('notice-of-intent-owner') +@UseGuards(PortalAuthGuard) +export class NoticeOfIntentOwnerController { + constructor( + private ownerService: NoticeOfIntentOwnerService, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private documentService: DocumentService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('submission/:submissionUuid') + async fetchByFileId( + @Param('submissionUuid') submissionUuid: string, + @Req() req, + ): Promise { + const noticeOfIntentSubmission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + submissionUuid, + req.user.entity, + ); + + return this.mapper.mapArrayAsync( + noticeOfIntentSubmission.owners, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } + + @Post() + async create( + @Body() createDto: NoticeOfIntentOwnerCreateDto, + @Req() req, + ): Promise { + this.verifyDto(createDto); + + const application = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + createDto.noticeOfIntentSubmissionUuid, + req.user.entity, + ); + const owner = await this.ownerService.create(createDto, application); + + return this.mapper.mapAsync( + owner, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } + + @Patch('/:uuid') + async update( + @Param('uuid') uuid: string, + @Body() updateDto: NoticeOfIntentOwnerUpdateDto, + @Req() req, + ) { + await this.verifyAccessAndGetOwner(req, uuid); + this.verifyDto(updateDto); + + const newParcel = await this.ownerService.update(uuid, updateDto); + + return this.mapper.mapAsync( + newParcel, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } + + @Delete('/:uuid') + async delete(@Param('uuid') uuid: string, @Req() req) { + const owner = await this.verifyAccessAndGetOwner(req, uuid); + if (owner.corporateSummary) { + await this.noticeOfIntentDocumentService.delete(owner.corporateSummary); + } + await this.ownerService.delete(owner); + return { uuid }; + } + + @Post('/:uuid/link/:parcelUuid') + async linkToParcel( + @Param('uuid') uuid: string, + @Param('parcelUuid') parcelUuid: string, + @Req() req, + ) { + await this.verifyAccessAndGetOwner(req, uuid); + + return { uuid: await this.ownerService.attachToParcel(uuid, parcelUuid) }; + } + + @Post('/:uuid/unlink/:parcelUuid') + async removeFromParcel( + @Param('uuid') uuid: string, + @Param('parcelUuid') parcelUuid: string, + @Req() req, + ) { + await this.verifyAccessAndGetOwner(req, uuid); + + return { uuid: await this.ownerService.removeFromParcel(uuid, parcelUuid) }; + } + + private verifyDto( + dto: NoticeOfIntentOwnerUpdateDto | NoticeOfIntentOwnerCreateDto, + ) { + if ( + dto.typeCode === OWNER_TYPE.INDIVIDUAL && + (!dto.firstName || !dto.lastName) + ) { + throw new BadRequestException( + 'Individuals require both first and last name', + ); + } + + if (dto.typeCode === OWNER_TYPE.ORGANIZATION && !dto.organizationName) { + throw new BadRequestException( + 'Organizations must have an organizationName', + ); + } + } + + @Post('setPrimaryContact') + async setPrimaryContact(@Body() data: SetPrimaryContactDto, @Req() req) { + const applicationSubmission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + data.noticeOfIntentSubmissionUuid, + req.user.entity, + ); + + //Create Owner + if (!data.ownerUuid) { + await this.ownerService.deleteNonParcelOwners(applicationSubmission.uuid); + const newOwner = await this.ownerService.create( + { + email: data.email, + typeCode: data.type, + lastName: data.lastName, + firstName: data.firstName, + phoneNumber: data.phoneNumber, + organizationName: data.organization, + noticeOfIntentSubmissionUuid: data.noticeOfIntentSubmissionUuid, + }, + applicationSubmission, + ); + await this.ownerService.setPrimaryContact( + applicationSubmission.uuid, + newOwner, + ); + } else if (data.ownerUuid) { + const primaryContactOwner = await this.ownerService.getOwner( + data.ownerUuid, + ); + + if ( + primaryContactOwner.type.code === OWNER_TYPE.AGENT || + primaryContactOwner.type.code === OWNER_TYPE.GOVERNMENT + ) { + //Update Fields for non parcel owners + await this.ownerService.update(primaryContactOwner.uuid, { + email: data.email, + typeCode: primaryContactOwner.type.code, + lastName: data.lastName, + firstName: data.firstName, + phoneNumber: data.phoneNumber, + organizationName: data.organization, + }); + } else { + //Delete Non parcel owners if we aren't using one + await this.ownerService.deleteNonParcelOwners( + applicationSubmission.uuid, + ); + } + + await this.ownerService.setPrimaryContact( + applicationSubmission.uuid, + primaryContactOwner, + ); + } + } + + private async verifyAccessAndGetOwner(@Req() req, ownerUuid: string) { + const owner = await this.ownerService.getOwner(ownerUuid); + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + owner.noticeOfIntentSubmissionUuid, + req.user.entity, + ); + + return owner; + } + + @Post('attachCorporateSummary') + async attachCorporateSummary( + @Req() req, + @Body() data: AttachCorporateSummaryDto, + ) { + await this.noticeOfIntentSubmissionService.verifyAccessByFileId( + data.fileNumber, + req.user.entity, + ); + + const document = await this.documentService.createDocumentRecord({ + ...data, + uploadedBy: req.user.entity, + source: DOCUMENT_SOURCE.APPLICANT, + system: DOCUMENT_SYSTEM.PORTAL, + }); + + const applicationDocument = + await this.noticeOfIntentDocumentService.attachExternalDocument( + data.fileNumber, + { + documentUuid: document.uuid, + type: DOCUMENT_TYPE.CORPORATE_SUMMARY, + }, + [ + VISIBILITY_FLAG.APPLICANT, + VISIBILITY_FLAG.GOVERNMENT, + VISIBILITY_FLAG.COMMISSIONER, + ], + ); + + return { + uuid: applicationDocument.uuid, + }; + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts new file mode 100644 index 0000000000..08ab45b2ea --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts @@ -0,0 +1,138 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsNumber, + IsOptional, + IsString, + IsUUID, + Matches, +} from 'class-validator'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { + OWNER_TYPE, + OwnerTypeDto, +} from '../../../common/owner-type/owner-type.entity'; +import { emailRegex } from '../../../utils/email.helper'; +import { NoticeOfIntentParcelDto } from '../notice-of-intent-parcel/notice-of-intent-parcel.dto'; + +export class NoticeOfIntentOwnerDto { + @AutoMap() + uuid: string; + + @AutoMap() + noticeOfIntentSubmissionUuid: string; + + @AutoMap(() => String) + corporateSummaryUuid?: string; + + displayName: string; + + @AutoMap(() => String) + firstName?: string | null; + + @AutoMap(() => String) + lastName?: string | null; + + @AutoMap(() => String) + organizationName?: string | null; + + @AutoMap(() => String) + phoneNumber?: string | null; + + @AutoMap(() => String) + email?: string | null; + + @AutoMap() + type: OwnerTypeDto; + + @AutoMap(() => NoticeOfIntentDocumentDto) + corporateSummary?: NoticeOfIntentDocumentDto; +} + +export class NoticeOfIntentOwnerDetailedDto extends NoticeOfIntentOwnerDto { + parcels: NoticeOfIntentParcelDto[]; +} + +export class NoticeOfIntentOwnerUpdateDto { + @IsString() + @IsOptional() + firstName?: string; + + @IsString() + @IsOptional() + lastName?: string; + + @IsString() + @IsOptional() + organizationName?: string; + + @IsString() + @IsOptional() + phoneNumber?: string; + + @Matches(emailRegex) + @IsOptional() + email?: string; + + @IsString() + @IsOptional() + typeCode?: string; + + @IsUUID() + @IsOptional() + corporateSummaryUuid?: string; +} + +export class NoticeOfIntentOwnerCreateDto extends NoticeOfIntentOwnerUpdateDto { + @IsString() + noticeOfIntentSubmissionUuid: string; +} + +export class SetPrimaryContactDto { + @IsString() + @IsOptional() + firstName?: string; + + @IsString() + @IsOptional() + lastName?: string; + + @IsString() + @IsOptional() + organization?: string; + + @IsString() + @IsOptional() + phoneNumber?: string; + + @IsString() + @IsOptional() + email?: string; + + @IsString() + @IsOptional() + type?: OWNER_TYPE; + + @IsUUID() + @IsOptional() + ownerUuid?: string; + + @IsString() + noticeOfIntentSubmissionUuid: string; +} + +export class AttachCorporateSummaryDto { + @IsString() + mimeType: string; + + @IsString() + fileName: string; + + @IsNumber() + fileSize: number; + + @IsString() + fileKey: string; + + @IsString() + fileNumber: string; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity.ts new file mode 100644 index 0000000000..62ec03a981 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity.ts @@ -0,0 +1,77 @@ +import { AutoMap } from '@automapper/classes'; +import { Column, Entity, JoinColumn, ManyToMany, ManyToOne } from 'typeorm'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { Base } from '../../../common/entities/base.entity'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; +import { NoticeOfIntentParcel } from '../notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; + +@Entity() +export class NoticeOfIntentOwner extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + firstName?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + lastName?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + organizationName?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + phoneNumber?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + email?: string | null; + + @AutoMap(() => NoticeOfIntentDocumentDto) + @ManyToOne(() => NoticeOfIntentDocument, { + onDelete: 'SET NULL', + }) + @JoinColumn() + corporateSummary: NoticeOfIntentDocument | null; + + @Column({ nullable: true }) + corporateSummaryUuid: string | null; + + @AutoMap() + @ManyToOne(() => OwnerType, { nullable: false }) + type: OwnerType; + + @ManyToOne(() => NoticeOfIntentSubmission, { nullable: false }) + noticeOfIntentSubmission: NoticeOfIntentSubmission; + + @AutoMap() + @Column() + noticeOfIntentSubmissionUuid: string; + + @ManyToMany(() => NoticeOfIntentParcel, (appParcel) => appParcel.owners) + parcels: NoticeOfIntentParcel[]; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts new file mode 100644 index 0000000000..9493d13e28 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts @@ -0,0 +1,341 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ApplicationDocumentService } from '../../../alcs/application/application-document/application-document.service'; +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 { OwnerType } from '../../../common/owner-type/owner-type.entity'; +import { NoticeOfIntentParcel } from '../notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from '../notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner.entity'; +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner.service'; + +describe('NoticeOfIntentOwnerService', () => { + let service: NoticeOfIntentOwnerService; + let mockParcelService: DeepMocked; + let mockRepo: DeepMocked>; + let mockTypeRepo: DeepMocked>; + let mockAppDocumentService: DeepMocked; + let mockApplicationservice: DeepMocked; + + beforeEach(async () => { + mockParcelService = createMock(); + mockRepo = createMock(); + mockTypeRepo = createMock(); + mockAppDocumentService = createMock(); + mockApplicationservice = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentOwnerService, + { + provide: NoticeOfIntentParcelService, + useValue: mockParcelService, + }, + { + provide: getRepositoryToken(NoticeOfIntentOwner), + useValue: mockRepo, + }, + { + provide: getRepositoryToken(OwnerType), + useValue: mockTypeRepo, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockAppDocumentService, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockApplicationservice, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentOwnerService, + ); + + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners: [new NoticeOfIntentOwner()], + }), + ]); + mockApplicationservice.update.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should call find for find', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + + await service.fetchByApplicationFileId(''); + + expect(mockRepo.find).toHaveBeenCalledTimes(1); + }); + + it('should load the type and then call save for create', async () => { + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + mockTypeRepo.findOneOrFail.mockResolvedValue(new OwnerType()); + + await service.create( + { + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: '', + }, + new NoticeOfIntentSubmission(), + ); + + expect(mockRepo.save).toHaveBeenCalledTimes(1); + expect(mockTypeRepo.findOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should load the type/parcel and then call save for attachToParcel', async () => { + const owner = new NoticeOfIntentOwner({ + parcels: [], + }); + mockRepo.findOneOrFail.mockResolvedValue(owner); + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + mockParcelService.getOneOrFail.mockResolvedValue( + new NoticeOfIntentParcel(), + ); + + await service.attachToParcel('', ''); + + expect(owner.parcels.length).toEqual(1); + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepo.save).toHaveBeenCalledTimes(1); + expect(mockParcelService.getOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should remove the parcel from the array then call save for removeFromParcel', async () => { + const parcelUuid = '1'; + const owner = new NoticeOfIntentOwner({ + parcels: [ + new NoticeOfIntentParcel({ + uuid: parcelUuid, + }), + ], + }); + mockRepo.findOneOrFail.mockResolvedValue(owner); + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + + await service.removeFromParcel('', parcelUuid); + + expect(owner.parcels.length).toEqual(0); + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepo.save).toHaveBeenCalledTimes(1); + }); + + it('should set properties and call save for update', async () => { + const owner = new NoticeOfIntentOwner({ + firstName: 'Bruce', + lastName: 'Wayne', + }); + mockRepo.findOneOrFail.mockResolvedValue(owner); + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + + await service.update('', { + firstName: 'I Am', + lastName: 'Batman', + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(owner.firstName).toEqual('I Am'); + expect(owner.lastName).toEqual('Batman'); + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepo.save).toHaveBeenCalledTimes(1); + }); + + it('should delete the existing document when updating', async () => { + const owner = new NoticeOfIntentOwner({ + firstName: 'Bruce', + lastName: 'Wayne', + corporateSummaryUuid: 'oldUuid', + corporateSummary: new NoticeOfIntentDocument(), + }); + mockRepo.findOneOrFail.mockResolvedValue(owner); + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + mockAppDocumentService.delete.mockResolvedValue({} as any); + + await service.update('', { + organizationName: '', + email: '', + phoneNumber: '', + typeCode: '', + corporateSummaryUuid: 'newUuid', + }); + + expect(owner.corporateSummaryUuid).toEqual('newUuid'); + expect(mockAppDocumentService.delete).toHaveBeenCalledTimes(1); + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepo.save).toHaveBeenCalledTimes(2); + }); + + it('should call through for delete', async () => { + mockRepo.remove.mockResolvedValue({} as any); + + await service.delete(new NoticeOfIntentOwner()); + + expect(mockRepo.remove).toHaveBeenCalledTimes(1); + }); + + it('should call through for verify', async () => { + mockRepo.findOneOrFail.mockResolvedValue(new NoticeOfIntentOwner()); + + await service.getOwner(''); + + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should call through for getMany', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + + await service.getMany([]); + + expect(mockRepo.find).toHaveBeenCalledTimes(1); + }); + + it('should call through for save', async () => { + mockRepo.save.mockResolvedValue(new NoticeOfIntentOwner()); + + await service.save(new NoticeOfIntentOwner()); + + expect(mockRepo.save).toHaveBeenCalledTimes(1); + }); + + it('should call update for the application with the first parcels last name', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + const owners = [ + new NoticeOfIntentOwner({ + firstName: 'B', + lastName: 'A', + }), + ]; + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners, + }), + ]); + + await service.updateSubmissionApplicant(''); + + expect(mockApplicationservice.update).toHaveBeenCalledTimes(1); + expect(mockApplicationservice.update.mock.calls[0][1].applicant).toEqual( + 'A', + ); + }); + + it('should call update for the application with the first parcels last name', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + const owners = [ + new NoticeOfIntentOwner({ + firstName: 'F', + lastName: 'B', + }), + new NoticeOfIntentOwner({ + firstName: 'F', + lastName: 'A', + }), + new NoticeOfIntentOwner({ + firstName: 'F', + lastName: '1', + }), + new NoticeOfIntentOwner({ + firstName: 'F', + lastName: 'C', + }), + ]; + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners, + }), + ]); + + await service.updateSubmissionApplicant(''); + + expect(mockApplicationservice.update).toHaveBeenCalledTimes(1); + expect(mockApplicationservice.update.mock.calls[0][1].applicant).toEqual( + 'A et al.', + ); + }); + + it('should call update for the application with the number owners last name', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + const owners = [ + new NoticeOfIntentOwner({ + firstName: '1', + lastName: '1', + }), + new NoticeOfIntentOwner({ + firstName: '2', + lastName: '2', + }), + ]; + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners, + }), + ]); + + await service.updateSubmissionApplicant(''); + + expect(mockApplicationservice.update).toHaveBeenCalledTimes(1); + expect(mockApplicationservice.update.mock.calls[0][1].applicant).toEqual( + '1 et al.', + ); + }); + + it('should use the first created parcel to set the application applicants name', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + const owners1 = [ + new NoticeOfIntentOwner({ + firstName: 'C', + lastName: 'C', + }), + ]; + const owners2 = [ + new NoticeOfIntentOwner({ + firstName: 'A', + lastName: 'A', + }), + ]; + mockParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([ + new NoticeOfIntentParcel({ + owners: owners1, + auditCreatedAt: new Date(1), + }), + new NoticeOfIntentParcel({ + owners: owners2, + auditCreatedAt: new Date(100), + }), + ]); + + await service.updateSubmissionApplicant(''); + + expect(mockApplicationservice.update).toHaveBeenCalledTimes(1); + expect(mockApplicationservice.update.mock.calls[0][1].applicant).toEqual( + 'A et al.', + ); + }); + + it('should load then delete non application owners', async () => { + mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); + mockRepo.remove.mockResolvedValue([] as any); + + await service.deleteNonParcelOwners('uuid'); + + expect(mockRepo.find).toHaveBeenCalledTimes(1); + expect(mockRepo.remove).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.ts new file mode 100644 index 0000000000..33d81961a7 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.ts @@ -0,0 +1,294 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Any, Repository } from 'typeorm'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { + OWNER_TYPE, + OwnerType, +} from '../../../common/owner-type/owner-type.entity'; +import { NoticeOfIntentParcelService } from '../notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { + NoticeOfIntentOwnerCreateDto, + NoticeOfIntentOwnerUpdateDto, +} from './notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner.entity'; + +@Injectable() +export class NoticeOfIntentOwnerService { + constructor( + @InjectRepository(NoticeOfIntentOwner) + private repository: Repository, + @InjectRepository(OwnerType) + private typeRepository: Repository, + @Inject(forwardRef(() => NoticeOfIntentParcelService)) + private noticeOfIntentParcelService: NoticeOfIntentParcelService, + @Inject(forwardRef(() => NoticeOfIntentSubmissionService)) + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + ) {} + + async fetchByApplicationFileId(fileId: string) { + return this.repository.find({ + where: { + noticeOfIntentSubmission: { + fileNumber: fileId, + }, + }, + relations: { + type: true, + corporateSummary: { + document: true, + }, + }, + }); + } + + async create( + createDto: NoticeOfIntentOwnerCreateDto, + noticeOfIntentSubmission: NoticeOfIntentSubmission, + ) { + const type = await this.typeRepository.findOneOrFail({ + where: { + code: createDto.typeCode, + }, + }); + + const newOwner = new NoticeOfIntentOwner({ + firstName: createDto.firstName, + lastName: createDto.lastName, + organizationName: createDto.organizationName, + email: createDto.email, + phoneNumber: createDto.phoneNumber, + corporateSummaryUuid: createDto.corporateSummaryUuid, + noticeOfIntentSubmission, + type, + }); + + return await this.repository.save(newOwner); + } + + async attachToParcel(uuid: string, parcelUuid: string) { + const existingOwner = await this.repository.findOneOrFail({ + where: { + uuid, + }, + relations: { + parcels: true, + }, + }); + + const parcel = await this.noticeOfIntentParcelService.getOneOrFail( + parcelUuid, + ); + existingOwner.parcels.push(parcel); + + await this.updateSubmissionApplicant( + existingOwner.noticeOfIntentSubmissionUuid, + ); + + await this.repository.save(existingOwner); + } + + async save(owner: NoticeOfIntentOwner) { + await this.repository.save(owner); + } + + async removeFromParcel(uuid: string, parcelUuid: string) { + const existingOwner = await this.repository.findOneOrFail({ + where: { + uuid, + }, + relations: { + parcels: true, + }, + }); + + existingOwner.parcels = existingOwner.parcels.filter( + (parcel) => parcel.uuid !== parcelUuid, + ); + + await this.updateSubmissionApplicant( + existingOwner.noticeOfIntentSubmissionUuid, + ); + + await this.repository.save(existingOwner); + } + + async update(uuid: string, updateDto: NoticeOfIntentOwnerUpdateDto) { + const existingOwner = await this.repository.findOneOrFail({ + where: { + uuid, + }, + relations: { + corporateSummary: { + document: true, + }, + }, + }); + + if (updateDto.typeCode) { + existingOwner.type = await this.typeRepository.findOneOrFail({ + where: { + code: updateDto.typeCode, + }, + }); + } + + //If attaching new document and old one was defined, delete it + if ( + existingOwner.corporateSummaryUuid !== updateDto.corporateSummaryUuid && + existingOwner.corporateSummary + ) { + const oldSummary = existingOwner.corporateSummary; + existingOwner.corporateSummary = null; + await this.repository.save(existingOwner); + await this.noticeOfIntentDocumentService.delete(oldSummary); + } + + existingOwner.corporateSummaryUuid = + updateDto.corporateSummaryUuid !== undefined + ? updateDto.corporateSummaryUuid + : existingOwner.corporateSummaryUuid; + + existingOwner.organizationName = + updateDto.organizationName !== undefined + ? updateDto.organizationName + : existingOwner.organizationName; + + existingOwner.firstName = + updateDto.firstName !== undefined + ? updateDto.firstName + : existingOwner.firstName; + + existingOwner.lastName = + updateDto.lastName !== undefined + ? updateDto.lastName + : existingOwner.lastName; + + existingOwner.phoneNumber = + updateDto.phoneNumber !== undefined + ? updateDto.phoneNumber + : existingOwner.phoneNumber; + + existingOwner.email = + updateDto.email !== undefined ? updateDto.email : existingOwner.email; + + await this.updateSubmissionApplicant( + existingOwner.noticeOfIntentSubmissionUuid, + ); + + return await this.repository.save(existingOwner); + } + + async delete(owner: NoticeOfIntentOwner) { + const res = await this.repository.remove(owner); + await this.updateSubmissionApplicant(owner.noticeOfIntentSubmissionUuid); + return res; + } + + async setPrimaryContact(submissionUuid: string, owner: NoticeOfIntentOwner) { + await this.noticeOfIntentSubmissionService.setPrimaryContact( + submissionUuid, + owner.uuid, + ); + } + + async getOwner(ownerUuid: string) { + return await this.repository.findOneOrFail({ + where: { + uuid: ownerUuid, + }, + relations: { + type: true, + corporateSummary: { + document: true, + }, + }, + }); + } + + async getMany(ownerUuids: string[]) { + return await this.repository.find({ + where: { + uuid: Any(ownerUuids), + }, + }); + } + + async deleteNonParcelOwners(submissionUuid: string) { + const agentOwners = await this.repository.find({ + where: [ + { + noticeOfIntentSubmission: { + uuid: submissionUuid, + }, + type: { + code: OWNER_TYPE.AGENT, + }, + }, + { + noticeOfIntentSubmission: { + uuid: submissionUuid, + }, + type: { + code: OWNER_TYPE.GOVERNMENT, + }, + }, + ], + }); + return await this.repository.remove(agentOwners); + } + + async updateSubmissionApplicant(submissionUuid: string) { + const parcels = + await this.noticeOfIntentParcelService.fetchByApplicationSubmissionUuid( + submissionUuid, + ); + + if (parcels.length > 0) { + const firstParcel = parcels.reduce((a, b) => + a.auditCreatedAt > b.auditCreatedAt ? a : b, + ); + + const ownerCount = parcels.reduce((count, parcel) => { + return count + parcel.owners.length; + }, 0); + + if (firstParcel) { + //Filter to only alphabetic + const alphabetOwners = firstParcel.owners.filter((owner) => + isNaN( + parseInt( + (owner.organizationName ?? owner.lastName ?? '').charAt(0), + ), + ), + ); + + //If no alphabetic use them all + if (alphabetOwners.length === 0) { + alphabetOwners.push(...firstParcel.owners); + } + + const firstOwner = alphabetOwners.sort((a, b) => { + const mappedA = a.organizationName ?? a.lastName ?? ''; + const mappedB = b.organizationName ?? b.lastName ?? ''; + return mappedA.localeCompare(mappedB); + })[0]; + if (firstOwner) { + let applicantName = firstOwner.organizationName + ? firstOwner.organizationName + : firstOwner.lastName; + if (ownerCount > 1) { + applicantName += ' et al.'; + } + + await this.noticeOfIntentSubmissionService.update(submissionUuid, { + applicant: applicantName || '', + }); + } + } + } + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts new file mode 100644 index 0000000000..e6d3229bd5 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts @@ -0,0 +1,5 @@ +import { Entity } from 'typeorm'; +import { BaseCodeEntity } from '../../../../common/entities/base.code.entity'; + +@Entity() +export class NoticeOfIntentParcelOwnershipType extends BaseCodeEntity {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts new file mode 100644 index 0000000000..b2cbd3aa93 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts @@ -0,0 +1,165 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentParcelProfile } from '../../../common/automapper/notice-of-intent-parcel.automapper.profile'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { NoticeOfIntentParcelController } from './notice-of-intent-parcel.controller'; +import { NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel.service'; + +describe('NoticeOfIntentParcelController', () => { + let controller: NoticeOfIntentParcelController; + let mockNOIParcelService: DeepMocked; + let mockNOIService: DeepMocked; + let mockNOIOwnerService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockNOIDocumentService: DeepMocked; + + beforeEach(async () => { + mockNOIParcelService = createMock(); + mockNOIService = createMock(); + mockNOIOwnerService = createMock(); + mockDocumentService = createMock(); + mockNOIDocumentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentParcelController], + providers: [ + NoticeOfIntentParcelProfile, + { + provide: NoticeOfIntentParcelService, + useValue: mockNOIParcelService, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNOIService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockNOIOwnerService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNOIDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NoticeOfIntentParcelController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call out to service when fetching parcels', async () => { + mockNOIParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue([]); + + const parcels = await controller.fetchByFileId('mockFileID'); + + expect(parcels).toBeDefined(); + expect( + mockNOIParcelService.fetchByApplicationSubmissionUuid, + ).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when creating parcels', async () => { + mockNOIService.getOrFailByUuid.mockResolvedValue( + {} as NoticeOfIntentSubmission, + ); + mockNOIParcelService.create.mockResolvedValue({} as NoticeOfIntentParcel); + mockNOIOwnerService.attachToParcel.mockResolvedValue(); + + const parcel = await controller.create({ + noticeOfIntentSubmissionUuid: 'fake', + }); + + expect(mockNOIService.getOrFailByUuid).toBeCalledTimes(1); + expect(mockNOIParcelService.create).toBeCalledTimes(1); + expect(mockNOIOwnerService.attachToParcel).toBeCalledTimes(0); + expect(parcel).toBeDefined(); + }); + + it('should call out to service and revert newly created "other" parcel if failed to link it to and owner during creation process', async () => { + const mockError = new Error('mock error'); + mockNOIService.getOrFailByUuid.mockResolvedValue( + {} as NoticeOfIntentSubmission, + ); + mockNOIParcelService.create.mockResolvedValue({} as NoticeOfIntentParcel); + mockNOIOwnerService.attachToParcel.mockRejectedValue(mockError); + mockNOIParcelService.deleteMany.mockResolvedValue([]); + + await expect( + controller.create({ + noticeOfIntentSubmissionUuid: 'fake', + ownerUuid: 'fake_uuid', + parcelType: 'other', + }), + ).rejects.toMatchObject(mockError); + + expect(mockNOIService.getOrFailByUuid).toBeCalledTimes(1); + expect(mockNOIParcelService.create).toBeCalledTimes(1); + expect(mockNOIParcelService.deleteMany).toBeCalledTimes(1); + expect(mockNOIOwnerService.attachToParcel).toBeCalledTimes(1); + }); + + it('should call out to service when updating parcel', async () => { + const mockUpdateDto: NoticeOfIntentParcelUpdateDto[] = [ + { + uuid: 'fake_uuid', + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legal', + mapAreaHectares: 2, + purchasedDate: 1, + isFarm: true, + isConfirmedByApplicant: true, + ownershipTypeCode: 'SMPL', + ownerUuids: null, + }, + ]; + + mockNOIParcelService.update.mockResolvedValue([ + {}, + ] as NoticeOfIntentParcel[]); + + const parcel = await controller.update(mockUpdateDto); + + expect(mockNOIParcelService.update).toBeCalledTimes(1); + expect(parcel).toBeDefined(); + }); + + it('should call out to service when deleting parcel', async () => { + const fakeUuid = 'fake_uuid'; + mockNOIParcelService.deleteMany.mockResolvedValue([]); + + const result = await controller.delete([fakeUuid]); + + expect(mockNOIParcelService.deleteMany).toBeCalledTimes(1); + expect(result).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts new file mode 100644 index 0000000000..d84a44e651 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts @@ -0,0 +1,158 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { VISIBILITY_FLAG } from '../../../alcs/application/application-document/application-document.entity'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +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 { PortalAuthGuard } from '../../../common/authorization/portal-auth-guard.service'; +import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; +import { + DOCUMENT_SOURCE, + DOCUMENT_SYSTEM, +} from '../../../document/document.dto'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission.service'; +import { + AttachCertificateOfTitleDto, + NoticeOfIntentParcelCreateDto, + NoticeOfIntentParcelDto, + NoticeOfIntentParcelUpdateDto, +} from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel.service'; + +@Controller('notice-of-intent-parcel') +@UseGuards(PortalAuthGuard) +export class NoticeOfIntentParcelController { + constructor( + private parcelService: NoticeOfIntentParcelService, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + @InjectMapper() private mapper: Mapper, + private ownerService: NoticeOfIntentOwnerService, + private documentService: DocumentService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + ) {} + + @Get('submission/:submissionUuid') + async fetchByFileId( + @Param('submissionUuid') submissionUuid: string, + ): Promise { + const parcels = await this.parcelService.fetchByApplicationSubmissionUuid( + submissionUuid, + ); + return this.mapper.mapArrayAsync( + parcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + + @Post() + async create( + @Body() createDto: NoticeOfIntentParcelCreateDto, + ): Promise { + const noticeOfIntentSubmission = + await this.noticeOfIntentSubmissionService.getOrFailByUuid( + createDto.noticeOfIntentSubmissionUuid, + ); + const parcel = await this.parcelService.create( + noticeOfIntentSubmission.uuid, + ); + + try { + if (createDto.ownerUuid) { + await this.ownerService.attachToParcel( + createDto.ownerUuid, + parcel.uuid, + ); + } + } catch (e) { + await this.delete([parcel.uuid]); + throw e; + } + + return this.mapper.mapAsync( + parcel, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + + @Put('/') + async update( + @Body() updateDtos: NoticeOfIntentParcelUpdateDto[], + ): Promise { + const updatedParcels = await this.parcelService.update(updateDtos); + + return this.mapper.mapArrayAsync( + updatedParcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + + @Delete() + async delete(@Body() uuids: string[]) { + const deletedParcels = await this.parcelService.deleteMany(uuids); + return this.mapper.mapArrayAsync( + deletedParcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + + @Post(':uuid/attachCertificateOfTitle') + async attachCorporateSummary( + @Req() req, + @Param('uuid') parcelUuid: string, + @Body() data: AttachCertificateOfTitleDto, + ) { + const parcel = await this.parcelService.getOneOrFail(parcelUuid); + const document = await this.documentService.createDocumentRecord({ + ...data, + uploadedBy: req.user.entity, + source: DOCUMENT_SOURCE.APPLICANT, + system: DOCUMENT_SYSTEM.PORTAL, + }); + + const noticeOfIntentSubmission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + parcel.noticeOfIntentSubmissionUuid, + req.user.entity, + ); + + const certificateOfTitle = + await this.noticeOfIntentDocumentService.attachExternalDocument( + noticeOfIntentSubmission!.fileNumber, + { + documentUuid: document.uuid, + type: DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + }, + [ + VISIBILITY_FLAG.APPLICANT, + VISIBILITY_FLAG.GOVERNMENT, + VISIBILITY_FLAG.COMMISSIONER, + ], + ); + + await this.parcelService.setCertificateOfTitle(parcel, certificateOfTitle); + + return this.mapper.map( + certificateOfTitle, + NoticeOfIntentDocument, + NoticeOfIntentDocumentDto, + ); + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts new file mode 100644 index 0000000000..2e6732dd14 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts @@ -0,0 +1,147 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsArray, + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { BaseCodeDto } from '../../../common/dtos/base.dto'; +import { NoticeOfIntentOwnerDto } from '../notice-of-intent-owner/notice-of-intent-owner.dto'; + +export class NoticeOfIntentParcelOwnershipTypeDto extends BaseCodeDto {} + +export class NoticeOfIntentParcelDto { + @AutoMap() + uuid: string; + + @AutoMap() + applicationSubmissionUuid: string; + + @AutoMap(() => String) + certificateOfTitleUuid: string | null; + + @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(() => Boolean) + isConfirmedByApplicant?: boolean; + + @AutoMap(() => String) + ownershipTypeCode?: string | null; + + @AutoMap(() => String) + crownLandOwnerType?: string | null; + + ownershipType?: NoticeOfIntentParcelOwnershipTypeDto; + + @AutoMap(() => String) + parcelType: string; + + @AutoMap(() => Number) + alrArea: number | null; + + certificateOfTitle?: NoticeOfIntentDocumentDto; + owners: NoticeOfIntentOwnerDto[]; +} + +export class NoticeOfIntentParcelCreateDto { + @IsNotEmpty() + @IsString() + noticeOfIntentSubmissionUuid: string; + + @IsOptional() + @IsString() + parcelType?: string; + + @IsOptional() + @IsString() + ownerUuid?: string; +} + +export class NoticeOfIntentParcelUpdateDto { + @IsString() + uuid: string; + + @IsString() + @IsOptional() + pid?: string | null; + + @IsString() + @IsOptional() + pin?: string | null; + + @IsString() + @IsOptional() + civicAddress?: string | null; + + @IsString() + @IsOptional() + legalDescription?: string | null; + + @IsNumber() + @IsOptional() + mapAreaHectares?: number | null; + + @IsNumber() + @IsOptional() + purchasedDate?: number | null; + + @IsBoolean() + @IsOptional() + isFarm?: boolean | null; + + @IsBoolean() + @IsOptional() + isConfirmedByApplicant?: boolean; + + @IsString() + @IsOptional() + ownershipTypeCode?: string | null; + + @IsString() + @IsOptional() + crownLandOwnerType?: string | null; + + @IsArray() + @IsOptional() + ownerUuids?: string[] | null; + + @IsNumber() + @IsOptional() + alrArea?: number | null; +} + +export class AttachCertificateOfTitleDto { + @IsString() + mimeType: string; + + @IsString() + fileName: string; + + @IsNumber() + fileSize: number; + + @IsString() + fileKey: string; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts new file mode 100644 index 0000000000..12b817142c --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts @@ -0,0 +1,147 @@ +import { AutoMap } from '@automapper/classes'; +import { + Column, + Entity, + JoinColumn, + JoinTable, + ManyToMany, + ManyToOne, +} from 'typeorm'; +import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { Base } from '../../../common/entities/base.entity'; +import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; +import { NoticeOfIntentOwner } from '../notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; +import { NoticeOfIntentParcelOwnershipType } from './notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity'; + +@Entity() +export class NoticeOfIntentParcel extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: + 'The Parcels pid entered by the user or populated from third-party data', + nullable: true, + }) + pid?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: + 'The Parcels pin entered by the user or populated from third-party data', + nullable: true, + }) + pin?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: + 'The Parcels legalDescription entered by the user or populated from third-party data', + nullable: true, + }) + legalDescription?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: 'The standard address for the parcel', + nullable: true, + }) + civicAddress?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'float', + comment: + 'The Parcels map are in hectares entered by the user or populated from third-party data', + nullable: true, + }) + mapAreaHectares?: number | null; + + @AutoMap(() => String) + @Column({ + type: 'boolean', + comment: 'The Parcels indication whether it is used as a farm', + nullable: true, + }) + isFarm?: boolean | null; + + @AutoMap() + @Column({ + type: 'timestamptz', + nullable: true, + comment: 'The Parcels purchase date provided by user', + }) + purchasedDate?: Date | null; + + @AutoMap(() => Boolean) + @Column({ + type: 'boolean', + comment: + 'The Parcels indication whether applicant signed off provided data including the Certificate of Title', + nullable: false, + default: false, + }) + isConfirmedByApplicant: boolean; + + @AutoMap() + @ManyToOne(() => NoticeOfIntentSubmission) + noticeOfIntentSubmission: NoticeOfIntentSubmission; + + @AutoMap() + @Column() + noticeOfIntentSubmissionUuid: string; + + @AutoMap(() => String) + @Column({ nullable: true }) + ownershipTypeCode?: string | null; + + @AutoMap() + @ManyToOne(() => NoticeOfIntentParcelOwnershipType) + ownershipType: NoticeOfIntentParcelOwnershipType; + + @AutoMap(() => Boolean) + @Column({ + type: 'text', + comment: + 'For Crown Land parcels to indicate whether they are provincially owned or federally owned', + nullable: true, + }) + crownLandOwnerType?: string | null; + + @ManyToMany(() => NoticeOfIntentOwner, (owner) => owner.parcels) + @JoinTable() + owners: NoticeOfIntentOwner[]; + + @AutoMap(() => NoticeOfIntentDocumentDto) + @JoinColumn() + @ManyToOne(() => NoticeOfIntentDocument, { + nullable: true, + onDelete: 'SET NULL', + }) + certificateOfTitle?: NoticeOfIntentDocument; + + @AutoMap(() => String) + @Column({ nullable: true }) + certificateOfTitleUuid: string | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + alrArea?: number | null; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts new file mode 100644 index 0000000000..099481434d --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts @@ -0,0 +1,248 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel.service'; + +describe('NoticeOfIntentParcelService', () => { + let service: NoticeOfIntentParcelService; + let mockParcelRepo: DeepMocked>; + let mockOwnerService: DeepMocked; + + const mockFileNumber = 'mock_applicationFileNumber'; + const mockUuid = 'mock_uuid'; + const mockNOIParcel = new NoticeOfIntentParcel({ + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isFarm: true, + purchasedDate: new Date(1, 1, 1), + isConfirmedByApplicant: true, + noticeOfIntentSubmissionUuid: mockFileNumber, + ownershipTypeCode: 'mock_ownershipTypeCode', + }); + const mockError = new Error('Parcel does not exist.'); + + beforeEach(async () => { + mockParcelRepo = createMock(); + mockOwnerService = createMock(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentParcelService, + { + provide: getRepositoryToken(NoticeOfIntentParcel), + useValue: mockParcelRepo, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockOwnerService, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentParcelService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should fetch parcels by fileNumber', async () => { + mockParcelRepo.find.mockResolvedValue([mockNOIParcel]); + + const result = await service.fetchByApplicationFileId(mockFileNumber); + + expect(result).toEqual([mockNOIParcel]); + expect(mockParcelRepo.find).toBeCalledTimes(1); + expect(mockParcelRepo.find).toBeCalledWith({ + where: { + noticeOfIntentSubmission: { + fileNumber: mockFileNumber, + isDraft: false, + }, + }, + order: { auditCreatedAt: 'ASC' }, + relations: { + certificateOfTitle: { document: true }, + owners: { + corporateSummary: { + document: true, + }, + type: true, + }, + ownershipType: true, + }, + }); + }); + + it('should get one parcel by id', async () => { + mockParcelRepo.findOneOrFail.mockResolvedValue(mockNOIParcel); + + const result = await service.getOneOrFail(mockUuid); + + expect(result).toEqual(mockNOIParcel); + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + }); + + it('should raise error on get parcel by uuid if the parcel does not exist', async () => { + mockParcelRepo.findOneOrFail.mockRejectedValue(mockError); + + await expect(service.getOneOrFail(mockUuid)).rejects.toMatchObject( + mockError, + ); + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + }); + + it('should successfully update parcel', async () => { + const updateParcelDto = [ + { + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isFarm: true, + purchasedDate: 1, + isConfirmedByApplicant: true, + ownershipTypeCode: 'mock_ownershipTypeCode', + }, + ] as NoticeOfIntentParcelUpdateDto[]; + + mockParcelRepo.findOneOrFail.mockResolvedValue(mockNOIParcel); + mockParcelRepo.save.mockResolvedValue({} as NoticeOfIntentParcel); + + await service.update(updateParcelDto); + + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + expect(mockParcelRepo.save).toBeCalledTimes(1); + }); + + it('should update the applicant if the parcel has owners', async () => { + const updateParcelDto = [ + { + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isFarm: true, + purchasedDate: 1, + isConfirmedByApplicant: true, + ownershipTypeCode: 'mock_ownershipTypeCode', + ownerUuids: ['cats'], + }, + ] as NoticeOfIntentParcelUpdateDto[]; + + mockParcelRepo.findOneOrFail.mockResolvedValue(mockNOIParcel); + mockParcelRepo.save.mockResolvedValue({} as NoticeOfIntentParcel); + mockOwnerService.updateSubmissionApplicant.mockResolvedValue(); + mockOwnerService.getMany.mockResolvedValue([]); + + await service.update(updateParcelDto); + + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + expect(mockParcelRepo.save).toBeCalledTimes(1); + expect(mockOwnerService.updateSubmissionApplicant).toHaveBeenCalledTimes(1); + }); + + it('it should fail to update a parcel if the parcel does not exist. ', async () => { + const updateParcelDto: NoticeOfIntentParcelUpdateDto[] = [ + { + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isFarm: true, + purchasedDate: 1, + isConfirmedByApplicant: true, + ownershipTypeCode: 'mock_ownershipTypeCode', + }, + ]; + const mockError = new Error('Parcel does not exist.'); + + mockParcelRepo.findOneOrFail.mockRejectedValue(mockError); + mockParcelRepo.save.mockResolvedValue(new NoticeOfIntentParcel()); + + await expect(service.update(updateParcelDto)).rejects.toMatchObject( + mockError, + ); + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + expect(mockParcelRepo.save).toBeCalledTimes(0); + }); + + it('should successfully delete a parcel and update applicant', async () => { + mockParcelRepo.find.mockResolvedValue([mockNOIParcel]); + mockParcelRepo.remove.mockResolvedValue(new NoticeOfIntentParcel()); + mockOwnerService.updateSubmissionApplicant.mockResolvedValue(); + + const result = await service.deleteMany([mockUuid]); + + expect(result).toBeDefined(); + expect(mockParcelRepo.find).toBeCalledTimes(1); + expect(mockParcelRepo.find).toBeCalledWith({ + where: { uuid: In([mockUuid]) }, + }); + expect(mockParcelRepo.remove).toBeCalledWith([mockNOIParcel]); + expect(mockParcelRepo.remove).toBeCalledTimes(1); + expect(mockOwnerService.updateSubmissionApplicant).toHaveBeenCalledTimes(1); + }); + + it('should not call remove if the parcel does not exist', async () => { + const exception = new ServiceValidationException( + `Unable to find parcels with provided uuids: ${mockUuid}.`, + ); + + mockParcelRepo.find.mockResolvedValue([]); + mockParcelRepo.remove.mockResolvedValue(new NoticeOfIntentParcel()); + + await expect(service.deleteMany([mockUuid])).rejects.toMatchObject( + exception, + ); + expect(mockParcelRepo.find).toBeCalledTimes(1); + expect(mockParcelRepo.find).toBeCalledWith({ + where: { uuid: In([mockUuid]) }, + }); + expect(mockParcelRepo.remove).toBeCalledTimes(0); + }); + + it('should successfully create a parcel', async () => { + mockParcelRepo.save.mockResolvedValue({ + uuid: mockUuid, + noticeOfIntentSubmissionUuid: mockFileNumber, + } as NoticeOfIntentParcel); + + const mockParcel = new NoticeOfIntentParcel({ + uuid: mockUuid, + noticeOfIntentSubmissionUuid: mockFileNumber, + }); + + const result = await service.create(mockFileNumber); + + expect(result).toEqual(mockParcel); + expect(mockParcelRepo.save).toBeCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts new file mode 100644 index 0000000000..ba69dc46bb --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts @@ -0,0 +1,140 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { filterUndefined } from '../../../utils/undefined'; +import { NoticeOfIntentOwnerService } from '../notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelUpdateDto } from './notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel.entity'; + +@Injectable() +export class NoticeOfIntentParcelService { + constructor( + @InjectRepository(NoticeOfIntentParcel) + private parcelRepository: Repository, + @Inject(forwardRef(() => NoticeOfIntentOwnerService)) + private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + ) {} + + async fetchByApplicationFileId(fileId: string) { + return this.parcelRepository.find({ + where: { + noticeOfIntentSubmission: { fileNumber: fileId, isDraft: false }, + }, + order: { auditCreatedAt: 'ASC' }, + relations: { + ownershipType: true, + certificateOfTitle: { document: true }, + owners: { + type: true, + corporateSummary: { + document: true, + }, + }, + }, + }); + } + + async fetchByApplicationSubmissionUuid(uuid: string) { + return this.parcelRepository.find({ + where: { noticeOfIntentSubmission: { uuid } }, + order: { auditCreatedAt: 'ASC' }, + relations: { + ownershipType: true, + certificateOfTitle: { document: true }, + owners: { + type: true, + corporateSummary: { + document: true, + }, + }, + }, + }); + } + + async create(noticeOfIntentSubmissionUuid: string) { + const parcel = new NoticeOfIntentParcel({ + noticeOfIntentSubmissionUuid, + }); + + return this.parcelRepository.save(parcel); + } + + async getOneOrFail(uuid: string) { + return this.parcelRepository.findOneOrFail({ + where: { uuid }, + }); + } + + async setCertificateOfTitle( + parcel: NoticeOfIntentParcel, + certificateOfTitle: NoticeOfIntentDocument, + ) { + parcel.certificateOfTitle = certificateOfTitle; + return await this.parcelRepository.save(parcel); + } + + async update(updateDtos: NoticeOfIntentParcelUpdateDto[]) { + const updatedParcels: NoticeOfIntentParcel[] = []; + + let hasOwnerUpdate = false; + for (const updateDto of updateDtos) { + const parcel = await this.getOneOrFail(updateDto.uuid); + + parcel.pid = updateDto.pid; + parcel.pin = updateDto.pin; + parcel.legalDescription = updateDto.legalDescription; + parcel.mapAreaHectares = updateDto.mapAreaHectares; + parcel.civicAddress = updateDto.civicAddress; + parcel.isFarm = updateDto.isFarm; + parcel.purchasedDate = formatIncomingDate(updateDto.purchasedDate); + parcel.ownershipTypeCode = updateDto.ownershipTypeCode; + parcel.isConfirmedByApplicant = filterUndefined( + updateDto.isConfirmedByApplicant, + parcel.isConfirmedByApplicant, + ); + parcel.crownLandOwnerType = updateDto.crownLandOwnerType; + parcel.alrArea = updateDto.alrArea; + + if (updateDto.ownerUuids) { + hasOwnerUpdate = true; + parcel.owners = await this.noticeOfIntentOwnerService.getMany( + updateDto.ownerUuids, + ); + } + + updatedParcels.push(parcel); + } + + const res = await this.parcelRepository.save(updatedParcels); + + if (hasOwnerUpdate) { + const firstParcel = updatedParcels[0]; + await this.noticeOfIntentOwnerService.updateSubmissionApplicant( + firstParcel.noticeOfIntentSubmissionUuid, + ); + } + return res; + } + + async deleteMany(uuids: string[]) { + const parcels = await this.parcelRepository.find({ + where: { uuid: In(uuids) }, + }); + + if (parcels.length === 0) { + throw new ServiceValidationException( + `Unable to find parcels with provided uuids: ${uuids}.`, + ); + } + + const result = await this.parcelRepository.remove(parcels); + await this.noticeOfIntentOwnerService.updateSubmissionApplicant( + parcels[0].noticeOfIntentSubmissionUuid, + ); + + return result; + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts index 790ccad385..498b9a0389 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts @@ -12,6 +12,7 @@ import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; import { Base } from '../../common/entities/base.entity'; import { User } from '../../user/user.entity'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; @Entity() export class NoticeOfIntentSubmission extends Base { @@ -187,6 +188,12 @@ export class NoticeOfIntentSubmission extends Base { }) noticeOfIntent: NoticeOfIntent; + @OneToMany( + () => NoticeOfIntentOwner, + (owner) => owner.noticeOfIntentSubmission, + ) + owners: NoticeOfIntentOwner[]; + @OneToMany( () => NoticeOfIntentSubmissionToSubmissionStatus, (status) => status.submission, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts index 2a7378251c..33a9ac799f 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts @@ -5,16 +5,32 @@ import { LocalGovernmentModule } from '../../alcs/local-government/local-governm import { NoticeOfIntentSubmissionStatusModule } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; import { AuthorizationModule } from '../../common/authorization/authorization.module'; +import { NoticeOfIntentOwnerProfile } from '../../common/automapper/notice-of-intent-owner.automapper.profile'; +import { NoticeOfIntentParcelProfile } from '../../common/automapper/notice-of-intent-parcel.automapper.profile'; import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; +import { NoticeOfIntentOwnerController } from './notice-of-intent-owner/notice-of-intent-owner.controller'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentOwnerService } from './notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelOwnershipType } from './notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity'; +import { NoticeOfIntentParcelController } from './notice-of-intent-parcel/notice-of-intent-parcel.controller'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel/notice-of-intent-parcel.service'; import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; @Module({ imports: [ - TypeOrmModule.forFeature([NoticeOfIntentSubmission]), + TypeOrmModule.forFeature([ + NoticeOfIntentSubmission, + NoticeOfIntentParcel, + NoticeOfIntentParcelOwnershipType, + OwnerType, + NoticeOfIntentOwner, + ]), NoticeOfIntentSubmissionStatusModule, forwardRef(() => NoticeOfIntentModule), AuthorizationModule, @@ -23,7 +39,19 @@ import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.s LocalGovernmentModule, FileNumberModule, ], - controllers: [NoticeOfIntentSubmissionController], - providers: [NoticeOfIntentSubmissionService, NoticeOfIntentSubmissionProfile], + controllers: [ + NoticeOfIntentSubmissionController, + NoticeOfIntentParcelController, + NoticeOfIntentOwnerController, + ], + providers: [ + NoticeOfIntentSubmissionService, + NoticeOfIntentParcelService, + NoticeOfIntentOwnerService, + NoticeOfIntentSubmissionProfile, + NoticeOfIntentOwnerProfile, + NoticeOfIntentParcelProfile, + ], + exports: [NoticeOfIntentSubmissionService], }) export class NoticeOfIntentSubmissionModule {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index abf85de386..db15158ca5 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -408,4 +408,8 @@ export class NoticeOfIntentSubmissionService { }, }); } + + async setPrimaryContact(submissionUuid: string, uuid: any) { + //TODO:? ?? + } } diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts index 68992e01a4..6d087bfb97 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts @@ -10,6 +10,7 @@ import { import * as config from 'config'; import * as dayjs from 'dayjs'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { OWNER_TYPE } from '../../common/owner-type/owner-type.entity'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocument, @@ -20,7 +21,6 @@ import { ApplicationService } from '../../alcs/application/application.service'; import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM } from '../../document/document.dto'; import { User } from '../../user/user.entity'; import { formatBooleanToYesNoString } from '../../utils/boolean-formatter'; -import { APPLICATION_OWNER } from '../application-submission/application-owner/application-owner.dto'; import { ApplicationOwnerService } from '../application-submission/application-owner/application-owner.service'; import { PARCEL_TYPE } from '../application-submission/application-parcel/application-parcel.dto'; import { ApplicationParcel } from '../application-submission/application-parcel/application-parcel.entity'; @@ -232,7 +232,7 @@ export class GenerateSubmissionDocumentService { applicant: submission.applicant, primaryContact, organizationText: - primaryContact?.type.code === APPLICATION_OWNER.CROWN + primaryContact?.type.code === OWNER_TYPE.CROWN ? 'Ministry/Department Responsible' : 'Organization (If Applicable)', diff --git a/services/apps/alcs/src/portal/portal.module.ts b/services/apps/alcs/src/portal/portal.module.ts index 4db394b272..8612f010e2 100644 --- a/services/apps/alcs/src/portal/portal.module.ts +++ b/services/apps/alcs/src/portal/portal.module.ts @@ -12,6 +12,7 @@ import { ApplicationSubmissionReviewModule } from './application-submission-revi import { ApplicationSubmissionModule } from './application-submission/application-submission.module'; import { CodeController } from './code/code.controller'; import { PortalDocumentModule } from './document/document.module'; +import { PortalNoticeOfIntentDocumentModule } from './notice-of-intent-document/notice-of-intent-document.module'; import { NoticeOfIntentSubmissionModule } from './notice-of-intent-submission/notice-of-intent-submission.module'; import { ParcelModule } from './parcel/parcel.module'; import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; @@ -32,6 +33,7 @@ import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; PortalApplicationDecisionModule, NoticeOfIntentModule, NoticeOfIntentSubmissionModule, + PortalNoticeOfIntentDocumentModule, RouterModule.register([ { path: 'portal', module: ApplicationSubmissionModule }, { path: 'portal', module: NoticeOfIntentSubmissionModule }, @@ -42,6 +44,7 @@ import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; { path: 'portal', module: ApplicationSubmissionDraftModule }, { path: 'portal', module: PdfGenerationModule }, { path: 'portal', module: PortalApplicationDecisionModule }, + { path: 'portal', module: PortalNoticeOfIntentDocumentModule }, ]), ], controllers: [CodeController], diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691692339215-add_noi_submission_parcels_and_owners.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691692339215-add_noi_submission_parcels_and_owners.ts new file mode 100644 index 0000000000..8547130c8c --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691692339215-add_noi_submission_parcels_and_owners.ts @@ -0,0 +1,111 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNoiSubmissionParcelsAndOwners1691692339215 + implements MigrationInterface +{ + name = 'addNoiSubmissionParcelsAndOwners1691692339215'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner" DROP CONSTRAINT "FK_05181ec6491ee0aa527bd55c714"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner_type" RENAME TO "owner_type"`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_parcel_ownership_type" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "label" character varying NOT NULL, "code" text NOT NULL, "description" text NOT NULL, CONSTRAINT "UQ_a7d503e16b2d7a04680dbfdcb59" UNIQUE ("description"), CONSTRAINT "PK_7b23cfdc8574f66dd2f88e37f3f" PRIMARY KEY ("code"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_parcel" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "pid" character varying, "pin" character varying, "legal_description" character varying, "civic_address" character varying, "map_area_hectares" double precision, "is_farm" boolean, "purchased_date" TIMESTAMP WITH TIME ZONE, "is_confirmed_by_applicant" boolean NOT NULL DEFAULT false, "notice_of_intent_submission_uuid" uuid NOT NULL, "ownership_type_code" text, "crown_land_owner_type" text, "certificate_of_title_uuid" uuid, "alr_area" numeric(12,2), CONSTRAINT "PK_6cfd592c4237000793b5a891aa7" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."pid" IS 'The Parcels pid entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."pin" IS 'The Parcels pin entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."legal_description" IS 'The Parcels legalDescription entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."civic_address" IS 'The standard address for the parcel'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."map_area_hectares" IS 'The Parcels map are in hectares entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."is_farm" IS 'The Parcels indication whether it is used as a farm'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."purchased_date" IS 'The Parcels purchase date provided by user'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."is_confirmed_by_applicant" IS 'The Parcels indication whether applicant signed off provided data including the Certificate of Title'; COMMENT ON COLUMN "alcs"."notice_of_intent_parcel"."crown_land_owner_type" IS 'For Crown Land parcels to indicate whether they are provincially owned or federally owned'`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_owner" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "first_name" character varying, "last_name" character varying, "organization_name" character varying, "phone_number" character varying, "email" character varying, "corporate_summary_uuid" uuid, "notice_of_intent_submission_uuid" uuid NOT NULL, "type_code" text NOT NULL, CONSTRAINT "PK_057d19217ea35bb20b4b7e7ed43" PRIMARY KEY ("uuid"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ("notice_of_intent_parcel_uuid" uuid NOT NULL, "notice_of_intent_owner_uuid" uuid NOT NULL, CONSTRAINT "PK_62cbd3e4f1802f240c40b1c37f8" PRIMARY KEY ("notice_of_intent_parcel_uuid", "notice_of_intent_owner_uuid"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_a419bb29b4bdb7e5d4aa4181d1" ON "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ("notice_of_intent_parcel_uuid") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_4346ccc22b3ca8dafe57b25128" ON "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ("notice_of_intent_owner_uuid") `, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner" ADD CONSTRAINT "FK_05181ec6491ee0aa527bd55c714" FOREIGN KEY ("type_code") REFERENCES "alcs"."owner_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" ADD CONSTRAINT "FK_8d14440209af146e7f351c7fbef" FOREIGN KEY ("notice_of_intent_submission_uuid") REFERENCES "alcs"."notice_of_intent_submission"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" ADD CONSTRAINT "FK_7b23cfdc8574f66dd2f88e37f3f" FOREIGN KEY ("ownership_type_code") REFERENCES "alcs"."notice_of_intent_parcel_ownership_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" ADD CONSTRAINT "FK_7b405f7700af3398ec38b70d634" FOREIGN KEY ("certificate_of_title_uuid") REFERENCES "alcs"."notice_of_intent_document"("uuid") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" ADD CONSTRAINT "FK_a27c5e1999ea94ce0dc0c923fb3" FOREIGN KEY ("corporate_summary_uuid") REFERENCES "alcs"."notice_of_intent_document"("uuid") ON DELETE SET NULL ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" ADD CONSTRAINT "FK_016562c9faf2b0d0ccc96212ee0" FOREIGN KEY ("type_code") REFERENCES "alcs"."owner_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" ADD CONSTRAINT "FK_df0c33df4e8ce83089e0645e928" FOREIGN KEY ("notice_of_intent_submission_uuid") REFERENCES "alcs"."notice_of_intent_submission"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ADD CONSTRAINT "FK_a419bb29b4bdb7e5d4aa4181d11" FOREIGN KEY ("notice_of_intent_parcel_uuid") REFERENCES "alcs"."notice_of_intent_parcel"("uuid") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" ADD CONSTRAINT "FK_4346ccc22b3ca8dafe57b251280" FOREIGN KEY ("notice_of_intent_owner_uuid") REFERENCES "alcs"."notice_of_intent_owner"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" DROP CONSTRAINT "FK_4346ccc22b3ca8dafe57b251280"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" DROP CONSTRAINT "FK_a419bb29b4bdb7e5d4aa4181d11"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" DROP CONSTRAINT "FK_df0c33df4e8ce83089e0645e928"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" DROP CONSTRAINT "FK_016562c9faf2b0d0ccc96212ee0"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_owner" DROP CONSTRAINT "FK_a27c5e1999ea94ce0dc0c923fb3"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" DROP CONSTRAINT "FK_7b405f7700af3398ec38b70d634"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" DROP CONSTRAINT "FK_7b23cfdc8574f66dd2f88e37f3f"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" DROP CONSTRAINT "FK_8d14440209af146e7f351c7fbef"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner" DROP CONSTRAINT "FK_05181ec6491ee0aa527bd55c714"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_4346ccc22b3ca8dafe57b25128"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_a419bb29b4bdb7e5d4aa4181d1"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner"`, + ); + await queryRunner.query(`DROP TABLE "alcs"."notice_of_intent_owner"`); + await queryRunner.query(`DROP TABLE "alcs"."notice_of_intent_parcel"`); + await queryRunner.query( + `DROP TABLE "alcs"."notice_of_intent_parcel_ownership_type"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."owner_type" RENAME TO "application_owner_type"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_owner" ADD CONSTRAINT "FK_05181ec6491ee0aa527bd55c714" FOREIGN KEY ("type_code") REFERENCES "alcs"."application_owner_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691705084117-seed_noi_parcel_owner_type.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691705084117-seed_noi_parcel_owner_type.ts new file mode 100644 index 0000000000..a400727ffd --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691705084117-seed_noi_parcel_owner_type.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class seedNoiParcelOwnerType1691705084117 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO "alcs"."notice_of_intent_parcel_ownership_type" + ("audit_deleted_date_at", "audit_created_at", "audit_updated_at", "audit_created_by", "audit_updated_by", "label", "code", "description") VALUES + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Crown', 'CRWN', 'Crown'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Fee Simple', 'SMPL', 'Fee Simple'); + `); + } + + public async down(queryRunner: QueryRunner): Promise {} +} From 4c8eda790135f7ffa3204aef27bedd6e76477b10 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 10:04:42 -0700 Subject: [PATCH 225/954] Code Review Feedback --- .../edit-submission/parcels/parcel-details.component.html | 8 ++++---- .../application-parcel/application-parcel.service.ts | 5 +++-- .../notice-of-intent-document.service.ts | 2 +- .../notice-of-intent-owner/notice-of-intent-owner.dto.ts | 8 ++++---- .../notice-of-intent-owner.service.ts | 8 ++++---- .../notice-of-intent-owner.service.spec.ts | 8 ++++---- 6 files changed, 20 insertions(+), 19 deletions(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html index 0a42627959..b2b1ca3ae7 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.html @@ -1,7 +1,7 @@
-

Identify Parcels Under Application

-

Provide parcel identification and registered ownership information for each parcel under application.

+

Identify Parcels Under Notice of Intent

+

Provide parcel identification and registered ownership information for each parcel.

*All fields are required unless stated optional.

Documents needed for this step:
@@ -23,7 +23,7 @@
Documents needed for this step:
Documents needed for this step:
- +
diff --git a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts index 1112a20315..061f34564a 100644 --- a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts +++ b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts @@ -7,7 +7,7 @@ import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spin import { ApplicationDocumentDto } from '../application-document/application-document.dto'; import { DocumentService } from '../document/document.service'; import { ToastService } from '../toast/toast.service'; -import { ApplicationParcelDto, ApplicationParcelUpdateDto } from './application-parcel.dto'; +import { ApplicationParcelDto, ApplicationParcelUpdateDto, PARCEL_TYPE } from './application-parcel.dto'; @Injectable({ providedIn: 'root', @@ -34,11 +34,12 @@ export class ApplicationParcelService { return undefined; } - async create(applicationSubmissionUuid: string, ownerUuid?: string) { + async create(applicationSubmissionUuid: string, parcelType?: PARCEL_TYPE, ownerUuid?: string) { try { return await firstValueFrom( this.httpClient.post(`${this.serviceUrl}`, { applicationSubmissionUuid, + parcelType, ownerUuid, }) ); diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts index 6b51562e5e..7ae8f404a9 100644 --- a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts @@ -39,7 +39,7 @@ export class NoticeOfIntentDocumentService { return res; } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to attach document to Application, please try again'); + this.toastService.showErrorToast('Failed to attach document, please try again'); } return undefined; } diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts index 21b23da93c..48cf7cee32 100644 --- a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.dto.ts @@ -1,6 +1,6 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; -import { ApplicationDocumentDto } from '../application-document/application-document.dto'; -import { ApplicationParcelDto } from '../application-parcel/application-parcel.dto'; +import { NoticeOfIntentDocumentDto } from '../notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentParcelDto } from '../notice-of-intent-parcel/notice-of-intent-parcel.dto'; export enum OWNER_TYPE { INDIVIDUAL = 'INDV', @@ -24,11 +24,11 @@ export interface NoticeOfIntentOwnerDto { phoneNumber: string | null; email: string | null; type: OwnerTypeDto; - corporateSummary?: ApplicationDocumentDto; + corporateSummary?: NoticeOfIntentDocumentDto; } export interface NoticeOfIntentOwnerDetailedDto extends NoticeOfIntentOwnerDto { - parcels: ApplicationParcelDto[]; + parcels: NoticeOfIntentParcelDto[]; } export interface NoticeOfIntentOwnerUpdateDto { diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts index 92abf5b9e6..66a48b3aab 100644 --- a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts @@ -67,11 +67,11 @@ export class NoticeOfIntentOwnerService { const res = await firstValueFrom( this.httpClient.post(`${this.serviceUrl}/setPrimaryContact`, updateDto) ); - this.toastService.showSuccessToast('Application saved'); + this.toastService.showSuccessToast('Notice of Intent saved'); return res; } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to update Application, please try again later'); + this.toastService.showErrorToast('Failed to update Notice of Intent, please try again later'); return undefined; } } @@ -126,10 +126,10 @@ export class NoticeOfIntentOwnerService { return 0; } - async uploadCorporateSummary(applicationFileId: string, file: File) { + async uploadCorporateSummary(noticeOfIntentFileId: string, file: File) { try { return await this.documentService.uploadFile<{ uuid: string }>( - applicationFileId, + noticeOfIntentFileId, file, DOCUMENT_TYPE.CORPORATE_SUMMARY, DOCUMENT_SOURCE.APPLICANT, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts index 9493d13e28..99eb1b68a1 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.service.spec.ts @@ -214,7 +214,7 @@ describe('NoticeOfIntentOwnerService', () => { expect(mockRepo.save).toHaveBeenCalledTimes(1); }); - it('should call update for the application with the first parcels last name', async () => { + it('should call update with the first parcels last name', async () => { mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); const owners = [ new NoticeOfIntentOwner({ @@ -236,7 +236,7 @@ describe('NoticeOfIntentOwnerService', () => { ); }); - it('should call update for the application with the first parcels last name', async () => { + it('should call update with the first parcels last name', async () => { mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); const owners = [ new NoticeOfIntentOwner({ @@ -270,7 +270,7 @@ describe('NoticeOfIntentOwnerService', () => { ); }); - it('should call update for the application with the number owners last name', async () => { + it('should call update with the number owners last name', async () => { mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); const owners = [ new NoticeOfIntentOwner({ @@ -296,7 +296,7 @@ describe('NoticeOfIntentOwnerService', () => { ); }); - it('should use the first created parcel to set the application applicants name', async () => { + it('should use the first created parcel to set the applicants name', async () => { mockRepo.find.mockResolvedValue([new NoticeOfIntentOwner()]); const owners1 = [ new NoticeOfIntentOwner({ From aeeba9a78248df29782d415cffefffd58b44b3bc Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 11:10:34 -0700 Subject: [PATCH 226/954] Finish Implementing Owners * Genericize owner dialogs to work for both NOI and App --- .../edit-submission-base.module.ts | 12 -- .../application-owners-dialog.component.ts | 41 ------- .../parcel-entry/parcel-entry.component.html | 4 + .../parcel-entry/parcel-entry.component.ts | 33 ++++-- .../parcel-owners.component.html | 73 ------------ .../parcel-owners/parcel-owners.component.ts | 91 --------------- .../edit-submission-base.module.ts | 3 - .../parcels/parcel-details.component.ts | 12 +- .../parcel-entry/parcel-entry.component.html | 26 +++-- .../parcel-entry/parcel-entry.component.ts | 23 ++-- .../parcel-owners.component.scss | 14 --- .../parcel-owners.component.spec.ts | 41 ------- .../parcel-owners/parcel-owners.component.ts | 91 --------------- .../application-owner.service.ts | 10 -- .../crown-owner-dialog.component.html} | 0 .../crown-owner-dialog.component.scss} | 4 +- .../crown-owner-dialog.component.spec.ts} | 12 +- .../crown-owner-dialog.component.ts} | 35 +++--- .../owner-dialog/owner-dialog.component.html} | 0 .../owner-dialog/owner-dialog.component.scss} | 4 +- .../owner-dialog.component.spec.ts} | 16 +-- .../owner-dialog/owner-dialog.component.ts} | 46 ++++---- .../all-owners-dialog.component.html} | 2 + .../all-owners-dialog.component.scss} | 0 .../all-owners-dialog.component.spec.ts} | 12 +- .../all-owners-dialog.component.ts | 47 ++++++++ .../parcel-owners.component.html | 2 +- .../parcel-owners.component.scss | 4 +- .../parcel-owners.component.spec.ts | 4 +- .../parcel-owners/parcel-owners.component.ts | 110 ++++++++++++++++++ .../src/app/shared/shared.module.ts | 21 ++++ ...of-intent-submission.automapper.profile.ts | 16 +++ .../notice-of-intent-submission.dto.ts | 5 +- .../notice-of-intent-submission.service.ts | 8 +- 34 files changed, 343 insertions(+), 479 deletions(-) delete mode 100644 portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts delete mode 100644 portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html delete mode 100644 portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts delete mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss delete mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts delete mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html => shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.html} (100%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss => shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.scss} (72%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts => shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.spec.ts} (67%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts => shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.ts} (64%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html => shared/owner-dialogs/owner-dialog/owner-dialog.component.html} (100%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss => shared/owner-dialogs/owner-dialog/owner-dialog.component.scss} (75%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts => shared/owner-dialogs/owner-dialog/owner-dialog.component.spec.ts} (69%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts => shared/owner-dialogs/owner-dialog/owner-dialog.component.ts} (76%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html => shared/owner-dialogs/owners-dialog/all-owners-dialog.component.html} (82%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss => shared/owner-dialogs/owners-dialog/all-owners-dialog.component.scss} (100%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts => shared/owner-dialogs/owners-dialog/all-owners-dialog.component.spec.ts} (66%) create mode 100644 portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.ts rename portal-frontend/src/app/{features/notice-of-intents/edit-submission/parcels => shared/owner-dialogs}/parcel-owners/parcel-owners.component.html (97%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details => shared/owner-dialogs}/parcel-owners/parcel-owners.component.scss (66%) rename portal-frontend/src/app/{features/applications/edit-submission/parcel-details => shared/owner-dialogs}/parcel-owners/parcel-owners.component.spec.ts (82%) create mode 100644 portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.ts diff --git a/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts index c70691f53a..ad2d84a82b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts @@ -15,14 +15,10 @@ import { LandUseComponent } from './land-use/land-use.component'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { OtherParcelConfirmationDialogComponent } from './other-parcels/other-parcel-confirmation-dialog/other-parcel-confirmation-dialog.component'; import { OtherParcelsComponent } from './other-parcels/other-parcels.component'; -import { ApplicationCrownOwnerDialogComponent } from './parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from './parcel-details/application-owner-dialog/application-owner-dialog.component'; -import { ApplicationOwnersDialogComponent } from './parcel-details/application-owners-dialog/application-owners-dialog.component'; import { DeleteParcelDialogComponent } from './parcel-details/delete-parcel/delete-parcel-dialog.component'; import { ParcelDetailsComponent } from './parcel-details/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcel-details/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { ParcelEntryComponent } from './parcel-details/parcel-entry/parcel-entry.component'; -import { ParcelOwnersComponent } from './parcel-details/parcel-owners/parcel-owners.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; import { ExclProposalComponent } from './proposal/excl-proposal/excl-proposal.component'; import { InclProposalComponent } from './proposal/incl-proposal/incl-proposal.component'; @@ -48,10 +44,6 @@ import { SelectGovernmentComponent } from './select-government/select-government EditSubmissionComponent, DeleteParcelDialogComponent, SelectGovernmentComponent, - ParcelOwnersComponent, - ApplicationOwnersDialogComponent, - ApplicationOwnerDialogComponent, - ApplicationCrownOwnerDialogComponent, LandUseComponent, OtherParcelsComponent, OtherAttachmentsComponent, @@ -92,10 +84,6 @@ import { SelectGovernmentComponent } from './select-government/select-government EditSubmissionComponent, DeleteParcelDialogComponent, SelectGovernmentComponent, - ParcelOwnersComponent, - ApplicationOwnersDialogComponent, - ApplicationOwnerDialogComponent, - ApplicationCrownOwnerDialogComponent, LandUseComponent, OtherParcelsComponent, OtherAttachmentsComponent, diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts deleted file mode 100644 index 8a884734aa..0000000000 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; - -@Component({ - selector: 'app-application-owner-dialog', - templateUrl: './application-owners-dialog.component.html', - styleUrls: ['./application-owners-dialog.component.scss'], -}) -export class ApplicationOwnersDialogComponent { - isDirty = false; - owners: ApplicationOwnerDto[] = []; - fileId: string; - submissionUuid: string; - - constructor( - private applicationOwnerService: ApplicationOwnerService, - @Inject(MAT_DIALOG_DATA) - public data: { - owners: ApplicationOwnerDto[]; - fileId: string; - submissionUuid: string; - } - ) { - this.fileId = data.fileId; - this.submissionUuid = data.submissionUuid; - this.owners = data.owners; - } - - async onUpdated() { - const updatedOwners = await this.applicationOwnerService.fetchBySubmissionId(this.submissionUuid); - if (updatedOwners) { - this.owners = updatedOwners.filter( - (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) - ); - this.isDirty = true; - } - } -} diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html index 0cdffa38c9..91d4429399 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html @@ -257,6 +257,8 @@
Owner Information
[owners]="parcel.owners" (onOwnersUpdated)="onOwnersUpdated.emit()" (onOwnerRemoved)="onOwnerRemoved($event)" + [documentService]="applicationDocumentService" + [ownerService]="applicationOwnerService" [disabled]="_disabled" >
@@ -339,6 +341,8 @@
Owner Information
[owners]="parcel.owners" (onOwnersUpdated)="onOwnersUpdated.emit()" (onOwnerRemoved)="onOwnerRemoved($event)" + [ownerService]="applicationOwnerService" + [documentService]="applicationDocumentService" [disabled]="_disabled" [isCrown]="true" > diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts index 072389f93b..1aef5b9bbe 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts @@ -18,9 +18,9 @@ import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; -import { ApplicationCrownOwnerDialogComponent } from '../application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from '../application-owner-dialog/application-owner-dialog.component'; -import { ApplicationOwnersDialogComponent } from '../application-owners-dialog/application-owners-dialog.component'; +import { CrownOwnerDialogComponent } from '../../../../../shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component'; +import { OwnerDialogComponent } from '../../../../../shared/owner-dialogs/owner-dialog/owner-dialog.component'; +import { AllOwnersDialogComponent } from '../../../../../shared/owner-dialogs/owners-dialog/all-owners-dialog.component'; import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; export interface ParcelEntryFormData { @@ -110,8 +110,8 @@ export class ParcelEntryComponent implements OnInit { constructor( private parcelService: ParcelService, private applicationParcelService: ApplicationParcelService, - private applicationOwnerService: ApplicationOwnerService, - private applicationDocumentService: ApplicationDocumentService, + public applicationOwnerService: ApplicationOwnerService, + public applicationDocumentService: ApplicationDocumentService, private dialog: MatDialog ) {} @@ -284,11 +284,13 @@ export class ParcelEntryComponent implements OnInit { } onAddNewOwner() { - const dialog = this.dialog.open(ApplicationOwnerDialogComponent, { + const dialog = this.dialog.open(OwnerDialogComponent, { data: { fileId: this.fileId, submissionUuid: this.submissionUuid, parcelUuid: this.parcel.uuid, + ownerService: this.applicationOwnerService, + documentService: this.applicationDocumentService, }, }); dialog.beforeClosed().subscribe((createdDto) => { @@ -301,11 +303,12 @@ export class ParcelEntryComponent implements OnInit { } onAddNewGovernmentContact() { - const dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { + const dialog = this.dialog.open(CrownOwnerDialogComponent, { data: { fileId: this.fileId, submissionUuid: this.submissionUuid, parcelUuid: this.parcel.uuid, + ownerService: this.applicationOwnerService, }, }); dialog.beforeClosed().subscribe((createdDto) => { @@ -345,7 +348,17 @@ export class ParcelEntryComponent implements OnInit { isSelected, }; }) - .sort(this.applicationOwnerService.sortOwners); + .sort(this.sortOwners); + } + + sortOwners(a: ApplicationOwnerDto, b: ApplicationOwnerDto) { + if (a.displayName < b.displayName) { + return -1; + } + if (a.displayName > b.displayName) { + return 1; + } + return 0; } onTypeOwner($event: Event) { @@ -357,11 +370,13 @@ export class ParcelEntryComponent implements OnInit { onSeeAllOwners() { this.dialog - .open(ApplicationOwnersDialogComponent, { + .open(AllOwnersDialogComponent, { data: { owners: this.owners, fileId: this.fileId, submissionUuid: this.submissionUuid, + ownerService: this.applicationOwnerService, + documentService: this.applicationDocumentService, }, }) .beforeClosed() diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html deleted file mode 100644 index 51ea96511d..0000000000 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.html +++ /dev/null @@ -1,73 +0,0 @@ -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Type{{ element.type.label }}{{ element.position }}Full Name{{ element.displayName }}Organization NameOrganization Name / Ministry / Department - - {{ element.organizationName }} - Ministry/ Department{{ element.organizationName }}Phone{{ element.phoneNumber | mask : '(000) 000-0000' }}Email{{ element.email }}Actions - - - -
No owner information
-
diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts deleted file mode 100644 index e0f04f417f..0000000000 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { ApplicationOwnerDto } from '../../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; -import { ApplicationCrownOwnerDialogComponent } from '../application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from '../application-owner-dialog/application-owner-dialog.component'; - -@Component({ - selector: 'app-parcel-owners[owners][fileId][submissionUuid]', - templateUrl: './parcel-owners.component.html', - styleUrls: ['./parcel-owners.component.scss'], -}) -export class ParcelOwnersComponent { - @Output() onOwnersUpdated = new EventEmitter(); - @Output() onOwnerRemoved = new EventEmitter(); - - @Input() - public set owners(owners: ApplicationOwnerDto[]) { - this._owners = owners.sort(this.appOwnerService.sortOwners); - } - - @Input() - public set disabled(disabled: boolean) { - this._disabled = disabled; - } - - @Input() submissionUuid!: string; - @Input() fileId!: string; - @Input() parcelUuid?: string | undefined; - @Input() isCrown = false; - @Input() isDraft = false; - @Input() isShowAllOwners = false; - - _owners: ApplicationOwnerDto[] = []; - _disabled = false; - displayedColumns = ['type', 'position', 'displayName', 'organizationName', 'phone', 'email', 'actions']; - - constructor( - private dialog: MatDialog, - private appOwnerService: ApplicationOwnerService, - private confDialogService: ConfirmationDialogService - ) {} - - onEdit(owner: ApplicationOwnerDto) { - let dialog; - if (owner.type.code === OWNER_TYPE.CROWN) { - dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { - data: { - isDraft: this.isDraft, - parcelUuid: this.parcelUuid, - existingOwner: owner, - submissionUuid: this.submissionUuid, - }, - }); - } else { - dialog = this.dialog.open(ApplicationOwnerDialogComponent, { - data: { - isDraft: this.isDraft, - fileId: this.fileId, - submissionUuid: this.submissionUuid, - parcelUuid: this.parcelUuid, - existingOwner: owner, - }, - }); - } - dialog.beforeClosed().subscribe((updatedUuid) => { - if (updatedUuid) { - this.onOwnersUpdated.emit(); - } - }); - } - - async onRemove(uuid: string) { - this.onOwnerRemoved.emit(uuid); - } - - async onDelete(owner: ApplicationOwnerDto) { - this.confDialogService - .openDialog({ - body: `This action will remove ${owner.displayName} and its usage from the entire application. Are you sure you want to remove this owner? `, - }) - .subscribe(async (didConfirm) => { - if (didConfirm) { - await this.appOwnerService.delete(owner.uuid); - this.onOwnersUpdated.emit(); - } - }); - } -} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts index 744f8fb3ee..57f9693ad8 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts @@ -13,7 +13,6 @@ import { DeleteParcelDialogComponent } from './parcels/delete-parcel/delete-parc import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; -import { ParcelOwnersComponent } from './parcels/parcel-owners/parcel-owners.component'; @NgModule({ declarations: [ @@ -22,7 +21,6 @@ import { ParcelOwnersComponent } from './parcels/parcel-owners/parcel-owners.com ParcelEntryComponent, ParcelEntryConfirmationDialogComponent, DeleteParcelDialogComponent, - ParcelOwnersComponent, ], imports: [ CommonModule, @@ -42,7 +40,6 @@ import { ParcelOwnersComponent } from './parcels/parcel-owners/parcel-owners.com ParcelEntryComponent, ParcelEntryConfirmationDialogComponent, DeleteParcelDialogComponent, - ParcelOwnersComponent, ], }) export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts index d0352c217a..596788e2a6 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-details.component.ts @@ -2,10 +2,12 @@ import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular import { MatDialog } from '@angular/material/dialog'; import { Router } from '@angular/router'; import { BehaviorSubject, takeUntil } from 'rxjs'; -import { ApplicationParcelUpdateDto } from '../../../../services/application-parcel/application-parcel.dto'; import { NoticeOfIntentOwnerDto } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; -import { NoticeOfIntentParcelDto } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { + NoticeOfIntentParcelDto, + NoticeOfIntentParcelUpdateDto, +} from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; import { ToastService } from '../../../../services/toast/toast.service'; import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; @@ -106,13 +108,13 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft parcel.crownLandOwnerType = formData.crownLandOwnerType !== undefined ? formData.crownLandOwnerType : parcel.crownLandOwnerType; if (formData.owners) { - //parcel.owners = formData.owners; + parcel.owners = formData.owners; } } private async saveProgress() { if (this.isDirty || this.newParcelAdded) { - const parcelsToUpdate: ApplicationParcelUpdateDto[] = []; + const parcelsToUpdate: NoticeOfIntentParcelUpdateDto[] = []; for (const parcel of this.parcels) { parcelsToUpdate.push({ uuid: parcel.uuid, @@ -126,7 +128,7 @@ export class ParcelDetailsComponent extends StepComponent implements OnInit, Aft ownershipTypeCode: parcel.ownershipTypeCode, isConfirmedByApplicant: parcel.isConfirmedByApplicant, crownLandOwnerType: parcel.crownLandOwnerType, - ownerUuids: [], + ownerUuids: parcel.owners.map((owner) => owner.uuid), }); } await this.noiParcelService.update(parcelsToUpdate); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html index ad624d34bd..9c3365cc68 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html @@ -2,8 +2,8 @@
- The answer to the following question will change the rest of the application form. Do not change this answer once - selected. + The answer to the following question will change the rest of the notice of intent form. Do not change this answer + once selected.
Owner Information [owners]="parcel.owners" (onOwnersUpdated)="onOwnersUpdated.emit()" (onOwnerRemoved)="onOwnerRemoved($event)" + [ownerService]="noticeOfIntentOwnerService" + [documentService]="noticeOfIntentDocumentService" [disabled]="_disabled" >
@@ -332,16 +334,16 @@
Owner Information
Add new government contact
- - - - - - - - - - + + + + + + + + + +
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts index bb0f985ece..8d7c068727 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts @@ -11,14 +11,13 @@ import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-in import { NoticeOfIntentParcelDto } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; import { ParcelService } from '../../../../../services/parcel/parcel.service'; -import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; +import { CrownOwnerDialogComponent } from '../../../../../shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component'; +import { OwnerDialogComponent } from '../../../../../shared/owner-dialogs/owner-dialog/owner-dialog.component'; +import { AllOwnersDialogComponent } from '../../../../../shared/owner-dialogs/owners-dialog/all-owners-dialog.component'; import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; import { RemoveFileConfirmationDialogComponent } from '../../../../applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; -import { ApplicationCrownOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component'; -import { ApplicationOwnersDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component'; import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; export interface ParcelEntryFormData { @@ -101,15 +100,14 @@ export class ParcelEntryComponent implements OnInit { ownerInput = new FormControl(null); - DOCUMENT_TYPES = DOCUMENT_TYPE; PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; maxPurchasedDate = new Date(); constructor( private parcelService: ParcelService, private noticeOfIntentParcelService: NoticeOfIntentParcelService, - private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, - private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + public noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + public noticeOfIntentDocumentService: NoticeOfIntentDocumentService, private dialog: MatDialog ) {} @@ -282,11 +280,13 @@ export class ParcelEntryComponent implements OnInit { } onAddNewOwner() { - const dialog = this.dialog.open(ApplicationOwnerDialogComponent, { + const dialog = this.dialog.open(OwnerDialogComponent, { data: { fileId: this.fileId, submissionUuid: this.submissionUuid, parcelUuid: this.parcel.uuid, + ownerService: this.noticeOfIntentOwnerService, + documentService: this.noticeOfIntentDocumentService, }, }); dialog.beforeClosed().subscribe((createdDto) => { @@ -299,11 +299,12 @@ export class ParcelEntryComponent implements OnInit { } onAddNewGovernmentContact() { - const dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { + const dialog = this.dialog.open(CrownOwnerDialogComponent, { data: { fileId: this.fileId, submissionUuid: this.submissionUuid, parcelUuid: this.parcel.uuid, + ownerService: this.noticeOfIntentOwnerService, }, }); dialog.beforeClosed().subscribe((createdDto) => { @@ -355,11 +356,13 @@ export class ParcelEntryComponent implements OnInit { onSeeAllOwners() { this.dialog - .open(ApplicationOwnersDialogComponent, { + .open(AllOwnersDialogComponent, { data: { owners: this.owners, fileId: this.fileId, submissionUuid: this.submissionUuid, + ownerService: this.noticeOfIntentOwnerService, + documentService: this.noticeOfIntentDocumentService, }, }) .beforeClosed() diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss deleted file mode 100644 index fb105e09eb..0000000000 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -@use '../../../../../../styles/functions' as *; -@use '../../../../../../styles/colors'; - -.actions { - button:not(:last-child) { - margin-right: rem(8) !important; - } -} - -.no-data-text { - text-align: center; - color: colors.$grey; - padding-top: rem(12); -} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts deleted file mode 100644 index b36089cde8..0000000000 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatDialog } from '@angular/material/dialog'; -import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; -import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; - -import { ParcelOwnersComponent } from './parcel-owners.component'; - -describe('ParcelOwnersComponent', () => { - let component: ParcelOwnersComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - providers: [ - { - provide: MatDialog, - useValue: {}, - }, - { - provide: NoticeOfIntentOwnerService, - useValue: {}, - }, - { - provide: ConfirmationDialogService, - useValue: {}, - }, - ], - declarations: [ParcelOwnersComponent], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(ParcelOwnersComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts deleted file mode 100644 index ebfcd50c62..0000000000 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { NoticeOfIntentOwnerDto } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; -import { NoticeOfIntentOwnerService } from '../../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; -import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; -import { ApplicationCrownOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component'; -import { ApplicationOwnerDialogComponent } from '../../../../applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component'; - -@Component({ - selector: 'app-parcel-owners[owners][fileId][submissionUuid]', - templateUrl: './parcel-owners.component.html', - styleUrls: ['./parcel-owners.component.scss'], -}) -export class ParcelOwnersComponent { - @Output() onOwnersUpdated = new EventEmitter(); - @Output() onOwnerRemoved = new EventEmitter(); - - @Input() - public set owners(owners: NoticeOfIntentOwnerDto[]) { - this._owners = owners.sort(this.noticeOfIntentOwnerService.sortOwners); - } - - @Input() - public set disabled(disabled: boolean) { - this._disabled = disabled; - } - - @Input() submissionUuid!: string; - @Input() fileId!: string; - @Input() parcelUuid?: string | undefined; - @Input() isCrown = false; - @Input() isDraft = false; - @Input() isShowAllOwners = false; - - _owners: NoticeOfIntentOwnerDto[] = []; - _disabled = false; - displayedColumns = ['type', 'position', 'displayName', 'organizationName', 'phone', 'email', 'actions']; - - constructor( - private dialog: MatDialog, - private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, - private confDialogService: ConfirmationDialogService - ) {} - - onEdit(owner: NoticeOfIntentOwnerDto) { - let dialog; - if (owner.type.code === OWNER_TYPE.CROWN) { - dialog = this.dialog.open(ApplicationCrownOwnerDialogComponent, { - data: { - isDraft: this.isDraft, - parcelUuid: this.parcelUuid, - existingOwner: owner, - submissionUuid: this.submissionUuid, - }, - }); - } else { - dialog = this.dialog.open(ApplicationOwnerDialogComponent, { - data: { - isDraft: this.isDraft, - fileId: this.fileId, - submissionUuid: this.submissionUuid, - parcelUuid: this.parcelUuid, - existingOwner: owner, - }, - }); - } - dialog.beforeClosed().subscribe((updatedUuid) => { - if (updatedUuid) { - this.onOwnersUpdated.emit(); - } - }); - } - - async onRemove(uuid: string) { - this.onOwnerRemoved.emit(uuid); - } - - async onDelete(owner: NoticeOfIntentOwnerDto) { - this.confDialogService - .openDialog({ - body: `This action will remove ${owner.displayName} and its usage from the entire application. Are you sure you want to remove this owner? `, - }) - .subscribe(async (didConfirm) => { - if (didConfirm) { - await this.noticeOfIntentOwnerService.delete(owner.uuid); - this.onOwnersUpdated.emit(); - } - }); - } -} diff --git a/portal-frontend/src/app/services/application-owner/application-owner.service.ts b/portal-frontend/src/app/services/application-owner/application-owner.service.ts index 290e3a83db..ba3684a4eb 100644 --- a/portal-frontend/src/app/services/application-owner/application-owner.service.ts +++ b/portal-frontend/src/app/services/application-owner/application-owner.service.ts @@ -116,16 +116,6 @@ export class ApplicationOwnerService { return undefined; } - sortOwners(a: ApplicationOwnerDto, b: ApplicationOwnerDto) { - if (a.displayName < b.displayName) { - return -1; - } - if (a.displayName > b.displayName) { - return 1; - } - return 0; - } - async uploadCorporateSummary(applicationFileId: string, file: File) { try { return await this.documentService.uploadFile<{ uuid: string }>( diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.html rename to portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.html diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.scss similarity index 72% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss rename to portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.scss index a4fd5b9b0f..f9fa4857b8 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.scss +++ b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../../styles/functions' as *; -@use '../../../../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; .actions { button:not(:last-child) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.spec.ts similarity index 67% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts rename to portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.spec.ts index 97ab2104aa..c51f7e8bb3 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.spec.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.spec.ts @@ -2,13 +2,13 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; -import { ApplicationCrownOwnerDialogComponent } from './application-crown-owner-dialog.component'; +import { CrownOwnerDialogComponent } from './crown-owner-dialog.component'; describe('ApplicationCrownOwnerDialogComponent', () => { - let component: ApplicationCrownOwnerDialogComponent; - let fixture: ComponentFixture; + let component: CrownOwnerDialogComponent; + let fixture: ComponentFixture; let mockAppOwnerService: DeepMocked; beforeEach(async () => { @@ -29,11 +29,11 @@ describe('ApplicationCrownOwnerDialogComponent', () => { useValue: {}, }, ], - declarations: [ApplicationCrownOwnerDialogComponent], + declarations: [CrownOwnerDialogComponent], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(ApplicationCrownOwnerDialogComponent); + fixture = TestBed.createComponent(CrownOwnerDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.ts similarity index 64% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts rename to portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.ts index 8aba0af222..4d4e80f488 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-crown-owner-dialog/application-crown-owner-dialog.component.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/crown-owner-dialog/crown-owner-dialog.component.ts @@ -5,16 +5,22 @@ import { ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, -} from '../../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; +} from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { + NoticeOfIntentOwnerCreateDto, + NoticeOfIntentOwnerDto, + NoticeOfIntentOwnerUpdateDto, +} from '../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { OWNER_TYPE } from '../../dto/owner.dto'; @Component({ - selector: 'app-application-crown-owner-dialog', - templateUrl: './application-crown-owner-dialog.component.html', - styleUrls: ['./application-crown-owner-dialog.component.scss'], + selector: 'app-crown-owner-dialog', + templateUrl: './crown-owner-dialog.component.html', + styleUrls: ['./crown-owner-dialog.component.scss'], }) -export class ApplicationCrownOwnerDialogComponent { +export class CrownOwnerDialogComponent { ministryName = new FormControl('', [Validators.required]); firstName = new FormControl('', [Validators.required]); lastName = new FormControl('', [Validators.required]); @@ -33,14 +39,14 @@ export class ApplicationCrownOwnerDialogComponent { }); constructor( - private dialogRef: MatDialogRef, - private appOwnerService: ApplicationOwnerService, + private dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: { submissionUuid: string; fileId: string; parcelUuid?: string; - existingOwner?: ApplicationOwnerDto; + ownerService: ApplicationOwnerService | NoticeOfIntentOwnerService; + existingOwner?: ApplicationOwnerDto | NoticeOfIntentOwnerDto; } ) { if (data && data.existingOwner) { @@ -55,7 +61,7 @@ export class ApplicationCrownOwnerDialogComponent { } async onCreate() { - const createDto: ApplicationOwnerCreateDto = { + const createDto: ApplicationOwnerCreateDto & NoticeOfIntentOwnerCreateDto = { organizationName: this.ministryName.getRawValue() || undefined, firstName: this.firstName.getRawValue() || undefined, lastName: this.lastName.getRawValue() || undefined, @@ -63,9 +69,10 @@ export class ApplicationCrownOwnerDialogComponent { phoneNumber: this.phoneNumber.getRawValue()!, typeCode: OWNER_TYPE.CROWN, applicationSubmissionUuid: this.data.submissionUuid, + noticeOfIntentSubmissionUuid: this.data.submissionUuid, }; - const res = await this.appOwnerService.create(createDto); + const res = await this.data.ownerService.create(createDto); this.dialogRef.close(res); } @@ -74,7 +81,7 @@ export class ApplicationCrownOwnerDialogComponent { } async onSave() { - const updateDto: ApplicationOwnerUpdateDto = { + const updateDto: ApplicationOwnerUpdateDto & NoticeOfIntentOwnerUpdateDto = { organizationName: this.ministryName.getRawValue(), firstName: this.firstName.getRawValue(), lastName: this.lastName.getRawValue(), @@ -83,7 +90,7 @@ export class ApplicationCrownOwnerDialogComponent { typeCode: OWNER_TYPE.CROWN, }; if (this.existingUuid) { - const res = await this.appOwnerService.update(this.existingUuid, updateDto); + const res = await this.data.ownerService.update(this.existingUuid, updateDto); this.dialogRef.close(res); } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.html similarity index 100% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.html rename to portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.html diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.scss similarity index 75% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss rename to portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.scss index 2af88008a7..a31d29d504 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.scss +++ b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../../styles/functions' as *; -@use '../../../../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; .actions { button:not(:last-child) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.spec.ts similarity index 69% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts rename to portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.spec.ts index 8e9cd1894e..0c3aad1e35 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.spec.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.spec.ts @@ -2,15 +2,15 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { CodeService } from '../../../../../services/code/code.service'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { CodeService } from '../../../services/code/code.service'; -import { ApplicationOwnerDialogComponent } from './application-owner-dialog.component'; +import { OwnerDialogComponent } from './owner-dialog.component'; describe('ApplicationOwnerDialogComponent', () => { - let component: ApplicationOwnerDialogComponent; - let fixture: ComponentFixture; + let component: OwnerDialogComponent; + let fixture: ComponentFixture; let mockAppOwnerService: DeepMocked; let mockCodeService: DeepMocked; let mockAppDocService: DeepMocked; @@ -47,11 +47,11 @@ describe('ApplicationOwnerDialogComponent', () => { useValue: {}, }, ], - declarations: [ApplicationOwnerDialogComponent], + declarations: [OwnerDialogComponent], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(ApplicationOwnerDialogComponent); + fixture = TestBed.createComponent(OwnerDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts similarity index 76% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts rename to portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts index 0826020249..cfd2303c8e 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owner-dialog/application-owner-dialog.component.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts @@ -2,26 +2,29 @@ import { Component, Inject } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { ApplicationDocumentDto } from '../../../../../services/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; import { ApplicationOwnerCreateDto, ApplicationOwnerDto, ApplicationOwnerUpdateDto, -} from '../../../../../services/application-owner/application-owner.dto'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { CodeService } from '../../../../../services/code/code.service'; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../../shared/dto/document.dto'; -import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; -import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; -import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; +} from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { CodeService } from '../../../services/code/code.service'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerCreateDto } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../dto/document.dto'; +import { OWNER_TYPE } from '../../dto/owner.dto'; +import { FileHandle } from '../../file-drag-drop/drag-drop.directive'; +import { RemoveFileConfirmationDialogComponent } from '../../../features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; @Component({ - selector: 'app-application-owner-dialog', - templateUrl: './application-owner-dialog.component.html', - styleUrls: ['./application-owner-dialog.component.scss'], + selector: 'app-owner-dialog', + templateUrl: './owner-dialog.component.html', + styleUrls: ['./owner-dialog.component.scss'], }) -export class ApplicationOwnerDialogComponent { +export class OwnerDialogComponent { OWNER_TYPE = OWNER_TYPE; type = new FormControl(OWNER_TYPE.INDIVIDUAL); firstName = new FormControl('', [Validators.required]); @@ -48,10 +51,8 @@ export class ApplicationOwnerDialogComponent { private documentCodes: DocumentTypeDto[] = []; constructor( - private dialogRef: MatDialogRef, - private appOwnerService: ApplicationOwnerService, + private dialogRef: MatDialogRef, private codeService: CodeService, - private documentService: ApplicationDocumentService, private dialog: MatDialog, @Inject(MAT_DIALOG_DATA) public data: { @@ -60,6 +61,8 @@ export class ApplicationOwnerDialogComponent { isDraft: boolean; parcelUuid?: string; existingOwner?: ApplicationOwnerDto; + documentService: ApplicationDocumentService | NoticeOfIntentDocumentService; + ownerService: ApplicationOwnerService | NoticeOfIntentOwnerService; } ) { if (data && data.existingOwner) { @@ -101,7 +104,7 @@ export class ApplicationOwnerDialogComponent { } const documentUuid = await this.uploadPendingFile(this.pendingFile); - const createDto: ApplicationOwnerCreateDto = { + const createDto: ApplicationOwnerCreateDto & NoticeOfIntentOwnerCreateDto = { organizationName: this.organizationName.getRawValue() || undefined, firstName: this.firstName.getRawValue() || undefined, lastName: this.lastName.getRawValue() || undefined, @@ -110,9 +113,10 @@ export class ApplicationOwnerDialogComponent { phoneNumber: this.phoneNumber.getRawValue()!, typeCode: this.type.getRawValue()!, applicationSubmissionUuid: this.data.submissionUuid, + noticeOfIntentSubmissionUuid: this.data.submissionUuid, }; - const res = await this.appOwnerService.create(createDto); + const res = await this.data.ownerService.create(createDto); this.dialogRef.close(res); } @@ -132,7 +136,7 @@ export class ApplicationOwnerDialogComponent { typeCode: this.type.getRawValue()!, }; if (this.existingUuid) { - const res = await this.appOwnerService.update(this.existingUuid, updateDto); + const res = await this.data.ownerService.update(this.existingUuid, updateDto); this.dialogRef.close(res); } } @@ -186,7 +190,7 @@ export class ApplicationOwnerDialogComponent { const fileURL = URL.createObjectURL(this.pendingFile); window.open(fileURL, '_blank'); } else if (this.existingUuid && this.data.existingOwner?.corporateSummary?.uuid) { - const res = await this.documentService.openFile(this.data.existingOwner?.corporateSummary?.uuid); + const res = await this.data.documentService.openFile(this.data.existingOwner?.corporateSummary?.uuid); if (res) { window.open(res.url, '_blank'); } @@ -196,7 +200,7 @@ export class ApplicationOwnerDialogComponent { private async uploadPendingFile(file?: File) { let documentUuid; if (file) { - documentUuid = await this.appOwnerService.uploadCorporateSummary(this.data.fileId, file); + documentUuid = await this.data.ownerService.uploadCorporateSummary(this.data.fileId, file); if (!documentUuid) { return; } diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.html similarity index 82% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html rename to portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.html index 209aac8011..261a10027b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.html +++ b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.html @@ -9,6 +9,8 @@

All Owners

[owners]="owners" [fileId]="fileId" [isShowAllOwners]="true" + [documentService]="data.documentService" + [ownerService]="data.ownerService" > diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.scss similarity index 100% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.scss rename to portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.scss diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.spec.ts similarity index 66% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts rename to portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.spec.ts index 002133241d..3dc13aead7 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/application-owners-dialog/application-owners-dialog.component.spec.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.spec.ts @@ -2,12 +2,12 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { ApplicationOwnersDialogComponent } from './application-owners-dialog.component'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { AllOwnersDialogComponent } from './all-owners-dialog.component'; describe('ApplicationOwnersDialogComponent', () => { - let component: ApplicationOwnersDialogComponent; - let fixture: ComponentFixture; + let component: AllOwnersDialogComponent; + let fixture: ComponentFixture; let mockAppOwnerService: DeepMocked; beforeEach(async () => { @@ -24,11 +24,11 @@ describe('ApplicationOwnersDialogComponent', () => { useValue: {}, }, ], - declarations: [ApplicationOwnersDialogComponent], + declarations: [AllOwnersDialogComponent], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); - fixture = TestBed.createComponent(ApplicationOwnersDialogComponent); + fixture = TestBed.createComponent(AllOwnersDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.ts b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.ts new file mode 100644 index 0000000000..445d22530c --- /dev/null +++ b/portal-frontend/src/app/shared/owner-dialogs/owners-dialog/all-owners-dialog.component.ts @@ -0,0 +1,47 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationOwnerDto } from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { OWNER_TYPE } from '../../dto/owner.dto'; + +@Component({ + selector: 'app-all-owners-dialog', + templateUrl: './all-owners-dialog.component.html', + styleUrls: ['./all-owners-dialog.component.scss'], +}) +export class AllOwnersDialogComponent { + isDirty = false; + owners: ApplicationOwnerDto[] | NoticeOfIntentOwnerDto[] = []; + fileId: string; + submissionUuid: string; + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: { + owners: ApplicationOwnerDto[] | NoticeOfIntentOwnerDto[]; + fileId: string; + submissionUuid: string; + ownerService: ApplicationOwnerService | NoticeOfIntentOwnerService; + documentService: ApplicationDocumentService | NoticeOfIntentDocumentService; + } + ) { + this.fileId = data.fileId; + this.submissionUuid = data.submissionUuid; + this.owners = data.owners; + } + + async onUpdated() { + const updatedOwners = await this.data.ownerService.fetchBySubmissionId(this.submissionUuid); + if (updatedOwners) { + // @ts-ignore Bug with Typescript https://github.com/microsoft/TypeScript/issues/44373 + this.owners = updatedOwners.filter( + (owner: { type: { code: OWNER_TYPE } }) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) + ); + this.isDirty = true; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.html similarity index 97% rename from portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html rename to portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.html index 51ea96511d..0041cf444a 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-owners/parcel-owners.component.html +++ b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.html @@ -1,5 +1,5 @@
- +
diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.scss similarity index 66% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss rename to portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.scss index fb105e09eb..bea80e08ec 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.scss +++ b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.scss @@ -1,5 +1,5 @@ -@use '../../../../../../styles/functions' as *; -@use '../../../../../../styles/colors'; +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; .actions { button:not(:last-child) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.spec.ts similarity index 82% rename from portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts rename to portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.spec.ts index 36430e28e6..1707807c8b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-owners/parcel-owners.component.spec.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.spec.ts @@ -1,8 +1,8 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; -import { ApplicationOwnerService } from '../../../../../services/application-owner/application-owner.service'; -import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { ConfirmationDialogService } from '../../confirmation-dialog/confirmation-dialog.service'; import { ParcelOwnersComponent } from './parcel-owners.component'; diff --git a/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.ts b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.ts new file mode 100644 index 0000000000..2d99ada35f --- /dev/null +++ b/portal-frontend/src/app/shared/owner-dialogs/parcel-owners/parcel-owners.component.ts @@ -0,0 +1,110 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; +import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; +import { ApplicationOwnerDto } from '../../../services/application-owner/application-owner.dto'; +import { ApplicationOwnerService } from '../../../services/application-owner/application-owner.service'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { ConfirmationDialogService } from '../../confirmation-dialog/confirmation-dialog.service'; +import { OWNER_TYPE } from '../../dto/owner.dto'; +import { CrownOwnerDialogComponent } from '../crown-owner-dialog/crown-owner-dialog.component'; +import { OwnerDialogComponent } from '../owner-dialog/owner-dialog.component'; + +@Component({ + selector: 'app-parcel-owners[owners][fileId][submissionUuid][ownerService]', + templateUrl: './parcel-owners.component.html', + styleUrls: ['./parcel-owners.component.scss'], +}) +export class ParcelOwnersComponent { + @Output() onOwnersUpdated = new EventEmitter(); + @Output() onOwnerRemoved = new EventEmitter(); + + @Input() ownerService!: ApplicationOwnerService | NoticeOfIntentOwnerService; + @Input() documentService!: ApplicationDocumentService | NoticeOfIntentDocumentService; + dataSource = new MatTableDataSource([]); + + @Input() + public set owners(owners: ApplicationOwnerDto[] | NoticeOfIntentOwnerDto[]) { + const sorted = owners.sort(this.sortOwners); + // @ts-ignore + this.dataSource = new MatTableDataSource(sorted); + } + + sortOwners(a: ApplicationOwnerDto | NoticeOfIntentOwnerDto, b: ApplicationOwnerDto | NoticeOfIntentOwnerDto) { + if (a.displayName < b.displayName) { + return -1; + } + if (a.displayName > b.displayName) { + return 1; + } + return 0; + } + + @Input() + public set disabled(disabled: boolean) { + this._disabled = disabled; + } + + @Input() submissionUuid!: string; + @Input() fileId!: string; + @Input() parcelUuid?: string | undefined; + @Input() isCrown = false; + @Input() isDraft = false; + @Input() isShowAllOwners = false; + + _disabled = false; + displayedColumns = ['type', 'position', 'displayName', 'organizationName', 'phone', 'email', 'actions']; + + constructor(private dialog: MatDialog, private confDialogService: ConfirmationDialogService) {} + + onEdit(owner: ApplicationOwnerDto) { + let dialog; + if (owner.type.code === OWNER_TYPE.CROWN) { + dialog = this.dialog.open(CrownOwnerDialogComponent, { + data: { + isDraft: this.isDraft, + parcelUuid: this.parcelUuid, + existingOwner: owner, + submissionUuid: this.submissionUuid, + ownerService: this.ownerService, + }, + }); + } else { + dialog = this.dialog.open(OwnerDialogComponent, { + data: { + isDraft: this.isDraft, + fileId: this.fileId, + submissionUuid: this.submissionUuid, + parcelUuid: this.parcelUuid, + existingOwner: owner, + ownerService: this.ownerService, + documentService: this.documentService, + }, + }); + } + dialog.beforeClosed().subscribe((updatedUuid) => { + if (updatedUuid) { + this.onOwnersUpdated.emit(); + } + }); + } + + async onRemove(uuid: string) { + this.onOwnerRemoved.emit(uuid); + } + + async onDelete(owner: ApplicationOwnerDto | NoticeOfIntentOwnerDto) { + this.confDialogService + .openDialog({ + body: `This action will remove ${owner.displayName} and its usage from the entire application. Are you sure you want to remove this owner? `, + }) + .subscribe(async (didConfirm) => { + if (didConfirm) { + await this.ownerService.delete(owner.uuid); + this.onOwnersUpdated.emit(); + } + }); + } +} diff --git a/portal-frontend/src/app/shared/shared.module.ts b/portal-frontend/src/app/shared/shared.module.ts index 3d55310071..75b24121ec 100644 --- a/portal-frontend/src/app/shared/shared.module.ts +++ b/portal-frontend/src/app/shared/shared.module.ts @@ -31,6 +31,10 @@ import { DragDropDirective } from './file-drag-drop/drag-drop.directive'; import { FileDragDropComponent } from './file-drag-drop/file-drag-drop.component'; import { InfoBannerComponent } from './info-banner/info-banner.component'; import { NoDataComponent } from './no-data/no-data.component'; +import { CrownOwnerDialogComponent } from './owner-dialogs/crown-owner-dialog/crown-owner-dialog.component'; +import { OwnerDialogComponent } from './owner-dialogs/owner-dialog/owner-dialog.component'; +import { AllOwnersDialogComponent } from './owner-dialogs/owners-dialog/all-owners-dialog.component'; +import { ParcelOwnersComponent } from './owner-dialogs/parcel-owners/parcel-owners.component'; import { EmailValidPipe } from './pipes/emailValid.pipe'; import { FileSizePipe } from './pipes/fileSize.pipe'; import { MomentPipe } from './pipes/moment.pipe'; @@ -59,6 +63,15 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen NgxMaskDirective, NgxMaskPipe, MatRadioModule, + MatDialogModule, + MatTableModule, + FormsModule, + ReactiveFormsModule, + MatFormFieldModule, + MatInputModule, + MatButtonToggleModule, + MatCheckboxModule, + MatIconModule, ], declarations: [ FileDragDropComponent, @@ -74,6 +87,10 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen CustomStepperComponent, MomentPipe, PresribedBodyComponent, + OwnerDialogComponent, + CrownOwnerDialogComponent, + AllOwnersDialogComponent, + ParcelOwnersComponent, ], exports: [ CommonModule, @@ -116,6 +133,10 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen NgxMaskPipe, MomentPipe, PresribedBodyComponent, + OwnerDialogComponent, + CrownOwnerDialogComponent, + AllOwnersDialogComponent, + ParcelOwnersComponent, ], }) export class SharedModule { diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts index 6c2e94ef6a..f577977c4a 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts @@ -7,6 +7,8 @@ import { NoticeOfIntentSubmissionToSubmissionStatusDto, } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.entity'; +import { NoticeOfIntentOwnerDto } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; import { NoticeOfIntentSubmissionDetailedDto, NoticeOfIntentSubmissionDto, @@ -43,6 +45,20 @@ export class NoticeOfIntentSubmissionProfile extends AutomapperProfile { return ad.status.statusType; }), ), + forMember( + (a) => a.owners, + mapFrom((ad) => { + if (ad.owners) { + return this.mapper.mapArray( + ad.owners, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + } else { + return []; + } + }), + ), ); createMap( diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts index 9cd7278d9d..ccb29748bd 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -1,14 +1,14 @@ import { AutoMap } from '@automapper/classes'; import { - IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength, } from 'class-validator'; -import { ApplicationStatusDto } from '../../alcs/application/application-submission-status/submission-status.dto'; import { NoticeOfIntentStatusDto } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { ApplicationOwnerDto } from '../application-submission/application-owner/application-owner.dto'; +import { NoticeOfIntentOwnerDto } from './notice-of-intent-owner/notice-of-intent-owner.dto'; export const MAX_DESCRIPTION_FIELD_LENGTH = 4000; @@ -35,6 +35,7 @@ export class NoticeOfIntentSubmissionDto { type: string; status: NoticeOfIntentStatusDto; + owners: NoticeOfIntentOwnerDto[]; canEdit: boolean; canView: boolean; diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index db15158ca5..dc5a46ed87 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -34,7 +34,13 @@ export class NoticeOfIntentSubmissionService { private logger: Logger = new Logger(NoticeOfIntentSubmissionService.name); private DEFAULT_RELATIONS: FindOptionsRelations = { - //TODO + owners: { + type: true, + corporateSummary: { + document: true, + }, + parcels: true, + }, }; constructor( From 1595b0e2124d5cf6ac952521b274c54d415197eb Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 11:19:58 -0700 Subject: [PATCH 227/954] Add table descriptions --- .../1691777711797-add_noi_table_comments.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691777711797-add_noi_table_comments.ts diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691777711797-add_noi_table_comments.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691777711797-add_noi_table_comments.ts new file mode 100644 index 0000000000..9ac4860e6c --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691777711797-add_noi_table_comments.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNoiTableComments1691777711797 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + COMMENT ON TABLE "alcs"."notice_of_intent_parcel_ownership_type" IS 'Parcel Ownership types used for NOI Parcels'; + COMMENT ON TABLE "alcs"."notice_of_intent_parcel" IS 'Parcels that are linked to Notice of Intent Submissions'; + COMMENT ON TABLE "alcs"."notice_of_intent_owner" IS 'Owners for Notice of Intent Submissions'; + COMMENT ON TABLE "alcs"."notice_of_intent_parcel_owners_notice_of_intent_owner" IS 'Join table that links Owners to Parcels'; + COMMENT ON TABLE "alcs"."notice_of_intent_submission_status_type" IS 'The code table for Notice of Intent Submissions Statuses'; + COMMENT ON TABLE "alcs"."notice_of_intent_submission_to_submission_status" IS 'Join table to link Notice of Intent Submissions to their Statuses'; + `, + ); + } + + public async down(): Promise { + //No + } +} From a7487e4bd2073cb93c9ec9050040bea452fb1e2d Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 13:04:18 -0700 Subject: [PATCH 228/954] Code Review Feedback --- .../parcel-entry/parcel-entry.component.html | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html index 9c3365cc68..70ccd10264 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html @@ -334,16 +334,18 @@
Owner Information
Add new government contact
- - - - - - - - - - +
From c8ab0853da9d6b3668d24f9912d1e6dca416be33 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 11 Aug 2023 13:06:00 -0700 Subject: [PATCH 229/954] found the bug --- .../applications/app_prep.py | 27 +++++++++++++--- .../sql/application-prep/application_prep.sql | 31 ++++++++++++------- .../common/alcs_application_enum.py | 4 +++ .../common/oats_application_code_values.py | 6 ++++ 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/bin/migrate-oats-data/applications/app_prep.py b/bin/migrate-oats-data/applications/app_prep.py index 88bacb10a2..eafd5bf0eb 100644 --- a/bin/migrate-oats-data/applications/app_prep.py +++ b/bin/migrate-oats-data/applications/app_prep.py @@ -9,6 +9,8 @@ AlcsAgCapSource, log_end, log_start, + OatsLegislationCodes, + AlcsApplicantType, ) from db import inject_conn_pool from constants import BATCH_UPLOAD_SIZE @@ -45,6 +47,11 @@ class OatsToAlcsAgCap(Enum): S = AlcsAgCap.Secondary.value U = AlcsAgCap.Unclassified.value +class OatsLegislationCodes(Enum): + SEC_30_1 = AlcsApplicantType.Land_owner.value + SEC_29_1 = AlcsApplicantType.LFNG.value + SEC_17_3 = AlcsApplicantType.Land_owner.value + SEC_17_1 = AlcsApplicantType.LFNG.value @inject_conn_pool def process_alcs_application_prep_fields(conn=None, batch_size=BATCH_UPLOAD_SIZE): @@ -165,7 +172,7 @@ def update_app_prep_records(conn, batch_size, cursor, rows): execute_batch( cursor, get_update_query_for_inc(), - exc_data_list, + inc_data_list, # sneaky bug page_size=batch_size, ) @@ -209,8 +216,10 @@ def prepare_app_prep_data(app_prep_raw_data_list): elif data["alr_change_code"] == ALRChangeCode.NAR.value: nar_data_list.append(data) elif data["alr_change_code"] == ALRChangeCode.EXC.value: + data = mapOatsToAlcsLeg(data) exc_data_list.append(data) elif data["alr_change_code"] == ALRChangeCode.INC.value: + data = mapOatsToAlcsLeg(data) inc_data_list.append(data) else: other_data_list.append(data) @@ -233,6 +242,14 @@ def mapOatsToAlcsAppPrep(data): return data +def mapOatsToAlcsLeg(data): + + if data["legislation_code"]: + data["legislation_code"] = str( + OatsLegislationCodes[data["legislation_code"]].value + ) + + return data def get_update_query_for_nfu(): query = """ @@ -280,7 +297,8 @@ def get_update_query_for_exc(): ag_cap_consultant = %(agri_cap_consultant)s, alr_area = %(component_area)s, ag_cap_source = %(capability_source_code)s, - staff_observations = %(staff_comment_observations)s + staff_observations = %(staff_comment_observations)s, + incl_excl_applicant_type = %(legislation_code)s WHERE alcs.application.file_number = %(alr_application_id)s::text; @@ -297,7 +315,8 @@ def get_update_query_for_inc(): ag_cap_consultant = %(agri_cap_consultant)s, alr_area = %(component_area)s, ag_cap_source = %(capability_source_code)s, - staff_observations = %(staff_comment_observations)s + staff_observations = %(staff_comment_observations)s, + incl_excl_applicant_type = %(legislation_code)s WHERE alcs.application.file_number = %(alr_application_id)s::text; @@ -333,7 +352,7 @@ def map_oats_to_alcs_nfu_subtypes(nfu_type_code, nfu_subtype_code): def map_basic_field(data): if data["capability_source_code"]: - data["capability_source_code"] = data["capability_source_code"] = str( + data["capability_source_code"] = str( OatsToAlcsAgCapSource[data["capability_source_code"]].value ) if data["agri_capability_code"]: diff --git a/bin/migrate-oats-data/applications/sql/application-prep/application_prep.sql b/bin/migrate-oats-data/applications/sql/application-prep/application_prep.sql index 72c23f5944..3e9649724a 100644 --- a/bin/migrate-oats-data/applications/sql/application-prep/application_prep.sql +++ b/bin/migrate-oats-data/applications/sql/application-prep/application_prep.sql @@ -1,12 +1,19 @@ -WITH appl_components_grouped AS ( - SELECT oaac.alr_application_id - FROM oats.oats_alr_appl_components oaac - JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = oaac.alr_application_id - WHERE oaa.application_class_code IN ('LOA', 'BLK') - GROUP BY oaac.alr_application_id - HAVING count(oaac.alr_application_id) < 2 -- ignore all applications wit multiple components -) -SELECT oaa.alr_application_id, +WITH + appl_components_grouped AS ( + SELECT + oaac.alr_application_id + FROM + oats.oats_alr_appl_components oaac + JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = oaac.alr_application_id + WHERE + oaa.application_class_code IN ('LOA', 'BLK') + GROUP BY + oaac.alr_application_id + HAVING + count(oaac.alr_application_id) < 2 -- ignore all applications wit multiple components + ) +SELECT + oaa.alr_application_id, oaac.agri_capability_code, oaac.agri_cap_map, oaac.agri_cap_consultant, @@ -19,7 +26,9 @@ SELECT oaa.alr_application_id, oaac.rsdntl_use_end_date, oaac.exclsn_app_type_code, oaa.staff_comment_observations, - oaac.alr_change_code -FROM appl_components_grouped acg + oaac.alr_change_code, + oaac.legislation_code +FROM + appl_components_grouped acg JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = acg.alr_application_id JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = acg.alr_application_id \ No newline at end of file diff --git a/bin/migrate-oats-data/common/alcs_application_enum.py b/bin/migrate-oats-data/common/alcs_application_enum.py index 02434a709b..298f64712c 100644 --- a/bin/migrate-oats-data/common/alcs_application_enum.py +++ b/bin/migrate-oats-data/common/alcs_application_enum.py @@ -48,3 +48,7 @@ class AlcsAgCapSource(Enum): BCLI = "BCLI" CLI = "CLI" On_site = "On-site" + +class AlcsApplicantType(Enum): + Land_owner = "Land Owner" + LFNG = "L/FNG Initiated" \ No newline at end of file diff --git a/bin/migrate-oats-data/common/oats_application_code_values.py b/bin/migrate-oats-data/common/oats_application_code_values.py index da8b8139bc..5dd8e7ca1a 100644 --- a/bin/migrate-oats-data/common/oats_application_code_values.py +++ b/bin/migrate-oats-data/common/oats_application_code_values.py @@ -28,6 +28,12 @@ class OatsAgriCapabilityCodes(Enum): S = "S" U = "U" +class OatsLegislationCodes(Enum): + SEC_30_1 = "SEC_30_1" + SEC_29_1 = "SEC_29_1" + SEC_17_3 = "SEC_17_3" + SEC_17_1 = "SEC_17_1" + OATS_NFU_SUBTYPES = [ {"type_key": "AGR", "subtype_key": "1", "value": "Accessory Buildings"}, {"type_key": "RES", "subtype_key": "2", "value": "Additional Dwelling(s)"}, From 4d534bd0ea2a4924c81f5900b094db9d1bdc2a5d Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 13:15:03 -0700 Subject: [PATCH 230/954] Add more subtext * Add subtext to all form fields so they line up * Change some fields back to non-full width * Remove a font size in typography so it can be changed --- .../parcel-entry/parcel-entry.component.html | 13 +++++++++--- .../parcel-entry/parcel-entry.component.html | 20 +++++++++++++------ portal-frontend/src/styles/typography.scss | 2 -- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html index 91d4429399..4048cca05d 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html @@ -103,6 +103,7 @@
Parcel Lookup
+
A unique nine-digit number found on the parcel's Certificate of Title
@@ -115,14 +116,16 @@
Parcel Lookup
+
Unique numeric identifier for Crown land parcels
-
+
- +
Date of the original registered purchase of the parcel.
+ Parcel Lookup
-
+
As determined by @@ -181,6 +184,10 @@
Parcel Lookup
+
+ Visit BC Land Title & Survey to obtain + a recent copy (not older than 1 year) of the Certificate of Title +
Parcel Lookup
- +
A unique nine-digit number found on the parcel's Certificate of Title
+
@@ -113,16 +114,18 @@
Parcel Lookup
-
+
- +
Unique numeric identifier for Crown land parcels
+
-
+
- +
Date of the original registered purchase of the parcel.
+ Parcel Lookup
-
+
As determined by @@ -181,6 +184,11 @@
Parcel Lookup
+
+ Visit + BC Land Title & Survey + to obtain a recent copy (not older than 1 year) of the Certificate of Title +
Date: Fri, 11 Aug 2023 13:23:38 -0700 Subject: [PATCH 231/954] updated validation --- bin/migrate-oats-data/applications/app_prep.py | 2 +- .../application_prep_basic_validation.sql | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/bin/migrate-oats-data/applications/app_prep.py b/bin/migrate-oats-data/applications/app_prep.py index eafd5bf0eb..0be19ec600 100644 --- a/bin/migrate-oats-data/applications/app_prep.py +++ b/bin/migrate-oats-data/applications/app_prep.py @@ -172,7 +172,7 @@ def update_app_prep_records(conn, batch_size, cursor, rows): execute_batch( cursor, get_update_query_for_inc(), - inc_data_list, # sneaky bug + inc_data_list, page_size=batch_size, ) diff --git a/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql b/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql index c2523a58f3..0e3767c3de 100644 --- a/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql +++ b/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql @@ -30,7 +30,14 @@ oats_app_prep_data AS ( WHEN onusc.description = 'Tourist Accomodations' THEN 'Tourist Accommodations' WHEN onusc.description = 'Office Buiding (Primary Use)' THEN 'Office Building (Primary Use)' ELSE onusc.description - END AS mapped_nonfarm_use_subtype_description + END AS mapped_nonfarm_use_subtype_description, + CASE + WHEN oaac.legislation_code = 'SEC_30_1' THEN 'Land Owner' + WHEN oaac.legislation_code = 'SEC_29_1' THEN 'L/FNG Initiated' + WHEN oaac.legislation_code = 'SEC_17_3' THEN 'Land Owner' + WHEN oaac.legislation_code = 'SEC_17_1' THEN 'L/FNG Initiated' + ELSE oaac.legislation_code + END AS mapped_legislation FROM appl_components_grouped acg JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = acg.alr_application_id JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = acg.alr_application_id @@ -54,11 +61,13 @@ SELECT oapd.alr_application_id, oapd.agri_cap_consultant, oapd.staff_comment_observations, oapd.nonfarm_use_type_description, - oapd.mapped_nonfarm_use_subtype_description + oapd.mapped_nonfarm_use_subtype_description, + oapd.mapped_legislation FROM alcs.application a LEFT JOIN oats_app_prep_data AS oapd ON a.file_number = oapd.alr_application_id::TEXT WHERE a.alr_area != oapd.component_area OR a.ag_cap_map != oapd.agri_cap_map OR a.ag_cap_consultant != oapd.agri_cap_consultant OR a.staff_observations != oapd.staff_comment_observations - OR a.nfu_use_sub_type != oapd.mapped_nonfarm_use_subtype_description \ No newline at end of file + OR a.nfu_use_sub_type != oapd.mapped_nonfarm_use_subtype_description + OR a.incl_excl_applicant_type != oapd.mapped_legislation \ No newline at end of file From 0b130ed7390894bce1ed826609958b94df12f68c Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 14:18:55 -0700 Subject: [PATCH 232/954] Add Steps 2,3,4,7 * Update stepper with correct number of steps and labels * Copy over components and setup for NOIs --- .../edit-submission-base.module.ts | 12 + .../edit-submission.component.html | 48 ++- .../edit-submission.component.spec.ts | 5 + .../edit-submission.component.ts | 33 +- .../edit-submission/files-step.partial.ts | 75 +++++ .../land-use/land-use.component.html | 307 ++++++++++++++++++ .../land-use/land-use.component.scss | 5 + .../land-use/land-use.component.spec.ts | 54 +++ .../land-use/land-use.component.ts | 119 +++++++ .../other-attachments.component.html | 107 ++++++ .../other-attachments.component.scss | 38 +++ .../other-attachments.component.spec.ts | 68 ++++ .../other-attachments.component.ts | 121 +++++++ .../primary-contact.component.html | 179 ++++++++++ .../primary-contact.component.scss | 58 ++++ .../primary-contact.component.spec.ts | 71 ++++ .../primary-contact.component.ts | 256 +++++++++++++++ .../select-government.component.html | 68 ++++ .../select-government.component.scss | 0 .../select-government.component.spec.ts | 43 +++ .../select-government.component.ts | 147 +++++++++ .../notice-of-intent-submission.dto.ts | 32 +- .../notice-of-intent-submission.service.ts | 38 ++- 23 files changed, 1856 insertions(+), 28 deletions(-) create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/files-step.partial.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.ts diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts index 57f9693ad8..63cf738713 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts @@ -9,10 +9,14 @@ import { MatTableModule } from '@angular/material/table'; import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; import { SharedModule } from '../../../shared/shared.module'; import { EditSubmissionComponent } from './edit-submission.component'; +import { LandUseComponent } from './land-use/land-use.component'; +import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { DeleteParcelDialogComponent } from './parcels/delete-parcel/delete-parcel-dialog.component'; import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; +import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { SelectGovernmentComponent } from './select-government/select-government.component'; @NgModule({ declarations: [ @@ -21,6 +25,10 @@ import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.compon ParcelEntryComponent, ParcelEntryConfirmationDialogComponent, DeleteParcelDialogComponent, + PrimaryContactComponent, + SelectGovernmentComponent, + LandUseComponent, + OtherAttachmentsComponent, ], imports: [ CommonModule, @@ -40,6 +48,10 @@ import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.compon ParcelEntryComponent, ParcelEntryConfirmationDialogComponent, DeleteParcelDialogComponent, + PrimaryContactComponent, + SelectGovernmentComponent, + LandUseComponent, + OtherAttachmentsComponent, ], }) export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html index ab2679138c..ddae45362f 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html @@ -11,9 +11,7 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | Download PDF
- + Notice of Intent ID: {{ noiSubmission.fileNumber }} |
Notice of Intent ID: {{ noiSubmission.fileNumber }} | -
Primary Contact
+
+ +
-
Select Government
+
+ +
-
Land Use
+
+ +
Proposal
+ + +
Additional Information
+
-
Other attachments
+
+ +
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts index b77ae01080..70c4567d05 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute } from '@angular/router'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; import { ToastService } from '../../../services/toast/toast.service'; @@ -18,6 +19,10 @@ describe('EditSubmissionComponent', () => { provide: NoticeOfIntentSubmissionService, useValue: {}, }, + { + provide: NoticeOfIntentDocumentService, + useValue: {}, + }, { provide: ToastService, useValue: {}, diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts index 0f183800a7..38969bd7de 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts @@ -3,14 +3,19 @@ import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; import { ToastService } from '../../../services/toast/toast.service'; import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; import { scrollToElement } from '../../../shared/utils/scroll-helper'; -import { EditApplicationSteps } from '../../applications/edit-submission/edit-submission.component'; +import { LandUseComponent } from './land-use/land-use.component'; +import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { ParcelDetailsComponent } from './parcels/parcel-details.component'; +import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { SelectGovernmentComponent } from './select-government/select-government.component'; export enum EditNoiSteps { Parcel = 0, @@ -33,6 +38,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { $destroy = new Subject(); $noiSubmission = new BehaviorSubject(undefined); + $noiDocuments = new BehaviorSubject([]); noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; steps = EditNoiSteps; @@ -42,9 +48,14 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; + @ViewChild(PrimaryContactComponent) primaryContactComponent!: PrimaryContactComponent; + @ViewChild(SelectGovernmentComponent) selectGovernmentComponent!: SelectGovernmentComponent; + @ViewChild(LandUseComponent) landUseComponent!: LandUseComponent; + @ViewChild(OtherAttachmentsComponent) otherAttachmentsComponent!: OtherAttachmentsComponent; constructor( private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, private activatedRoute: ActivatedRoute, private dialog: MatDialog, private toastService: ToastService, @@ -117,9 +128,21 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { async saveSubmission(step: number) { switch (step) { - case EditApplicationSteps.AppParcel: + case EditNoiSteps.Parcel: await this.parcelDetailsComponent.onSave(); break; + case EditNoiSteps.PrimaryContact: + await this.primaryContactComponent.onSave(); + break; + case EditNoiSteps.Government: + await this.selectGovernmentComponent.onSave(); + break; + case EditNoiSteps.LandUse: + await this.landUseComponent.onSave(); + break; + case EditNoiSteps.Attachments: + await this.otherAttachmentsComponent.onSave(); + break; default: this.toastService.showErrorToast('Error updating notice of intent.'); } @@ -140,6 +163,12 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { this.overlayService.showSpinner(); this.noiSubmission = await this.noticeOfIntentSubmissionService.getByFileId(fileId); this.fileId = fileId; + + const documents = await this.noticeOfIntentDocumentService.getByFileId(fileId); + if (documents) { + this.$noiDocuments.next(documents); + } + this.$noiSubmission.next(this.noiSubmission); this.overlayService.hideSpinner(); } diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/files-step.partial.ts new file mode 100644 index 0000000000..5c5cdcf681 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/files-step.partial.ts @@ -0,0 +1,75 @@ +import { Component, Input } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; +import { FileHandle } from '../../../shared/file-drag-drop/drag-drop.directive'; +import { RemoveFileConfirmationDialogComponent } from '../../applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; +import { StepComponent } from './step.partial'; + +@Component({ + selector: 'app-file-step', + template: '

', + styleUrls: [], +}) +export abstract class FilesStepComponent extends StepComponent { + @Input() $noiDocuments!: BehaviorSubject; + + DOCUMENT_TYPE = DOCUMENT_TYPE; + + protected fileId = ''; + + protected abstract save(): Promise; + + protected constructor( + protected noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + protected dialog: MatDialog + ) { + super(); + } + + async attachFile(file: FileHandle, documentType: DOCUMENT_TYPE | null) { + if (this.fileId) { + await this.save(); + const mappedFiles = file.file; + await this.noticeOfIntentDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); + const documents = await this.noticeOfIntentDocumentService.getByFileId(this.fileId); + if (documents) { + this.$noiDocuments.next(documents); + } + } + } + + async onDeleteFile($event: NoticeOfIntentDocumentDto) { + if (this.draftMode) { + this.dialog + .open(RemoveFileConfirmationDialogComponent) + .beforeClosed() + .subscribe(async (didConfirm) => { + if (didConfirm) { + this.deleteFile($event); + } + }); + } else { + await this.deleteFile($event); + } + } + + private async deleteFile($event: NoticeOfIntentDocumentDto) { + await this.noticeOfIntentDocumentService.deleteExternalFile($event.uuid); + if (this.fileId) { + const documents = await this.noticeOfIntentDocumentService.getByFileId(this.fileId); + if (documents) { + this.$noiDocuments.next(documents); + } + } + } + + async openFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + if (res) { + window.open(res.url, '_blank'); + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.html new file mode 100644 index 0000000000..a1a532ff09 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.html @@ -0,0 +1,307 @@ +
+
+

Land Use

+

Current land use of parcel(s) under notice of intent and adjacent parcels.

+

*All fields are required unless stated optional.

+
+ + Please consult the + 'What the Commission Considers' + page of the ALC website for more information. + +
+
Land Use of Parcel(s) under Notice of Intent
+
+
+ +
You may describe multiple parcels collectively or individually.
+ + + +
+ warning +
This field is required
+
+ Example 1: PID 001-002-003: 60 ha hay crop, 40 ha grazing, 200 sheep. Example 2: Parcel 1: 10% + blueberry crop, 30% vegetables, 60% hay. Parcel 2: 100% hay. + +
Characters left: {{ 4000 - parcelsAgricultureDescriptionText.textLength }}
+
+
+
+ +
+
+ Describe any irrigation, drainage, fencing, material enhancement, clearing, etc. undertaken on the parcel(s). + If there have been no agricultural improvements on the parcel(s), please specify "No Agricultural + Improvements". +
+ + + +
+ warning +
This field is required
+
+ Example: 40 ha of grazing land fenced in 2010. +
Characters left: {{ 4000 - parcelsAgricultureImprovementDescriptionText.textLength }}
+
+
+ +
+ Describe any non-agricultural uses such as home-based businesses, commercial, recreational, institutional, + industrial, etc. If all activity is agricultural, please specify "No non-agricultural activity". +
+ + + +
+ warning +
This field is required
+
+ Example: House and 100 square metre detached auto repair shop. +
Characters left: {{ 4000 - parcelsNonAgricultureUseDescriptionText.textLength }}
+
+
+
Identify the land uses surrounding the parcel(s) under notice of intent.
+
+ Choose the Primary Land Use Type from the drop-down list. If there is more than one land use type, choose the main + land use and describe all the uses under Specific Activity. +
+
+
+ +
+
+ + + {{ + enum.value + }} + + +
+ warning +
This field is required
+
+
+
+ + + +
+ warning +
This field is required
+
+
+
+
+ +
+ +
+
+ + + {{ + enum.value + }} + + +
+ warning +
This field is required
+
+
+
+ + + +
+ warning +
This field is required
+
+
+
+
+ +
+ +
+
+ + + {{ + enum.value + }} + + +
+ warning +
This field is required
+
+
+
+ + + +
+ warning +
This field is required
+
+
+
+
+ +
+ +
+
+ + + {{ + enum.value + }} + + +
+ warning +
This field is required
+
+
+
+ + + +
+ warning +
This field is required
+
+
+
+
+
+ + +
+ + +
+ + +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.scss new file mode 100644 index 0000000000..26003d9afa --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.scss @@ -0,0 +1,5 @@ +@use '../../../../../styles/functions' as *; + +h5 { + margin-top: rem(12) !important; +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.spec.ts new file mode 100644 index 0000000000..28d919e005 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.spec.ts @@ -0,0 +1,54 @@ +import { HttpClient } from '@angular/common/http'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { LandUseComponent } from './land-use.component'; + +describe('LandUseComponent', () => { + let component: LandUseComponent; + let fixture: ComponentFixture; + let mockAppService: DeepMocked; + let mockHttpClient: DeepMocked; + let mockRouter: DeepMocked; + let noiPipe = new BehaviorSubject(undefined); + + beforeEach(async () => { + mockAppService = createMock(); + mockHttpClient = createMock(); + mockRouter = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: Router, + useValue: mockRouter, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockAppService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + declarations: [LandUseComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(LandUseComponent); + component = fixture.componentInstance; + component.$noiSubmission = noiPipe; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.ts new file mode 100644 index 0000000000..96ccbebde9 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/land-use/land-use.component.ts @@ -0,0 +1,119 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { takeUntil } from 'rxjs'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { EditNoiSteps } from '../edit-submission.component'; +import { StepComponent } from '../step.partial'; + +export enum MainLandUseTypeOptions { + AgriculturalFarm = 'Agricultural / Farm', + CivicInstitutional = 'Civic / Institutional', + CommercialRetail = 'Commercial / Retail', + Industrial = 'Industrial', + Other = 'Other', + Recreational = 'Recreational', + Residential = 'Residential', + TransportationUtilities = 'Transportation / Utilities', + Unused = 'Unused', +} + +@Component({ + selector: 'app-land-use', + templateUrl: './land-use.component.html', + styleUrls: ['./land-use.component.scss'], +}) +export class LandUseComponent extends StepComponent implements OnInit, OnDestroy { + currentStep = EditNoiSteps.LandUse; + + fileId = ''; + submissionUuid = ''; + + MainLandUseTypeOptions = MainLandUseTypeOptions; + + parcelsAgricultureDescription = new FormControl('', [Validators.required]); + parcelsAgricultureImprovementDescription = new FormControl('', [Validators.required]); + parcelsNonAgricultureUseDescription = new FormControl('', [Validators.required]); + northLandUseType = new FormControl('', [Validators.required]); + northLandUseTypeDescription = new FormControl('', [Validators.required]); + eastLandUseType = new FormControl('', [Validators.required]); + eastLandUseTypeDescription = new FormControl('', [Validators.required]); + southLandUseType = new FormControl('', [Validators.required]); + southLandUseTypeDescription = new FormControl('', [Validators.required]); + westLandUseType = new FormControl('', [Validators.required]); + westLandUseTypeDescription = new FormControl('', [Validators.required]); + landUseForm = new FormGroup({ + parcelsAgricultureDescription: this.parcelsAgricultureDescription, + parcelsAgricultureImprovementDescription: this.parcelsAgricultureImprovementDescription, + parcelsNonAgricultureUseDescription: this.parcelsNonAgricultureUseDescription, + northLandUseType: this.northLandUseType, + northLandUseTypeDescription: this.northLandUseTypeDescription, + eastLandUseType: this.eastLandUseType, + eastLandUseTypeDescription: this.eastLandUseTypeDescription, + southLandUseType: this.southLandUseType, + southLandUseTypeDescription: this.southLandUseTypeDescription, + westLandUseType: this.westLandUseType, + westLandUseTypeDescription: this.westLandUseTypeDescription, + }); + + constructor(private router: Router, private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService) { + super(); + } + + ngOnInit(): void { + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.fileId = noiSubmission.fileNumber; + this.submissionUuid = noiSubmission.uuid; + this.populateFormValues(noiSubmission); + } + }); + + if (this.showErrors) { + this.landUseForm.markAllAsTouched(); + } + } + + populateFormValues(application: NoticeOfIntentSubmissionDetailedDto) { + this.landUseForm.patchValue({ + parcelsAgricultureDescription: application.parcelsAgricultureDescription, + parcelsAgricultureImprovementDescription: application.parcelsAgricultureImprovementDescription, + parcelsNonAgricultureUseDescription: application.parcelsNonAgricultureUseDescription, + northLandUseType: application.northLandUseType, + northLandUseTypeDescription: application.northLandUseTypeDescription, + eastLandUseType: application.eastLandUseType, + eastLandUseTypeDescription: application.eastLandUseTypeDescription, + southLandUseType: application.southLandUseType, + southLandUseTypeDescription: application.southLandUseTypeDescription, + westLandUseType: application.westLandUseType, + westLandUseTypeDescription: application.westLandUseTypeDescription, + }); + } + + async saveProgress() { + if (this.landUseForm.dirty) { + const formValues = this.landUseForm.getRawValue(); + const updatedSubmission = await this.noticeOfIntentSubmissionService.updatePending(this.submissionUuid, { + parcelsAgricultureDescription: formValues.parcelsAgricultureDescription, + parcelsAgricultureImprovementDescription: formValues.parcelsAgricultureImprovementDescription, + parcelsNonAgricultureUseDescription: formValues.parcelsNonAgricultureUseDescription, + northLandUseType: formValues.northLandUseType, + northLandUseTypeDescription: formValues.northLandUseTypeDescription, + eastLandUseType: formValues.eastLandUseType, + eastLandUseTypeDescription: formValues.eastLandUseTypeDescription, + southLandUseType: formValues.southLandUseType, + southLandUseTypeDescription: formValues.southLandUseTypeDescription, + westLandUseType: formValues.westLandUseType, + westLandUseTypeDescription: formValues.westLandUseTypeDescription, + }); + if (updatedSubmission) { + this.$noiSubmission.next(updatedSubmission); + } + } + } + + async onSave() { + await this.saveProgress(); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.html new file mode 100644 index 0000000000..744ad73277 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.html @@ -0,0 +1,107 @@ +

Optional Attachments

+

+ Please upload any optional supporting documents. Where possible, provide KML/KMZ Google Earth files or GIS shapefiles + and geodatabases. +

+

+ NOTE: All documents submitted as part of your notice of intent will be viewable to the public on the ALC website. Do + not include confidential material within your notice of intent. +

+
+

Optional Attachments (Max. 100 MB per attachment)

+
+
+
Type {{ element.type.label }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Type + + + {{ type.label }} + + +
+ warning +
This field is required
+
+
Description + + + +
+ warning +
+ This field is required +
+
+
File Name + {{ element.fileName }} + Action + +
No attachments
+
+ +
+ +
+ +
+ + +
+ + +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.scss new file mode 100644 index 0000000000..7a223440c8 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.scss @@ -0,0 +1,38 @@ +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; + +section { + margin-top: rem(32); +} + +.uploader { + margin-top: rem(24); +} + +h4 { + margin-bottom: rem(8) !important; +} + +.scrollable { + overflow-x: auto; +} + +.mat-mdc-table .mdc-data-table__row { + height: rem(75); +} + +.mat-mdc-form-field { + width: 100%; +} + +:host::ng-deep { + .mdc-text-field--invalid { + margin-top: rem(8); + } +} + +.no-data-text { + text-align: center; + color: colors.$grey; + padding-top: rem(12); +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.spec.ts new file mode 100644 index 0000000000..6bca59e7d2 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.spec.ts @@ -0,0 +1,68 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { CodeService } from '../../../../services/code/code.service'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; + +import { OtherAttachmentsComponent } from './other-attachments.component'; + +describe('OtherAttachmentsComponent', () => { + let component: OtherAttachmentsComponent; + let fixture: ComponentFixture; + let mockAppService: DeepMocked; + let mockAppDocumentService: DeepMocked; + let mockRouter: DeepMocked; + let mockCodeService: DeepMocked; + + let noiDocumentPipe = new BehaviorSubject([]); + + beforeEach(async () => { + mockAppService = createMock(); + mockAppDocumentService = createMock(); + mockRouter = createMock(); + mockCodeService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: NoticeOfIntentSubmissionService, + useValue: mockAppService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockAppDocumentService, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: CodeService, + useValue: mockCodeService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + declarations: [OtherAttachmentsComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(OtherAttachmentsComponent); + component = fixture.componentInstance; + component.$noiSubmission = new BehaviorSubject(undefined); + component.$noiDocuments = noiDocumentPipe; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.ts new file mode 100644 index 0000000000..dd2d8c4c27 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.ts @@ -0,0 +1,121 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { takeUntil } from 'rxjs'; +import { CodeService } from '../../../../services/code/code.service'; +import { + NoticeOfIntentDocumentDto, + NoticeOfIntentDocumentUpdateDto, +} from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../shared/dto/document.dto'; +import { EditNoiSteps } from '../edit-submission.component'; +import { FilesStepComponent } from '../files-step.partial'; + +const USER_CONTROLLED_TYPES = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; + +@Component({ + selector: 'app-other-attachments', + templateUrl: './other-attachments.component.html', + styleUrls: ['./other-attachments.component.scss'], +}) +export class OtherAttachmentsComponent extends FilesStepComponent implements OnInit, OnDestroy { + currentStep = EditNoiSteps.Attachments; + + displayedColumns = ['type', 'description', 'fileName', 'actions']; + selectableTypes: DocumentTypeDto[] = []; + otherFiles: NoticeOfIntentDocumentDto[] = []; + + private isDirty = false; + + form = new FormGroup({} as any); + private documentCodes: DocumentTypeDto[] = []; + + constructor( + private router: Router, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private codeService: CodeService, + noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + dialog: MatDialog + ) { + super(noticeOfIntentDocumentService, dialog); + } + + ngOnInit(): void { + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.fileId = noiSubmission.fileNumber; + } + }); + + this.loadDocumentCodes(); + + this.$noiDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.otherFiles = documents + .filter((file) => (file.type ? USER_CONTROLLED_TYPES.includes(file.type.code) : true)) + .filter((file) => file.source === DOCUMENT_SOURCE.APPLICANT) + .sort((a, b) => { + return a.uploadedAt - b.uploadedAt; + }); + const newForm = new FormGroup({}); + for (const file of this.otherFiles) { + newForm.addControl(`${file.uuid}-type`, new FormControl(file.type?.code, [Validators.required])); + newForm.addControl(`${file.uuid}-description`, new FormControl(file.description, [Validators.required])); + } + this.form = newForm; + if (this.showErrors) { + this.form.markAllAsTouched(); + } + }); + } + + async onSave() { + await this.save(); + } + + protected async save() { + if (this.isDirty) { + const updateDtos: NoticeOfIntentDocumentUpdateDto[] = this.otherFiles.map((file) => ({ + uuid: file.uuid, + description: file.description, + type: file.type?.code ?? null, + })); + await this.noticeOfIntentDocumentService.update(this.fileId, updateDtos); + } + } + + onChangeDescription(uuid: string, event: Event) { + this.isDirty = true; + const input = event.target as HTMLInputElement; + const description = input.value; + this.otherFiles = this.otherFiles.map((file) => { + if (uuid === file.uuid) { + file.description = description; + } + return file; + }); + } + + onChangeType(uuid: string, selectedValue: DOCUMENT_TYPE) { + this.isDirty = true; + this.otherFiles = this.otherFiles.map((file) => { + if (uuid === file.uuid) { + const newType = this.documentCodes.find((code) => code.code === selectedValue); + if (newType) { + file.type = newType; + } else { + console.error('Failed to find matching document type'); + } + } + return file; + }); + } + + private async loadDocumentCodes() { + const codes = await this.codeService.loadCodes(); + this.documentCodes = codes.documentTypes; + this.selectableTypes = this.documentCodes.filter((code) => USER_CONTROLLED_TYPES.includes(code.code)); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.html new file mode 100644 index 0000000000..66bd3b7777 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.html @@ -0,0 +1,179 @@ +
+

Primary Contact

+

Select from the listed parcel owners or identify a third party agent

+

+ Identify staff from the local or first nation government listed below or a third-party agent to act as the primary + contact. +

+

*All fields are required unless stated optional.

+
+
Documents needed for this step:
+
    +
  • Authorization Letters (if applicable)
  • +
+
+
+
+
+ +
{{ owner.displayName }}
+ +
checkPrimary Contact
+
+ +
{{ governmentName ?? 'Local / First Nation Government' }} Staff
+ +
checkPrimary Contact
+
+
Third-Party Agent
+ +
checkPrimary Contact
+
+ + Please select a primary contact + +
+ +
+

Primary Contact Information

+
+
+
+ + + + +
+ warning +
This field is required
+
+
+
+ + + + +
+ warning +
This field is required
+
+
+
+ + + + + + +
+ warning +
This field is required
+
+
+
+ + + + +
+ warning +
This field is required
+
Invalid format
+
+
+
+ + + + +
+ warning +
This field is required
+
Invalid format
+
+
+
+
+
+
+
+

Primary Contact Authorization Letters

+
+ + An authorization letter must be provided if: +
    +
  1. the parcel under notice of intent is owned by more than one person;
  2. +
  3. the parcel(s) is owned by an organization; or
  4. +
  5. the parcel(s) is owned by a corporation (private, Crown, local government, First Nations); or
  6. +
  7. the notice of intent is being submitted by a third-party agent on behalf of the land owner(s)
  8. +
+

+ The authorization letter must be signed by all individual land owners and the majority of directors in + organization land owners listed in Step 1. Please consult the Supporting Documentation page of ALC website for + further instruction and an Authorization Letter template. +

+
+ + An authorization letter must be provided only if the notice of intent is being submitted by a third-party agent. Please + consult the Supporting Documentation page of the TODO: FIX THIS: ALC website for further instruction. + +
+
Authorization Letters (if applicable)
+
+ +
+
+
+ + +
+ + +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.scss new file mode 100644 index 0000000000..ba5b4f2a09 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.scss @@ -0,0 +1,58 @@ +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; + +h4 { + margin-bottom: rem(24) !important; +} + +h6 { + margin: rem(32) 0 rem(16) !important; +} + +section { + margin-bottom: rem(32); +} + +.agent-form { + .form-row { + grid-template-columns: 1fr; + } + + .form-row .full-row { + grid-column: 1/2; + } + + @media screen and (min-width: $tabletBreakpoint) { + .form-row { + grid-template-columns: 1fr 1fr; + } + + .form-row .full-row { + grid-column: 1/3; + } + } +} + +.contacts { + margin: rem(24) 0; + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: rem(30); + grid-row-gap: rem(24); +} + +.uploader { + margin-bottom: rem(24); +} + +.selected { + color: colors.$primary-color; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + + .mat-icon { + margin-right: rem(8); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.spec.ts new file mode 100644 index 0000000000..e55c98fd20 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.spec.ts @@ -0,0 +1,71 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { UserDto } from '../../../../services/authentication/authentication.dto'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; + +import { PrimaryContactComponent } from './primary-contact.component'; + +describe('PrimaryContactComponent', () => { + let component: PrimaryContactComponent; + let fixture: ComponentFixture; + let mockAppService: DeepMocked; + let mockAppDocumentService: DeepMocked; + let mockAppOwnerService: DeepMocked; + let mockAuthService: DeepMocked; + + let noiDocumentPipe = new BehaviorSubject([]); + + beforeEach(async () => { + mockAppService = createMock(); + mockAppDocumentService = createMock(); + mockAppOwnerService = createMock(); + mockAuthService = createMock(); + + mockAuthService.$currentProfile = new BehaviorSubject(undefined); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: NoticeOfIntentSubmissionService, + useValue: mockAppService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockAppDocumentService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockAppOwnerService, + }, + { + provide: MatDialog, + useValue: {}, + }, + { + provide: AuthenticationService, + useValue: mockAuthService, + }, + ], + declarations: [PrimaryContactComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PrimaryContactComponent); + component = fixture.componentInstance; + component.$noiSubmission = new BehaviorSubject(undefined); + component.$noiDocuments = noiDocumentPipe; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.ts new file mode 100644 index 0000000000..b3afb5274e --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/primary-contact/primary-contact.component.ts @@ -0,0 +1,256 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { takeUntil } from 'rxjs'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; +import { EditNoiSteps } from '../edit-submission.component'; +import { FilesStepComponent } from '../files-step.partial'; + +@Component({ + selector: 'app-primary-contact', + templateUrl: './primary-contact.component.html', + styleUrls: ['./primary-contact.component.scss'], +}) +export class PrimaryContactComponent extends FilesStepComponent implements OnInit, OnDestroy { + currentStep = EditNoiSteps.PrimaryContact; + + parcelOwners: NoticeOfIntentOwnerDto[] = []; + owners: NoticeOfIntentOwnerDto[] = []; + files: (NoticeOfIntentDocumentDto & { errorMessage?: string })[] = []; + + needsAuthorizationLetter = false; + selectedThirdPartyAgent = false; + selectedLocalGovernment = false; + selectedOwnerUuid: string | undefined = undefined; + isCrownOwner = false; + isGovernmentUser = false; + governmentName: string | undefined; + isDirty = false; + + firstName = new FormControl('', [Validators.required]); + lastName = new FormControl('', [Validators.required]); + organizationName = new FormControl(''); + phoneNumber = new FormControl('', [Validators.required]); + email = new FormControl('', [Validators.required, Validators.email]); + + form = new FormGroup({ + firstName: this.firstName, + lastName: this.lastName, + organizationName: this.organizationName, + phoneNumber: this.phoneNumber, + email: this.email, + }); + + private submissionUuid = ''; + + constructor( + private router: Router, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, + private authenticationService: AuthenticationService, + dialog: MatDialog + ) { + super(noticeOfIntentDocumentService, dialog); + } + + ngOnInit(): void { + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + if (submission) { + this.fileId = submission.fileNumber; + this.submissionUuid = submission.uuid; + this.loadOwners(submission.uuid, submission.primaryContactOwnerUuid); + } + }); + + this.authenticationService.$currentProfile.pipe(takeUntil(this.$destroy)).subscribe((profile) => { + this.isGovernmentUser = !!profile?.isLocalGovernment || !!profile?.isFirstNationGovernment; + this.governmentName = profile?.government; + if (this.isGovernmentUser || this.selectedLocalGovernment) { + this.prepareGovernmentOwners(); + } + }); + + this.$noiDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.files = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.AUTHORIZATION_LETTER); + }); + } + + async onSave() { + await this.save(); + } + + onSelectAgent() { + this.onSelectOwner('agent'); + } + + onSelectGovernment() { + this.onSelectOwner('government'); + } + + onSelectOwner(uuid: string) { + this.isDirty = true; + this.selectedOwnerUuid = uuid; + const selectedOwner = this.parcelOwners.find((owner) => owner.uuid === uuid); + this.parcelOwners = this.parcelOwners.map((owner) => ({ + ...owner, + isSelected: owner.uuid === uuid, + })); + this.selectedThirdPartyAgent = (selectedOwner && selectedOwner.type.code === OWNER_TYPE.AGENT) || uuid == 'agent'; + this.selectedLocalGovernment = + (selectedOwner && selectedOwner.type.code === OWNER_TYPE.GOVERNMENT) || uuid == 'government'; + this.form.reset(); + + if (this.selectedLocalGovernment) { + this.organizationName.setValidators([Validators.required]); + } else { + this.organizationName.setValidators([]); + } + + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { + this.firstName.enable(); + this.lastName.enable(); + this.organizationName.enable(); + this.email.enable(); + this.phoneNumber.enable(); + this.isCrownOwner = false; + } else { + this.firstName.disable(); + this.lastName.disable(); + this.organizationName.disable(); + this.email.disable(); + this.phoneNumber.disable(); + + if (selectedOwner) { + this.form.patchValue({ + firstName: selectedOwner.firstName, + lastName: selectedOwner.lastName, + organizationName: selectedOwner.organizationName, + phoneNumber: selectedOwner.phoneNumber, + email: selectedOwner.email, + }); + this.isCrownOwner = selectedOwner.type.code === OWNER_TYPE.CROWN; + } + } + this.calculateLetterRequired(); + } + + private calculateLetterRequired() { + if (this.selectedLocalGovernment) { + this.needsAuthorizationLetter = false; + } else { + const isSelfApplicant = this.owners.length > 0 && this.owners[0].type.code === OWNER_TYPE.INDIVIDUAL; + this.needsAuthorizationLetter = + this.selectedThirdPartyAgent || + !( + isSelfApplicant && + (this.owners.length === 1 || + (this.owners.length === 2 && + this.owners[1].type.code === OWNER_TYPE.AGENT && + !this.selectedThirdPartyAgent)) + ); + } + + this.files = this.files.map((file) => ({ + ...file, + errorMessage: !this.needsAuthorizationLetter + ? 'Authorization Letter not required. Please remove this file.' + : undefined, + })); + } + + protected async save() { + if (this.isDirty || this.form.dirty) { + let selectedOwner: NoticeOfIntentOwnerDto | undefined = this.owners.find( + (owner) => owner.uuid === this.selectedOwnerUuid + ); + + if (this.selectedThirdPartyAgent || this.selectedLocalGovernment) { + await this.noticeOfIntentOwnerService.setPrimaryContact({ + noticeOfIntentSubmissionUuid: this.submissionUuid, + organization: this.organizationName.getRawValue() ?? '', + firstName: this.firstName.getRawValue() ?? '', + lastName: this.lastName.getRawValue() ?? '', + email: this.email.getRawValue() ?? '', + phoneNumber: this.phoneNumber.getRawValue() ?? '', + ownerUuid: selectedOwner?.uuid, + type: this.selectedThirdPartyAgent ? OWNER_TYPE.AGENT : OWNER_TYPE.GOVERNMENT, + }); + } else if (selectedOwner) { + await this.noticeOfIntentOwnerService.setPrimaryContact({ + noticeOfIntentSubmissionUuid: this.submissionUuid, + ownerUuid: selectedOwner.uuid, + }); + } + await this.reloadSubmission(); + } + } + + private async reloadSubmission() { + const noiSubmission = await this.noticeOfIntentSubmissionService.getByUuid(this.submissionUuid); + this.$noiSubmission.next(noiSubmission); + } + + private async loadOwners(submissionUuid: string, primaryContactOwnerUuid?: string | null) { + const owners = await this.noticeOfIntentOwnerService.fetchBySubmissionId(submissionUuid); + if (owners) { + const selectedOwner = owners.find((owner) => owner.uuid === primaryContactOwnerUuid); + this.parcelOwners = owners.filter( + (owner) => ![OWNER_TYPE.AGENT, OWNER_TYPE.GOVERNMENT].includes(owner.type.code) + ); + this.owners = owners; + + if (selectedOwner) { + this.selectedThirdPartyAgent = selectedOwner.type.code === OWNER_TYPE.AGENT; + this.selectedLocalGovernment = selectedOwner.type.code === OWNER_TYPE.GOVERNMENT; + } + + if (this.selectedLocalGovernment) { + this.organizationName.setValidators([Validators.required]); + } else { + this.organizationName.setValidators([]); + } + + if (selectedOwner && (this.selectedThirdPartyAgent || this.selectedLocalGovernment)) { + this.selectedOwnerUuid = selectedOwner.uuid; + this.form.patchValue({ + firstName: selectedOwner.firstName, + lastName: selectedOwner.lastName, + organizationName: selectedOwner.organizationName, + phoneNumber: selectedOwner.phoneNumber, + email: selectedOwner.email, + }); + } else if (selectedOwner) { + this.onSelectOwner(selectedOwner.uuid); + } else { + this.firstName.disable(); + this.lastName.disable(); + this.organizationName.disable(); + this.email.disable(); + this.phoneNumber.disable(); + } + + if (this.isGovernmentUser || this.selectedLocalGovernment) { + this.prepareGovernmentOwners(); + } + + if (this.showErrors) { + this.form.markAllAsTouched(); + } + this.isDirty = false; + this.calculateLetterRequired(); + } + } + + private prepareGovernmentOwners() { + this.parcelOwners = []; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.html new file mode 100644 index 0000000000..33dcb4ec8d --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.html @@ -0,0 +1,68 @@ +
+

Government

+

+ Please indicate the local government in which the parcel(s) under the notice of intent are located. If the property + is located within the Islands Trust, please select 'Islands Trust' and not a specific island. +

+

*All fields are required unless stated optional.

+
+
+
+
+ + + + + + {{ option.name }} + + + +
+ warning +
This field is required
+
+
+
+
+ + This Local/First Nation Government has not yet been set up with the ALC Portal to receive notice of intents. To + submit, you will need to contact the ALC directly:  ALC.Portal@gov.bc.ca / 236-468-3342 + + + You're logged in with a Business BCeID that is associated with the government selected above. You will have the + opportunity to complete the local or first nation government review form immediately after this notice of intent is + submitted. + +

+ Please Note: If your Local or First Nation Government is not listed, please contact the ALC directly. + ALC.Portal@gov.bc.ca / 236-468-3342 +

+
+ + +
+ + +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.spec.ts new file mode 100644 index 0000000000..e2b7139ef2 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.spec.ts @@ -0,0 +1,43 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { CodeService } from '../../../../services/code/code.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; + +import { SelectGovernmentComponent } from './select-government.component'; + +describe('SelectGovernmentComponent', () => { + let component: SelectGovernmentComponent; + let fixture: ComponentFixture; + let mockCodeService: DeepMocked; + let mockAppService: DeepMocked; + + beforeEach(async () => { + mockCodeService = createMock(); + mockAppService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [SelectGovernmentComponent, MatAutocomplete], + providers: [ + { + provide: CodeService, + useValue: mockCodeService, + }, + { provide: NoticeOfIntentSubmissionService, useValue: mockAppService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SelectGovernmentComponent); + component = fixture.componentInstance; + component.$noiSubmission = new BehaviorSubject(undefined); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.ts new file mode 100644 index 0000000000..8188a55e2f --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.ts @@ -0,0 +1,147 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { map, Observable, startWith, takeUntil } from 'rxjs'; +import { LocalGovernmentDto } from '../../../../services/code/code.dto'; +import { CodeService } from '../../../../services/code/code.service'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { EditNoiSteps } from '../edit-submission.component'; +import { StepComponent } from '../step.partial'; + +@Component({ + selector: 'app-select-government', + templateUrl: './select-government.component.html', + styleUrls: ['./select-government.component.scss'], +}) +export class SelectGovernmentComponent extends StepComponent implements OnInit, OnDestroy { + currentStep = EditNoiSteps.Government; + + private fileId = ''; + private submissionUuid = ''; + + localGovernment = new FormControl('', [Validators.required]); + showWarning = false; + selectedOwnGovernment = false; + selectGovernmentUuid = ''; + localGovernments: LocalGovernmentDto[] = []; + filteredLocalGovernments!: Observable; + isDirty = false; + + form = new FormGroup({ + localGovernment: this.localGovernment, + }); + + constructor( + private codeService: CodeService, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService + ) { + super(); + } + + ngOnInit(): void { + this.loadGovernments(); + + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.selectGovernmentUuid = noiSubmission.localGovernmentUuid; + this.fileId = noiSubmission.fileNumber; + this.submissionUuid = noiSubmission.uuid; + this.populateLocalGovernment(noiSubmission.localGovernmentUuid); + } + }); + + this.filteredLocalGovernments = this.localGovernment.valueChanges.pipe( + startWith(''), + map((value) => this.filter(value || '')) + ); + + if (this.showErrors) { + this.form.markAllAsTouched(); + } + } + + onChange($event: MatAutocompleteSelectedEvent) { + this.isDirty = true; + const localGovernmentName = $event.option.value; + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + if (localGovernment) { + this.showWarning = !localGovernment.hasGuid; + + this.localGovernment.setValue(localGovernment.name); + if (localGovernment.hasGuid) { + this.localGovernment.setErrors(null); + } else { + this.localGovernment.setErrors({ invalid: localGovernment.hasGuid }); + } + + this.selectedOwnGovernment = localGovernment.matchesUserGuid; + } + } + } + + onBlur() { + //Blur will fire before onChange above, so use setTimeout to delay it + setTimeout(() => { + const localGovernmentName = this.localGovernment.getRawValue(); + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + if (!localGovernment) { + this.localGovernment.setValue(null); + console.log('Clearing Local Government field'); + } + } + }, 500); + } + + async onSave() { + await this.save(); + } + + private async save() { + if (this.isDirty) { + const localGovernmentName = this.localGovernment.getRawValue(); + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + + if (localGovernment) { + const res = await this.noticeOfIntentSubmissionService.updatePending(this.submissionUuid, { + localGovernmentUuid: localGovernment.uuid, + }); + this.$noiSubmission.next(res); + } + } + this.isDirty = false; + } + } + + private filter(value: string): LocalGovernmentDto[] { + if (this.localGovernments) { + const filterValue = value.toLowerCase(); + return this.localGovernments.filter((localGovernment) => + localGovernment.name.toLowerCase().includes(filterValue) + ); + } + return []; + } + + private async loadGovernments() { + const codes = await this.codeService.loadCodes(); + this.localGovernments = codes.localGovernments.sort((a, b) => (a.name > b.name ? 1 : -1)); + if (this.selectGovernmentUuid) { + this.populateLocalGovernment(this.selectGovernmentUuid); + } + } + + private populateLocalGovernment(governmentUuid: string) { + const lg = this.localGovernments.find((lg) => lg.uuid === governmentUuid); + if (lg) { + this.localGovernment.patchValue(lg.name); + this.showWarning = !lg.hasGuid; + if (!lg.hasGuid) { + this.localGovernment.setErrors({ invalid: true }); + } + this.selectedOwnGovernment = lg.matchesUserGuid; + } + } +} diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts index 242403d17c..49ec815506 100644 --- a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -51,23 +51,23 @@ export interface NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmi southLandUseTypeDescription: string; westLandUseType: string; westLandUseTypeDescription: string; - primaryContactOwnerUuid?: string | null; + primaryContactOwnerUuid: string | null; } export interface NoticeOfIntentSubmissionUpdateDto { - applicant?: string; - purpose?: string; - localGovernmentUuid?: string; - typeCode?: string; - parcelsAgricultureDescription?: string; - parcelsAgricultureImprovementDescription?: string; - parcelsNonAgricultureUseDescription?: string; - northLandUseType?: string; - northLandUseTypeDescription?: string; - eastLandUseType?: string; - eastLandUseTypeDescription?: string; - southLandUseType?: string; - southLandUseTypeDescription?: string; - westLandUseType?: string; - westLandUseTypeDescription?: string; + applicant?: string | null; + purpose?: string | null; + localGovernmentUuid?: string | null; + typeCode?: string | null; + parcelsAgricultureDescription?: string | null; + parcelsAgricultureImprovementDescription?: string | null; + parcelsNonAgricultureUseDescription?: string | null; + northLandUseType?: string | null; + northLandUseTypeDescription?: string | null; + eastLandUseType?: string | null; + eastLandUseTypeDescription?: string | null; + southLandUseType?: string | null; + southLandUseTypeDescription?: string | null; + westLandUseType?: string | null; + westLandUseTypeDescription?: string | null; } diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index dc5a46ed87..233549c30a 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -22,6 +22,8 @@ import { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; import { filterUndefined } from '../../utils/undefined'; +import { ApplicationSubmissionUpdateDto } from '../application-submission/application-submission.dto'; +import { ApplicationSubmission } from '../application-submission/application-submission.entity'; import { NoticeOfIntentSubmissionDetailedDto, NoticeOfIntentSubmissionDto, @@ -128,6 +130,8 @@ export class NoticeOfIntentSubmissionService { noticeOfIntentSubmission.localGovernmentUuid = updateDto.localGovernmentUuid; + this.setLandUseFields(noticeOfIntentSubmission, updateDto); + await this.noticeOfIntentSubmissionRepository.save( noticeOfIntentSubmission, ); @@ -144,6 +148,32 @@ export class NoticeOfIntentSubmissionService { return this.getOrFailByUuid(submissionUuid, this.DEFAULT_RELATIONS); } + private setLandUseFields( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + updateDto: NoticeOfIntentSubmissionUpdateDto, + ) { + noticeOfIntentSubmission.parcelsAgricultureDescription = + updateDto.parcelsAgricultureDescription; + noticeOfIntentSubmission.parcelsAgricultureImprovementDescription = + updateDto.parcelsAgricultureImprovementDescription; + noticeOfIntentSubmission.parcelsNonAgricultureUseDescription = + updateDto.parcelsNonAgricultureUseDescription; + noticeOfIntentSubmission.northLandUseType = updateDto.northLandUseType; + noticeOfIntentSubmission.northLandUseTypeDescription = + updateDto.northLandUseTypeDescription; + noticeOfIntentSubmission.eastLandUseType = updateDto.eastLandUseType; + noticeOfIntentSubmission.eastLandUseTypeDescription = + updateDto.eastLandUseTypeDescription; + noticeOfIntentSubmission.southLandUseType = updateDto.southLandUseType; + noticeOfIntentSubmission.southLandUseTypeDescription = + updateDto.southLandUseTypeDescription; + noticeOfIntentSubmission.westLandUseType = updateDto.westLandUseType; + noticeOfIntentSubmission.westLandUseTypeDescription = + updateDto.westLandUseTypeDescription; + + return noticeOfIntentSubmission; + } + //TODO: Uncomment when adding submitting // async submitToAlcs( // application: ValidatedApplicationSubmission, @@ -415,7 +445,11 @@ export class NoticeOfIntentSubmissionService { }); } - async setPrimaryContact(submissionUuid: string, uuid: any) { - //TODO:? ?? + async setPrimaryContact(submissionUuid: string, primaryContactUuid: any) { + const noticeOfIntentSubmission = await this.getOrFailByUuid(submissionUuid); + noticeOfIntentSubmission.primaryContactOwnerUuid = primaryContactUuid; + await this.noticeOfIntentSubmissionRepository.save( + noticeOfIntentSubmission, + ); } } From 7808242758208b4c90f8ca6d72ab84b3215343fe Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 11 Aug 2023 15:51:16 -0700 Subject: [PATCH 233/954] ROSO Proposal for NOIs * Add new DB fields for all types * Add ROSO Proposal Component for NOI Submissions * Move soil table to shared module --- .../edit-submission-base.module.ts | 3 - .../naru-proposal/naru-proposal.component.ts | 2 +- .../nfu-proposal/nfu-proposal.component.ts | 2 +- .../pfrs-proposal/pfrs-proposal.component.ts | 2 +- .../pofo-proposal/pofo-proposal.component.ts | 2 +- .../roso-proposal/roso-proposal.component.ts | 2 +- .../edit-submission-base.module.ts | 25 +- .../edit-submission.component.html | 25 +- .../edit-submission.component.ts | 9 +- .../roso/roso-proposal.component.html | 332 ++++++++++++++++++ .../roso/roso-proposal.component.scss | 5 + .../roso/roso-proposal.component.spec.ts | 61 ++++ .../proposal/roso/roso-proposal.component.ts | 180 ++++++++++ .../notice-of-intent-submission.dto.ts | 57 +++ .../src/app/shared/shared.module.ts | 3 + .../soil-table/soil-table.component.html | 0 .../soil-table/soil-table.component.scss | 2 +- .../soil-table/soil-table.component.spec.ts | 0 .../soil-table/soil-table.component.ts | 0 .../notice-of-intent-submission.dto.ts | 193 +++++++++- .../notice-of-intent-submission.entity.ts | 208 +++++++++++ .../notice-of-intent-submission.service.ts | 129 ++++++- ...845-add_soil_fields_for_noi_submissions.ts | 169 +++++++++ 23 files changed, 1380 insertions(+), 31 deletions(-) create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts rename portal-frontend/src/app/{features/applications/edit-submission/proposal => shared}/soil-table/soil-table.component.html (100%) rename portal-frontend/src/app/{features/applications/edit-submission/proposal => shared}/soil-table/soil-table.component.scss (95%) rename portal-frontend/src/app/{features/applications/edit-submission/proposal => shared}/soil-table/soil-table.component.spec.ts (100%) rename portal-frontend/src/app/{features/applications/edit-submission/proposal => shared}/soil-table/soil-table.component.ts (100%) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691792941845-add_soil_fields_for_noi_submissions.ts diff --git a/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts index ad2d84a82b..d55740124f 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission-base.module.ts @@ -28,7 +28,6 @@ import { NfuProposalComponent } from './proposal/nfu-proposal/nfu-proposal.compo import { PfrsProposalComponent } from './proposal/pfrs-proposal/pfrs-proposal.component'; import { PofoProposalComponent } from './proposal/pofo-proposal/pofo-proposal.component'; import { RosoProposalComponent } from './proposal/roso-proposal/roso-proposal.component'; -import { SoilTableComponent } from './proposal/soil-table/soil-table.component'; import { SubdProposalComponent } from './proposal/subd-proposal/subd-proposal.component'; import { TurProposalComponent } from './proposal/tur-proposal/tur-proposal.component'; import { ReviewAndSubmitComponent } from './review-and-submit/review-and-submit.component'; @@ -59,7 +58,6 @@ import { SelectGovernmentComponent } from './select-government/select-government PfrsProposalComponent, NaruProposalComponent, ChangeSubtypeConfirmationDialogComponent, - SoilTableComponent, ExclProposalComponent, InclProposalComponent, ], @@ -97,7 +95,6 @@ import { SelectGovernmentComponent } from './select-government/select-government PofoProposalComponent, PfrsProposalComponent, NaruProposalComponent, - SoilTableComponent, ExclProposalComponent, InclProposalComponent, ], diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts index 7c9c17c3a4..a24d2e6eb1 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts @@ -16,7 +16,7 @@ import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { formatBooleanToYesNoString } from '../../../../../shared/utils/boolean-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; -import { SoilTableData } from '../soil-table/soil-table.component'; +import { SoilTableData } from '../../../../../shared/soil-table/soil-table.component'; import { ChangeSubtypeConfirmationDialogComponent } from './change-subtype-confirmation-dialog/change-subtype-confirmation-dialog.component'; @Component({ diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts index ccb753d4c9..a366031721 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts @@ -7,7 +7,7 @@ import { ApplicationSubmissionService } from '../../../../../services/applicatio import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { StepComponent } from '../../step.partial'; -import { SoilTableData } from '../soil-table/soil-table.component'; +import { SoilTableData } from '../../../../../shared/soil-table/soil-table.component'; @Component({ selector: 'app-nfu-proposal', diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts index f657a6f1aa..2b0e865f2c 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts @@ -13,7 +13,7 @@ import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; -import { SoilTableData } from '../soil-table/soil-table.component'; +import { SoilTableData } from '../../../../../shared/soil-table/soil-table.component'; @Component({ selector: 'app-pfrs-proposal', diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts index 6f06250f2c..2548b6d2b2 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts @@ -12,7 +12,7 @@ import { formatBooleanToString } from '../../../../../shared/utils/boolean-helpe import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; -import { SoilTableData } from '../soil-table/soil-table.component'; +import { SoilTableData } from '../../../../../shared/soil-table/soil-table.component'; @Component({ selector: 'app-pofo-proposal', diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts index 3528028be2..4860d4b274 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts @@ -12,7 +12,7 @@ import { formatBooleanToString } from '../../../../../shared/utils/boolean-helpe import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; -import { SoilTableData } from '../soil-table/soil-table.component'; +import { SoilTableData } from '../../../../../shared/soil-table/soil-table.component'; @Component({ selector: 'app-roso-proposal', diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts index 63cf738713..224d720b0d 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts @@ -16,20 +16,10 @@ import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { RosoProposalComponent } from './proposal/roso/roso-proposal.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; @NgModule({ - declarations: [ - EditSubmissionComponent, - ParcelDetailsComponent, - ParcelEntryComponent, - ParcelEntryConfirmationDialogComponent, - DeleteParcelDialogComponent, - PrimaryContactComponent, - SelectGovernmentComponent, - LandUseComponent, - OtherAttachmentsComponent, - ], imports: [ CommonModule, SharedModule, @@ -42,6 +32,18 @@ import { SelectGovernmentComponent } from './select-government/select-government MatSelectModule, MatTableModule, ], + declarations: [ + EditSubmissionComponent, + ParcelDetailsComponent, + ParcelEntryComponent, + ParcelEntryConfirmationDialogComponent, + DeleteParcelDialogComponent, + PrimaryContactComponent, + SelectGovernmentComponent, + LandUseComponent, + OtherAttachmentsComponent, + RosoProposalComponent, + ], exports: [ EditSubmissionComponent, ParcelDetailsComponent, @@ -52,6 +54,7 @@ import { SelectGovernmentComponent } from './select-government/select-government SelectGovernmentComponent, LandUseComponent, OtherAttachmentsComponent, + RosoProposalComponent, ], }) export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html index ddae45362f..f608c3e8be 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html @@ -27,7 +27,7 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | class="edit-application" #cdkStepper (selectionChange)="onStepChange($event)" - (beforeSwitchStep)="onBeforeSwitchStep($event)" + (beforeSwitchStep)="switchStep($event)" > @@ -35,7 +35,7 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | @@ -48,7 +48,7 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | [showErrors]="showValidationErrors" [$noiSubmission]="$noiSubmission" [$noiDocuments]="$noiDocuments" - (navigateToStep)="onBeforeSwitchStep($event)" + (navigateToStep)="switchStep($event)" (exit)="onExit()" > @@ -59,7 +59,7 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | @@ -70,14 +70,25 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | -
Proposal
+
+ + +
@@ -90,7 +101,7 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | [$noiSubmission]="$noiSubmission" [$noiDocuments]="$noiDocuments" [showErrors]="showValidationErrors" - (navigateToStep)="onBeforeSwitchStep($event)" + (navigateToStep)="switchStep($event)" (exit)="onExit()" > diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts index 38969bd7de..a0cd07b3d8 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts @@ -15,6 +15,7 @@ import { LandUseComponent } from './land-use/land-use.component'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { RosoProposalComponent } from './proposal/roso/roso-proposal.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; export enum EditNoiSteps { @@ -52,6 +53,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { @ViewChild(SelectGovernmentComponent) selectGovernmentComponent!: SelectGovernmentComponent; @ViewChild(LandUseComponent) landUseComponent!: LandUseComponent; @ViewChild(OtherAttachmentsComponent) otherAttachmentsComponent!: OtherAttachmentsComponent; + @ViewChild(RosoProposalComponent) rosoProposalComponent!: RosoProposalComponent; constructor( private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, @@ -119,7 +121,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { scrollToElement({ id: `stepWrapper_${$event.selectedIndex}`, center: false }); } - async onBeforeSwitchStep(index: number) { + async switchStep(index: number) { // navigation to url will cause step change based on the index (index starts from 0) // The save will be triggered using canDeactivate guard this.showValidationErrors = this.customStepper.selectedIndex === EditNoiSteps.ReviewAndSubmit; @@ -143,6 +145,11 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { case EditNoiSteps.Attachments: await this.otherAttachmentsComponent.onSave(); break; + case EditNoiSteps.Proposal: + if (this.rosoProposalComponent) { + await this.rosoProposalComponent.onSave(); + } + break; default: this.toastService.showErrorToast('Error updating notice of intent.'); } diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html new file mode 100644 index 0000000000..00e3d6ec48 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html @@ -0,0 +1,332 @@ +
+

Proposal

+

+ Considerations are subject to + FIXME: Section 6 of the ALC Act. +

+

All fields are required unless stated optional.

+
+ + In order to complete this step, please consult the following pages on the ALC website: + + +
+
+
+ +
If yes, provide the ALC Application or Notice of Intent ID in the field below
+
+ + Yes + + No + + +
+
+
+ +
If you have multiple IDs, please separate with a comma.
+ + + + +
+ warning +
This field is required
+
+
+
+ +
+ Include why you are applying for removal of soil, what the proposal will achieve, and any benefits to + agriculture that the proposal provides. +
+ + + +
+ warning +
This field is required
+
+
Characters left: {{ 4000 - purposeText.textLength }}
+
+
+ +
List all proposed soil types to be removed.
+ + + +
+ warning +
This field is required
+
+ Example: Aggregate, Extraction, Placer Mining, Peat Extraction, Soil etc. +
Characters left: {{ 4000 - soilTypeRemovedText.textLength }}
+
+
+ +
+ Please provide accurate numbers that align with your cross sections, reclamation plan, and site plan. The + Commission will consider your proposal dimensions when determining the potential impact it will have on the + agricultural capability of the land. +
+ Please refer to FIXME: Contact Us on the ALC website for more detail. + +
+
+ +
If no proposed soil has already been removed, please leave 0 in the fields below
+ +
+ +
+

Project Duration

+
Estimated duration of the project
+
+
+ + + + Days + Months + Years + + +
+ warning +
This field is required
+
+
+
+ + + + +
+ warning +
This field is required
+
+
+
+
+ +
+ +
A visual representation of your proposal.
+ +
+ +
+ +
+ + Yes + + No + + +
+
+ +
+ +
Include North-South and East-West cross sections
+ + Please refer to FIXME: Removal of Soil on the ALC website for more information + + +
+
+ +
+ The Reclamation Plan should be completed by a qualified Professional Agrologist and contain the area's + agricultural capability assessment. +
+ + Please refer to FIXME: Removal of Soil on the ALC website for more information + + +
+ +
+ +
If yes, please attach the Notice of Work in the section below.
+
+ + Yes + + No + + +
+
+ +
+ + + Please refer to FIXME: Removal of Soil on the ALC website for more information + + +
+
+
+
+ + +
+ + +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.scss new file mode 100644 index 0000000000..56caa68a82 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.scss @@ -0,0 +1,5 @@ +@use '../../../../../../styles/functions' as *; + +section { + margin-top: rem(36); +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.spec.ts new file mode 100644 index 0000000000..26d01c861e --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.spec.ts @@ -0,0 +1,61 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDocumentDto } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; + +import { RosoProposalComponent } from './roso-proposal.component'; + +describe('RosoProposalComponent', () => { + let component: RosoProposalComponent; + let fixture: ComponentFixture; + let mockNOISubmissionService: DeepMocked; + let mockNOIDocumentService: DeepMocked; + let mockRouter: DeepMocked; + + let noiDocumentPipe = new BehaviorSubject([]); + + beforeEach(async () => { + mockNOISubmissionService = createMock(); + mockRouter = createMock(); + mockNOIDocumentService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNOISubmissionService, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNOIDocumentService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + declarations: [RosoProposalComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(RosoProposalComponent); + component = fixture.componentInstance; + component.$noiSubmission = new BehaviorSubject(undefined); + component.$noiDocuments = noiDocumentPipe; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts new file mode 100644 index 0000000000..c0966f7b86 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts @@ -0,0 +1,180 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { takeUntil } from 'rxjs'; +import { NoticeOfIntentDocumentDto } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionUpdateDto } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; +import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; +import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; +import { SoilTableData } from '../../../../../shared/soil-table/soil-table.component'; +import { EditNoiSteps } from '../../edit-submission.component'; +import { FilesStepComponent } from '../../files-step.partial'; + +@Component({ + selector: 'app-roso-proposal', + templateUrl: './roso-proposal.component.html', + styleUrls: ['./roso-proposal.component.scss'], +}) +export class RosoProposalComponent extends FilesStepComponent implements OnInit, OnDestroy { + currentStep = EditNoiSteps.Proposal; + + DOCUMENT = DOCUMENT_TYPE; + + allowMiningUploads = false; + requiresNoticeOfWork = false; + + proposalMap: NoticeOfIntentDocumentDto[] = []; + crossSections: NoticeOfIntentDocumentDto[] = []; + reclamationPlan: NoticeOfIntentDocumentDto[] = []; + noticeOfWork: NoticeOfIntentDocumentDto[] = []; + + isFollowUp = new FormControl(null, [Validators.required]); + followUpIds = new FormControl({ value: null, disabled: true }, [Validators.required]); + purpose = new FormControl(null, [Validators.required]); + soilTypeRemoved = new FormControl(null, [Validators.required]); + projectDurationAmount = new FormControl(null, [Validators.required]); + projectDurationUnit = new FormControl(null, [Validators.required]); + areComponentsDirty = false; + isExtractionOrMining = new FormControl(null, [Validators.required]); + hasSubmittedNotice = new FormControl(null, [Validators.required]); + + form = new FormGroup({ + isFollowUp: this.isFollowUp, + followUpIds: this.followUpIds, + purpose: this.purpose, + soilTypeRemoved: this.soilTypeRemoved, + projectDurationAmount: this.projectDurationAmount, + projectDurationUnit: this.projectDurationUnit, + isExtractionOrMining: this.isExtractionOrMining, + hasSubmittedNotice: this.hasSubmittedNotice, + }); + + private submissionUuid = ''; + removalTableData: SoilTableData = {}; + alreadyRemovedTableData: SoilTableData = {}; + + constructor( + private router: Router, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + dialog: MatDialog + ) { + super(noticeOfIntentDocumentService, dialog); + } + + ngOnInit(): void { + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.fileId = noiSubmission.fileNumber; + this.submissionUuid = noiSubmission.uuid; + + this.alreadyRemovedTableData = { + volume: noiSubmission.soilAlreadyRemovedVolume ?? 0, + area: noiSubmission.soilAlreadyRemovedArea ?? 0, + averageDepth: noiSubmission.soilAlreadyRemovedAverageDepth ?? 0, + maximumDepth: noiSubmission.soilAlreadyRemovedMaximumDepth ?? 0, + }; + + this.removalTableData = { + volume: noiSubmission.soilToRemoveVolume ?? undefined, + area: noiSubmission.soilToRemoveArea ?? undefined, + averageDepth: noiSubmission.soilToRemoveAverageDepth ?? undefined, + maximumDepth: noiSubmission.soilToRemoveMaximumDepth ?? undefined, + }; + + if (noiSubmission.soilIsFollowUp) { + this.followUpIds.enable(); + } + + if (noiSubmission.soilIsExtractionOrMining) { + this.allowMiningUploads = true; + } + + this.form.patchValue({ + isFollowUp: formatBooleanToString(noiSubmission.soilIsFollowUp), + followUpIds: noiSubmission.soilFollowUpIDs, + purpose: noiSubmission.purpose, + soilTypeRemoved: noiSubmission.soilTypeRemoved, + projectDurationAmount: noiSubmission.soilProjectDurationAmount?.toString() ?? null, + projectDurationUnit: noiSubmission.soilProjectDurationUnit, + isExtractionOrMining: formatBooleanToString(noiSubmission.soilIsExtractionOrMining), + hasSubmittedNotice: formatBooleanToString(noiSubmission.soilHasSubmittedNotice), + }); + if (this.showErrors) { + this.form.markAllAsTouched(); + } + } + }); + + this.$noiDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.crossSections = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.CROSS_SECTIONS); + this.reclamationPlan = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.RECLAMATION_PLAN); + this.noticeOfWork = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.NOTICE_OF_WORK); + }); + } + + async onSave() { + await this.save(); + } + + protected async save() { + if (this.fileId && this.form.dirty) { + const isNOIFollowUp = this.isFollowUp.getRawValue(); + const soilFollowUpIDs = this.followUpIds.getRawValue(); + const purpose = this.purpose.getRawValue(); + const soilTypeRemoved = this.soilTypeRemoved.getRawValue(); + const isExtractionOrMining = this.isExtractionOrMining.getRawValue(); + const hasSubmittedNotice = this.hasSubmittedNotice.getRawValue(); + + const updateDto: NoticeOfIntentSubmissionUpdateDto = { + purpose, + soilTypeRemoved, + soilIsFollowUp: parseStringToBoolean(isNOIFollowUp), + soilFollowUpIDs, + soilToRemoveVolume: this.removalTableData?.volume ?? null, + soilToRemoveArea: this.removalTableData?.area ?? null, + soilToRemoveMaximumDepth: this.removalTableData?.maximumDepth ?? null, + soilToRemoveAverageDepth: this.removalTableData?.averageDepth ?? null, + soilAlreadyRemovedVolume: this.alreadyRemovedTableData?.volume ?? null, + soilAlreadyRemovedArea: this.alreadyRemovedTableData?.area ?? null, + soilAlreadyRemovedMaximumDepth: this.alreadyRemovedTableData?.maximumDepth ?? null, + soilAlreadyRemovedAverageDepth: this.alreadyRemovedTableData?.averageDepth ?? null, + soilProjectDurationAmount: this.projectDurationAmount.value + ? parseFloat(this.projectDurationAmount.value) + : null, + soilProjectDurationUnit: this.projectDurationUnit.value, + soilIsExtractionOrMining: parseStringToBoolean(isExtractionOrMining), + soilHasSubmittedNotice: parseStringToBoolean(hasSubmittedNotice), + }; + + const updatedApp = await this.noticeOfIntentSubmissionService.updatePending(this.submissionUuid, updateDto); + this.$noiSubmission.next(updatedApp); + } + } + + onChangeIsFollowUp(selectedValue: string) { + if (selectedValue === 'true') { + this.followUpIds.enable(); + } else if (selectedValue === 'false') { + this.followUpIds.disable(); + this.followUpIds.setValue(null); + } + } + + onChangeIsExtractionOrMining(selectedValue: string) { + this.allowMiningUploads = selectedValue === 'true'; + } + + markDirty() { + this.areComponentsDirty = true; + } + + onChangeNoticeOfWork(selectedValue: string) { + this.requiresNoticeOfWork = selectedValue === 'true'; + } +} diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts index 49ec815506..2cecd2086f 100644 --- a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -31,6 +31,7 @@ export interface NoticeOfIntentSubmissionDto { applicant: string; localGovernmentUuid: string; type: string; + typeCode: string; status: NoticeOfIntentSubmissionStatusDto; submissionStatuses: NoticeOfIntentSubmissionToSubmissionStatusDto[]; owners: NoticeOfIntentOwnerDto[]; @@ -52,6 +53,34 @@ export interface NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmi westLandUseType: string; westLandUseTypeDescription: string; primaryContactOwnerUuid: string | null; + + //Soil Fields + soilIsFollowUp: boolean | null; + soilFollowUpIDs: string | null; + soilTypeRemoved: string | null; + soilReduceNegativeImpacts: string | null; + soilToRemoveVolume: number | null; + soilToRemoveArea: number | null; + soilToRemoveMaximumDepth: number | null; + soilToRemoveAverageDepth: number | null; + soilAlreadyRemovedVolume: number | null; + soilAlreadyRemovedArea: number | null; + soilAlreadyRemovedMaximumDepth: number | null; + soilAlreadyRemovedAverageDepth: number | null; + soilToPlaceVolume: number | null; + soilToPlaceArea: number | null; + soilToPlaceMaximumDepth: number | null; + soilToPlaceAverageDepth: number | null; + soilAlreadyPlacedVolume: number | null; + soilAlreadyPlacedArea: number | null; + soilAlreadyPlacedMaximumDepth: number | null; + soilAlreadyPlacedAverageDepth: number | null; + soilProjectDurationAmount: number | null; + soilProjectDurationUnit?: string | null; + soilFillTypeToPlace?: string | null; + soilAlternativeMeasures?: string | null; + soilIsExtractionOrMining?: boolean; + soilHasSubmittedNotice?: boolean; } export interface NoticeOfIntentSubmissionUpdateDto { @@ -70,4 +99,32 @@ export interface NoticeOfIntentSubmissionUpdateDto { southLandUseTypeDescription?: string | null; westLandUseType?: string | null; westLandUseTypeDescription?: string | null; + + //Soil Fields + soilIsFollowUp?: boolean | null; + soilFollowUpIDs?: string | null; + soilTypeRemoved?: string | null; + soilReduceNegativeImpacts?: string | null; + soilToRemoveVolume?: number | null; + soilToRemoveArea?: number | null; + soilToRemoveMaximumDepth?: number | null; + soilToRemoveAverageDepth?: number | null; + soilAlreadyRemovedVolume?: number | null; + soilAlreadyRemovedArea?: number | null; + soilAlreadyRemovedMaximumDepth?: number | null; + soilAlreadyRemovedAverageDepth?: number | null; + soilToPlaceVolume?: number | null; + soilToPlaceArea?: number | null; + soilToPlaceMaximumDepth?: number | null; + soilToPlaceAverageDepth?: number | null; + soilAlreadyPlacedVolume?: number | null; + soilAlreadyPlacedArea?: number | null; + soilAlreadyPlacedMaximumDepth?: number | null; + soilAlreadyPlacedAverageDepth?: number | null; + soilProjectDurationAmount?: number | null; + soilProjectDurationUnit?: string | null; + soilFillTypeToPlace?: string | null; + soilAlternativeMeasures?: string | null; + soilIsExtractionOrMining?: boolean | null; + soilHasSubmittedNotice?: boolean | null; } diff --git a/portal-frontend/src/app/shared/shared.module.ts b/portal-frontend/src/app/shared/shared.module.ts index 75b24121ec..14966fe720 100644 --- a/portal-frontend/src/app/shared/shared.module.ts +++ b/portal-frontend/src/app/shared/shared.module.ts @@ -40,6 +40,7 @@ import { FileSizePipe } from './pipes/fileSize.pipe'; import { MomentPipe } from './pipes/moment.pipe'; import { PhoneValidPipe } from './pipes/phoneValid.pipe'; import { PresribedBodyComponent } from './presribed-body/presribed-body.component'; +import { SoilTableComponent } from './soil-table/soil-table.component'; import { UpdatedBannerComponent } from './updated-banner/updated-banner.component'; import { DATE_FORMATS } from './utils/date-format'; import { ValidationErrorComponent } from './validation-error/validation-error.component'; @@ -91,6 +92,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen CrownOwnerDialogComponent, AllOwnersDialogComponent, ParcelOwnersComponent, + SoilTableComponent, ], exports: [ CommonModule, @@ -137,6 +139,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen CrownOwnerDialogComponent, AllOwnersDialogComponent, ParcelOwnersComponent, + SoilTableComponent, ], }) export class SharedModule { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.html b/portal-frontend/src/app/shared/soil-table/soil-table.component.html similarity index 100% rename from portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.html rename to portal-frontend/src/app/shared/soil-table/soil-table.component.html diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.scss b/portal-frontend/src/app/shared/soil-table/soil-table.component.scss similarity index 95% rename from portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.scss rename to portal-frontend/src/app/shared/soil-table/soil-table.component.scss index d1987e6e0e..d7e7c37d6a 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.scss +++ b/portal-frontend/src/app/shared/soil-table/soil-table.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../../styles/functions' as *; +@use '../../../styles/functions' as *; .container { border: 1px solid #000; diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.spec.ts b/portal-frontend/src/app/shared/soil-table/soil-table.component.spec.ts similarity index 100% rename from portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.spec.ts rename to portal-frontend/src/app/shared/soil-table/soil-table.component.spec.ts diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.ts b/portal-frontend/src/app/shared/soil-table/soil-table.component.ts similarity index 100% rename from portal-frontend/src/app/features/applications/edit-submission/proposal/soil-table/soil-table.component.ts rename to portal-frontend/src/app/shared/soil-table/soil-table.component.ts diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts index ccb29748bd..3a47818495 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -1,13 +1,14 @@ import { AutoMap } from '@automapper/classes'; import { + IsBoolean, IsNotEmpty, + IsNumber, IsOptional, IsString, IsUUID, MaxLength, } from 'class-validator'; import { NoticeOfIntentStatusDto } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; -import { ApplicationOwnerDto } from '../application-submission/application-owner/application-owner.dto'; import { NoticeOfIntentOwnerDto } from './notice-of-intent-owner/notice-of-intent-owner.dto'; export const MAX_DESCRIPTION_FIELD_LENGTH = 4000; @@ -34,6 +35,9 @@ export class NoticeOfIntentSubmissionDto { @AutoMap() type: string; + @AutoMap() + typeCode: string; + status: NoticeOfIntentStatusDto; owners: NoticeOfIntentOwnerDto[]; @@ -68,6 +72,85 @@ export class NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmissio westLandUseTypeDescription: string; @AutoMap(() => String) primaryContactOwnerUuid?: string | null; + + //Soil Fields + @AutoMap(() => Boolean) + soilIsFollowUp: boolean | null; + + @AutoMap(() => String) + soilFollowUpIDs: string | null; + + @AutoMap(() => String) + soilTypeRemoved: string | null; + + @AutoMap(() => String) + soilReduceNegativeImpacts: string | null; + + @AutoMap(() => Number) + soilToRemoveVolume: number | null; + + @AutoMap(() => Number) + soilToRemoveArea: number | null; + + @AutoMap(() => Number) + soilToRemoveMaximumDepth: number | null; + + @AutoMap(() => Number) + soilToRemoveAverageDepth: number | null; + + @AutoMap(() => Number) + soilAlreadyRemovedVolume: number | null; + + @AutoMap(() => Number) + soilAlreadyRemovedArea: number | null; + + @AutoMap(() => Number) + soilAlreadyRemovedMaximumDepth: number | null; + + @AutoMap(() => Number) + soilAlreadyRemovedAverageDepth: number | null; + + @AutoMap(() => Number) + soilToPlaceVolume: number | null; + + @AutoMap(() => Number) + soilToPlaceArea: number | null; + + @AutoMap(() => Number) + soilToPlaceMaximumDepth: number | null; + + @AutoMap(() => Number) + soilToPlaceAverageDepth: number | null; + + @AutoMap(() => Number) + soilAlreadyPlacedVolume: number | null; + + @AutoMap(() => Number) + soilAlreadyPlacedArea: number | null; + + @AutoMap(() => Number) + soilAlreadyPlacedMaximumDepth: number | null; + + @AutoMap(() => Number) + soilAlreadyPlacedAverageDepth: number | null; + + @AutoMap(() => Number) + soilProjectDurationAmount: number | null; + + @AutoMap(() => String) + soilProjectDurationUnit?: string | null; + + @AutoMap(() => String) + soilFillTypeToPlace?: string | null; + + @AutoMap(() => String) + soilAlternativeMeasures?: string | null; + + @AutoMap(() => Boolean) + soilIsExtractionOrMining?: boolean; + + @AutoMap(() => Boolean) + soilHasSubmittedNotice?: boolean; } export class NoticeOfIntentSubmissionCreateDto { @@ -144,4 +227,112 @@ export class NoticeOfIntentSubmissionUpdateDto { @IsString() @IsOptional() westLandUseTypeDescription?: string; + + //Soil Fields + @IsBoolean() + @IsOptional() + soilIsFollowUp?: boolean | null; + + @IsString() + @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) + @IsOptional() + soilFollowUpIDs?: string | null; + + @IsString() + @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) + @IsOptional() + soilTypeRemoved?: string | null; + + @IsString() + @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) + @IsOptional() + soilReduceNegativeImpacts?: string | null; + + @IsNumber() + @IsOptional() + soilToRemoveVolume?: number | null; + + @IsNumber() + @IsOptional() + soilToRemoveArea?: number | null; + + @IsNumber() + @IsOptional() + soilToRemoveMaximumDepth?: number | null; + + @IsNumber() + @IsOptional() + soilToRemoveAverageDepth?: number | null; + + @IsNumber() + @IsOptional() + soilAlreadyRemovedVolume?: number | null; + + @IsNumber() + @IsOptional() + soilAlreadyRemovedArea?: number | null; + + @IsNumber() + @IsOptional() + soilAlreadyRemovedMaximumDepth?: number | null; + + @IsNumber() + @IsOptional() + soilAlreadyRemovedAverageDepth?: number | null; + + @IsNumber() + @IsOptional() + soilToPlaceVolume?: number | null; + + @IsNumber() + @IsOptional() + soilToPlaceArea?: number | null; + + @IsNumber() + @IsOptional() + soilToPlaceMaximumDepth?: number | null; + + @IsNumber() + @IsOptional() + soilToPlaceAverageDepth?: number | null; + + @IsNumber() + @IsOptional() + soilAlreadyPlacedVolume?: number | null; + + @IsNumber() + @IsOptional() + soilAlreadyPlacedArea?: number | null; + + @IsNumber() + @IsOptional() + soilAlreadyPlacedMaximumDepth?: number | null; + + @IsNumber() + @IsOptional() + soilAlreadyPlacedAverageDepth?: number | null; + + @IsNumber() + @IsOptional() + soilProjectDurationAmount?: number | null; + + @IsString() + @IsOptional() + soilProjectDurationUnit?: string | null; + + @IsString() + @IsOptional() + soilFillTypeToPlace?: string | null; + + @IsString() + @IsOptional() + soilAlternativeMeasures?: string | null; + + @IsBoolean() + @IsOptional() + soilIsExtractionOrMining?: boolean; + + @IsBoolean() + @IsOptional() + soilHasSubmittedNotice?: boolean; } diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts index 498b9a0389..3d71859500 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts @@ -12,6 +12,7 @@ import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; import { Base } from '../../common/entities/base.entity'; import { User } from '../../user/user.entity'; +import { ColumnNumericTransformer } from '../../utils/column-numeric-transform'; import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; @Entity() @@ -180,6 +181,213 @@ export class NoticeOfIntentSubmission extends Base { }) typeCode: string; + //Soil & Fill + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + soilIsFollowUp: boolean | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true, name: 'soil_follow_up_ids' }) + soilFollowUpIDs: string | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilTypeRemoved: string | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilReduceNegativeImpacts: string | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToRemoveVolume: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToRemoveArea: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToRemoveMaximumDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToRemoveAverageDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilAlreadyRemovedVolume: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilAlreadyRemovedArea: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilAlreadyRemovedMaximumDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilAlreadyRemovedAverageDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToPlaceVolume: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToPlaceArea: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToPlaceMaximumDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToPlaceAverageDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilAlreadyPlacedVolume: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilAlreadyPlacedArea: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilAlreadyPlacedMaximumDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilAlreadyPlacedAverageDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilProjectDurationAmount: number | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilProjectDurationUnit: string | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilFillTypeToPlace: string | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilAlternativeMeasures: string | null; + + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + soilIsExtractionOrMining: boolean | null; + + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + soilHasSubmittedNotice: boolean | null; + @AutoMap(() => NoticeOfIntent) @ManyToOne(() => NoticeOfIntent) @JoinColumn({ diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index 233549c30a..a35a2c0bb2 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -19,11 +19,10 @@ import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-int import { NoticeOfIntentSubmissionStatusService } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service'; import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; import { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; import { filterUndefined } from '../../utils/undefined'; -import { ApplicationSubmissionUpdateDto } from '../application-submission/application-submission.dto'; -import { ApplicationSubmission } from '../application-submission/application-submission.entity'; import { NoticeOfIntentSubmissionDetailedDto, NoticeOfIntentSubmissionDto, @@ -131,6 +130,7 @@ export class NoticeOfIntentSubmissionService { updateDto.localGovernmentUuid; this.setLandUseFields(noticeOfIntentSubmission, updateDto); + await this.setSoilFields(noticeOfIntentSubmission, updateDto); await this.noticeOfIntentSubmissionRepository.save( noticeOfIntentSubmission, @@ -174,6 +174,131 @@ export class NoticeOfIntentSubmissionService { return noticeOfIntentSubmission; } + private async setSoilFields( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + updateDto: NoticeOfIntentSubmissionUpdateDto, + ) { + noticeOfIntentSubmission.soilIsFollowUp = filterUndefined( + updateDto.soilIsFollowUp, + noticeOfIntentSubmission.soilIsFollowUp, + ); + noticeOfIntentSubmission.soilFollowUpIDs = filterUndefined( + updateDto.soilFollowUpIDs, + noticeOfIntentSubmission.soilFollowUpIDs, + ); + noticeOfIntentSubmission.soilTypeRemoved = filterUndefined( + updateDto.soilTypeRemoved, + noticeOfIntentSubmission.soilTypeRemoved, + ); + noticeOfIntentSubmission.soilReduceNegativeImpacts = filterUndefined( + updateDto.soilReduceNegativeImpacts, + noticeOfIntentSubmission.soilReduceNegativeImpacts, + ); + noticeOfIntentSubmission.soilToRemoveVolume = filterUndefined( + updateDto.soilToRemoveVolume, + noticeOfIntentSubmission.soilToRemoveVolume, + ); + noticeOfIntentSubmission.soilToRemoveArea = filterUndefined( + updateDto.soilToRemoveArea, + noticeOfIntentSubmission.soilToRemoveArea, + ); + noticeOfIntentSubmission.soilToRemoveMaximumDepth = filterUndefined( + updateDto.soilToRemoveMaximumDepth, + noticeOfIntentSubmission.soilToRemoveMaximumDepth, + ); + noticeOfIntentSubmission.soilToRemoveAverageDepth = filterUndefined( + updateDto.soilToRemoveAverageDepth, + noticeOfIntentSubmission.soilToRemoveAverageDepth, + ); + noticeOfIntentSubmission.soilAlreadyRemovedVolume = filterUndefined( + updateDto.soilAlreadyRemovedVolume, + noticeOfIntentSubmission.soilAlreadyRemovedVolume, + ); + noticeOfIntentSubmission.soilAlreadyRemovedArea = filterUndefined( + updateDto.soilAlreadyRemovedArea, + noticeOfIntentSubmission.soilAlreadyRemovedArea, + ); + noticeOfIntentSubmission.soilAlreadyRemovedMaximumDepth = filterUndefined( + updateDto.soilAlreadyRemovedMaximumDepth, + noticeOfIntentSubmission.soilAlreadyRemovedMaximumDepth, + ); + noticeOfIntentSubmission.soilAlreadyRemovedAverageDepth = filterUndefined( + updateDto.soilAlreadyRemovedAverageDepth, + noticeOfIntentSubmission.soilAlreadyRemovedAverageDepth, + ); + noticeOfIntentSubmission.soilToPlaceVolume = filterUndefined( + updateDto.soilToPlaceVolume, + noticeOfIntentSubmission.soilToPlaceVolume, + ); + noticeOfIntentSubmission.soilToPlaceArea = filterUndefined( + updateDto.soilToPlaceArea, + noticeOfIntentSubmission.soilToPlaceArea, + ); + noticeOfIntentSubmission.soilToPlaceMaximumDepth = filterUndefined( + updateDto.soilToPlaceMaximumDepth, + noticeOfIntentSubmission.soilToPlaceMaximumDepth, + ); + noticeOfIntentSubmission.soilToPlaceAverageDepth = filterUndefined( + updateDto.soilToPlaceAverageDepth, + noticeOfIntentSubmission.soilToPlaceAverageDepth, + ); + noticeOfIntentSubmission.soilAlreadyPlacedVolume = filterUndefined( + updateDto.soilAlreadyPlacedVolume, + noticeOfIntentSubmission.soilAlreadyPlacedVolume, + ); + noticeOfIntentSubmission.soilAlreadyPlacedArea = filterUndefined( + updateDto.soilAlreadyPlacedArea, + noticeOfIntentSubmission.soilAlreadyPlacedArea, + ); + noticeOfIntentSubmission.soilAlreadyPlacedMaximumDepth = filterUndefined( + updateDto.soilAlreadyPlacedMaximumDepth, + noticeOfIntentSubmission.soilAlreadyPlacedMaximumDepth, + ); + noticeOfIntentSubmission.soilAlreadyPlacedAverageDepth = filterUndefined( + updateDto.soilAlreadyPlacedAverageDepth, + noticeOfIntentSubmission.soilAlreadyPlacedAverageDepth, + ); + noticeOfIntentSubmission.soilProjectDurationAmount = filterUndefined( + updateDto.soilProjectDurationAmount, + noticeOfIntentSubmission.soilProjectDurationAmount, + ); + noticeOfIntentSubmission.soilProjectDurationUnit = filterUndefined( + updateDto.soilProjectDurationUnit, + noticeOfIntentSubmission.soilProjectDurationUnit, + ); + noticeOfIntentSubmission.soilFillTypeToPlace = filterUndefined( + updateDto.soilFillTypeToPlace, + noticeOfIntentSubmission.soilFillTypeToPlace, + ); + noticeOfIntentSubmission.soilAlternativeMeasures = filterUndefined( + updateDto.soilAlternativeMeasures, + noticeOfIntentSubmission.soilAlternativeMeasures, + ); + + noticeOfIntentSubmission.soilIsExtractionOrMining = filterUndefined( + updateDto.soilIsExtractionOrMining, + noticeOfIntentSubmission.soilIsExtractionOrMining, + ); + + noticeOfIntentSubmission.soilHasSubmittedNotice = filterUndefined( + updateDto.soilHasSubmittedNotice, + noticeOfIntentSubmission.soilHasSubmittedNotice, + ); + + if ( + updateDto.soilHasSubmittedNotice === false || + updateDto.soilIsExtractionOrMining === false + ) { + const noiUuid = await this.noticeOfIntentService.getUuid( + noticeOfIntentSubmission.fileNumber, + ); + await this.noticeOfIntentDocumentService.deleteByType( + DOCUMENT_TYPE.NOTICE_OF_WORK, + noiUuid, + ); + } + } + //TODO: Uncomment when adding submitting // async submitToAlcs( // application: ValidatedApplicationSubmission, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691792941845-add_soil_fields_for_noi_submissions.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691792941845-add_soil_fields_for_noi_submissions.ts new file mode 100644 index 0000000000..12114e1696 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691792941845-add_soil_fields_for_noi_submissions.ts @@ -0,0 +1,169 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addSoilFieldsForNoiSubmissions1691792941845 + implements MigrationInterface +{ + name = 'addSoilFieldsForNoiSubmissions1691792941845'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_is_follow_up" boolean`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_follow_up_ids" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_type_removed" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_reduce_negative_impacts" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_to_remove_volume" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_to_remove_area" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_to_remove_maximum_depth" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_to_remove_average_depth" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_already_removed_volume" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_already_removed_area" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_already_removed_maximum_depth" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_already_removed_average_depth" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_to_place_volume" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_to_place_area" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_to_place_maximum_depth" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_to_place_average_depth" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_already_placed_volume" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_already_placed_area" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_already_placed_maximum_depth" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_already_placed_average_depth" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_project_duration_amount" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_project_duration_unit" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_fill_type_to_place" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_alternative_measures" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_is_extraction_or_mining" boolean`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_has_submitted_notice" boolean`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_has_submitted_notice"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_is_extraction_or_mining"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_alternative_measures"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_fill_type_to_place"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_project_duration_unit"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_project_duration_amount"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_already_placed_average_depth"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_already_placed_maximum_depth"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_already_placed_area"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_already_placed_volume"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_to_place_average_depth"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_to_place_maximum_depth"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_to_place_area"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_to_place_volume"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_already_removed_average_depth"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_already_removed_maximum_depth"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_already_removed_area"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_already_removed_volume"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_to_remove_average_depth"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_to_remove_maximum_depth"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_to_remove_area"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_to_remove_volume"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_reduce_negative_impacts"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_type_removed"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_follow_up_ids"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_is_follow_up"`, + ); + } +} From 370ad1d0ebc11a50f0d53f2b370a0d49804f18ca Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Fri, 11 Aug 2023 16:04:33 -0700 Subject: [PATCH 234/954] Feature/alcs 693 - no status and various fixes (#867) update LG visibility rules of applications prevent user from manually navigating to edit in the portal prevent update api calls for non-eligible submission add missing table comments introduce "no status" status --- .../overview/overview.component.ts | 11 ++++- .../uncancel-application-dialog.component.ts | 20 +++++++- .../application-dialog.component.ts | 30 +++++++++--- .../application-submission-status.dto.ts | 6 +++ .../application-submission-status.service.ts | 18 +++++-- .../details-header.component.ts | 23 +++++---- .../edit-submission.component.ts | 14 +++++- ...ation-submission-review.controller.spec.ts | 47 ++++--------------- ...pplication-submission-review.controller.ts | 12 ++--- .../application-submission.controller.spec.ts | 42 +++++++++++++++-- .../application-submission.controller.ts | 14 +++++- .../application-submission.service.ts | 18 ++++--- .../1691785736403-more_table_comments.ts | 17 +++++++ 13 files changed, 193 insertions(+), 79 deletions(-) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1691785736403-more_table_comments.ts diff --git a/alcs-frontend/src/app/features/application/overview/overview.component.ts b/alcs-frontend/src/app/features/application/overview/overview.component.ts index ce0e893608..0749c04df4 100644 --- a/alcs-frontend/src/app/features/application/overview/overview.component.ts +++ b/alcs-frontend/src/app/features/application/overview/overview.component.ts @@ -359,7 +359,16 @@ export class OverviewComponent implements OnInit, OnDestroy { } private async loadStatusHistory(fileNumber: string) { - const statusHistory = await this.applicationSubmissionStatusService.fetchSubmissionStatusesByFileNumber(fileNumber); + let statusHistory: ApplicationSubmissionToSubmissionStatusDto[] = []; + try { + statusHistory = await this.applicationSubmissionStatusService.fetchSubmissionStatusesByFileNumber( + fileNumber, + false + ); + } catch (e) { + console.warn(`No statuses for ${fileNumber}. Is it a manually created submission?`); + } + this.isCancelled = statusHistory.filter((status) => status.effectiveDate && status.statusTypeCode === SUBMISSION_STATUS.CANCELLED) .length > 0; diff --git a/alcs-frontend/src/app/features/application/overview/uncancel-application-dialog/uncancel-application-dialog.component.ts b/alcs-frontend/src/app/features/application/overview/uncancel-application-dialog/uncancel-application-dialog.component.ts index 1ca143957d..7eea9e85d5 100644 --- a/alcs-frontend/src/app/features/application/overview/uncancel-application-dialog/uncancel-application-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/overview/uncancel-application-dialog/uncancel-application-dialog.component.ts @@ -1,5 +1,9 @@ import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { + ApplicationSubmissionToSubmissionStatusDto, + DEFAULT_NO_STATUS, +} from '../../../../services/application/application-submission-status/application-submission-status.dto'; import { ApplicationSubmissionStatusService } from '../../../../services/application/application-submission-status/application-submission-status.service'; import { SUBMISSION_STATUS } from '../../../../services/application/application.dto'; import { ApplicationSubmissionStatusPill } from '../../../../shared/application-submission-status-type-pill/application-submission-status-type-pill.component'; @@ -21,7 +25,17 @@ export class UncancelApplicationDialogComponent { } async calculateStatusChange(fileNumber: string) { - const statusHistory = await this.applicationSubmissionStatusService.fetchSubmissionStatusesByFileNumber(fileNumber); + let statusHistory: ApplicationSubmissionToSubmissionStatusDto[] = []; + + try { + statusHistory = await this.applicationSubmissionStatusService.fetchSubmissionStatusesByFileNumber( + fileNumber, + false + ); + } catch (e) { + console.warn(`No statuses for ${fileNumber}. Is it a manually created submission?`); + } + const validStatuses = statusHistory .filter( (status) => @@ -37,6 +51,8 @@ export class UncancelApplicationDialogComponent { textColor: validStatus.alcsColor, label: validStatus.label, }; + } else { + this.status = DEFAULT_NO_STATUS; } } diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts index 8963a37698..673ae390c2 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.ts @@ -1,6 +1,10 @@ import { Component, Inject, OnInit } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { Router } from '@angular/router'; +import { + ApplicationSubmissionToSubmissionStatusDto, + DEFAULT_NO_STATUS, +} from '../../../../services/application/application-submission-status/application-submission-status.dto'; import { ApplicationSubmissionStatusService } from '../../../../services/application/application-submission-status/application-submission-status.service'; import { ApplicationDto } from '../../../../services/application/application.dto'; import { ApplicationService } from '../../../../services/application/application.service'; @@ -56,12 +60,26 @@ export class ApplicationDialogComponent extends CardDialogComponent implements O } async populateApplicationSubmissionStatus(fileNumber: string) { - const submissionStatus = await this.applicationSubmissionStatusService.fetchCurrentStatusByFileNumber(fileNumber); - this.status = { - backgroundColor: submissionStatus.status.alcsBackgroundColor, - textColor: submissionStatus.status.alcsColor, - label: submissionStatus.status.label, - }; + let submissionStatus: ApplicationSubmissionToSubmissionStatusDto | null = null; + + try { + submissionStatus = await this.applicationSubmissionStatusService.fetchCurrentStatusByFileNumber( + fileNumber, + false + ); + } catch (e) { + console.warn(`No statuses for ${fileNumber}. Is it a manually created submission?`); + } + + if (submissionStatus) { + this.status = { + backgroundColor: submissionStatus.status.alcsBackgroundColor, + textColor: submissionStatus.status.alcsColor, + label: submissionStatus.status.label, + }; + } else { + this.status = DEFAULT_NO_STATUS; + } } async onBoardSelected(board: BoardWithFavourite) { diff --git a/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.dto.ts b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.dto.ts index afdc12ca34..e9c903981c 100644 --- a/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.dto.ts +++ b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.dto.ts @@ -24,3 +24,9 @@ export interface ApplicationSubmissionToSubmissionStatusDto { status: ApplicationStatusDto; } + +export const DEFAULT_NO_STATUS = { + backgroundColor: '#929292', + textColor: '#EFEFEF', + label: 'No Status', +}; diff --git a/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.ts b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.ts index 84bcd69a31..b7427b7ae3 100644 --- a/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.ts +++ b/alcs-frontend/src/app/services/application/application-submission-status/application-submission-status.service.ts @@ -13,26 +13,36 @@ export class ApplicationSubmissionStatusService { constructor(private http: HttpClient, private toastService: ToastService) {} - async fetchSubmissionStatusesByFileNumber(fileNumber: string): Promise { + async fetchSubmissionStatusesByFileNumber( + fileNumber: string, + showErrorToast = true + ): Promise { try { const result = await firstValueFrom( this.http.get(`${this.baseUrl}/${fileNumber}`) ); return result; } catch (e) { - this.toastService.showErrorToast('Failed to fetch Application Submission Statuses'); + if (showErrorToast) { + this.toastService.showErrorToast('Failed to fetch Application Submission Statuses'); + } throw e; } } - async fetchCurrentStatusByFileNumber(fileNumber: string): Promise { + async fetchCurrentStatusByFileNumber( + fileNumber: string, + showErrorToast = true + ): Promise { try { const result = await firstValueFrom( this.http.get(`${this.baseUrl}/current-status/${fileNumber}`) ); return result; } catch (e) { - this.toastService.showErrorToast('Failed to fetch Application Submission Status'); + if (showErrorToast) { + this.toastService.showErrorToast('Failed to fetch Application Submission Status'); + } throw e; } } diff --git a/alcs-frontend/src/app/shared/details-header/details-header.component.ts b/alcs-frontend/src/app/shared/details-header/details-header.component.ts index 302115e4c5..50b5fdb2a7 100644 --- a/alcs-frontend/src/app/shared/details-header/details-header.component.ts +++ b/alcs-frontend/src/app/shared/details-header/details-header.component.ts @@ -4,6 +4,7 @@ import { Subject } from 'rxjs'; import { ApplicationTypeDto } from '../../services/application/application-code.dto'; import { ApplicationModificationDto } from '../../services/application/application-modification/application-modification.dto'; import { ApplicationReconsiderationDto } from '../../services/application/application-reconsideration/application-reconsideration.dto'; +import { DEFAULT_NO_STATUS } from '../../services/application/application-submission-status/application-submission-status.dto'; import { ApplicationSubmissionStatusService } from '../../services/application/application-submission-status/application-submission-status.service'; import { ApplicationDto } from '../../services/application/application.dto'; import { CardDto } from '../../services/card/card.dto'; @@ -55,14 +56,20 @@ export class DetailsHeaderComponent { } if (this.showStatus) { - this.submissionStatusService.fetchCurrentStatusByFileNumber(application.fileNumber).then( - (status) => - (this.currentStatus = { - label: status.status.label, - backgroundColor: status.status.alcsBackgroundColor, - textColor: status.status.alcsColor, - }) - ); + this.submissionStatusService + .fetchCurrentStatusByFileNumber(application.fileNumber, false) + .then( + (status) => + (this.currentStatus = { + label: status.status.label, + backgroundColor: status.status.alcsBackgroundColor, + textColor: status.status.alcsColor, + }) + ) + .catch((e) => { + console.warn(`No statuses for ${application.fileNumber}. Is it a manually created submission?`); + this.currentStatus = DEFAULT_NO_STATUS; + }); } } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts index e1240cebe5..ee61cfcd8c 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.ts @@ -6,7 +6,10 @@ import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } fr import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../services/application-document/application-document.service'; import { ApplicationSubmissionReviewService } from '../../../services/application-submission-review/application-submission-review.service'; -import { ApplicationSubmissionDetailedDto } from '../../../services/application-submission/application-submission.dto'; +import { + ApplicationSubmissionDetailedDto, + SUBMISSION_STATUS, +} from '../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../services/application-submission/application-submission.service'; import { CodeService } from '../../../services/code/code.service'; import { PdfGenerationService } from '../../../services/pdf-generation/pdf-generation.service'; @@ -98,6 +101,15 @@ export class EditSubmissionComponent implements OnInit, OnDestroy, AfterViewInit this.$applicationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { this.applicationSubmission = submission; + if ( + submission?.status.code && + ![SUBMISSION_STATUS.IN_PROGRESS, SUBMISSION_STATUS.WRONG_GOV, SUBMISSION_STATUS.INCOMPLETE].includes( + submission?.status.code + ) + ) { + this.toastService.showErrorToast('Editing is not allowed. Please contact ALC for more details'); + this.router.navigate(['/home']); + } }); } diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts index 370f48ebbd..a5dd309102 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts @@ -3,25 +3,25 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; -import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; -import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; -import { - DocumentCode, - DOCUMENT_TYPE, -} from '../../document/document-code.entity'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; -import { Application } from '../../alcs/application/application.entity'; -import { ApplicationService } from '../../alcs/application/application.service'; import { ApplicationSubmissionStatusService } from '../../alcs/application/application-submission-status/application-submission-status.service'; import { ApplicationSubmissionStatusType } from '../../alcs/application/application-submission-status/submission-status-type.entity'; import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; import { ApplicationSubmissionToSubmissionStatus } from '../../alcs/application/application-submission-status/submission-status.entity'; +import { Application } from '../../alcs/application/application.entity'; +import { ApplicationService } from '../../alcs/application/application.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; +import { + DocumentCode, + DOCUMENT_TYPE, +} from '../../document/document-code.entity'; import { DOCUMENT_SOURCE } from '../../document/document.dto'; import { Document } from '../../document/document.entity'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; -import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; import { ApplicationSubmissionValidatorService, @@ -177,35 +177,6 @@ describe('ApplicationSubmissionReviewController', () => { expect(res).toBeDefined(); }); - it('should throw an exception when user loads review that is not complete', async () => { - mockLGService.getByGuid.mockResolvedValue(mockLG); - - const reviewWithApp = new ApplicationSubmissionReview({ - ...applicationReview, - }); - - mockAppReviewService.getByFileNumber.mockResolvedValue(reviewWithApp); - mockAppSubmissionService.getByFileNumber.mockResolvedValue( - new ApplicationSubmission({ - localGovernmentUuid: mockLG.uuid, - status: new ApplicationSubmissionToSubmissionStatus({ - statusTypeCode: SUBMISSION_STATUS.IN_PROGRESS, - effectiveDate: new Date(1, 1, 1), - submissionUuid: 'fake', - }), - }), - ); - - const promise = controller.get(fileNumber, { - user: { - entity: new User({}), - }, - }); - await expect(promise).rejects.toMatchObject( - new Error('Failed to load review'), - ); - }); - it('should update the applications status when calling create', async () => { mockLGService.getByGuid.mockResolvedValue(mockLG); mockAppReviewService.startReview.mockResolvedValue(applicationReview); diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index ebec8444d1..a808c0ec2c 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -29,6 +29,7 @@ import { ApplicationOwner } from '../application-submission/application-owner/ap import { ApplicationSubmissionValidatorService } from '../application-submission/application-submission-validator.service'; import { ApplicationSubmission } from '../application-submission/application-submission.entity'; import { ApplicationSubmissionService } from '../application-submission/application-submission.service'; +import { APPLICATION_SUBMISSION_TYPES } from '../pdf-generation/generate-submission-document.service'; import { ReturnApplicationSubmissionDto, UpdateApplicationSubmissionReviewDto, @@ -111,15 +112,8 @@ export class ApplicationSubmissionReviewController { ); } - if ( - ![ - SUBMISSION_STATUS.SUBMITTED_TO_ALC, - SUBMISSION_STATUS.REFUSED_TO_FORWARD_LG, - ].includes( - applicationSubmission.status.statusTypeCode as SUBMISSION_STATUS, - ) - ) { - throw new NotFoundException('Failed to load review'); + if (applicationSubmission.typeCode === APPLICATION_SUBMISSION_TYPES.TURP) { + throw new NotFoundException('Not subject to review'); } const localGovernments = await this.localGovernmentService.list(); diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts index c8e9baf3d4..25d591ff2a 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts @@ -4,14 +4,15 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { BadRequestException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; +import { ServiceValidationException } from '../../../../../libs/common/src/exceptions/base.exception'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; -import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; -import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; -import { Application } from '../../alcs/application/application.entity'; import { ApplicationSubmissionStatusType } from '../../alcs/application/application-submission-status/submission-status-type.entity'; import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; import { ApplicationSubmissionToSubmissionStatus } from '../../alcs/application/application-submission-status/submission-status.entity'; +import { Application } from '../../alcs/application/application.entity'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { ApplicationProfile } from '../../common/automapper/application.automapper.profile'; import { User } from '../../user/user.entity'; import { @@ -276,6 +277,14 @@ describe('ApplicationSubmissionController', () => { {} as ApplicationSubmissionDetailedDto, ); + mockAppSubmissionService.verifyAccessByUuid.mockResolvedValue( + new ApplicationSubmission({ + status: new ApplicationSubmissionToSubmissionStatus({ + statusTypeCode: SUBMISSION_STATUS.INCOMPLETE, + }), + }), + ); + await controller.update( 'file-id', { @@ -295,6 +304,33 @@ describe('ApplicationSubmissionController', () => { expect(mockAppSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(1); }); + it('should throw exception on update if submission is not it in "In Progress" status ', async () => { + mockAppSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as ApplicationSubmissionDetailedDto, + ); + + const promise = controller.update( + 'file-id', + { + localGovernmentUuid, + applicant, + }, + { + user: { + entity: new User(), + }, + }, + ); + + await expect(promise).rejects.toMatchObject( + new ServiceValidationException('Not allowed to update submission'), + ); + + expect(mockAppSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( + 1, + ); + }); + it('should call out to service on submitAlcs if application type is TURP', async () => { const mockFileId = 'file-id'; mockAppSubmissionService.submitToAlcs.mockResolvedValue(new Application()); diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts index beaa60d24b..0fd7474ca0 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts @@ -10,9 +10,10 @@ import { Req, UseGuards, } from '@nestjs/common'; +import { ServiceValidationException } from '../../../../../libs/common/src/exceptions/base.exception'; +import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; -import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; import { User } from '../../user/user.entity'; import { ApplicationSubmissionValidatorService } from './application-submission-validator.service'; @@ -141,6 +142,17 @@ export class ApplicationSubmissionController { req.user.entity, ); + if ( + !submission.status || + ![ + SUBMISSION_STATUS.INCOMPLETE.toString(), + SUBMISSION_STATUS.WRONG_GOV.toString(), + SUBMISSION_STATUS.IN_PROGRESS.toString(), + ].includes(submission.status.statusTypeCode) + ) { + throw new ServiceValidationException('Not allowed to update submission'); + } + const updatedSubmission = await this.applicationSubmissionService.update( submission.uuid, updateDto, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index 901c03a3cf..4444f5a30d 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -7,16 +7,16 @@ import { InjectMapper } from '@automapper/nestjs'; import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsRelations, Repository } from 'typeorm'; -import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; -import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; -import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { ApplicationDocumentService } from '../../alcs/application/application-document/application-document.service'; -import { Application } from '../../alcs/application/application.entity'; -import { ApplicationService } from '../../alcs/application/application.service'; import { ApplicationSubmissionStatusService } from '../../alcs/application/application-submission-status/application-submission-status.service'; import { ApplicationSubmissionStatusType } from '../../alcs/application/application-submission-status/submission-status-type.entity'; import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; +import { Application } from '../../alcs/application/application.entity'; +import { ApplicationService } from '../../alcs/application/application.service'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; import { filterUndefined } from '../../utils/undefined'; @@ -33,10 +33,16 @@ import { ApplicationSubmission } from './application-submission.entity'; import { NaruSubtype } from './naru-subtype/naru-subtype.entity'; const LG_VISIBLE_STATUSES = [ + SUBMISSION_STATUS.INCOMPLETE, SUBMISSION_STATUS.SUBMITTED_TO_LG, SUBMISSION_STATUS.IN_REVIEW_BY_LG, - SUBMISSION_STATUS.REFUSED_TO_FORWARD_LG, SUBMISSION_STATUS.SUBMITTED_TO_ALC, + SUBMISSION_STATUS.SUBMITTED_TO_ALC_INCOMPLETE, + SUBMISSION_STATUS.RECEIVED_BY_ALC, + SUBMISSION_STATUS.IN_REVIEW_BY_ALC, + SUBMISSION_STATUS.ALC_DECISION, + SUBMISSION_STATUS.REFUSED_TO_FORWARD_LG, + SUBMISSION_STATUS.CANCELLED, ]; @Injectable() diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1691785736403-more_table_comments.ts b/services/apps/alcs/src/providers/typeorm/migrations/1691785736403-more_table_comments.ts new file mode 100644 index 0000000000..a8ad58ec54 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1691785736403-more_table_comments.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class moreTableComments1691785736403 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + COMMENT ON TABLE "alcs"."board_create_card_types_card_type" IS 'Contains the type of cards can be created from each board'; + COMMENT ON TABLE "alcs"."application_submission_to_submission_status" IS 'Join table that links submission with its status'; + COMMENT ON TABLE "alcs"."application_decision_condition_component" IS 'Join table that links decision condition with decision components'; + `, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // nope + } +} From ddf3265199e586cea398791ff07def92fb23c173 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 11 Aug 2023 16:28:21 -0700 Subject: [PATCH 235/954] updated function name --- bin/migrate-oats-data/applications/app_prep.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/migrate-oats-data/applications/app_prep.py b/bin/migrate-oats-data/applications/app_prep.py index 0be19ec600..dd9696946c 100644 --- a/bin/migrate-oats-data/applications/app_prep.py +++ b/bin/migrate-oats-data/applications/app_prep.py @@ -216,10 +216,10 @@ def prepare_app_prep_data(app_prep_raw_data_list): elif data["alr_change_code"] == ALRChangeCode.NAR.value: nar_data_list.append(data) elif data["alr_change_code"] == ALRChangeCode.EXC.value: - data = mapOatsToAlcsLeg(data) + data = mapOatsToAlcsLegislationCode(data) exc_data_list.append(data) elif data["alr_change_code"] == ALRChangeCode.INC.value: - data = mapOatsToAlcsLeg(data) + data = mapOatsToAlcsLegislationCode(data) inc_data_list.append(data) else: other_data_list.append(data) @@ -242,7 +242,7 @@ def mapOatsToAlcsAppPrep(data): return data -def mapOatsToAlcsLeg(data): +def mapOatsToAlcsLegislationCode(data): if data["legislation_code"]: data["legislation_code"] = str( From 3d760fbfa7d1d40006e5cfb50e8724a95b74e666 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 14 Aug 2023 10:27:25 -0700 Subject: [PATCH 236/954] Add new payment fields and hook up statuses * Add more payment fields * Set statuses when setting certain fields in update * Add tooltips --- .../intake/intake.component.html | 54 +++++++++++++++---- .../intake/intake.component.scss | 11 ++-- .../intake/intake.component.ts | 2 +- .../notice-of-intent/notice-of-intent.dto.ts | 6 +++ .../notice-of-intent/notice-of-intent.dto.ts | 24 ++++++++- .../notice-of-intent.entity.ts | 26 ++++++++- .../notice-of-intent.module.ts | 2 + .../notice-of-intent.service.spec.ts | 41 +++++++++++++- .../notice-of-intent.service.ts | 50 ++++++++++++++++- .../1692030508798-add_noi_intake_fields.ts | 29 ++++++++++ 10 files changed, 225 insertions(+), 20 deletions(-) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692030508798-add_noi_intake_fields.ts diff --git a/alcs-frontend/src/app/features/notice-of-intent/intake/intake.component.html b/alcs-frontend/src/app/features/notice-of-intent/intake/intake.component.html index d25d278216..bce5a79543 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/intake/intake.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/intake/intake.component.html @@ -7,17 +7,44 @@
Submitted to ALC
(save)="updateDate('dateSubmittedToAlc', $event)" > - -
-
-
Fee Received Date
- +
Payment
+
+
+
Payment Date
+ +
+
+
Payment Amount
+ +
+
+
Split with L/FNG
+ +
+
+
Fee Waived
+ +
-
Acknowledged Incomplete
+
+
Acknowledged Incomplete
+ info_outline + +
Acknowledged Incomplete
-
Received All Items
+
+
Received All Items
+ info_outline + +
Boolean) + feeWaived?: boolean | null; + + @AutoMap(() => Boolean) + feeSplitWithLg?: boolean | null; + + @AutoMap(() => Number) + feeAmount?: number | null; + dateAcknowledgedIncomplete?: number; dateReceivedAllItems?: number; dateAcknowledgedComplete?: number; @@ -96,6 +106,18 @@ export class UpdateNoticeOfIntentDto { @IsNumber() feePaidDate?: number; + @IsBoolean() + @IsOptional() + feeWaived?: boolean | null; + + @IsBoolean() + @IsOptional() + feeSplitWithLg?: boolean | null; + + @IsOptional() + @IsNumber() + feeAmount?: number | null; + @IsOptional() @IsNumber() dateAcknowledgedIncomplete?: number; diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts index f36dfc11ac..3e3d233a1a 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts @@ -11,10 +11,10 @@ import { OneToOne, } from 'typeorm'; import { Base } from '../../common/entities/base.entity'; +import { ColumnNumericTransformer } from '../../utils/column-numeric-transform'; import { LocalGovernment } from '../local-government/local-government.entity'; import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; -import { ApplicationType } from '../code/application-code/application-type/application-type.entity'; import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; @@ -84,6 +84,30 @@ export class NoticeOfIntent extends Base { }) feePaidDate: Date | null; + @AutoMap(() => Boolean) + @Column({ + type: 'boolean', + nullable: true, + }) + feeWaived?: boolean | null; + + @AutoMap(() => Boolean) + @Column({ + type: 'boolean', + nullable: true, + }) + feeSplitWithLg?: boolean | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + feeAmount?: number | null; + @AutoMap() @Column({ type: 'timestamptz', diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts index 8876b3f228..ffad50cbda 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts @@ -17,6 +17,7 @@ import { NoticeOfIntentMeetingType } from './notice-of-intent-meeting/notice-of- import { NoticeOfIntentMeetingController } from './notice-of-intent-meeting/notice-of-intent-meeting.controller'; import { NoticeOfIntentMeeting } from './notice-of-intent-meeting/notice-of-intent-meeting.entity'; import { NoticeOfIntentMeetingService } from './notice-of-intent-meeting/notice-of-intent-meeting.service'; +import { NoticeOfIntentSubmissionStatusModule } from './notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentController } from './notice-of-intent.controller'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; @@ -41,6 +42,7 @@ import { NoticeOfIntentService } from './notice-of-intent.service'; DocumentModule, CodeModule, LocalGovernmentModule, + NoticeOfIntentSubmissionStatusModule, ], providers: [ NoticeOfIntentService, diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts index eb5d47adc1..213ee7e109 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts @@ -12,8 +12,10 @@ import { CardService } from '../card/card.service'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; import { CodeService } from '../code/code.service'; -import { LocalGovernment } from '../local-government/local-government.entity'; import { LocalGovernmentService } from '../local-government/local-government.service'; +import { NOI_SUBMISSION_STATUS } from './notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NoticeOfIntentSubmissionToSubmissionStatus } from './notice-of-intent-submission-status/notice-of-intent-status.entity'; +import { NoticeOfIntentSubmissionStatusService } from './notice-of-intent-submission-status/notice-of-intent-submission-status.service'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; import { NoticeOfIntent } from './notice-of-intent.entity'; import { NoticeOfIntentService } from './notice-of-intent.service'; @@ -27,6 +29,7 @@ describe('NoticeOfIntentService', () => { let mockFileNumberService: DeepMocked; let mockLocalGovernmentService: DeepMocked; let mockCodeService: DeepMocked; + let mockSubmissionStatusService: DeepMocked; beforeEach(async () => { mockCardService = createMock(); @@ -36,6 +39,7 @@ describe('NoticeOfIntentService', () => { mockTypeRepository = createMock(); mockLocalGovernmentService = createMock(); mockCodeService = createMock(); + mockSubmissionStatusService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -74,6 +78,10 @@ describe('NoticeOfIntentService', () => { provide: CodeService, useValue: mockCodeService, }, + { + provide: NoticeOfIntentSubmissionStatusService, + useValue: mockSubmissionStatusService, + }, ], }).compile(); @@ -178,6 +186,37 @@ describe('NoticeOfIntentService', () => { expect(notice.summary).toEqual('new-summary'); }); + it('should set the X status when setting Acknowledge Complete', async () => { + const notice = new NoticeOfIntent({ + summary: 'old-summary', + }); + mockRepository.findOneOrFail.mockResolvedValue(notice); + mockRepository.save.mockResolvedValue(new NoticeOfIntent()); + mockSubmissionStatusService.setStatusDateByFileNumber.mockResolvedValue( + new NoticeOfIntentSubmissionToSubmissionStatus(), + ); + + const res = await service.update('file', { + summary: 'new-summary', + dateAcknowledgedIncomplete: 5, + }); + + expect(res).toBeDefined(); + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(2); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + expect( + mockSubmissionStatusService.setStatusDateByFileNumber, + ).toHaveBeenCalledTimes(1); + expect( + mockSubmissionStatusService.setStatusDateByFileNumber, + ).toHaveBeenCalledWith( + undefined, + NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC_INCOMPLETE, + new Date(5), + ); + expect(notice.summary).toEqual('new-summary'); + }); + it('should load deleted cards', async () => { mockRepository.find.mockResolvedValue([]); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts index a33b7ba440..dea6f1754b 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts @@ -4,7 +4,7 @@ import { } from '@app/common/exceptions/base.exception'; import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsRelations, @@ -17,7 +17,7 @@ import { import { FileNumberService } from '../../file-number/file-number.service'; import { formatIncomingDate } from '../../utils/incoming-date.formatter'; import { filterUndefined } from '../../utils/undefined'; -import { LocalGovernmentService } from '../local-government/local-government.service'; +import { SUBMISSION_STATUS } from '../application/application-submission-status/submission-status.dto'; import { ApplicationTimeData } from '../application/application-time-tracking.service'; import { Board } from '../board/board.entity'; import { CARD_TYPE } from '../card/card-type/card-type.entity'; @@ -25,6 +25,9 @@ import { Card } from '../card/card.entity'; import { CardService } from '../card/card.service'; import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; import { CodeService } from '../code/code.service'; +import { LocalGovernmentService } from '../local-government/local-government.service'; +import { NOI_SUBMISSION_STATUS } from './notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NoticeOfIntentSubmissionStatusService } from './notice-of-intent-submission-status/notice-of-intent-submission-status.service'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; import { CreateNoticeOfIntentServiceDto, @@ -35,6 +38,8 @@ import { NoticeOfIntent } from './notice-of-intent.entity'; @Injectable() export class NoticeOfIntentService { + private logger = new Logger(NoticeOfIntentService.name); + private CARD_RELATIONS = { board: true, type: true, @@ -61,6 +66,7 @@ export class NoticeOfIntentService { private fileNumberService: FileNumberService, private codeService: CodeService, private localGovernmentService: LocalGovernmentService, + private noticeOfIntentSubmissionStatusService: NoticeOfIntentSubmissionStatusService, ) {} async create( @@ -247,6 +253,21 @@ export class NoticeOfIntentService { noticeOfIntent.feePaidDate, ); + noticeOfIntent.feeWaived = filterUndefined( + updateDto.feeWaived, + noticeOfIntent.feeWaived, + ); + + noticeOfIntent.feeSplitWithLg = filterUndefined( + updateDto.feeSplitWithLg, + noticeOfIntent.feeSplitWithLg, + ); + + noticeOfIntent.feeAmount = filterUndefined( + updateDto.feeAmount, + noticeOfIntent.feeAmount, + ); + noticeOfIntent.dateSubmittedToAlc = filterUndefined( formatIncomingDate(updateDto.dateSubmittedToAlc), noticeOfIntent.dateSubmittedToAlc, @@ -259,6 +280,31 @@ export class NoticeOfIntentService { await this.repository.save(noticeOfIntent); + //Statuses + try { + if (updateDto.dateAcknowledgedIncomplete !== undefined) { + await this.noticeOfIntentSubmissionStatusService.setStatusDateByFileNumber( + noticeOfIntent.fileNumber, + NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC_INCOMPLETE, + formatIncomingDate(updateDto.dateAcknowledgedIncomplete), + ); + } + + if (updateDto.dateReceivedAllItems !== undefined) { + await this.noticeOfIntentSubmissionStatusService.setStatusDateByFileNumber( + noticeOfIntent.fileNumber, + NOI_SUBMISSION_STATUS.RECEIVED_BY_ALC, + formatIncomingDate(updateDto.dateReceivedAllItems), + ); + } + } catch (error) { + if (error instanceof ServiceNotFoundException) { + this.logger.warn(error.message, error); + } else { + throw error; + } + } + return this.getByFileNumber(noticeOfIntent.fileNumber); } diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692030508798-add_noi_intake_fields.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692030508798-add_noi_intake_fields.ts new file mode 100644 index 0000000000..098e890f2c --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692030508798-add_noi_intake_fields.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNoiIntakeFields1692030508798 implements MigrationInterface { + name = 'addNoiIntakeFields1692030508798'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD "fee_waived" boolean`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD "fee_split_with_lg" boolean`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD "fee_amount" numeric(12,2)`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP COLUMN "fee_amount"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP COLUMN "fee_split_with_lg"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP COLUMN "fee_waived"`, + ); + } +} From 5da0224ea019e466b6fdd560b12414716be939ee Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 14 Aug 2023 11:59:43 -0700 Subject: [PATCH 237/954] commit before restructure --- bin/migrate-oats-data/applications/app_prep.py | 13 +++++++++++++ .../sql/application-prep/application_prep.sql | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/applications/app_prep.py b/bin/migrate-oats-data/applications/app_prep.py index dd9696946c..12c3e276dc 100644 --- a/bin/migrate-oats-data/applications/app_prep.py +++ b/bin/migrate-oats-data/applications/app_prep.py @@ -251,6 +251,19 @@ def mapOatsToAlcsLegislationCode(data): return data +def get_update_query(unique_fields): + query = """ + UPDATE alcs.application + SET ag_cap = %(agri_capability_code)s, + ag_cap_map = %(agri_cap_map)s, + ag_cap_consultant = %(agri_cap_consultant)s, + alr_area = %(component_area)s, + ag_cap_source = %(capability_source_code)s, + staff_observations = %(staff_comment_observations)s + {unique_fields} + WHERE + alcs.application.file_number = %(alr_application_id)s::TEXT; +""" def get_update_query_for_nfu(): query = """ UPDATE alcs.application diff --git a/bin/migrate-oats-data/applications/sql/application-prep/application_prep.sql b/bin/migrate-oats-data/applications/sql/application-prep/application_prep.sql index 3e9649724a..00af775a77 100644 --- a/bin/migrate-oats-data/applications/sql/application-prep/application_prep.sql +++ b/bin/migrate-oats-data/applications/sql/application-prep/application_prep.sql @@ -27,7 +27,11 @@ SELECT oaac.exclsn_app_type_code, oaa.staff_comment_observations, oaac.alr_change_code, - oaac.legislation_code + oaac.legislation_code, + oaa.applied_fee_amt, + oaa.split_fee_with_local_gov_ind, + oaa.fee_waived_ind, + oaa.fee_received_date FROM appl_components_grouped acg JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = acg.alr_application_id From e055549a7c05ac3e05269493301abbc421448dc6 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 14 Aug 2023 12:16:21 -0700 Subject: [PATCH 238/954] implemented --- .../applications/app_prep.py | 153 ++++++++++-------- 1 file changed, 84 insertions(+), 69 deletions(-) diff --git a/bin/migrate-oats-data/applications/app_prep.py b/bin/migrate-oats-data/applications/app_prep.py index 12c3e276dc..a75f626ba7 100644 --- a/bin/migrate-oats-data/applications/app_prep.py +++ b/bin/migrate-oats-data/applications/app_prep.py @@ -259,98 +259,113 @@ def get_update_query(unique_fields): ag_cap_consultant = %(agri_cap_consultant)s, alr_area = %(component_area)s, ag_cap_source = %(capability_source_code)s, - staff_observations = %(staff_comment_observations)s + staff_observations = %(staff_comment_observations)s, + fee_paid_date = %(fee_received_date)s, + fee_waived = %(fee_waived_ind)s, + fee_amount = %(applied_fee_amt)s, + fee_split_with_lg = %(split_fee_with_local_gov_ind)s {unique_fields} WHERE alcs.application.file_number = %(alr_application_id)s::TEXT; -""" -def get_update_query_for_nfu(): - query = """ - UPDATE alcs.application - SET ag_cap = %(agri_capability_code)s, - ag_cap_map = %(agri_cap_map)s, - ag_cap_consultant = %(agri_cap_consultant)s, - alr_area = %(component_area)s, - ag_cap_source = %(capability_source_code)s, - nfu_use_type = %(nonfarm_use_type_code)s, - nfu_use_sub_type = %(nonfarm_use_subtype_code)s, - proposal_end_date = %(nonfarm_use_end_date)s, - staff_observations = %(staff_comment_observations)s - WHERE - alcs.application.file_number = %(alr_application_id)s::text; """ - return query + return query.format(unique_fields=unique_fields) + +def get_update_query_for_nfu(): + # query = """ + # UPDATE alcs.application + # SET ag_cap = %(agri_capability_code)s, + # ag_cap_map = %(agri_cap_map)s, + # ag_cap_consultant = %(agri_cap_consultant)s, + # alr_area = %(component_area)s, + # ag_cap_source = %(capability_source_code)s, + # nfu_use_type = %(nonfarm_use_type_code)s, + # nfu_use_sub_type = %(nonfarm_use_subtype_code)s, + # proposal_end_date = %(nonfarm_use_end_date)s, + # staff_observations = %(staff_comment_observations)s + # WHERE + # alcs.application.file_number = %(alr_application_id)s::text; + # """ + # return query + unique_fields =",\n nfu_use_type = %(nonfarm_use_type_code)s,\n nfu_use_sub_type = %(nonfarm_use_subtype_code)s,\n proposal_end_date = %(nonfarm_use_end_date)s" + return get_update_query(unique_fields) def get_update_query_for_nar(): # naruSubtype is a part of submission, import there - query = """ - UPDATE alcs.application - SET ag_cap = %(agri_capability_code)s, - ag_cap_map = %(agri_cap_map)s, - ag_cap_consultant = %(agri_cap_consultant)s, - alr_area = %(component_area)s, - ag_cap_source = %(capability_source_code)s, - proposal_end_date = %(rsdntl_use_end_date)s, - staff_observations = %(staff_comment_observations)s - WHERE - alcs.application.file_number = %(alr_application_id)s::text; - """ - return query + # query = """ + # UPDATE alcs.application + # SET ag_cap = %(agri_capability_code)s, + # ag_cap_map = %(agri_cap_map)s, + # ag_cap_consultant = %(agri_cap_consultant)s, + # alr_area = %(component_area)s, + # ag_cap_source = %(capability_source_code)s, + # proposal_end_date = %(rsdntl_use_end_date)s, + # staff_observations = %(staff_comment_observations)s + # WHERE + # alcs.application.file_number = %(alr_application_id)s::text; + # """ + # return query + unique_fields = ",\n proposal_end_date = %(rsdntl_use_end_date)s" + return get_update_query(unique_fields) def get_update_query_for_exc(): # TODO Will be finalized in ALCS-834. # exclsn_app_type_code is out of scope. It is a part of submission - query = """ - UPDATE alcs.application - SET ag_cap = %(agri_capability_code)s, - ag_cap_map = %(agri_cap_map)s, - ag_cap_consultant = %(agri_cap_consultant)s, - alr_area = %(component_area)s, - ag_cap_source = %(capability_source_code)s, - staff_observations = %(staff_comment_observations)s, - incl_excl_applicant_type = %(legislation_code)s + # query = """ + # UPDATE alcs.application + # SET ag_cap = %(agri_capability_code)s, + # ag_cap_map = %(agri_cap_map)s, + # ag_cap_consultant = %(agri_cap_consultant)s, + # alr_area = %(component_area)s, + # ag_cap_source = %(capability_source_code)s, + # staff_observations = %(staff_comment_observations)s, + # incl_excl_applicant_type = %(legislation_code)s - WHERE - alcs.application.file_number = %(alr_application_id)s::text; - """ - return query + # WHERE + # alcs.application.file_number = %(alr_application_id)s::text; + # """ + # return query + unique_fields = ",\n incl_excl_applicant_type = %(legislation_code)s" + return get_update_query(unique_fields) def get_update_query_for_inc(): # TODO Will be finalized in ALCS-834. - query = """ - UPDATE alcs.application - SET ag_cap = %(agri_capability_code)s, - ag_cap_map = %(agri_cap_map)s, - ag_cap_consultant = %(agri_cap_consultant)s, - alr_area = %(component_area)s, - ag_cap_source = %(capability_source_code)s, - staff_observations = %(staff_comment_observations)s, - incl_excl_applicant_type = %(legislation_code)s + # query = """ + # UPDATE alcs.application + # SET ag_cap = %(agri_capability_code)s, + # ag_cap_map = %(agri_cap_map)s, + # ag_cap_consultant = %(agri_cap_consultant)s, + # alr_area = %(component_area)s, + # ag_cap_source = %(capability_source_code)s, + # staff_observations = %(staff_comment_observations)s, + # incl_excl_applicant_type = %(legislation_code)s - WHERE - alcs.application.file_number = %(alr_application_id)s::text; - """ - return query + # WHERE + # alcs.application.file_number = %(alr_application_id)s::text; + # """ + # return query + unique_fields = ",\n incl_excl_applicant_type = %(legislation_code)s" + return get_update_query(unique_fields) def get_update_query_for_other(): - query = """ - UPDATE alcs.application - SET ag_cap = %(agri_capability_code)s, - ag_cap_map = %(agri_cap_map)s, - ag_cap_consultant = %(agri_cap_consultant)s, - alr_area = %(component_area)s, - ag_cap_source = %(capability_source_code)s, - staff_observations = %(staff_comment_observations)s - WHERE - alcs.application.file_number = %(alr_application_id)s::text; - """ - return query - + # query = """ + # UPDATE alcs.application + # SET ag_cap = %(agri_capability_code)s, + # ag_cap_map = %(agri_cap_map)s, + # ag_cap_consultant = %(agri_cap_consultant)s, + # alr_area = %(component_area)s, + # ag_cap_source = %(capability_source_code)s, + # staff_observations = %(staff_comment_observations)s + # WHERE + # alcs.application.file_number = %(alr_application_id)s::text; + # """ + # return query + unique_fields = "" + return get_update_query(unique_fields) def map_oats_to_alcs_nfu_subtypes(nfu_type_code, nfu_subtype_code): for dict_obj in OATS_NFU_SUBTYPES: From fed109308c3e098a2e0e5484e56e49bf11f5cb6a Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 14 Aug 2023 12:37:45 -0700 Subject: [PATCH 239/954] updated validation & confirmed functionality --- .../application_prep_basic_validation.sql | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql b/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql index 0e3767c3de..a4bda71aed 100644 --- a/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql +++ b/bin/migrate-oats-data/applications/sql/application-prep/application_prep_basic_validation.sql @@ -37,7 +37,11 @@ oats_app_prep_data AS ( WHEN oaac.legislation_code = 'SEC_17_3' THEN 'Land Owner' WHEN oaac.legislation_code = 'SEC_17_1' THEN 'L/FNG Initiated' ELSE oaac.legislation_code - END AS mapped_legislation + END AS mapped_legislation, + split_fee_with_local_gov_ind AS fee_lg, + fee_received_date AS fee_date, + fee_waived_ind AS fee_waived, + applied_fee_amt AS fee_amount FROM appl_components_grouped acg JOIN oats.oats_alr_appl_components oaac ON oaac.alr_application_id = acg.alr_application_id JOIN oats.oats_alr_applications oaa ON oaa.alr_application_id = acg.alr_application_id @@ -62,7 +66,13 @@ SELECT oapd.alr_application_id, oapd.staff_comment_observations, oapd.nonfarm_use_type_description, oapd.mapped_nonfarm_use_subtype_description, - oapd.mapped_legislation + oapd.mapped_legislation, + oapd.fee_date, + oapd.fee_amount, + a.fee_split_with_lg, + oapd.fee_lg, + oapd.fee_waived, + a.fee_waived FROM alcs.application a LEFT JOIN oats_app_prep_data AS oapd ON a.file_number = oapd.alr_application_id::TEXT WHERE a.alr_area != oapd.component_area @@ -70,4 +80,5 @@ WHERE a.alr_area != oapd.component_area OR a.ag_cap_consultant != oapd.agri_cap_consultant OR a.staff_observations != oapd.staff_comment_observations OR a.nfu_use_sub_type != oapd.mapped_nonfarm_use_subtype_description - OR a.incl_excl_applicant_type != oapd.mapped_legislation \ No newline at end of file + OR a.incl_excl_applicant_type != oapd.mapped_legislation + OR a.fee_amount != oapd.fee_amount \ No newline at end of file From e286416d73285e1356fe07ffd9ef592d0baa8e86 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 14 Aug 2023 12:52:27 -0700 Subject: [PATCH 240/954] cleaned up code for MR --- .../applications/app_prep.py | 88 +++---------------- 1 file changed, 13 insertions(+), 75 deletions(-) diff --git a/bin/migrate-oats-data/applications/app_prep.py b/bin/migrate-oats-data/applications/app_prep.py index a75f626ba7..73840819f4 100644 --- a/bin/migrate-oats-data/applications/app_prep.py +++ b/bin/migrate-oats-data/applications/app_prep.py @@ -252,6 +252,7 @@ def mapOatsToAlcsLegislationCode(data): return data def get_update_query(unique_fields): + # unique_fields takes input from called function and appends to query query = """ UPDATE alcs.application SET ag_cap = %(agri_capability_code)s, @@ -271,100 +272,37 @@ def get_update_query(unique_fields): return query.format(unique_fields=unique_fields) def get_update_query_for_nfu(): - # query = """ - # UPDATE alcs.application - # SET ag_cap = %(agri_capability_code)s, - # ag_cap_map = %(agri_cap_map)s, - # ag_cap_consultant = %(agri_cap_consultant)s, - # alr_area = %(component_area)s, - # ag_cap_source = %(capability_source_code)s, - # nfu_use_type = %(nonfarm_use_type_code)s, - # nfu_use_sub_type = %(nonfarm_use_subtype_code)s, - # proposal_end_date = %(nonfarm_use_end_date)s, - # staff_observations = %(staff_comment_observations)s - # WHERE - # alcs.application.file_number = %(alr_application_id)s::text; - # """ - # return query - unique_fields =",\n nfu_use_type = %(nonfarm_use_type_code)s,\n nfu_use_sub_type = %(nonfarm_use_subtype_code)s,\n proposal_end_date = %(nonfarm_use_end_date)s" + unique_fields = """, + nfu_use_type = %(nonfarm_use_type_code)s, + nfu_use_sub_type = %(nonfarm_use_subtype_code)s, + proposal_end_date = %(nonfarm_use_end_date)s""" return get_update_query(unique_fields) - def get_update_query_for_nar(): # naruSubtype is a part of submission, import there - # query = """ - # UPDATE alcs.application - # SET ag_cap = %(agri_capability_code)s, - # ag_cap_map = %(agri_cap_map)s, - # ag_cap_consultant = %(agri_cap_consultant)s, - # alr_area = %(component_area)s, - # ag_cap_source = %(capability_source_code)s, - # proposal_end_date = %(rsdntl_use_end_date)s, - # staff_observations = %(staff_comment_observations)s - # WHERE - # alcs.application.file_number = %(alr_application_id)s::text; - # """ - # return query - unique_fields = ",\n proposal_end_date = %(rsdntl_use_end_date)s" + unique_fields = """, + proposal_end_date = %(rsdntl_use_end_date)s""" return get_update_query(unique_fields) def get_update_query_for_exc(): # TODO Will be finalized in ALCS-834. # exclsn_app_type_code is out of scope. It is a part of submission - - # query = """ - # UPDATE alcs.application - # SET ag_cap = %(agri_capability_code)s, - # ag_cap_map = %(agri_cap_map)s, - # ag_cap_consultant = %(agri_cap_consultant)s, - # alr_area = %(component_area)s, - # ag_cap_source = %(capability_source_code)s, - # staff_observations = %(staff_comment_observations)s, - # incl_excl_applicant_type = %(legislation_code)s - - # WHERE - # alcs.application.file_number = %(alr_application_id)s::text; - # """ - # return query - unique_fields = ",\n incl_excl_applicant_type = %(legislation_code)s" + unique_fields = """, + incl_excl_applicant_type = %(legislation_code)s""" return get_update_query(unique_fields) def get_update_query_for_inc(): # TODO Will be finalized in ALCS-834. - # query = """ - # UPDATE alcs.application - # SET ag_cap = %(agri_capability_code)s, - # ag_cap_map = %(agri_cap_map)s, - # ag_cap_consultant = %(agri_cap_consultant)s, - # alr_area = %(component_area)s, - # ag_cap_source = %(capability_source_code)s, - # staff_observations = %(staff_comment_observations)s, - # incl_excl_applicant_type = %(legislation_code)s - - # WHERE - # alcs.application.file_number = %(alr_application_id)s::text; - # """ - # return query - unique_fields = ",\n incl_excl_applicant_type = %(legislation_code)s" + unique_fields = """, + incl_excl_applicant_type = %(legislation_code)s""" return get_update_query(unique_fields) def get_update_query_for_other(): - # query = """ - # UPDATE alcs.application - # SET ag_cap = %(agri_capability_code)s, - # ag_cap_map = %(agri_cap_map)s, - # ag_cap_consultant = %(agri_cap_consultant)s, - # alr_area = %(component_area)s, - # ag_cap_source = %(capability_source_code)s, - # staff_observations = %(staff_comment_observations)s - # WHERE - # alcs.application.file_number = %(alr_application_id)s::text; - # """ - # return query - unique_fields = "" + # leaving blank insert for now + unique_fields = """""" return get_update_query(unique_fields) def map_oats_to_alcs_nfu_subtypes(nfu_type_code, nfu_subtype_code): From 31356a542562c59e651a2ee230413b17f686af16 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 14 Aug 2023 13:27:09 -0700 Subject: [PATCH 241/954] Add Step 8 for NOI + Roso Add Step 8 for NOIs and ROSO Proposal, not including Step 6 --- .../review-and-submit.component.spec.ts | 8 - .../review-and-submit.component.ts | 4 +- .../edit-submission-base.module.ts | 8 + .../edit-submission.component.html | 10 +- .../edit-submission.component.spec.ts | 7 + .../edit-submission.component.ts | 43 +++ .../review-and-submit.component.html | 33 +++ .../review-and-submit.component.scss | 0 .../review-and-submit.component.spec.ts | 57 ++++ .../review-and-submit.component.ts | 67 +++++ .../submit-confirmation-dialog.component.html | 40 +++ .../submit-confirmation-dialog.component.scss | 18 ++ ...bmit-confirmation-dialog.component.spec.ts | 39 +++ .../submit-confirmation-dialog.component.ts | 16 ++ .../notice-of-intent-details.component.html | 263 ++++++++++++++++++ .../notice-of-intent-details.component.scss | 143 ++++++++++ ...notice-of-intent-details.component.spec.ts | 71 +++++ .../notice-of-intent-details.component.ts | 120 ++++++++ .../notice-of-intent-details.module.ts | 14 + .../parcel/parcel.component.html | 171 ++++++++++++ .../parcel/parcel.component.scss | 39 +++ .../parcel/parcel.component.spec.ts | 59 ++++ .../parcel/parcel.component.ts | 222 +++++++++++++++ .../roso-details/roso-details.component.html | 186 +++++++++++++ .../roso-details/roso-details.component.scss | 9 + .../roso-details.component.spec.ts | 40 +++ .../roso-details/roso-details.component.ts | 55 ++++ .../file-drag-drop.component.html | 6 +- 28 files changed, 1732 insertions(+), 16 deletions(-) create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.ts diff --git a/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.spec.ts b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.spec.ts index acf4ab4aad..a4199f8aca 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.spec.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.spec.ts @@ -44,14 +44,6 @@ describe('ReviewAndSubmitComponent', () => { provide: PdfGenerationService, useValue: {}, }, - { - provide: MatDialog, - useValue: {}, - }, - { - provide: CodeService, - useValue: {}, - }, ], }).compileComponents(); diff --git a/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts index 49dbe47874..fc122f5c0b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/review-and-submit/review-and-submit.component.ts @@ -28,9 +28,7 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O private router: Router, private toastService: ToastService, private applicationService: ApplicationSubmissionService, - private pdfGenerationService: PdfGenerationService, - private codeService: CodeService, - private dialog: MatDialog + private pdfGenerationService: PdfGenerationService ) { super(); } diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts index 224d720b0d..f544a4d2f2 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts @@ -8,6 +8,7 @@ import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; import { SharedModule } from '../../../shared/shared.module'; +import { NoticeOfIntentDetailsModule } from '../notice-of-intent-details/notice-of-intent-details.module'; import { EditSubmissionComponent } from './edit-submission.component'; import { LandUseComponent } from './land-use/land-use.component'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; @@ -17,6 +18,8 @@ import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/p import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; import { RosoProposalComponent } from './proposal/roso/roso-proposal.component'; +import { ReviewAndSubmitComponent } from './review-and-submit/review-and-submit.component'; +import { SubmitConfirmationDialogComponent } from './review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; @NgModule({ @@ -31,6 +34,7 @@ import { SelectGovernmentComponent } from './select-government/select-government MatOptionModule, MatSelectModule, MatTableModule, + NoticeOfIntentDetailsModule, ], declarations: [ EditSubmissionComponent, @@ -43,6 +47,8 @@ import { SelectGovernmentComponent } from './select-government/select-government LandUseComponent, OtherAttachmentsComponent, RosoProposalComponent, + ReviewAndSubmitComponent, + SubmitConfirmationDialogComponent, ], exports: [ EditSubmissionComponent, @@ -55,6 +61,8 @@ import { SelectGovernmentComponent } from './select-government/select-government LandUseComponent, OtherAttachmentsComponent, RosoProposalComponent, + ReviewAndSubmitComponent, + SubmitConfirmationDialogComponent, ], }) export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html index f608c3e8be..46b5f6be22 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html @@ -83,7 +83,6 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | [$noiSubmission]="$noiSubmission" [$noiDocuments]="$noiDocuments" [showErrors]="showValidationErrors" - [draftMode]="true" (navigateToStep)="switchStep($event)" (exit)="onExit()" > @@ -108,6 +107,13 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | -
Review and Submit
+
+ +
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts index 70c4567d05..46cbe54919 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts @@ -1,6 +1,8 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute } from '@angular/router'; +import { CodeService } from '../../../services/code/code.service'; import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; import { ToastService } from '../../../services/toast/toast.service'; @@ -35,7 +37,12 @@ describe('EditSubmissionComponent', () => { provide: MatDialog, useValue: {}, }, + { + provide: CodeService, + useValue: {}, + }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(EditSubmissionComponent); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts index a0cd07b3d8..c46203b65c 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts @@ -3,6 +3,7 @@ import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; +import { CodeService } from '../../../services/code/code.service'; import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; @@ -16,6 +17,7 @@ import { OtherAttachmentsComponent } from './other-attachments/other-attachments import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; import { RosoProposalComponent } from './proposal/roso/roso-proposal.component'; +import { SubmitConfirmationDialogComponent } from './review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; export enum EditNoiSteps { @@ -58,6 +60,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { constructor( private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private codeService: CodeService, private activatedRoute: ActivatedRoute, private dialog: MatDialog, private toastService: ToastService, @@ -150,6 +153,9 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { await this.rosoProposalComponent.onSave(); } break; + case EditNoiSteps.ReviewAndSubmit: + //DO NOTHING + break; default: this.toastService.showErrorToast('Error updating notice of intent.'); } @@ -165,6 +171,43 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { //TODO: Hook this up later } + async onSubmit() { + if (this.noiSubmission) { + const government = await this.loadGovernment(this.noiSubmission.localGovernmentUuid); + this.dialog + .open(SubmitConfirmationDialogComponent, { + data: { + governmentName: government?.name ?? 'selected local / first nation government', + }, + }) + .beforeClosed() + .subscribe((didConfirm) => { + if (didConfirm) { + this.submit(); + } + }); + } + } + + private async submit() { + const submission = this.noiSubmission; + if (submission) { + const didSubmit = await this.noticeOfIntentSubmissionService.submitToAlcs(submission.uuid); + if (didSubmit) { + await this.router.navigateByUrl(`/notice-of-intent/${submission?.fileNumber}`); + } + } + } + + private async loadGovernment(uuid: string) { + const codes = await this.codeService.loadCodes(); + const localGovernment = codes.localGovernments.find((a) => a.uuid === uuid); + if (localGovernment) { + return localGovernment; + } + return; + } + private async loadSubmission(fileId: string, reload = false) { if (!this.noiSubmission || reload) { this.overlayService.showSpinner(); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.html new file mode 100644 index 0000000000..9e095b9717 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.html @@ -0,0 +1,33 @@ +
+ +
+ +
+ +
+ + + +
+ + +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.spec.ts new file mode 100644 index 0000000000..d1c2937991 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.spec.ts @@ -0,0 +1,57 @@ +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 { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../../services/toast/toast.service'; + +import { ReviewAndSubmitComponent } from './review-and-submit.component'; + +describe('ReviewAndSubmitComponent', () => { + let component: ReviewAndSubmitComponent; + let fixture: ComponentFixture; + let mockToastService: DeepMocked; + let mockRouter: DeepMocked; + let mockNoiSubmissionService: DeepMocked; + + beforeEach(async () => { + mockToastService = createMock(); + mockRouter = createMock(); + mockNoiSubmissionService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ReviewAndSubmitComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoiSubmissionService, + }, + { + provide: PdfGenerationService, + useValue: {}, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ReviewAndSubmitComponent); + component = fixture.componentInstance; + component.$noiSubmission = new BehaviorSubject(undefined); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts new file mode 100644 index 0000000000..73b9b2c006 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts @@ -0,0 +1,67 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Router } from '@angular/router'; +import { BehaviorSubject, takeUntil } from 'rxjs'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { StepComponent } from '../step.partial'; + +@Component({ + selector: 'app-review-and-submit', + templateUrl: './review-and-submit.component.html', + styleUrls: ['./review-and-submit.component.scss'], +}) +export class ReviewAndSubmitComponent extends StepComponent implements OnInit, OnDestroy { + @Input() $noiDocuments!: BehaviorSubject; + @Input() updatedFields: string[] = []; + @Input() originalSubmissionUuid = ''; + @Output() submit = new EventEmitter(); + + noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + + constructor( + private router: Router, + private toastService: ToastService, + private applicationService: NoticeOfIntentSubmissionService, + private pdfGenerationService: PdfGenerationService + ) { + super(); + } + + ngOnInit(): void { + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + this.noiSubmission = submission; + }); + } + + override async onNavigateToStep(step: number) { + if (this.draftMode) { + await this.router.navigateByUrl(`alcs/application/${this.noiSubmission?.fileNumber}/edit/${step}?errors=t`); + } else { + await this.router.navigateByUrl(`application/${this.noiSubmission?.fileNumber}/edit/${step}?errors=t`); + } + } + + async onSubmitToAlcs() { + if (this.noiSubmission) { + const el = document.getElementsByClassName('error'); + if (el && el.length > 0) { + el[0].scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + this.toastService.showErrorToast('Please correct all errors before submitting the form'); + } else { + this.submit.emit(); + } + } + } + + async onDownloadPdf(fileNumber: string | undefined) { + if (fileNumber) { + await this.pdfGenerationService.generateSubmission(fileNumber); + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html new file mode 100644 index 0000000000..6f76d04448 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html @@ -0,0 +1,40 @@ +
+

Submit Application

+
+ +
+

Your notice of intent will be submitted to the {{ data.governmentName }}.

+
+
Terms and Conditions:
+ + I/we consent to the use of the information provided in the application and all supporting documents to process the + notice of intent in accordance with the Agricultural Land Commission Act, the Agricultural Land Reserve General + Regulation, and the Agricultural Land Reserve Use Regulation. + + + I/we declare that the information provided in the notice of intent and all the supporting documents are, to the + best of my/our knowledge, true and correct. + + + I/we understand that the Agricultural Land Commission will take the steps necessary to confirm the accuracy of the + information and documents provided. This information will be available for review by any member of the public. + +
+

+ If you have any questions about the collection or use of this information, please contact the Agricultural Land + Commission. +

+
+
+ + +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss new file mode 100644 index 0000000000..93f22cbbcb --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss @@ -0,0 +1,18 @@ +@use '../../../../../../styles/functions' as *; + +.checkbox { + margin: rem(10) 0; +} + +p { + margin: rem(28) 0 !important; +} + +.step-controls { + display: flex; + justify-content: flex-end; + + button:not(:last-child) { + margin-right: rem(16) !important; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts new file mode 100644 index 0000000000..7d6d06ed68 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { SubmitConfirmationDialogComponent } from './submit-confirmation-dialog.component'; + +describe('SubmitConfirmationDialogComponent', () => { + let component: SubmitConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SubmitConfirmationDialogComponent], + providers: [ + { + provide: MatDialog, + useValue: {}, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + { + provide: MAT_DIALOG_DATA, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmitConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts new file mode 100644 index 0000000000..cf84e55daa --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts @@ -0,0 +1,16 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'app-submit-confirmation-dialog', + templateUrl: './submit-confirmation-dialog.component.html', + styleUrls: ['./submit-confirmation-dialog.component.scss'], +}) +export class SubmitConfirmationDialogComponent { + constructor( + @Inject(MAT_DIALOG_DATA) + protected data: { + governmentName: string; + } + ) {} +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.html new file mode 100644 index 0000000000..d4ad467c8f --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.html @@ -0,0 +1,263 @@ +
+ +
+
+

2. Primary Contact

+ + Changes made to this section will not be flagged as + - ensure accuracy before saving. + +
+
Type
+
+ {{ primaryContact?.type?.label }} + +
+
First Name
+
+ {{ primaryContact?.firstName }} + +
+
Last Name
+
+ {{ primaryContact?.lastName }} + +
+
+ Organization (optional) + Ministry/Department Responsible + Department +
+
+ {{ primaryContact?.organizationName }} + +
+
Phone
+
+ {{ primaryContact?.phoneNumber }} + + Invalid Format +
+
Email
+
+ {{ primaryContact?.email }} + + Invalid Format +
+ +
Authorization Letter(s)
+
+ + + + Authorization letters are not required, please remove them + +
+
+
+ +
+
+
+
+

3. Government

+
+
+ Local or First Nation Government + +
+
+ {{ localGovernment?.name }} + + + This Local/First Nation Government has not yet been set up with the ALC Portal to receive notice of intents. To + submit, you will need to contact the ALC directly:  ALC.Portal@gov.bc.ca / 236-468-3342 + +
+
+ +
+
+
+
+

4. Land Use

+
+
+

Land Use of Parcel(s) under Notice of Intent

+
+
+ Quantify and describe in detail all agriculture that currently takes place on the parcel(s). + +
+
+ {{ noiSubmission.parcelsAgricultureDescription }} + +
+
+ Quantify and describe in detail all agricultural improvements made to the parcel(s). + +
+
+ {{ noiSubmission.parcelsAgricultureImprovementDescription }} + +
+
+ Quantify and describe all non-agricultural uses that currently take place on the parcel(s). + +
+
+ {{ noiSubmission.parcelsNonAgricultureUseDescription }} + +
+
+

+ Land Use of Adjacent Parcels + +

+
+
+
+
Main Land Use Type
+
Specific Activity
+
North
+
+ {{ noiSubmission.northLandUseType }} + +
+
+ {{ noiSubmission.northLandUseTypeDescription }} + +
+
East
+
+ {{ noiSubmission.eastLandUseType }} + +
+
+ {{ noiSubmission.eastLandUseTypeDescription }} + +
+
South
+
+ {{ noiSubmission.southLandUseType }} + +
+
+ {{ noiSubmission.southLandUseTypeDescription }} + +
+
West
+
+ {{ noiSubmission.westLandUseType }} + +
+
+ {{ noiSubmission.westLandUseTypeDescription }} + +
+
+
+ +
+
+
+
+

5. Proposal

+ +
+
+

6. Additional Information

+ TODO +
+
+

7. Optional Documents

+
+
+
Type
+
Description
+
File Name
+ +
+ {{ file.type?.label }} + +
+
+ {{ file.description }} + +
+ +
+
+ +
+
+
+ +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.scss b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.scss new file mode 100644 index 0000000000..8df832ca0f --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.scss @@ -0,0 +1,143 @@ +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; + +: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; + } + + .no-data-text { + text-align: center; + color: colors.$grey; + padding-top: rem(12); + + .error { + justify-content: center; + } + } + + .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; + } + + .other-attachments { + display: grid; + grid-template-columns: max-content max-content max-content; + overflow-x: auto; + grid-column-gap: rem(36); + grid-row-gap: rem(12); + + .full-width { + grid-column: 1/3; + } + } + + .adjacent-parcels { + display: grid; + grid-template-columns: max-content max-content max-content; + overflow-x: auto; + grid-column-gap: rem(36); + grid-row-gap: rem(12); + + .full-width { + grid-column: 1/4; + } + } + + .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; + } + + .edit-button { + grid-column: 1/5; + } + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.spec.ts new file mode 100644 index 0000000000..470a008ab5 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.spec.ts @@ -0,0 +1,71 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { CodeService } from '../../../services/code/code.service'; +import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { ToastService } from '../../../services/toast/toast.service'; + +import { NoticeOfIntentDetailsComponent } from './notice-of-intent-details.component'; + +describe('NoticeOfIntentDetailsComponent', () => { + let component: NoticeOfIntentDetailsComponent; + let fixture: ComponentFixture; + let mockCodeService: DeepMocked; + let mockNoiDocumentService: DeepMocked; + let mockRouter: DeepMocked; + let mockToastService: DeepMocked; + let mockNoiSubmissionService: DeepMocked; + + let noiDocumentPipe = new BehaviorSubject([]); + + beforeEach(async () => { + mockCodeService = createMock(); + mockNoiDocumentService = createMock(); + mockRouter = createMock(); + mockNoiSubmissionService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: CodeService, + useValue: mockCodeService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNoiDocumentService, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoiSubmissionService, + }, + ], + declarations: [NoticeOfIntentDetailsComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(NoticeOfIntentDetailsComponent); + component = fixture.componentInstance; + component.$noticeOfIntentSubmission = new BehaviorSubject( + undefined + ); + component.$noiDocuments = noiDocumentPipe; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts new file mode 100644 index 0000000000..75c34df486 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts @@ -0,0 +1,120 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; +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 { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../shared/dto/owner.dto'; + +@Component({ + selector: 'app-noi-details', + templateUrl: './notice-of-intent-details.component.html', + styleUrls: ['./notice-of-intent-details.component.scss'], +}) +export class NoticeOfIntentDetailsComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + + @Input() $noticeOfIntentSubmission!: BehaviorSubject; + @Input() $noiDocuments!: BehaviorSubject; + @Input() showErrors = true; + @Input() showEdit = true; + @Input() draftMode = false; + @Input() originalSubmissionUuid = ''; + @Input() updatedFields: string[] = []; + + parcelType = PARCEL_TYPE; + noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + primaryContact: NoticeOfIntentOwnerDto | undefined; + localGovernment: LocalGovernmentDto | undefined; + authorizationLetters: NoticeOfIntentDocumentDto[] = []; + otherFiles: NoticeOfIntentDocumentDto[] = []; + needsAuthorizationLetter = true; + appDocuments: NoticeOfIntentDocumentDto[] = []; + OWNER_TYPE = OWNER_TYPE; + + private localGovernments: LocalGovernmentDto[] = []; + private otherFileTypes = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; + + constructor( + private codeService: CodeService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private router: Router + ) {} + + ngOnInit(): void { + this.loadGovernments(); + this.$noticeOfIntentSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + this.noiSubmission = noiSubmission; + if (noiSubmission) { + this.primaryContact = noiSubmission.owners.find( + (owner) => owner.uuid === noiSubmission.primaryContactOwnerUuid + ); + this.populateLocalGovernment(noiSubmission.localGovernmentUuid); + + this.needsAuthorizationLetter = + !(this.primaryContact?.type.code === OWNER_TYPE.GOVERNMENT) && + !( + noiSubmission.owners.length === 1 && + (noiSubmission.owners[0].type.code === OWNER_TYPE.INDIVIDUAL || + noiSubmission.owners[0].type.code === OWNER_TYPE.GOVERNMENT) + ); + } + }); + + this.$noiDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.otherFiles = documents + .filter((file) => (file.type ? this.otherFileTypes.includes(file.type.code) : true)) + .filter((file) => file.source === DOCUMENT_SOURCE.APPLICANT) + .sort((a, b) => { + return a.uploadedAt - b.uploadedAt; + }); + + this.authorizationLetters = documents + .filter((file) => file.type?.code === DOCUMENT_TYPE.AUTHORIZATION_LETTER) + .filter((file) => file.source === DOCUMENT_SOURCE.APPLICANT) + .sort((a, b) => { + return a.uploadedAt - b.uploadedAt; + }); + + this.appDocuments = documents; + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + async openFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } + + async onNavigateToStep(step: number) { + if (this.draftMode) { + await this.router.navigateByUrl(`alcs/notice-of-intent/${this.noiSubmission?.fileNumber}/edit/${step}?errors=t`); + } else { + await this.router.navigateByUrl(`notice-of-intent/${this.noiSubmission?.fileNumber}/edit/${step}?errors=t`); + } + } + + private async loadGovernments() { + const codes = await this.codeService.loadCodes(); + this.localGovernments = codes.localGovernments.sort((a, b) => (a.name > b.name ? 1 : -1)); + if (this.noiSubmission?.localGovernmentUuid) { + this.populateLocalGovernment(this.noiSubmission?.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/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts new file mode 100644 index 0000000000..7fbf9b5cd3 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NgxMaskPipe } from 'ngx-mask'; +import { SharedModule } from '../../../shared/shared.module'; +import { NoticeOfIntentDetailsComponent } from './notice-of-intent-details.component'; +import { ParcelComponent } from './parcel/parcel.component'; +import { RosoDetailsComponent } from './roso-details/roso-details.component'; + +@NgModule({ + declarations: [ParcelComponent, RosoDetailsComponent, NoticeOfIntentDetailsComponent], + imports: [CommonModule, SharedModule, NgxMaskPipe], + exports: [NoticeOfIntentDetailsComponent], +}) +export class NoticeOfIntentDetailsModule {} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.html new file mode 100644 index 0000000000..41191e9061 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.html @@ -0,0 +1,171 @@ +

1. Identify Parcel(s) Under Application

+
+ +
+

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

+
+ +
+
+
Parcel 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' }} + + + Invalid Format + +
+ +
+ PIN (optional) +
+
+ {{ parcel.pin }} + +
+
+ +
+ Purchase Date + +
+
+ {{ parcel.purchasedDate | date }} + + +
+
+
+ Farm Classification + +
+
+ {{ parcel.isFarm ? 'Yes' : 'No' }} + +
+
+ Civic Address + +
+
+ {{ parcel.civicAddress }} + +
+ +
Crown Selection
+
+ {{ parcel.crownLandOwnerType }} + +
+
+
+ Certificate Of Title + +
+ +
+ Owner Information +
+
+
Type
+
Full Name
+
Organization
+
+ Ministry/ Department +
+
Phone
+
Email
+
Corporate Summary
+ +
{{ owner.type.label }}
+
{{ owner.displayName }}
+
+ {{ owner.organizationName }} + +
+
{{ owner.phoneNumber }}
+
{{ owner.email }}
+ +
+
+ +
+
+
+ I confirm that the owner information provided above matches the current Certificate of Title +
+
+ Yes + +
+
+ +
+
+
+ +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.scss b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.scss new file mode 100644 index 0000000000..ecf9d4a24c --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.scss @@ -0,0 +1,39 @@ +@use '../../../../../styles/functions' as *; + +.owner-information { + display: grid; + grid-template-columns: max-content max-content max-content max-content max-content max-content; + overflow-x: auto; + grid-column-gap: rem(36); + grid-row-gap: rem(12); + + .full-width { + grid-column: 1/3; + } +} + +.review-table { + grid-template-columns: 1fr 1fr !important; + + .full-width { + grid-column: 1/3; + } + + .edit-button { + grid-column: 1/3; + } + + @media screen and (min-width: $tabletBreakpoint) { + .full-width { + grid-column: 1/5; + } + + .edit-button { + grid-column: 1/5; + } + } +} + +.crown-land { + text-transform: capitalize; +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.spec.ts new file mode 100644 index 0000000000..30e85cf5d5 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.spec.ts @@ -0,0 +1,59 @@ +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 { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { ParcelComponent } from './parcel.component'; + +describe('ParcelComponent', () => { + let component: ParcelComponent; + let fixture: ComponentFixture; + + let mockNoiParcelService: DeepMocked; + let mockNoiOwnerService: DeepMocked; + let mockNoiDocService: DeepMocked; + + beforeEach(async () => { + mockNoiParcelService = createMock(); + mockNoiOwnerService = createMock(); + mockNoiDocService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ParcelComponent], + providers: [ + { + provide: NoticeOfIntentParcelService, + useValue: mockNoiParcelService, + }, + { + provide: NoticeOfIntentOwnerService, + useValue: mockNoiOwnerService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNoiDocService, + }, + { + provides: Router, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelComponent); + component = fixture.componentInstance; + component.$noticeOfIntentSubmission = new BehaviorSubject( + undefined + ); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.ts new file mode 100644 index 0000000000..240116e40c --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/parcel/parcel.component.ts @@ -0,0 +1,222 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { getDiff } from 'recursive-diff'; +import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; +import { PARCEL_OWNERSHIP_TYPE } from '../../../../services/application-parcel/application-parcel.dto'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentOwnerDto } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwnerService } from '../../../../services/notice-of-intent-owner/notice-of-intent-owner.service'; +import { + NoticeOfIntentParcelDto, + NoticeOfIntentParcelUpdateDto, +} from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { BaseCodeDto } from '../../../../shared/dto/base.dto'; +import { formatBooleanToYesNoString } from '../../../../shared/utils/boolean-helper'; +import { getLetterCombinations } from '../../../../shared/utils/number-to-letter-helper'; + +export class NoticeOfIntentParcelBasicValidation { + // indicates general validity check state, including owner related information + isInvalid = false; + + isTypeRequired = false; + isPidRequired = false; + isPinRequired = false; + isLegalDescriptionRequired = false; + isMapAreaHectaresRequired = false; + isPurchasedDateRequired = false; + isFarmRequired = false; + isCertificateRequired = false; + isCertificateUploaded = false; + isConfirmedByApplicant = false; + isCrownSelectionMandatory = false; +} + +interface NoticeOfIntentParcelExtended extends Omit { + isFarmText?: string; + ownershipType?: BaseCodeDto; + validation?: NoticeOfIntentParcelBasicValidation; + owners: NoticeOfIntentOwnerDto[]; + certificateOfTitle?: NoticeOfIntentDocumentDto; +} + +@Component({ + selector: 'app-parcel', + templateUrl: './parcel.component.html', + styleUrls: ['./parcel.component.scss'], +}) +export class ParcelComponent { + $destroy = new Subject(); + + @Input() $noticeOfIntentSubmission!: BehaviorSubject; + @Input() originalSubmissionUuid: string | undefined; + @Input() showErrors = true; + @Input() showEdit = true; + @Input() draftMode = false; + + PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; + + fileId = ''; + submissionUuid = ''; + parcels: NoticeOfIntentParcelExtended[] = []; + noticeOfIntentSubmission!: NoticeOfIntentSubmissionDetailedDto; + updatedFields: string[] = []; + + constructor( + private noticeOfIntentParcelService: NoticeOfIntentParcelService, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private ownerService: NoticeOfIntentOwnerService, + private router: Router + ) {} + + ngOnInit(): void { + this.$noticeOfIntentSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.fileId = noiSubmission.fileNumber; + this.submissionUuid = noiSubmission.uuid; + this.noticeOfIntentSubmission = noiSubmission; + this.loadParcels().then(async () => await this.validateParcelDetails()); + } + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + async loadParcels() { + const parcels = (await this.noticeOfIntentParcelService.fetchBySubmissionUuid(this.submissionUuid)) || []; + this.parcels = parcels.map((p) => ({ ...p, isFarmText: formatBooleanToYesNoString(p.isFarm) })); + + if (this.originalSubmissionUuid) { + await this.calculateParcelDiffs(this.originalSubmissionUuid); + } + } + + private async calculateParcelDiffs(originalSubmissionUuid: string) { + const oldParcels = await this.noticeOfIntentParcelService.fetchBySubmissionUuid(originalSubmissionUuid); + if (oldParcels) { + const diffResult = getDiff(oldParcels, this.parcels); + const changedFields = new Set(); + for (const diff of diffResult) { + const partialPath = []; + const fullPath = diff.path.join('.'); + if (!fullPath.toLowerCase().includes('uuid')) { + for (const path of diff.path) { + if (typeof path !== 'string' || !path.includes('Uuid')) { + partialPath.push(path); + changedFields.add(partialPath.join('.')); + } + } + if ((diff.op = 'add') && typeof diff.val === 'object') { + for (const key of Object.keys(diff.val)) { + changedFields.add(`${diff.path.join('.')}.${key}`); + } + } + } + } + this.updatedFields = [...changedFields.keys()]; + } + } + + private async validateParcelDetails() { + if (this.parcels) { + this.parcels.forEach((p) => { + p.validation = this.validateParcelBasic(p); + }); + } + } + + async onOpenFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + if (res) { + window.open(res.url, '_blank'); + } + } + + private validateParcelBasic(parcel: NoticeOfIntentParcelDto) { + const validation = new NoticeOfIntentParcelBasicValidation(); + + if (!parcel.ownershipType) { + validation.isTypeRequired = true; + + if (!parcel.pid && !parcel.pin) { + validation.isPidRequired = true; + } + + if (!parcel.purchasedDate) { + validation.isPurchasedDateRequired = true; + } + } + + if (!parcel.pid && parcel.ownershipType?.code === this.PARCEL_OWNERSHIP_TYPES.FEE_SIMPLE) { + validation.isPidRequired = true; + } + + if (!parcel.legalDescription) { + validation.isLegalDescriptionRequired = true; + } + + if (!parcel.mapAreaHectares) { + validation.isMapAreaHectaresRequired = true; + } + + if (!parcel.purchasedDate && parcel.ownershipType?.code === this.PARCEL_OWNERSHIP_TYPES.FEE_SIMPLE) { + validation.isPurchasedDateRequired = true; + } + + if (parcel.ownershipType?.code === this.PARCEL_OWNERSHIP_TYPES.CROWN) { + validation.isCrownSelectionMandatory = true; + } + + if (!parcel.isFarm) { + validation.isFarmRequired = true; + } + + validation.isCertificateUploaded = !!parcel.certificateOfTitle; + const isCrownWithPid = + parcel.ownershipType?.code === this.PARCEL_OWNERSHIP_TYPES.CROWN && parcel.pid && parcel.pid.length > 0; + const isFeeSimple = parcel.ownershipType?.code === this.PARCEL_OWNERSHIP_TYPES.FEE_SIMPLE; + if (isCrownWithPid || isFeeSimple) { + validation.isCertificateRequired = true; + } + + validation.isInvalid = this.isInvalid(validation); + + return validation; + } + + private isInvalid(validationObj: NoticeOfIntentParcelBasicValidation) { + for (const prop in validationObj) { + if (validationObj[prop as keyof typeof validationObj]) { + return true; + } + } + + return false; + } + + async onEditParcelsClick($event: any) { + $event.stopPropagation(); + if (this.draftMode) { + await this.router.navigateByUrl(`alcs/notice-of-intent/${this.fileId}/edit/0?errors=t`); + } else { + await this.router.navigateByUrl(`notice-of-intent/${this.fileId}/edit/0?errors=t`); + } + } + + async onEditParcelClick(uuid: string) { + if (this.draftMode) { + await this.router.navigateByUrl(`alcs/notice-of-intent/${this.fileId}/edit/0?parcelUuid=${uuid}&errors=t`); + } else { + await this.router.navigateByUrl(`notice-of-intent/${this.fileId}/edit/0?parcelUuid=${uuid}&errors=t`); + } + } + + getLetterIndex(num: number) { + return getLetterCombinations(num); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html new file mode 100644 index 0000000000..5969731d85 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html @@ -0,0 +1,186 @@ +
+
+ Has the ALC previously received an application or Notice of Intent for this proposal? + +
+
+ + {{ _noiSubmission.soilIsFollowUp ? 'Yes' : 'No' }} + + +
+ +
+ Application or NOI ID + +
+
+ {{ _noiSubmission.soilFollowUpIDs }} + +
+ +
+ What is the purpose of the proposal? + +
+
+ {{ _noiSubmission.purpose }} + +
+ +
+ Describe the type of soil proposed to be removed. + +
+
+ {{ _noiSubmission.soilTypeRemoved }} + +
+ +
+
+
+
+ Soil to be Removed + +
+
Volume
+
+ {{ _noiSubmission.soilToRemoveVolume }} + m3 + +
+
Area
+
+ {{ _noiSubmission.soilToRemoveArea }} ha + +
+
Maximum Depth
+
+ {{ _noiSubmission.soilToRemoveMaximumDepth }} + m + +
+
Average Depth
+
+ {{ _noiSubmission.soilToRemoveAverageDepth }} + m + +
+
+ +
+
+
+
+ Soil already Removed + +
+
Volume
+
+ {{ _noiSubmission.soilAlreadyRemovedVolume }} + m3 + +
+
Area
+
+ {{ _noiSubmission.soilAlreadyRemovedArea }} + ha + +
+
Maximum Depth
+
+ {{ _noiSubmission.soilAlreadyRemovedMaximumDepth }} + m + +
+
Average Depth
+
+ {{ _noiSubmission.soilAlreadyRemovedAverageDepth }} + m + +
+
+ +
+

Project Duration

+
+
+ Duration +
+
+ {{ _noiSubmission.soilProjectDurationUnit }} + +
+
+ Length +
+
+ {{ _noiSubmission.soilProjectDurationAmount }} + +
+ +
Proposal Map / Site Plan
+ + +
Is your proposal for aggregate extraction or placer mining?
+
+ + {{ _noiSubmission.soilIsExtractionOrMining ? 'Yes' : 'No' }} + + +
+ + +
Cross Sections
+ + +
Reclamation Plan
+ + +
+ Have you submitted a Notice of Work to the Ministry of Energy, Mines and Low Carbon Innovation (EMLI)? +
+
+ + {{ _noiSubmission.soilHasSubmittedNotice ? 'Yes' : 'No' }} + + +
+ + +
Notice of Work
+ +
+
+ +
+ +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.scss b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.scss new file mode 100644 index 0000000000..0c540ce5fb --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.scss @@ -0,0 +1,9 @@ +@use '../../../../../styles/functions' as *; + +.soil-table { + display: grid; + grid-template-columns: max-content max-content; + overflow-x: auto; + grid-column-gap: rem(36); + grid-row-gap: rem(12); +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.spec.ts new file mode 100644 index 0000000000..44fdd3578f --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; + +import { RosoDetailsComponent } from './roso-details.component'; + +describe('RosoDetailsComponent', () => { + let component: RosoDetailsComponent; + let fixture: ComponentFixture; + let mockNoiDocumentService: DeepMocked; + let mockNoiParcelService: DeepMocked; + + beforeEach(async () => { + mockNoiParcelService = createMock(); + mockNoiDocumentService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [RosoDetailsComponent], + providers: [ + { + provide: NoticeOfIntentDocumentService, + useValue: mockNoiDocumentService, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNoiParcelService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(RosoDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.ts new file mode 100644 index 0000000000..cceb8fc61a --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.ts @@ -0,0 +1,55 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; + +@Component({ + selector: 'app-roso-details[noiSubmission]', + templateUrl: './roso-details.component.html', + styleUrls: ['./roso-details.component.scss'], +}) +export class RosoDetailsComponent { + @Input() showErrors = true; + @Input() showEdit = true; + @Input() draftMode = false; + @Input() updatedFields: string[] = []; + + _noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + + @Input() set noiSubmission(noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined) { + if (noiSubmission) { + this._noiSubmission = noiSubmission; + } + } + + @Input() set noiDocuments(documents: NoticeOfIntentDocumentDto[]) { + this.crossSections = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.CROSS_SECTIONS); + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.reclamationPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.RECLAMATION_PLAN); + this.noticeOfWorks = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.NOTICE_OF_WORK); + } + + crossSections: NoticeOfIntentDocumentDto[] = []; + proposalMap: NoticeOfIntentDocumentDto[] = []; + reclamationPlans: NoticeOfIntentDocumentDto[] = []; + noticeOfWorks: NoticeOfIntentDocumentDto[] = []; + + constructor(private router: Router, private noticeOfIntentDocumentService: NoticeOfIntentDocumentService) {} + + async onEditSection(step: number) { + if (this.draftMode) { + await this.router.navigateByUrl( + `/alcs/notice-of-intent/${this._noiSubmission?.fileNumber}/edit/${step}?errors=t` + ); + } else { + await this.router.navigateByUrl(`notice-of-intent/${this._noiSubmission?.fileNumber}/edit/${step}?errors=t`); + } + } + + async openFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } +} diff --git a/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.html b/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.html index fc46ac4787..d5915e5dbf 100644 --- a/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.html +++ b/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.html @@ -4,7 +4,7 @@
{{ file.fileName }} @@ -27,7 +27,7 @@ *ngIf="!uploadedFiles.length || allowMultiple" [ngClass]="{ 'desktop-file-drag-drop': true, - 'error-outline': isRequired && showErrors && !uploadedFiles.length, + 'error-outline': isRequired && !disabled && showErrors && !uploadedFiles.length, disabled: disabled }" dragDropFile @@ -52,6 +52,6 @@ > Upload File -This file upload is required From 1c633f1f1d6b38839e1c023f388fe862b69fbae6 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 14 Aug 2023 15:10:19 -0700 Subject: [PATCH 242/954] Hide rescinded decision fields instead of disabling * Add new requireComponents flag to make components optional for rescinded decisions --- .../decision-input-v2.component.html | 50 +++++++++++-------- .../decision-input-v2.component.ts | 18 +++---- .../decision-v2/decision-v2.component.html | 23 +++------ 3 files changed, 43 insertions(+), 48 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html index f0bb7692b7..d500be1a92 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html @@ -26,7 +26,9 @@

Decisions > Edit Decision Draft

mat-stroked-button color="primary" (click)="onGenerateResolutionNumber()" - [ngClass]="{ 'error-field-outlined ng-invalid': !resolutionNumberControl.valid && resolutionNumberControl.touched }" + [ngClass]="{ + 'error-field-outlined ng-invalid': !resolutionNumberControl.valid && resolutionNumberControl.touched + }" > Generate Number @@ -151,7 +153,8 @@

Decisions > Edit Decision Draft

name="isSubjectToConditions" (change)="onChangeSubjectToConditions($event)" [ngClass]="{ - 'error-field-outlined': !form.controls.isSubjectToConditions.valid && form.controls.isSubjectToConditions.touched + 'error-field-outlined': + !form.controls.isSubjectToConditions.valid && form.controls.isSubjectToConditions.touched }" > Yes @@ -174,7 +177,8 @@

Decisions > Edit Decision Draft

formControlName="isStatsRequired" name="isStatsRequired" [ngClass]="{ - 'error-field-outlined ng-invalid': !form.controls.isStatsRequired.valid && form.controls.isStatsRequired.touched + 'error-field-outlined ng-invalid': + !form.controls.isStatsRequired.valid && form.controls.isStatsRequired.touched }" > Yes @@ -187,25 +191,29 @@

Decisions > Edit Decision Draft

- - Rescinded Date - - - - + + + Rescinded Date + + + + +
- - Rescinded Comment - - + + + Rescinded Comment + + +
Decisions > Edit Decision Draft
[fileNumber]="fileNumber" [components]="components" (componentsChange)="onComponentChange($event)" - [showError]="form.touched && components.length < 1" + [showError]="form.touched && components.length < 1 && requireComponents" > diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index 2bc38b496b..15eaaee3f1 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -54,6 +54,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { minDate = new Date(0); isFirstDecision = false; showComponents = false; + requireComponents = false; showConditions = false; conditionsValid = true; componentsValid = true; @@ -98,8 +99,8 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { decisionDescription: new FormControl(undefined, [Validators.required]), isStatsRequired: new FormControl(undefined, [Validators.required]), daysHideFromPublic: new FormControl('2', [Validators.required]), - rescindedDate: new FormControl({ disabled: true, value: null }), - rescindedComment: new FormControl({ disabled: true, value: null }), + rescindedDate: new FormControl(null), + rescindedComment: new FormControl(null), }); constructor( @@ -329,15 +330,16 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { this.components = existingDecision.components; } + this.requireComponents = ['APPR', 'APPA'].includes(existingDecision.outcome.code); + if (['APPR', 'APPA', 'RESC'].includes(existingDecision.outcome.code)) { this.showComponents = true; } else { + this.showComponents = false; this.form.controls.isSubjectToConditions.disable(); } if (existingDecision.outcome.code === 'RESC') { - this.form.controls.rescindedComment.enable(); - this.form.controls.rescindedDate.enable(); this.form.controls.rescindedComment.setValidators([Validators.required]); this.form.controls.rescindedDate.setValidators([Validators.required]); } @@ -529,7 +531,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { private runValidation() { this.form.markAllAsTouched(); const requiresConditions = this.showConditions; - const requiresComponents = this.showComponents; + const requiresComponents = this.showComponents && this.requireComponents; if (this.decisionComponentsComponent) { this.decisionComponentsComponent.onValidate(); @@ -618,16 +620,12 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { }); } - if (selectedOutcome.code === 'RESC' && this.form.controls.rescindedComment.disabled) { - this.form.controls.rescindedComment.enable(); - this.form.controls.rescindedDate.enable(); + if (selectedOutcome.code === 'RESC') { this.form.controls.rescindedComment.setValidators([Validators.required]); this.form.controls.rescindedDate.setValidators([Validators.required]); this.form.controls.rescindedComment.updateValueAndValidity(); this.form.controls.rescindedDate.updateValueAndValidity(); } else if (this.form.controls.rescindedComment.enabled) { - this.form.controls.rescindedComment.disable(); - this.form.controls.rescindedDate.disable(); this.form.controls.rescindedComment.setValue(null); this.form.controls.rescindedDate.setValue(null); this.form.controls.rescindedComment.setValidators([]); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html index b912c09605..c496fc29d5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html @@ -5,13 +5,6 @@

Decision

- - @@ -156,21 +149,17 @@

Condition Compliance

(save)="onStatsRequiredUpdate(decision.uuid, $event)" >
-
-
Rescinded Date
- {{ decision.rescindedDate | momentFormat }} - -
+
+
Rescinded Date
+ {{ decision.rescindedDate | momentFormat }} + +
Rescinded Comment
{{ decision.rescindedComment }} - +
From dc414877a520894febf0fe76f1f5234f36e3fa66 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 14 Aug 2023 15:19:26 -0700 Subject: [PATCH 243/954] Change NOI Parcel Dialog to use NOI Parcel Service * Was accidentally still using Application Parcel Service --- .../delete-parcel-dialog.component.spec.ts | 15 +++++++-------- .../delete-parcel-dialog.component.ts | 12 ++++++------ .../parcel-entry-confirmation-dialog.component.ts | 6 +++--- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts index 9beff5eaf0..59d4818b54 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts @@ -1,21 +1,20 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - import { HttpClient } from '@angular/common/http'; import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; import { DeleteParcelDialogComponent } from './delete-parcel-dialog.component'; describe('DeleteParcelDialogComponent', () => { let component: DeleteParcelDialogComponent; let fixture: ComponentFixture; let mockHttpClient: DeepMocked; - let mockApplicationParcelService: DeepMocked; + let mockNoiParcelService: DeepMocked; beforeEach(async () => { mockHttpClient = createMock(); - mockApplicationParcelService = createMock(); + mockNoiParcelService = createMock(); await TestBed.configureTestingModule({ declarations: [DeleteParcelDialogComponent], @@ -25,8 +24,8 @@ describe('DeleteParcelDialogComponent', () => { useValue: mockHttpClient, }, { - provide: ApplicationParcelService, - useValue: mockApplicationParcelService, + provide: NoticeOfIntentParcelService, + useValue: mockNoiParcelService, }, { provide: MatDialogRef, diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts index f86e0979ab..827eabbefd 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts @@ -1,8 +1,8 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; -export enum ApplicationParcelDeleteStepsEnum { +export enum NoticeOfIntentParcelDeleteStepsEnum { warning = 0, confirmation = 1, } @@ -18,11 +18,11 @@ export class DeleteParcelDialogComponent { stepIdx = 0; - warningStep = ApplicationParcelDeleteStepsEnum.warning; - confirmationStep = ApplicationParcelDeleteStepsEnum.confirmation; + warningStep = NoticeOfIntentParcelDeleteStepsEnum.warning; + confirmationStep = NoticeOfIntentParcelDeleteStepsEnum.confirmation; constructor( - private applicationParcelService: ApplicationParcelService, + private noticeOfIntentParcelService: NoticeOfIntentParcelService, private dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: DeleteParcelDialogComponent ) { @@ -43,7 +43,7 @@ export class DeleteParcelDialogComponent { } async onDelete() { - const result = await this.applicationParcelService.deleteMany([this.parcelUuid]); + const result = await this.noticeOfIntentParcelService.deleteMany([this.parcelUuid]); if (result) { this.onCancel(true); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts index dfe4bb5e16..7a5436516d 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { MatDialogRef } from '@angular/material/dialog'; -import { ApplicationParcelDeleteStepsEnum } from '../../delete-parcel/delete-parcel-dialog.component'; +import { NoticeOfIntentParcelDeleteStepsEnum } from '../../delete-parcel/delete-parcel-dialog.component'; @Component({ selector: 'app-parcel-entry-confirmation-dialog', @@ -10,8 +10,8 @@ import { ApplicationParcelDeleteStepsEnum } from '../../delete-parcel/delete-par export class ParcelEntryConfirmationDialogComponent { stepIdx = 0; - warningStep = ApplicationParcelDeleteStepsEnum.warning; - confirmationStep = ApplicationParcelDeleteStepsEnum.confirmation; + warningStep = NoticeOfIntentParcelDeleteStepsEnum.warning; + confirmationStep = NoticeOfIntentParcelDeleteStepsEnum.confirmation; constructor(private dialogRef: MatDialogRef) {} From 0329386730e66d943d15aa97d835bc783b98d319 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 14 Aug 2023 16:18:57 -0700 Subject: [PATCH 244/954] Bug fixes for Decision Dialogs * Sort statuses by weight, not date, to find correct revert status * Update missed DTO to new codes value to fix release dialog --- .../revert-to-draft-dialog.component.ts | 3 ++- .../src/app/services/application/application-code.dto.ts | 2 +- .../src/app/services/application/application.dto.ts | 8 ++++---- .../src/app/services/application/application.service.ts | 2 +- .../submission-status.dto.ts | 6 +++--- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts index 874f314f32..c9a77874a7 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts @@ -29,7 +29,8 @@ export class RevertToDraftDialogComponent { status.statusTypeCode !== SUBMISSION_STATUS.ALC_DECISION && status.effectiveDate < Date.now() ) - .sort((a, b) => a.effectiveDate! - b.effectiveDate!); + .sort((a, b) => b.status.weight! - a.status.weight!); + if (validStatuses && validStatuses.length > 0) { const newStatus = validStatuses[0].status; this.mappedType = { diff --git a/alcs-frontend/src/app/services/application/application-code.dto.ts b/alcs-frontend/src/app/services/application/application-code.dto.ts index ff201daca9..1d7307f365 100644 --- a/alcs-frontend/src/app/services/application/application-code.dto.ts +++ b/alcs-frontend/src/app/services/application/application-code.dto.ts @@ -16,5 +16,5 @@ export interface ApplicationMasterCodesDto { status: CardStatusDto[]; region: ApplicationRegionDto[]; reconsiderationType: ReconsiderationTypeDto[]; - applicationStatuses: ApplicationStatusDto[]; + applicationStatusType: ApplicationStatusDto[]; } diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 83fd61d3f7..7e350fc3e4 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -22,11 +22,11 @@ export enum SUBMISSION_STATUS { SUBMITTED_TO_LG = 'SUBG', //Submitted to L/FNG IN_REVIEW_BY_LG = 'REVG', //new Under Review by L/FNG SUBMITTED_TO_ALC = 'SUBM', //Submitted to ALC - SUBMITTED_TO_ALC_INCOMPLETE = 'SUIN', //new Submitted to ALC - Incomplete - RECEIVED_BY_ALC = 'RECA', //new Received By ALC - IN_REVIEW_BY_ALC = 'REVA', //new Under Review by ALC + SUBMITTED_TO_ALC_INCOMPLETE = 'SUIN', //Submitted to ALC - Incomplete + RECEIVED_BY_ALC = 'RECA', //Received By ALC + IN_REVIEW_BY_ALC = 'REVA', //Under Review by ALC ALC_DECISION = 'ALCD', // Decision Released - REFUSED_TO_FORWARD_LG = 'RFFG', //new L/FNG Refused to Forward + REFUSED_TO_FORWARD_LG = 'RFFG', //L/FNG Refused to Forward CANCELLED = 'CANC', //Cancelled } diff --git a/alcs-frontend/src/app/services/application/application.service.ts b/alcs-frontend/src/app/services/application/application.service.ts index 3e04215beb..feeddfa362 100644 --- a/alcs-frontend/src/app/services/application/application.service.ts +++ b/alcs-frontend/src/app/services/application/application.service.ts @@ -97,7 +97,7 @@ export class ApplicationService { this.regions = codes.region; this.$applicationRegions.next(this.regions); - this.applicationStatuses = codes.applicationStatuses; + this.applicationStatuses = codes.applicationStatusType; this.$applicationStatuses.next(this.applicationStatuses); } diff --git a/services/apps/alcs/src/alcs/application/application-submission-status/submission-status.dto.ts b/services/apps/alcs/src/alcs/application/application-submission-status/submission-status.dto.ts index 5ff8a00827..c2ffce23b3 100644 --- a/services/apps/alcs/src/alcs/application/application-submission-status/submission-status.dto.ts +++ b/services/apps/alcs/src/alcs/application/application-submission-status/submission-status.dto.ts @@ -6,11 +6,11 @@ export enum SUBMISSION_STATUS { INCOMPLETE = 'INCM', // L/FNG Returned as Incomplete WRONG_GOV = 'WRNG', //Wrong L/FNG SUBMITTED_TO_LG = 'SUBG', //Submitted to L/FNG - IN_REVIEW_BY_LG = 'REVG', //new Under Review by L/FNG + IN_REVIEW_BY_LG = 'REVG', //Under Review by L/FNG SUBMITTED_TO_ALC = 'SUBM', //Submitted to ALC SUBMITTED_TO_ALC_INCOMPLETE = 'SUIN', //new Submitted to ALC - Incomplete - RECEIVED_BY_ALC = 'RECA', //new Received By ALC - IN_REVIEW_BY_ALC = 'REVA', //new Under Review by ALC + RECEIVED_BY_ALC = 'RECA', //Received By ALC + IN_REVIEW_BY_ALC = 'REVA', //Under Review by ALC ALC_DECISION = 'ALCD', // Decision Released REFUSED_TO_FORWARD_LG = 'RFFG', //new L/FNG Refused to Forward CANCELLED = 'CANC', From d2a2e108c70217e8de9aefb08ce8d482e25d61ab Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 14 Aug 2023 16:21:24 -0700 Subject: [PATCH 245/954] Update portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html Co-authored-by: to. sandra <76515860+sandratoh@users.noreply.github.com> --- .../submit-confirmation-dialog.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html index 6f76d04448..cb623c2ba8 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html @@ -1,5 +1,5 @@
-

Submit Application

+

Submit Notice of Intent

From dbe1bd82cae073f60eea6a9c5619c87758aa4c0f Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 14 Aug 2023 16:21:31 -0700 Subject: [PATCH 246/954] Update portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html Co-authored-by: to. sandra <76515860+sandratoh@users.noreply.github.com> --- .../submit-confirmation-dialog.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html index cb623c2ba8..44e41840fe 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html @@ -7,7 +7,7 @@

Submit Notice of Intent

Terms and Conditions:
- I/we consent to the use of the information provided in the application and all supporting documents to process the + I/we consent to the use of the information provided in the notice of intent and all supporting documents to process the notice of intent in accordance with the Agricultural Land Commission Act, the Agricultural Land Reserve General Regulation, and the Agricultural Land Reserve Use Regulation. From 15d13a3c43f772762d14979999d832012ce17e53 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 14 Aug 2023 16:28:20 -0700 Subject: [PATCH 247/954] Code Review Feedback --- .../notice-of-intent-details.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts index 75c34df486..71df72edbb 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.ts @@ -57,7 +57,7 @@ export class NoticeOfIntentDetailsComponent implements OnInit, OnDestroy { this.populateLocalGovernment(noiSubmission.localGovernmentUuid); this.needsAuthorizationLetter = - !(this.primaryContact?.type.code === OWNER_TYPE.GOVERNMENT) && + this.primaryContact?.type.code !== OWNER_TYPE.GOVERNMENT && !( noiSubmission.owners.length === 1 && (noiSubmission.owners[0].type.code === OWNER_TYPE.INDIVIDUAL || From c2ce1a2c0376f31250d2bf6bbb03c76dd8e68f4f Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 14 Aug 2023 16:31:19 -0700 Subject: [PATCH 248/954] Update services/apps/alcs/src/alcs/application/application-submission-status/submission-status.dto.ts Co-authored-by: to. sandra <76515860+sandratoh@users.noreply.github.com> --- .../application-submission-status/submission-status.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/alcs/src/alcs/application/application-submission-status/submission-status.dto.ts b/services/apps/alcs/src/alcs/application/application-submission-status/submission-status.dto.ts index c2ffce23b3..5475b5cc99 100644 --- a/services/apps/alcs/src/alcs/application/application-submission-status/submission-status.dto.ts +++ b/services/apps/alcs/src/alcs/application/application-submission-status/submission-status.dto.ts @@ -12,7 +12,7 @@ export enum SUBMISSION_STATUS { RECEIVED_BY_ALC = 'RECA', //Received By ALC IN_REVIEW_BY_ALC = 'REVA', //Under Review by ALC ALC_DECISION = 'ALCD', // Decision Released - REFUSED_TO_FORWARD_LG = 'RFFG', //new L/FNG Refused to Forward + REFUSED_TO_FORWARD_LG = 'RFFG', //L/FNG Refused to Forward CANCELLED = 'CANC', } From c0cab944b2626661aceae6680643e3423dfb7c9d Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Tue, 15 Aug 2023 11:01:06 -0700 Subject: [PATCH 249/954] Feature/alcs 981 - NOI step 6 ROSO (#875) step 6 for ROSO unit tests --- ...ructure-confirmation-dialog.component.html | 14 + ...ructure-confirmation-dialog.component.scss | 24 ++ ...ture-confirmation-dialog.component.spec.ts | 25 ++ ...structure-confirmation-dialog.component.ts | 8 + ...roso-additional-information.component.html | 288 ++++++++++++++++++ ...roso-additional-information.component.scss | 0 ...o-additional-information.component.spec.ts | 43 +++ .../roso-additional-information.component.ts | 282 +++++++++++++++++ ...removal-confirmation-dialog.component.html | 14 + ...removal-confirmation-dialog.component.scss | 24 ++ ...oval-confirmation-dialog.component.spec.ts | 24 ++ ...l-removal-confirmation-dialog.component.ts | 20 ++ .../edit-submission-base.module.ts | 7 + .../edit-submission.component.html | 12 +- .../edit-submission.component.ts | 7 + .../notice-of-intent-document.service.spec.ts | 18 ++ .../notice-of-intent-document.service.ts | 12 + .../notice-of-intent-submission.dto.ts | 18 ++ .../src/app/shared/dto/document.dto.ts | 5 +- .../notice-of-intent-document.service.spec.ts | 40 ++- .../notice-of-intent-document.service.ts | 18 +- .../alcs/src/document/document-code.entity.ts | 1 + .../src/document/document.service.spec.ts | 16 +- .../alcs/src/document/document.service.ts | 5 +- ...tice-of-intent-document.controller.spec.ts | 10 +- .../notice-of-intent-document.controller.ts | 11 + .../notice-of-intent-submission.dto.ts | 44 +++ .../notice-of-intent-submission.entity.ts | 39 +++ .../notice-of-intent-submission.service.ts | 33 ++ .../1692031918906-new_document_type.ts | 15 + ...6187722-roso_noi_additional_soil_fields.ts | 55 ++++ 31 files changed, 1123 insertions(+), 9 deletions(-) create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692031918906-new_document_type.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692046187722-roso_noi_additional_soil_fields.ts diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.html new file mode 100644 index 0000000000..413c84b902 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.html @@ -0,0 +1,14 @@ +
+

Delete Structure Type

+
+ +
+ + Warning: Deleting the structure type can remove some content already saved to this page.
+ Do you want to continue? +
+
+ + +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.scss new file mode 100644 index 0000000000..245cf0a559 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.scss @@ -0,0 +1,24 @@ +@use '../../../../../../../styles/functions' as *; + +.margin-bottom-1 { + margin-bottom: rem(16); +} + +.controls { + display: flex; + justify-content: space-between; +} + +.confirm-content { + margin: rem(24) 0; +} + +@media screen and (min-width: $desktopBreakpoint) { + .controls { + justify-content: flex-end; + + button { + margin-left: rem(25) !important; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.spec.ts new file mode 100644 index 0000000000..e99c9e81b6 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; + +import { DeleteStructureConfirmationDialogComponent } from './delete-structure-confirmation-dialog.component'; + +describe('DeleteStructureConfirmationDialogComponent', () => { + let component: DeleteStructureConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DeleteStructureConfirmationDialogComponent ], + providers: [{ provide: MatDialogRef, useValue: {} }], + }) + .compileComponents(); + + fixture = TestBed.createComponent(DeleteStructureConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.ts new file mode 100644 index 0000000000..55ca0f2995 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-delete-structure-confirmation-dialog', + templateUrl: './delete-structure-confirmation-dialog.component.html', + styleUrls: ['./delete-structure-confirmation-dialog.component.scss'], +}) +export class DeleteStructureConfirmationDialogComponent {} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.html new file mode 100644 index 0000000000..6c46489b3a --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.html @@ -0,0 +1,288 @@ +
+

Additional Proposal Information

+

+ Considerations are subject to + FIXME: Section 6 of the ALC Act. +

+

All fields are required unless stated optional.

+
+ + In order to complete this step, please consult the following pages on the ALC website: + + + +
+
+
+ +
Structures include farm buildings, residences, or accessory buildings
+
+ + Yes + + No + + +
+ Note: The form will be updated with additional required questions if you are building a structure +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#{{ i + 1 }}Type + + + + {{ type }} + + + + Total Floor Area + + + m2 + + Action + +
+ No Proposed Structures Entered. Use the button below to add your first structure. +
+
+
+ +
+
+ + + At least one structure is required + +
+ +
+ + Selected proposed structure type(s) will determine the proposal questions below + +
+ +
+ + + + +
+ warning +
This field is required
+
+
Characters left: {{ 4000 - soilStructureFarmUseReasonText.textLength }}
+
+ +
+ + + + +
+ warning +
This field is required
+
+
Characters left: {{ 4000 - soilStructureResidentialUseReasonText.textLength }}
+
+ +
+ +
Include the area, yields, crop types, and farm equipment size and attachments
+ + + +
+ warning +
This field is required
+
+
Characters left: {{ 4000 - soilAgriParcelActivityText.textLength }}
+
+ +
+ + + + +
+ warning +
This field is required
+
+
Characters left: {{ 4000 - structureResidentialAccessoryUseReasonText.textLength }}
+
+ +
+ +
+ Building plans must be the most up to date, current version and should include (1) the total floor area of all + levels and the intended use; and (2) interior and exterior views +
+ +
+
+
+
+
+ + +
+ + +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.spec.ts new file mode 100644 index 0000000000..5c598f894f --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.spec.ts @@ -0,0 +1,43 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDocumentDto } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; + +import { RosoAdditionalInformationComponent } from './roso-additional-information.component'; + +describe('RosoAdditionalInformationComponent', () => { + let component: RosoAdditionalInformationComponent; + let fixture: ComponentFixture; + let mockNoticeOfIntentSubmissionService: DeepMocked; + let mockNoticeOfIntentDocumentService: DeepMocked; + + beforeEach(async () => { + mockNoticeOfIntentSubmissionService = createMock(); + mockNoticeOfIntentDocumentService = createMock(); + await TestBed.configureTestingModule({ + declarations: [RosoAdditionalInformationComponent], + providers: [ + { provide: MatDialog, useValue: {} }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoticeOfIntentSubmissionService, + }, + { provide: NoticeOfIntentDocumentService, useValue: mockNoticeOfIntentDocumentService }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(RosoAdditionalInformationComponent); + component = fixture.componentInstance; + component.$noiSubmission = new BehaviorSubject(undefined); + component.$noiDocuments = new BehaviorSubject([]); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.ts new file mode 100644 index 0000000000..1d694c58f3 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.ts @@ -0,0 +1,282 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; +import { takeUntil } from 'rxjs'; +import { NoticeOfIntentDocumentDto } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionUpdateDto } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; +import { formatBooleanToString } from '../../../../../shared/utils/boolean-helper'; +import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; +import { EditNoiSteps } from '../../edit-submission.component'; +import { FilesStepComponent } from '../../files-step.partial'; +import { DeleteStructureConfirmationDialogComponent } from './delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component'; +import { SoilRemovalConfirmationDialogComponent } from './soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component'; + +export enum STRUCTURE_TYPES { + FARM_STRUCTURE = 'Farm Structure', + PRINCIPAL_RESIDENCE = 'Residential - Principal Residence', + ADDITIONAL_RESIDENCE = 'Residential - Additional Residence', + ACCESSORY_STRUCTURE = 'Residential - Accessory Structure', +} + +type ProposedStructure = { type: STRUCTURE_TYPES | null; area: string | null }; + +const RESIDENTIAL_STRUCTURE_TYPES = [ + STRUCTURE_TYPES.ACCESSORY_STRUCTURE, + STRUCTURE_TYPES.ADDITIONAL_RESIDENCE, + STRUCTURE_TYPES.PRINCIPAL_RESIDENCE, +]; + +@Component({ + selector: 'app-roso-additional-information', + templateUrl: './roso-additional-information.component.html', + styleUrls: ['./roso-additional-information.component.scss'], +}) +export class RosoAdditionalInformationComponent extends FilesStepComponent implements OnInit, OnDestroy { + currentStep = EditNoiSteps.ExtraInfo; + + DOCUMENT = DOCUMENT_TYPE; + STRUCTURE_TYPES = [ + STRUCTURE_TYPES.FARM_STRUCTURE, + STRUCTURE_TYPES.PRINCIPAL_RESIDENCE, + STRUCTURE_TYPES.ADDITIONAL_RESIDENCE, + STRUCTURE_TYPES.ACCESSORY_STRUCTURE, + ]; + + private submissionUuid = ''; + + confirmRemovalOfSoil = false; + buildingPlans: NoticeOfIntentDocumentDto[] = []; + + proposedStructures: ProposedStructure[] = []; + structuresSource = new MatTableDataSource(this.proposedStructures); + displayedColumns = ['index', 'type', 'area', 'action']; + + isSoilStructureFarmUseReasonVisible = false; + isSoilStructureResidentialUseReasonVisible = false; + isSoilAgriParcelActivityVisible = false; + isSoilStructureResidentialAccessoryUseReasonVisible = false; + + isRemovingSoilForNewStructure = new FormControl(null, [Validators.required]); + soilStructureFarmUseReason = new FormControl(null); + soilStructureResidentialUseReason = new FormControl(null); + soilAgriParcelActivity = new FormControl(null); + soilStructureResidentialAccessoryUseReason = new FormControl(null); + + form = new FormGroup({ + isRemovingSoilForNewStructure: this.isRemovingSoilForNewStructure, + soilStructureFarmUseReason: this.soilStructureFarmUseReason, + soilStructureResidentialUseReason: this.soilStructureResidentialUseReason, + soilAgriParcelActivity: this.soilAgriParcelActivity, + soilStructureResidentialAccessoryUseReason: this.soilStructureResidentialAccessoryUseReason, + }); + + constructor( + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + dialog: MatDialog + ) { + super(noticeOfIntentDocumentService, dialog); + } + + ngOnInit(): void { + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.fileId = noiSubmission.fileNumber; + this.submissionUuid = noiSubmission.uuid; + + if (noiSubmission.soilIsRemovingSoilForNewStructure) { + this.confirmRemovalOfSoil = true; + } + + this.form.patchValue({ + isRemovingSoilForNewStructure: formatBooleanToString(noiSubmission.soilIsRemovingSoilForNewStructure), + soilStructureFarmUseReason: noiSubmission.soilStructureFarmUseReason, + soilStructureResidentialUseReason: noiSubmission.soilStructureResidentialUseReason, + soilAgriParcelActivity: noiSubmission.soilAgriParcelActivity, + soilStructureResidentialAccessoryUseReason: noiSubmission.soilStructureResidentialAccessoryUseReason, + }); + + this.proposedStructures = noiSubmission.soilProposedStructures.map((structure) => ({ + ...structure, + area: structure.area ? structure.area.toString(10) : null, + })); + this.structuresSource = new MatTableDataSource(this.proposedStructures); + this.prepareStructureSpecificTextInputs(); + + if (this.showErrors) { + this.form.markAllAsTouched(); + } + } + }); + + this.$noiDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.buildingPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.BUILDING_PLAN); + }); + } + + prepareStructureSpecificTextInputs() { + this.setVisibilityAndValidatorsForFarmFields(); + + this.setVisibilityAndValidatorsForAccessoryFields(); + + this.setVisibilityAndValidatorsForResidentialFields(); + } + + private setVisibilityAndValidatorsForResidentialFields() { + if ( + this.proposedStructures.some( + (structure) => structure.type && RESIDENTIAL_STRUCTURE_TYPES.includes(structure.type) + ) + ) { + this.isSoilStructureResidentialUseReasonVisible = true; + this.soilStructureResidentialUseReason.setValidators([Validators.required]); + } else { + this.isSoilStructureResidentialUseReasonVisible = false; + this.soilStructureResidentialUseReason.removeValidators([Validators.required]); + this.soilStructureResidentialUseReason.reset(); + } + } + + private setVisibilityAndValidatorsForAccessoryFields() { + if (this.proposedStructures.some((structure) => structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE)) { + this.isSoilStructureResidentialAccessoryUseReasonVisible = true; + this.soilStructureResidentialAccessoryUseReason.setValidators([Validators.required]); + } else { + this.isSoilStructureResidentialAccessoryUseReasonVisible = false; + this.soilStructureResidentialAccessoryUseReason.removeValidators([Validators.required]); + this.soilStructureResidentialAccessoryUseReason.reset(); + } + } + + private setVisibilityAndValidatorsForFarmFields() { + if (this.proposedStructures.some((structure) => structure.type === STRUCTURE_TYPES.FARM_STRUCTURE)) { + this.isSoilAgriParcelActivityVisible = true; + this.isSoilStructureFarmUseReasonVisible = true; + this.soilAgriParcelActivity.setValidators([Validators.required]); + this.soilStructureFarmUseReason.setValidators([Validators.required]); + } else { + this.isSoilAgriParcelActivityVisible = false; + this.isSoilStructureFarmUseReasonVisible = false; + this.soilAgriParcelActivity.removeValidators([Validators.required]); + this.soilStructureFarmUseReason.removeValidators([Validators.required]); + this.soilAgriParcelActivity.reset(); + this.soilStructureFarmUseReason.reset(); + } + } + + async onSave() { + await this.save(); + } + + protected async save(): Promise { + if (this.fileId && this.form.dirty) { + const isRemovingSoilForNewStructure = this.isRemovingSoilForNewStructure.getRawValue(); + const soilStructureFarmUseReason = this.soilStructureFarmUseReason.getRawValue(); + const soilStructureResidentialUseReason = this.soilStructureResidentialUseReason.getRawValue(); + const soilAgriParcelActivity = this.soilAgriParcelActivity.getRawValue(); + const soilStructureResidentialAccessoryUseReason = this.soilStructureResidentialAccessoryUseReason.getRawValue(); + + const updateDto: NoticeOfIntentSubmissionUpdateDto = { + soilStructureFarmUseReason, + soilStructureResidentialUseReason, + soilIsRemovingSoilForNewStructure: parseStringToBoolean(isRemovingSoilForNewStructure), + soilAgriParcelActivity, + soilStructureResidentialAccessoryUseReason, + soilProposedStructures: this.proposedStructures.map((structure) => ({ + ...structure, + area: structure.area ? parseFloat(structure.area) : null, + })), + }; + + const updatedApp = await this.noticeOfIntentSubmissionService.updatePending(this.submissionUuid, updateDto); + this.$noiSubmission.next(updatedApp); + } + } + + onChangeIsRemovingSoilForNewStructure($event: MatButtonToggleChange) { + const parsedSelectedValue = parseStringToBoolean($event.value); + + if (this.confirmRemovalOfSoil === true && parsedSelectedValue === false) { + this.dialog + .open(SoilRemovalConfirmationDialogComponent, { + panelClass: 'no-padding', + disableClose: true, + }) + .beforeClosed() + .subscribe(async (result) => { + if (result) { + await this.noticeOfIntentDocumentService.deleteExternalFiles(this.buildingPlans.map((doc) => doc.uuid)); + this.buildingPlans = []; + + this.confirmRemovalOfSoil = false; + this.form.reset(); + this.form.controls.isRemovingSoilForNewStructure.setValue('false'); + this.proposedStructures = []; + this.structuresSource = new MatTableDataSource(this.proposedStructures); + + await this.save(); + } else { + this.form.controls.isRemovingSoilForNewStructure.setValue('true'); + } + }); + } else { + this.confirmRemovalOfSoil = parsedSelectedValue ?? false; + } + } + + onChangeStructureType(index: number, value: STRUCTURE_TYPES) { + this.proposedStructures[index].type = value; + + this.prepareStructureSpecificTextInputs(); + + this.form.markAsDirty(); + } + + onStructureRemove(index: number) { + this.dialog + .open(DeleteStructureConfirmationDialogComponent, { + panelClass: 'no-padding', + disableClose: true, + }) + .beforeClosed() + .subscribe(async (result) => { + if (result) { + this.deleteStructure(index); + } + }); + } + + private deleteStructure(index: number) { + const deletedStructure: ProposedStructure = this.proposedStructures.splice(index, 1)[0]; + this.structuresSource = new MatTableDataSource(this.proposedStructures); + + if (deletedStructure.type === STRUCTURE_TYPES.FARM_STRUCTURE) { + this.setVisibilityAndValidatorsForFarmFields(); + } + + if (deletedStructure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE) { + this.setVisibilityAndValidatorsForAccessoryFields(); + } + + if (deletedStructure.type && RESIDENTIAL_STRUCTURE_TYPES.includes(deletedStructure.type)) { + this.setVisibilityAndValidatorsForResidentialFields(); + } + + this.form.markAsDirty(); + } + + onStructureAdd() { + this.proposedStructures.push({ type: null, area: '' }); + this.structuresSource = new MatTableDataSource(this.proposedStructures); + this.form.markAsDirty(); + } + + onAreaChange() { + this.form.markAsDirty(); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.html new file mode 100644 index 0000000000..4650212c0f --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.html @@ -0,0 +1,14 @@ +
+

Are you removing soil in order to build a structure?

+
+ +
+ + Warning: Changing your answer will remove all the saved progress on this step. Do you want to + continue? +
+ + +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.scss b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.scss new file mode 100644 index 0000000000..245cf0a559 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.scss @@ -0,0 +1,24 @@ +@use '../../../../../../../styles/functions' as *; + +.margin-bottom-1 { + margin-bottom: rem(16); +} + +.controls { + display: flex; + justify-content: space-between; +} + +.confirm-content { + margin: rem(24) 0; +} + +@media screen and (min-width: $desktopBreakpoint) { + .controls { + justify-content: flex-end; + + button { + margin-left: rem(25) !important; + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.spec.ts new file mode 100644 index 0000000000..b726b84ca4 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialogRef } from '@angular/material/dialog'; + +import { SoilRemovalConfirmationDialogComponent } from './soil-removal-confirmation-dialog.component'; + +describe('SoilRemovalConfirmationDialogComponent', () => { + let component: SoilRemovalConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SoilRemovalConfirmationDialogComponent], + providers: [{ provide: MatDialogRef, useValue: {} }], + }).compileComponents(); + + fixture = TestBed.createComponent(SoilRemovalConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.ts new file mode 100644 index 0000000000..ff0f70decf --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { DeleteStructureConfirmationDialogComponent } from '../delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component'; + +@Component({ + selector: 'app-soil-removal-confirmation-dialog', + templateUrl: './soil-removal-confirmation-dialog.component.html', + styleUrls: ['./soil-removal-confirmation-dialog.component.scss'], +}) +export class SoilRemovalConfirmationDialogComponent { + constructor(private dialogRef: MatDialogRef) {} + + async onCancel(dialogResult: boolean = false) { + this.dialogRef.close(dialogResult); + } + + async onConfirm() { + this.onCancel(true); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts index f544a4d2f2..20505d8ff1 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission-base.module.ts @@ -8,7 +8,10 @@ import { MatSelectModule } from '@angular/material/select'; import { MatTableModule } from '@angular/material/table'; import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; import { SharedModule } from '../../../shared/shared.module'; +import { RosoAdditionalInformationComponent } from './additional-information/roso/roso-additional-information.component'; import { NoticeOfIntentDetailsModule } from '../notice-of-intent-details/notice-of-intent-details.module'; +import { DeleteStructureConfirmationDialogComponent } from './additional-information/roso/delete-structure-confirmation-dialog/delete-structure-confirmation-dialog.component'; +import { SoilRemovalConfirmationDialogComponent } from './additional-information/roso/soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component'; import { EditSubmissionComponent } from './edit-submission.component'; import { LandUseComponent } from './land-use/land-use.component'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; @@ -47,6 +50,9 @@ import { SelectGovernmentComponent } from './select-government/select-government LandUseComponent, OtherAttachmentsComponent, RosoProposalComponent, + RosoAdditionalInformationComponent, + DeleteStructureConfirmationDialogComponent, + SoilRemovalConfirmationDialogComponent, ReviewAndSubmitComponent, SubmitConfirmationDialogComponent, ], @@ -61,6 +67,7 @@ import { SelectGovernmentComponent } from './select-government/select-government LandUseComponent, OtherAttachmentsComponent, RosoProposalComponent, + RosoAdditionalInformationComponent, ReviewAndSubmitComponent, SubmitConfirmationDialogComponent, ], diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html index 46b5f6be22..003efb3fb6 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.html @@ -91,7 +91,17 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} | -
Additional Information
+
+ +
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts index c46203b65c..a6aca4df59 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts @@ -12,6 +12,7 @@ import { ToastService } from '../../../services/toast/toast.service'; import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; import { scrollToElement } from '../../../shared/utils/scroll-helper'; +import { RosoAdditionalInformationComponent } from './additional-information/roso/roso-additional-information.component'; import { LandUseComponent } from './land-use/land-use.component'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { ParcelDetailsComponent } from './parcels/parcel-details.component'; @@ -56,6 +57,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { @ViewChild(LandUseComponent) landUseComponent!: LandUseComponent; @ViewChild(OtherAttachmentsComponent) otherAttachmentsComponent!: OtherAttachmentsComponent; @ViewChild(RosoProposalComponent) rosoProposalComponent!: RosoProposalComponent; + @ViewChild(RosoAdditionalInformationComponent) rosoAdditionalInfoComponent!: RosoAdditionalInformationComponent; constructor( private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, @@ -153,6 +155,11 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { await this.rosoProposalComponent.onSave(); } break; + case EditNoiSteps.ExtraInfo: + if (this.rosoAdditionalInfoComponent) { + await this.rosoAdditionalInfoComponent.onSave(); + } + break; case EditNoiSteps.ReviewAndSubmit: //DO NOTHING break; diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts index 5c07e25903..19295bb9e9 100644 --- a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.spec.ts @@ -88,4 +88,22 @@ describe('NoticeOfIntentDocumentService', () => { expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); }); + + it('should make a post request for deleting multiple files', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.deleteExternalFiles(['fileId']); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notice-of-intent-document'); + }); + + it('should show an error toast if deleting a file fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.deleteExternalFiles(['fileId']); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); }); diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts index 7ae8f404a9..b2b363f816 100644 --- a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts @@ -67,6 +67,18 @@ export class NoticeOfIntentDocumentService { } } + async deleteExternalFiles(fileUuids: string[]) { + try { + this.overlayService.showSpinner(); + await firstValueFrom(this.httpClient.post(`${this.serviceUrl}/delete-files`, fileUuids)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete documents'); + } finally { + this.overlayService.hideSpinner(); + } + } + async update(fileNumber: string | undefined, updateDtos: NoticeOfIntentDocumentUpdateDto[]) { try { await firstValueFrom(this.httpClient.patch(`${this.serviceUrl}/application/${fileNumber}`, updateDtos)); diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts index 2cecd2086f..798c1ada78 100644 --- a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -1,3 +1,4 @@ +import { STRUCTURE_TYPES } from '../../features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component'; import { BaseCodeDto } from '../../shared/dto/base.dto'; import { NoticeOfIntentOwnerDto } from '../notice-of-intent-owner/notice-of-intent-owner.dto'; @@ -81,6 +82,12 @@ export interface NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmi soilAlternativeMeasures?: string | null; soilIsExtractionOrMining?: boolean; soilHasSubmittedNotice?: boolean; + soilIsRemovingSoilForNewStructure: boolean | null; + soilStructureFarmUseReason?: string | null; + soilStructureResidentialUseReason?: string | null; + soilAgriParcelActivity?: string | null; + soilStructureResidentialAccessoryUseReason?: string | null; + soilProposedStructures: ProposedStructure[]; } export interface NoticeOfIntentSubmissionUpdateDto { @@ -127,4 +134,15 @@ export interface NoticeOfIntentSubmissionUpdateDto { soilAlternativeMeasures?: string | null; soilIsExtractionOrMining?: boolean | null; soilHasSubmittedNotice?: boolean | null; + soilIsRemovingSoilForNewStructure?: boolean | null; + soilStructureFarmUseReason?: string | null; + soilStructureResidentialUseReason?: string | null; + soilAgriParcelActivity?: string | null; + soilStructureResidentialAccessoryUseReason?: string | null; + soilProposedStructures?: ProposedStructure[]; +} + +export interface ProposedStructure { + type: STRUCTURE_TYPES | null; + area: number | null; } diff --git a/portal-frontend/src/app/shared/dto/document.dto.ts b/portal-frontend/src/app/shared/dto/document.dto.ts index e2dfebeef6..b68976bb1e 100644 --- a/portal-frontend/src/app/shared/dto/document.dto.ts +++ b/portal-frontend/src/app/shared/dto/document.dto.ts @@ -16,7 +16,7 @@ export enum DOCUMENT_TYPE { AUTHORIZATION_LETTER = 'AAGR', CERTIFICATE_OF_TITLE = 'CERT', - //App Documents + //App Documents and NOI Documents SERVING_NOTICE = 'POSN', PROPOSAL_MAP = 'PRSK', HOMESITE_SEVERANCE = 'HOME', @@ -26,6 +26,9 @@ export enum DOCUMENT_TYPE { PROOF_OF_SIGNAGE = 'POSA', REPORT_OF_PUBLIC_HEARING = 'ROPH', PROOF_OF_ADVERTISING = 'POAA', + + //NOI DOCUMENTS + BUILDING_PLAN = 'BLDP', } export enum DOCUMENT_SOURCE { diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.spec.ts index d9cdd8ca16..173cc26427 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.spec.ts @@ -3,11 +3,11 @@ import { MultipartFile } from '@fastify/multipart'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { initApplicationMockEntity } from '../../../../test/mocks/mockEntities'; import { - DOCUMENT_TYPE, DocumentCode, + DOCUMENT_TYPE, } from '../../../document/document-code.entity'; import { DOCUMENT_SOURCE, @@ -112,7 +112,7 @@ describe('NoticeOfIntentDocumentService', () => { expect(res).toBe(mockSavedDocument); }); - it('should delete document and application document when deleting', async () => { + it('should delete document and noi document when deleting', async () => { const mockDocument = {}; const mockAppDocument = { uuid: '1', @@ -340,4 +340,38 @@ describe('NoticeOfIntentDocumentService', () => { expect(mockDocumentService.create).toHaveBeenCalledTimes(1); expect(mockRepository.save).toHaveBeenCalledTimes(1); }); + + it('should delete multiple documents and noi documents linked to it when deleting', async () => { + const mockDocuments = [new Document(), new Document()]; + const mockNoiDocuments = [ + new NoticeOfIntentDocument({ + document: new Document(), + }), + new NoticeOfIntentDocument({ + document: new Document(), + }), + ]; + + mockDocumentService.softRemoveMany.mockResolvedValue(); + mockRepository.remove.mockResolvedValue({} as any); + mockRepository.find.mockResolvedValue(mockNoiDocuments); + + await service.deleteMany(['fileId_1', 'fileId_2']); + + expect(mockDocumentService.softRemoveMany.mock.calls[0][0]).toEqual( + mockDocuments, + ); + expect(mockDocumentService.softRemoveMany).toHaveBeenCalledTimes(1); + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(mockRepository.find).toHaveBeenCalledWith({ + where: { + uuid: In(['fileId_1', 'fileId_2']), + }, + relations: { + document: true, + }, + }); + expect(mockRepository.remove).toHaveBeenCalledTimes(1); + expect(mockRepository.remove.mock.calls[0][0]).toBe(mockNoiDocuments); + }); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts index 91509763b4..1fcbd1a212 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service.ts @@ -9,11 +9,12 @@ import { ArrayOverlap, FindOptionsRelations, FindOptionsWhere, + In, Repository, } from 'typeorm'; import { - DOCUMENT_TYPE, DocumentCode, + DOCUMENT_TYPE, } from '../../../document/document-code.entity'; import { DOCUMENT_SOURCE, @@ -149,6 +150,21 @@ export class NoticeOfIntentDocumentService { return document; } + async deleteMany(noiDocumentUuids: string[]) { + const noiDocuments = await this.noticeOfIntentDocumentRepository.find({ + where: { + uuid: In(noiDocumentUuids), + }, + relations: { + document: true, + }, + }); + await this.noticeOfIntentDocumentRepository.remove(noiDocuments); + await this.documentService.softRemoveMany( + noiDocuments.map((doc) => doc.document), + ); + } + async list(fileNumber: string, visibilityFlags?: VISIBILITY_FLAG[]) { const where: FindOptionsWhere = { noticeOfIntent: { diff --git a/services/apps/alcs/src/document/document-code.entity.ts b/services/apps/alcs/src/document/document-code.entity.ts index 98120bbab4..b4618f9a4a 100644 --- a/services/apps/alcs/src/document/document-code.entity.ts +++ b/services/apps/alcs/src/document/document-code.entity.ts @@ -26,6 +26,7 @@ export enum DOCUMENT_TYPE { PROOF_OF_SIGNAGE = 'POSA', REPORT_OF_PUBLIC_HEARING = 'ROPH', PROOF_OF_ADVERTISING = 'POAA', + BUILDING_PLAN = 'BLDP', ORIGINAL_SUBMISSION = 'SUBO', UPDATED_SUBMISSION = 'SUBU', diff --git a/services/apps/alcs/src/document/document.service.spec.ts b/services/apps/alcs/src/document/document.service.spec.ts index 970d417347..6d17fb7a65 100644 --- a/services/apps/alcs/src/document/document.service.spec.ts +++ b/services/apps/alcs/src/document/document.service.spec.ts @@ -70,7 +70,7 @@ describe('DocumentService', () => { expect(mockRepository.save).toHaveBeenCalledTimes(1); }); - it('should delete from s3 and repo on delete', async () => { + it('should delete file from repo on delete', async () => { mockRepository.softRemove.mockResolvedValue({} as any); const documentUuid = 'fake-uuid'; @@ -82,6 +82,20 @@ describe('DocumentService', () => { expect(mockRepository.softRemove).toHaveBeenCalledTimes(1); }); + it('should delete multiple files from repo on delete', async () => { + mockRepository.softRemove.mockResolvedValue({} as any); + + const documentUuid = 'fake-uuid'; + + await service.softRemoveMany([ + { + uuid: documentUuid, + } as Document, + ]); + + expect(mockRepository.softRemove).toHaveBeenCalledTimes(1); + }); + it('should call repository save on create Document', async () => { const mockDoc = { mimeType: 'mimeType', diff --git a/services/apps/alcs/src/document/document.service.ts b/services/apps/alcs/src/document/document.service.ts index 269c0139fd..3502af41a9 100644 --- a/services/apps/alcs/src/document/document.service.ts +++ b/services/apps/alcs/src/document/document.service.ts @@ -2,7 +2,6 @@ import { CONFIG_TOKEN, IConfig } from '@app/common/config/config.module'; import { GetObjectCommand, PutObjectCommand, - PutObjectTaggingCommand, S3Client, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; @@ -117,6 +116,10 @@ export class DocumentService { await this.documentRepository.softRemove(document); } + async softRemoveMany(documents: Document[]) { + await this.documentRepository.softRemove(documents); + } + async getUploadUrl(filePath: string) { const fileKey = `${filePath}/${v4()}`; const command = new PutObjectCommand({ diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts index 920aa7d08b..f7c0ffb97c 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.spec.ts @@ -85,7 +85,7 @@ describe('NoticeOfIntentDocumentController', () => { expect(controller).toBeDefined(); }); - it('should call through to delete documents', async () => { + it('should call through to delete document', async () => { noiDocumentService.delete.mockResolvedValue(mockDocument); noiDocumentService.get.mockResolvedValue(mockDocument); @@ -176,4 +176,12 @@ describe('NoticeOfIntentDocumentController', () => { expect(res.uploadedBy).toEqual(user.user.entity); expect(res.uuid).toEqual(fakeUuid); }); + + it('should call through to delete multiple documents', async () => { + noiDocumentService.deleteMany.mockResolvedValue(); + + await controller.deleteMany(['fake-uuid']); + + expect(noiDocumentService.deleteMany).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts index fb61df26e8..5fb933f583 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts @@ -111,6 +111,17 @@ export class NoticeOfIntentDocumentController { return {}; } + @Post('/delete-files') + async deleteMany(@Body() fileUuids: string[]) { + //TODO: How do we know which documents applicant can delete? + // await this.applicationSubmissionService.verifyAccess( + // document.applicationUuid, + // req.user.entity, + // ); + await this.noticeOfIntentDocumentService.deleteMany(fileUuids); + return {}; + } + @Post('/application/:uuid/attachExternal') async attachExternalDocument( @Param('uuid') fileNumber: string, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts index 3a47818495..30c3dd7f0f 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -1,5 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { + IsArray, IsBoolean, IsNotEmpty, IsNumber, @@ -10,6 +11,7 @@ import { } from 'class-validator'; import { NoticeOfIntentStatusDto } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NoticeOfIntentOwnerDto } from './notice-of-intent-owner/notice-of-intent-owner.dto'; +import { ProposedStructure } from './notice-of-intent-submission.entity'; export const MAX_DESCRIPTION_FIELD_LENGTH = 4000; @@ -151,6 +153,24 @@ export class NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmissio @AutoMap(() => Boolean) soilHasSubmittedNotice?: boolean; + + @AutoMap(() => Boolean) + soilIsRemovingSoilForNewStructure?: boolean; + + @AutoMap(() => String) + soilStructureFarmUseReason?: string | null; + + @AutoMap(() => String) + soilStructureResidentialUseReason?: string | null; + + @AutoMap(() => String) + soilAgriParcelActivity?: string | null; + + @AutoMap(() => String) + soilStructureResidentialAccessoryUseReason?: string | null; + + @AutoMap(() => [ProposedStructure]) + soilProposedStructures: ProposedStructure[]; } export class NoticeOfIntentSubmissionCreateDto { @@ -335,4 +355,28 @@ export class NoticeOfIntentSubmissionUpdateDto { @IsBoolean() @IsOptional() soilHasSubmittedNotice?: boolean; + + @IsBoolean() + @IsOptional() + soilIsRemovingSoilForNewStructure?: boolean; + + @IsString() + @IsOptional() + soilStructureFarmUseReason?: string | null; + + @IsString() + @IsOptional() + soilStructureResidentialUseReason?: string | null; + + @IsString() + @IsOptional() + soilAgriParcelActivity?: string | null; + + @IsString() + @IsOptional() + soilStructureResidentialAccessoryUseReason?: string | null; + + @IsArray() + @IsOptional() + soilProposedStructures?: ProposedStructure[]; } diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts index 3d71859500..515a813bdf 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts @@ -15,6 +15,16 @@ import { User } from '../../user/user.entity'; import { ColumnNumericTransformer } from '../../utils/column-numeric-transform'; import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; +export class ProposedStructure { + type: + | 'Farm Structure' + | 'Residential - Principal Residence' + | 'Residential - Additional Residence' + | 'Residential - Accessory Structure' + | null; + area?: number | null; +} + @Entity() export class NoticeOfIntentSubmission extends Base { constructor(data?: Partial) { @@ -388,6 +398,35 @@ export class NoticeOfIntentSubmission extends Base { @Column({ type: 'boolean', nullable: true }) soilHasSubmittedNotice: boolean | null; + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + soilIsRemovingSoilForNewStructure: boolean | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilStructureFarmUseReason: string | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilStructureResidentialUseReason: string | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilAgriParcelActivity: string | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilStructureResidentialAccessoryUseReason: string | null; + + @AutoMap(() => ProposedStructure) + @Column({ + comment: 'JSONB Column containing the proposed structures', + type: 'jsonb', + array: false, + default: () => `'[]'`, + }) + soilProposedStructures: ProposedStructure[]; + @AutoMap(() => NoticeOfIntent) @ManyToOne(() => NoticeOfIntent) @JoinColumn({ diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index a35a2c0bb2..2a808b8aa3 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -285,6 +285,39 @@ export class NoticeOfIntentSubmissionService { noticeOfIntentSubmission.soilHasSubmittedNotice, ); + noticeOfIntentSubmission.soilIsRemovingSoilForNewStructure = + filterUndefined( + updateDto.soilIsRemovingSoilForNewStructure, + noticeOfIntentSubmission.soilIsRemovingSoilForNewStructure, + ); + + noticeOfIntentSubmission.soilStructureFarmUseReason = filterUndefined( + updateDto.soilStructureFarmUseReason, + noticeOfIntentSubmission.soilStructureFarmUseReason, + ); + + noticeOfIntentSubmission.soilStructureResidentialUseReason = + filterUndefined( + updateDto.soilStructureResidentialUseReason, + noticeOfIntentSubmission.soilStructureResidentialUseReason, + ); + + noticeOfIntentSubmission.soilAgriParcelActivity = filterUndefined( + updateDto.soilAgriParcelActivity, + noticeOfIntentSubmission.soilAgriParcelActivity, + ); + + noticeOfIntentSubmission.soilStructureResidentialAccessoryUseReason = + filterUndefined( + updateDto.soilStructureResidentialAccessoryUseReason, + noticeOfIntentSubmission.soilStructureResidentialAccessoryUseReason, + ); + + noticeOfIntentSubmission.soilProposedStructures = filterUndefined( + updateDto.soilProposedStructures, + noticeOfIntentSubmission.soilProposedStructures, + ); + if ( updateDto.soilHasSubmittedNotice === false || updateDto.soilIsExtractionOrMining === false diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692031918906-new_document_type.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692031918906-new_document_type.ts new file mode 100644 index 0000000000..8f71d5958b --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692031918906-new_document_type.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class newDocumentType1692031918906 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO "alcs"."document_code" + ("audit_deleted_date_at", "audit_created_at", "audit_updated_at", "audit_created_by", "audit_updated_by", "label", "code", "description", "oats_code") VALUES + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Detailed Building Plan', 'BLDP', 'Detailed Building Plan', 'BLDP'); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // nope + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692046187722-roso_noi_additional_soil_fields.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692046187722-roso_noi_additional_soil_fields.ts new file mode 100644 index 0000000000..4a94164f2f --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692046187722-roso_noi_additional_soil_fields.ts @@ -0,0 +1,55 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class rosoNoiAdditionalSoilFields1692046187722 + implements MigrationInterface +{ + name = 'rosoNoiAdditionalSoilFields1692046187722'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_is_removing_soil_for_new_structure" boolean`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_structure_farm_use_reason" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_structure_residential_use_reason" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_agri_parcel_activity" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_structure_residential_accessory_use_reason" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_proposed_structures" jsonb NOT NULL DEFAULT '[]'`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."soil_proposed_structures" IS 'JSONB Column containing the proposed structures'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_submission"."soil_proposed_structures" IS 'JSONB Column containing the proposed structures'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_proposed_structures"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_structure_residential_accessory_use_reason"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_agri_parcel_activity"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_structure_residential_use_reason"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_structure_farm_use_reason"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_is_removing_soil_for_new_structure"`, + ); + } +} From 443a754523c81f25a801b122afa4026f34d3cbcb Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 15 Aug 2023 11:51:38 -0700 Subject: [PATCH 250/954] fixed reference to old table --- .../sql/documents_app/alcs_documents_to_app_documents.sql | 2 +- .../alcs_documents_to_app_documents_total_count.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents.sql b/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents.sql index a5ce9e008d..ac775c2ff8 100644 --- a/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents.sql +++ b/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents.sql @@ -12,7 +12,7 @@ with oats_documents_to_map as ( join alcs."document" d on d.oats_document_id = od.document_id::text - join alcs.application_document_code adc + join alcs.document_code adc on adc.oats_code = od.document_code join alcs.application a diff --git a/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents_total_count.sql b/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents_total_count.sql index a326237443..60069badfd 100644 --- a/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents_total_count.sql +++ b/bin/migrate-oats-data/sql/documents_app/alcs_documents_to_app_documents_total_count.sql @@ -12,7 +12,7 @@ with oats_documents_to_map as ( join alcs."document" d on d.oats_document_id = od.document_id::text - join alcs.application_document_code adc + join alcs.document_code adc on adc.oats_code = od.document_code join alcs.application a From 5bec6e71c2f78379e547c6713a60f23b0e31a93b Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 15 Aug 2023 12:04:39 -0700 Subject: [PATCH 251/954] missed a letter --- .../documents/alcs_documents_to_noi_documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/documents/alcs_documents_to_noi_documents.py b/bin/migrate-oats-data/documents/alcs_documents_to_noi_documents.py index b06b2101e1..744b78a933 100644 --- a/bin/migrate-oats-data/documents/alcs_documents_to_noi_documents.py +++ b/bin/migrate-oats-data/documents/alcs_documents_to_noi_documents.py @@ -23,7 +23,7 @@ def compile_document_insert_query(number_of_rows_to_insert): ) VALUES {documents_to_insert} ON CONFLICT (oats_document_id, oats_application_id) DO UPDATE SET - notice_of_intent_uuid = EXCLUDED.notice_of_intnent_uuid, + notice_of_intent_uuid = EXCLUDED.notice_of_intent_uuid, document_uuid = EXCLUDED.document_uuid, type_code = EXCLUDED.type_code, visibility_flags = EXCLUDED.visibility_flags, From a45f9bcf824a0c091cb51a72ce62292c2c2cc05d Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 15 Aug 2023 11:03:04 -0700 Subject: [PATCH 252/954] Add submission and validation for ROSO NOIs * Add noi validator service * Update confirmation dialog to new UX --- .../edit-submission.component.spec.ts | 4 - .../edit-submission.component.ts | 17 +- .../proposal/roso/roso-proposal.component.ts | 8 + .../submit-confirmation-dialog.component.html | 6 +- .../submit-confirmation-dialog.component.ts | 4 +- .../roso-details/roso-details.component.html | 2 +- .../notice-of-intent.service.ts | 1 + .../notice-of-intent-parcel.service.spec.ts | 2 +- .../notice-of-intent-parcel.service.ts | 2 +- ...ntent-submission-validator.service.spec.ts | 1036 +++++++++++++++++ ...-of-intent-submission-validator.service.ts | 655 +++++++++++ ...ce-of-intent-submission.controller.spec.ts | 29 +- .../notice-of-intent-submission.controller.ts | 32 +- .../notice-of-intent-submission.module.ts | 2 + ...otice-of-intent-submission.service.spec.ts | 9 +- .../notice-of-intent-submission.service.ts | 17 +- 16 files changed, 1767 insertions(+), 59 deletions(-) create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission-validator.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission-validator.service.ts diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts index 46cbe54919..6b41598188 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.spec.ts @@ -37,10 +37,6 @@ describe('EditSubmissionComponent', () => { provide: MatDialog, useValue: {}, }, - { - provide: CodeService, - useValue: {}, - }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts index a6aca4df59..cdfb2ec897 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.component.ts @@ -62,7 +62,6 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { constructor( private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, - private codeService: CodeService, private activatedRoute: ActivatedRoute, private dialog: MatDialog, private toastService: ToastService, @@ -180,13 +179,8 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { async onSubmit() { if (this.noiSubmission) { - const government = await this.loadGovernment(this.noiSubmission.localGovernmentUuid); this.dialog - .open(SubmitConfirmationDialogComponent, { - data: { - governmentName: government?.name ?? 'selected local / first nation government', - }, - }) + .open(SubmitConfirmationDialogComponent) .beforeClosed() .subscribe((didConfirm) => { if (didConfirm) { @@ -206,15 +200,6 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { } } - private async loadGovernment(uuid: string) { - const codes = await this.codeService.loadCodes(); - const localGovernment = codes.localGovernments.find((a) => a.uuid === uuid); - if (localGovernment) { - return localGovernment; - } - return; - } - private async loadSubmission(fileId: string, reload = false) { if (!this.noiSubmission || reload) { this.overlayService.showSpinner(); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts index c0966f7b86..18fdbba025 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts @@ -92,6 +92,9 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, if (noiSubmission.soilIsExtractionOrMining) { this.allowMiningUploads = true; + this.hasSubmittedNotice.enable(); + } else { + this.hasSubmittedNotice.disable(); } this.form.patchValue({ @@ -168,6 +171,11 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, onChangeIsExtractionOrMining(selectedValue: string) { this.allowMiningUploads = selectedValue === 'true'; + if (selectedValue === 'true') { + this.hasSubmittedNotice.enable(); + } else { + this.hasSubmittedNotice.disable(); + } } markDirty() { diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html index 44e41840fe..56f646bb69 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html @@ -3,12 +3,12 @@

Submit Notice of Intent

-

Your notice of intent will be submitted to the {{ data.governmentName }}.

+

Your notice of intent will be submitted to the ALC.

Terms and Conditions:
- I/we consent to the use of the information provided in the notice of intent and all supporting documents to process the - notice of intent in accordance with the Agricultural Land Commission Act, the Agricultural Land Reserve General + I/we consent to the use of the information provided in the application and all supporting documents to process the + application in accordance with the Agricultural Land Commission Act, the Agricultural Land Reserve General Regulation, and the Agricultural Land Reserve Use Regulation. diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts index cf84e55daa..418ad271a2 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts @@ -9,8 +9,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; export class SubmitConfirmationDialogComponent { constructor( @Inject(MAT_DIALOG_DATA) - protected data: { - governmentName: string; - } + protected data: {} ) {} } diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html index 5969731d85..7cab8ff23b 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html @@ -163,7 +163,7 @@

Project Duration

Have you submitted a Notice of Work to the Ministry of Energy, Mines and Low Carbon Innovation (EMLI)?
- + {{ _noiSubmission.soilHasSubmittedNotice ? 'Yes' : 'No' }} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts index dea6f1754b..459849bea5 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts @@ -429,6 +429,7 @@ export class NoticeOfIntentService { existingNoticeOfIntent.typeCode = createDto.typeCode; existingNoticeOfIntent.region = region; existingNoticeOfIntent.card = new Card(); + existingNoticeOfIntent.card.typeCode = CARD_TYPE.NOI; await this.repository.save(existingNoticeOfIntent); return this.getByFileNumber(createDto.fileNumber); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts index 099481434d..a16a25df9c 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts @@ -58,7 +58,7 @@ describe('NoticeOfIntentParcelService', () => { it('should fetch parcels by fileNumber', async () => { mockParcelRepo.find.mockResolvedValue([mockNOIParcel]); - const result = await service.fetchByApplicationFileId(mockFileNumber); + const result = await service.fetchByFileId(mockFileNumber); expect(result).toEqual([mockNOIParcel]); expect(mockParcelRepo.find).toBeCalledTimes(1); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts index ba69dc46bb..7287652876 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service.ts @@ -18,7 +18,7 @@ export class NoticeOfIntentParcelService { private noticeOfIntentOwnerService: NoticeOfIntentOwnerService, ) {} - async fetchByApplicationFileId(fileId: string) { + async fetchByFileId(fileId: string) { return this.parcelRepository.find({ where: { noticeOfIntentSubmission: { fileNumber: fileId, isDraft: false }, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission-validator.service.spec.ts new file mode 100644 index 0000000000..ee38a790eb --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission-validator.service.spec.ts @@ -0,0 +1,1036 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +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 { + DocumentCode, + DOCUMENT_TYPE, +} from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE } from '../../document/document.dto'; +import { Document } from '../../document/document.entity'; +import { + OWNER_TYPE, + OwnerType, +} from '../../common/owner-type/owner-type.entity'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmissionValidatorService } from './notice-of-intent-submission-validator.service'; +import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; + +function includesError(errors: Error[], target: Error) { + return errors.some((error) => error.message === target.message); +} + +describe('NoticeOfIntentSubmissionValidatorService', () => { + let service: NoticeOfIntentSubmissionValidatorService; + let mockLGService: DeepMocked; + let mockNoiParcelService: DeepMocked; + let mockNoiDocumentService: DeepMocked; + + beforeEach(async () => { + mockLGService = createMock(); + mockNoiParcelService = createMock(); + mockNoiDocumentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentSubmissionValidatorService, + { + provide: LocalGovernmentService, + useValue: mockLGService, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNoiParcelService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNoiDocumentService, + }, + ], + }).compile(); + + mockLGService.list.mockResolvedValue([]); + mockNoiParcelService.fetchByFileId.mockResolvedValue([]); + mockNoiDocumentService.getApplicantDocuments.mockResolvedValue([]); + + service = module.get( + NoticeOfIntentSubmissionValidatorService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should return an error for missing applicant', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect(includesError(res.errors, new Error('Missing applicant'))).toBe( + true, + ); + }); + + it('should return an error for missing purpose', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect(includesError(res.errors, new Error('Missing purpose'))).toBe(true); + }); + + it('should return an error for no parcels', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect(includesError(res.errors, new Error('Missing applicant'))).toBe( + true, + ); + }); + + it('provide errors for invalid parcel', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + }); + const parcel = new NoticeOfIntentParcel({ + uuid: 'parcel-1', + owners: [], + ownershipTypeCode: 'SMPL', + }); + + mockNoiParcelService.fetchByFileId.mockResolvedValue([parcel]); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new ServiceValidationException(`Invalid Parcel ${parcel.uuid}`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new ServiceValidationException(`Parcel has no Owners ${parcel.uuid}`), + ), + ).toBe(true); + expect( + includesError( + res.errors, + new ServiceValidationException( + `Parcel is missing certificate of title ${parcel.uuid}`, + ), + ), + ).toBe(true); + expect( + includesError( + res.errors, + new ServiceValidationException( + `Fee Simple Parcel ${parcel.uuid} has no PID`, + ), + ), + ).toBe(true); + }); + + it('should report an invalid PID', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + }); + const parcel = new NoticeOfIntentParcel({ + uuid: 'parcel-1', + owners: [], + ownershipTypeCode: 'SMPL', + pid: '1251251', + }); + + mockNoiParcelService.fetchByFileId.mockResolvedValue([parcel]); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new ServiceValidationException(`Parcel ${parcel.uuid} has invalid PID`), + ), + ).toBe(true); + }); + + it('should require certificate of title and crown description for CRWN parcels with PID and with CRWN owners', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [ + new NoticeOfIntentOwner({ + type: new OwnerType({ + code: OWNER_TYPE.CROWN, + }), + }), + ], + soilProposedStructures: [], + }); + const parcel = new NoticeOfIntentParcel({ + uuid: 'parcel-1', + owners: [], + ownershipTypeCode: 'CRWN', + pid: '12512', + }); + + mockNoiParcelService.fetchByFileId.mockResolvedValue([parcel]); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Crown Parcel ${parcel.uuid} has no ownership type`), + ), + ).toBe(true); + expect( + includesError( + res.errors, + new Error(`Parcel is missing certificate of title ${parcel.uuid}`), + ), + ).toBe(true); + }); + + it('should return an error for no primary contact', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Notice of Intent has no primary contact`), + ), + ).toBe(true); + }); + + it('should return errors for an invalid third party agent', async () => { + const mockOwner = new NoticeOfIntentOwner({ + uuid: 'owner-uuid', + type: new OwnerType({ + code: OWNER_TYPE.AGENT, + }), + firstName: 'Bruce', + lastName: 'Wayne', + }); + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [mockOwner], + primaryContactOwnerUuid: mockOwner.uuid, + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Invalid Third Party Agent Information`), + ), + ).toBe(true); + }); + + it('should require an authorization letter for more than one owner', async () => { + const mockOwner = new NoticeOfIntentOwner({ + uuid: 'owner-uuid', + type: new OwnerType({ + code: OWNER_TYPE.AGENT, + }), + firstName: 'Bruce', + lastName: 'Wayne', + }); + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [mockOwner, mockOwner], + primaryContactOwnerUuid: mockOwner.uuid, + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Notice of Intent has no authorization letters`), + ), + ).toBe(true); + }); + + it('should not require an authorization letter for a single owner', async () => { + const mockOwner = new NoticeOfIntentOwner({ + uuid: 'owner-uuid', + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, + }), + firstName: 'Bruce', + lastName: 'Wayne', + }); + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [mockOwner], + primaryContactOwnerUuid: mockOwner.uuid, + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Notice of Intent has no authorization letters`), + ), + ).toBe(false); + }); + + it('should not require an authorization letter when contact is government', async () => { + const mockOwner = new NoticeOfIntentOwner({ + uuid: 'owner-uuid', + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, + }), + firstName: 'Bruce', + lastName: 'Wayne', + }); + + const governmentOwner = new NoticeOfIntentOwner({ + uuid: 'government-owner-uuid', + type: new OwnerType({ + code: OWNER_TYPE.GOVERNMENT, + }), + firstName: 'Govern', + lastName: 'Ment', + }); + + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [mockOwner, governmentOwner], + primaryContactOwnerUuid: governmentOwner.uuid, + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Notice of Intent has no authorization letters`), + ), + ).toBe(false); + }); + + it('should not have an authorization letter error when one is provided', async () => { + const mockOwner = new NoticeOfIntentOwner({ + uuid: 'owner-uuid', + type: new OwnerType({ + code: OWNER_TYPE.INDIVIDUAL, + }), + firstName: 'Bruce', + lastName: 'Wayne', + }); + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [mockOwner, mockOwner], + primaryContactOwnerUuid: mockOwner.uuid, + soilProposedStructures: [], + }); + + const documents = [ + new NoticeOfIntentDocument({ + type: new DocumentCode({ + code: DOCUMENT_TYPE.AUTHORIZATION_LETTER, + }), + }), + ]; + mockNoiDocumentService.getApplicantDocuments.mockResolvedValue(documents); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Notice of Intent has no authorization letters`), + ), + ).toBe(false); + }); + + it('should produce an error for missing local government', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error('Notice of Intent has no local government'), + ), + ).toBe(true); + }); + + it('should accept local government when its valid', async () => { + const mockLg = new LocalGovernment({ + uuid: 'lg-uuid', + name: 'lg', + bceidBusinessGuid: 'CATS', + isFirstNation: false, + }); + mockLGService.list.mockResolvedValue([mockLg]); + + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + localGovernmentUuid: mockLg.uuid, + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error( + `Selected local government is setup in portal ${mockLg.name}`, + ), + ), + ).toBe(false); + }); + + it('should not have land use errors when all fields are filled', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + parcelsAgricultureDescription: 'VALID', + parcelsAgricultureImprovementDescription: 'VALID', + parcelsNonAgricultureUseDescription: 'VALID', + northLandUseType: 'VALID', + northLandUseTypeDescription: 'VALID', + eastLandUseType: 'VALID', + eastLandUseTypeDescription: 'VALID', + southLandUseType: 'VALID', + southLandUseTypeDescription: 'VALID', + westLandUseType: 'VALID', + westLandUseTypeDescription: 'VALID', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError(res.errors, new Error(`Invalid Parcel Description`)), + ).toBe(false); + expect( + includesError(res.errors, new Error(`Invalid Adjacent Parcels`)), + ).toBe(false); + }); + + it('should have land use errors when not all fields are filled', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + parcelsAgricultureDescription: undefined, + parcelsAgricultureImprovementDescription: 'VALID', + parcelsNonAgricultureUseDescription: 'VALID', + northLandUseType: undefined, + northLandUseTypeDescription: 'VALID', + eastLandUseType: 'VALID', + eastLandUseTypeDescription: 'VALID', + southLandUseType: 'VALID', + southLandUseTypeDescription: 'VALID', + westLandUseType: undefined, + westLandUseTypeDescription: 'VALID', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError(res.errors, new Error(`Invalid Parcel Description`)), + ).toBe(true); + expect( + includesError(res.errors, new Error(`Invalid Adjacent Parcels`)), + ).toBe(true); + }); + + it('should report error for document missing type', async () => { + const incompleteDocument = new NoticeOfIntentDocument({ + type: undefined, + document: new Document({ + source: DOCUMENT_SOURCE.APPLICANT, + }), + }); + + const documents = [incompleteDocument]; + mockNoiDocumentService.getApplicantDocuments.mockResolvedValue(documents); + + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Document ${incompleteDocument.uuid} missing type`), + ), + ).toBe(true); + }); + + it('should report error for other document missing description', async () => { + const incompleteDocument = new NoticeOfIntentDocument({ + type: new DocumentCode({ + code: DOCUMENT_TYPE.OTHER, + }), + document: new Document({ + source: DOCUMENT_SOURCE.APPLICANT, + }), + description: undefined, + }); + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + }); + + const documents = [incompleteDocument]; + mockNoiDocumentService.getApplicantDocuments.mockResolvedValue(documents); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Document ${incompleteDocument.uuid} missing description`), + ), + ).toBe(true); + }); + + describe('Additional Info', () => { + it('should be happy with complete structures', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilIsRemovingSoilForNewStructure: true, + soilProposedStructures: [ + { type: 'Residential - Accessory Structure', area: 5 }, + { type: 'Farm Structure', area: 5 }, + { type: 'Residential - Additional Residence', area: 5 }, + ], + soilStructureFarmUseReason: 'VALID', + soilAgriParcelActivity: 'VALID', + soilStructureResidentialAccessoryUseReason: 'VALID', + typeCode: 'ROSO', + }); + + const buildingPlan = new NoticeOfIntentDocument({ + type: new DocumentCode({ + code: DOCUMENT_TYPE.BUILDING_PLAN, + }), + typeCode: DOCUMENT_TYPE.BUILDING_PLAN, + }); + + const documents = [buildingPlan]; + mockNoiDocumentService.getApplicantDocuments.mockResolvedValue(documents); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing structures`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal incomplete structure`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing Building Plans`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing farm use reason`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing agricultural activity`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing accessory use reason`), + ), + ).toBe(false); + }); + + it('should be happy with no structures', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilIsRemovingSoilForNewStructure: false, + soilProposedStructures: [], + typeCode: 'ROSO', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing structures`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal incomplete structure`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing Building Plans`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing farm use reason`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing agricultural activity`), + ), + ).toBe(false); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing accessory use reason`), + ), + ).toBe(false); + }); + + it('should require structures', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilIsRemovingSoilForNewStructure: true, + soilProposedStructures: [], + typeCode: 'ROSO', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing structures`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing Building Plans`), + ), + ).toBe(true); + }); + + it('should require additional questions when having all structures', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilIsRemovingSoilForNewStructure: true, + soilProposedStructures: [ + { type: 'Residential - Accessory Structure', area: 5 }, + { type: 'Farm Structure', area: 5 }, + { type: 'Residential - Additional Residence', area: null }, + ], + typeCode: 'ROSO', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal incomplete structure`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing farm use reason`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing agricultural activity`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing residential use reason`), + ), + ).toBe(true); + }); + }); + + describe('ROSO Notice of Intents', () => { + it('should not have an error when base information is filled correctly', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', + soilIsFollowUp: false, + soilAlreadyRemovedVolume: 5, + soilAlreadyRemovedMaximumDepth: 5, + soilToRemoveMaximumDepth: 5, + soilAlreadyRemovedAverageDepth: 5, + soilAlreadyRemovedArea: 5, + soilToRemoveAverageDepth: 5, + soilToRemoveVolume: 5, + soilToRemoveArea: 5, + soilTypeRemoved: 'soilTypeRemoved', + typeCode: 'ROSO', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError(res.errors, new Error(`ROSO proposal incomplete`)), + ).toBe(false); + }); + + it('should report errors when information is missing', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + soilTypeRemoved: null, + soilToRemoveVolume: null, + typeCode: 'ROSO', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError(res.errors, new Error(`ROSO proposal incomplete`)), + ).toBe(true); + + expect( + includesError(res.errors, new Error(`ROSO Soil Table Incomplete`)), + ).toBe(true); + }); + + it('should require NOIDs or ApplicationIDs', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + soilIsFollowUp: true, + soilFollowUpIDs: null, + typeCode: 'ROSO', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing Application or NOI IDs`), + ), + ).toBe(true); + }); + + it('should complain about missing files when soilIsExtractionOrMining is true', async () => { + const noticeOfIntentSubmission = new NoticeOfIntentSubmission({ + owners: [], + soilProposedStructures: [], + soilIsFollowUp: true, + soilIsExtractionOrMining: true, + typeCode: 'ROSO', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing Proposal Map / Site Plan`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing Cross Section Diagrams`), + ), + ).toBe(true); + + expect( + includesError( + res.errors, + new Error(`ROSO proposal missing Reclamation Plans`), + ), + ).toBe(true); + }); + }); + + // describe('POFO Applications', () => { + // it('should not have errors when base information is filled correctly', async () => { + // const application = new NoticeOfIntentSubmission({ + // owners: [], + // soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', + // soilIsFollowUp: false, + // soilAlreadyPlacedVolume: 5, + // soilAlreadyPlacedMaximumDepth: 5, + // soilToPlaceMaximumDepth: 5, + // soilAlreadyPlacedAverageDepth: 5, + // soilAlreadyPlacedArea: 5, + // soilToPlaceAverageDepth: 5, + // soilToPlaceVolume: 5, + // soilToPlaceArea: 5, + // soilAlternativeMeasures: 'soilAlternativeMeasures', + // soilFillTypeToPlace: 'soilFillTypeToPlace', + // typeCode: 'POFO', + // }); + // + // const res = await service.validateSubmission(application); + // + // expect( + // includesError(res.errors, new Error(`POFO Proposal incomplete`)), + // ).toBe(false); + // + // expect( + // includesError(res.errors, new Error(`POFO Soil Table Incomplete`)), + // ).toBe(false); + // }); + // + // it('should report errors when information is missing', async () => { + // const application = new NoticeOfIntentSubmission({ + // owners: [], + // soilFillTypeToPlace: null, + // soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', + // soilToPlaceArea: null, + // typeCode: 'POFO', + // }); + // + // const res = await service.validateSubmission(application); + // + // expect( + // includesError(res.errors, new Error(`POFO Proposal incomplete`)), + // ).toBe(true); + // + // expect( + // includesError(res.errors, new Error(`POFO Soil Table Incomplete`)), + // ).toBe(true); + // }); + // + // it('should require NOI IDs or ApplicationIDs', async () => { + // const application = new NoticeOfIntentSubmission({ + // owners: [], + // soilIsFollowUp: true, + // typeCode: 'POFO', + // }); + // + // const res = await service.validateSubmission(application); + // + // expect( + // includesError( + // res.errors, + // new Error(`POFO Proposal missing Application or NOI IDs`), + // ), + // ).toBe(true); + // }); + // + // it('should complain about missing files', async () => { + // const application = new NoticeOfIntentSubmission({ + // owners: [], + // soilIsFollowUp: true, + // typeCode: 'POFO', + // }); + // + // const res = await service.validateSubmission(application); + // + // expect( + // includesError( + // res.errors, + // new Error(`POFO proposal missing Proposal Map / Site Plan`), + // ), + // ).toBe(true); + // + // expect( + // includesError( + // res.errors, + // new Error(`POFO proposal missing Cross Section Diagrams`), + // ), + // ).toBe(true); + // + // expect( + // includesError( + // res.errors, + // new Error(`POFO proposal missing Reclamation Plans`), + // ), + // ).toBe(true); + // }); + // }); + // + // describe('PFRS Applications', () => { + // it('should not have errors when base information is filled correctly', async () => { + // const application = new NoticeOfIntentSubmission({ + // owners: [], + // purpose: 'purpose', + // soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', + // soilIsFollowUp: false, + // soilAlreadyPlacedVolume: 5, + // soilAlreadyPlacedMaximumDepth: 5, + // soilToPlaceMaximumDepth: 5, + // soilAlreadyPlacedAverageDepth: 5, + // soilAlreadyPlacedArea: 5, + // soilToPlaceAverageDepth: 5, + // soilToPlaceVolume: 5, + // soilToPlaceArea: 5, + // soilAlternativeMeasures: 'soilAlternativeMeasures', + // soilFillTypeToPlace: 'soilFillTypeToPlace', + // typeCode: 'PFRS', + // }); + // + // const res = await service.validateSubmission(application); + // + // expect( + // includesError(res.errors, new Error(`PFRS Proposal incomplete`)), + // ).toBe(false); + // + // expect( + // includesError(res.errors, new Error(`PFRS Soil Table Incomplete`)), + // ).toBe(false); + // }); + // + // it('should report errors when information is missing', async () => { + // const application = new NoticeOfIntentSubmission({ + // owners: [], + // purpose: 'purpose', + // soilFillTypeToPlace: null, + // soilReduceNegativeImpacts: 'soilReduceNegativeImpacts', + // soilToPlaceArea: null, + // typeCode: 'PFRS', + // }); + // + // const res = await service.validateSubmission(application); + // + // expect( + // includesError(res.errors, new Error(`PFRS Proposal incomplete`)), + // ).toBe(true); + // + // expect( + // includesError(res.errors, new Error(`PFRS Soil Table Incomplete`)), + // ).toBe(true); + // }); + // + // it('should require NOI IDs or ApplicationIDs', async () => { + // const application = new NoticeOfIntentSubmission({ + // owners: [], + // soilIsFollowUp: true, + // typeCode: 'PFRS', + // }); + // + // const res = await service.validateSubmission(application); + // + // expect( + // includesError( + // res.errors, + // new Error(`PFRS Proposal missing Application or NOI IDs`), + // ), + // ).toBe(true); + // }); + // + // it('should complain about missing files', async () => { + // const application = new NoticeOfIntentSubmission({ + // owners: [], + // soilIsFollowUp: true, + // typeCode: 'PFRS', + // }); + // + // const res = await service.validateSubmission(application); + // + // expect( + // includesError( + // res.errors, + // new Error(`PFRS proposal missing Proposal Map / Site Plan`), + // ), + // ).toBe(true); + // + // expect( + // includesError( + // res.errors, + // new Error(`PFRS proposal missing Cross Section Diagrams`), + // ), + // ).toBe(true); + // + // expect( + // includesError( + // res.errors, + // new Error(`PFRS proposal missing Reclamation Plans`), + // ), + // ).toBe(true); + // }); + // + // it('should require a notice of work for both mining and notice true', async () => { + // const application = new NoticeOfIntentSubmission({ + // owners: [], + // soilIsFollowUp: true, + // soilIsExtractionOrMining: true, + // soilHasSubmittedNotice: true, + // typeCode: 'PFRS', + // }); + // + // const res = await service.validateSubmission(application); + // + // expect( + // includesError( + // res.errors, + // new Error( + // `PFRS proposal has yes to notice of work but is not attached`, + // ), + // ), + // ).toBe(true); + // }); + // }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission-validator.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission-validator.service.ts new file mode 100644 index 0000000000..d2d59990bb --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission-validator.service.ts @@ -0,0 +1,655 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { Injectable, Logger } from '@nestjs/common'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +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 { OWNER_TYPE } from '../../common/owner-type/owner-type.entity'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentParcel } from './notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; + +export class ValidatedNoticeOfIntentSubmission extends NoticeOfIntentSubmission { + applicant: string; + localGovernmentUuid: string; + parcels: NoticeOfIntentParcel[]; + primaryContact: NoticeOfIntentOwner; + parcelsAgricultureDescription: string; + parcelsAgricultureImprovementDescription: string; + parcelsNonAgricultureUseDescription: string; + northLandUseType: string; + northLandUseTypeDescription: string; + eastLandUseType: string; + eastLandUseTypeDescription: string; + southLandUseType: string; + southLandUseTypeDescription: string; + westLandUseType: string; + westLandUseTypeDescription: string; +} + +@Injectable() +export class NoticeOfIntentSubmissionValidatorService { + private logger: Logger = new Logger( + NoticeOfIntentSubmissionValidatorService.name, + ); + + constructor( + private localGovernmentService: LocalGovernmentService, + private noiParcelService: NoticeOfIntentParcelService, + private noiDocumentService: NoticeOfIntentDocumentService, + ) {} + + async validateSubmission(noticeOfIntentSubmission: NoticeOfIntentSubmission) { + const errors: Error[] = []; + + if (!noticeOfIntentSubmission.applicant) { + errors.push(new ServiceValidationException('Missing applicant')); + } + + if (!noticeOfIntentSubmission.purpose) { + errors.push(new ServiceValidationException('Missing purpose')); + } + + await this.validateParcels(noticeOfIntentSubmission, errors); + + const applicantDocuments = + await this.noiDocumentService.getApplicantDocuments( + noticeOfIntentSubmission.fileNumber, + ); + + await this.validatePrimaryContact( + noticeOfIntentSubmission, + applicantDocuments, + errors, + ); + + await this.validateLocalGovernment(noticeOfIntentSubmission, errors); + await this.validateLandUse(noticeOfIntentSubmission, errors); + this.validateAdditionalInfo( + noticeOfIntentSubmission, + applicantDocuments, + errors, + ); + await this.validateOptionalDocuments(applicantDocuments, errors); + + if (noticeOfIntentSubmission.typeCode === 'ROSO') { + await this.validateRosoProposal( + noticeOfIntentSubmission, + applicantDocuments, + errors, + ); + } + if (noticeOfIntentSubmission.typeCode === 'POFO') { + await this.validatePofoProposal( + noticeOfIntentSubmission, + applicantDocuments, + errors, + ); + } + if (noticeOfIntentSubmission.typeCode === 'PFRS') { + await this.validatePofoProposal( + noticeOfIntentSubmission, + applicantDocuments, + errors, + ); + await this.validateRosoProposal( + noticeOfIntentSubmission, + applicantDocuments, + errors, + ); + await this.validatePfrsProposal( + noticeOfIntentSubmission, + applicantDocuments, + errors, + ); + } + + return { + errors, + noticeOfIntentSubmission: + errors.length === 0 + ? (noticeOfIntentSubmission as ValidatedNoticeOfIntentSubmission) + : undefined, + }; + } + + private async validateParcels( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + errors: Error[], + ) { + const parcels = await this.noiParcelService.fetchByFileId( + noticeOfIntentSubmission.fileNumber, + ); + + if (parcels.length === 0) { + errors.push( + new ServiceValidationException(`Notice of Intent has no parcels`), + ); + } + + for (const parcel of parcels) { + if ( + parcel.ownershipTypeCode === null || + parcel.legalDescription === null || + parcel.mapAreaHectares === null || + parcel.civicAddress === null || + parcel.isFarm === null || + !parcel.isConfirmedByApplicant + ) { + errors.push( + new ServiceValidationException(`Invalid Parcel ${parcel.uuid}`), + ); + } + + if (parcel.ownershipTypeCode === 'SMPL' && !parcel.pid) { + errors.push( + new ServiceValidationException( + `Fee Simple Parcel ${parcel.uuid} has no PID`, + ), + ); + } + + if (parcel.pid && parcel.pid.length !== 9) { + errors.push( + new ServiceValidationException( + `Parcel ${parcel.uuid} has invalid PID`, + ), + ); + } + + if (parcel.ownershipTypeCode === 'SMPL' && !parcel.purchasedDate) { + errors.push( + new ServiceValidationException( + `Fee Simple Parcel ${parcel.uuid} has no purchase date`, + ), + ); + } + + if (parcel.ownershipTypeCode === 'CRWN' && !parcel.crownLandOwnerType) { + errors.push( + new ServiceValidationException( + `Crown Parcel ${parcel.uuid} has no ownership type`, + ), + ); + } + + if (parcel.owners.length === 0) { + errors.push( + new ServiceValidationException(`Parcel has no Owners ${parcel.uuid}`), + ); + } + + if ( + !parcel.certificateOfTitle && + (parcel.ownershipTypeCode === 'SMPL' || parcel.pid) + ) { + errors.push( + new ServiceValidationException( + `Parcel is missing certificate of title ${parcel.uuid}`, + ), + ); + } + } + + if (errors.length === 0) { + return parcels; + } + } + + private async validatePrimaryContact( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + documents: NoticeOfIntentDocument[], + errors: Error[], + ): Promise { + const primaryOwner = noticeOfIntentSubmission.owners.find( + (owner) => + owner.uuid === noticeOfIntentSubmission.primaryContactOwnerUuid, + ); + + if (!primaryOwner) { + errors.push( + new ServiceValidationException( + `Notice of Intent has no primary contact`, + ), + ); + return; + } + + const onlyHasIndividualOwner = + noticeOfIntentSubmission.owners.length === 1 && + noticeOfIntentSubmission.owners[0].type.code === OWNER_TYPE.INDIVIDUAL; + + const isGovernmentContact = + primaryOwner.type.code === OWNER_TYPE.GOVERNMENT; + + if (!onlyHasIndividualOwner && !isGovernmentContact) { + const authorizationLetters = documents.filter( + (document) => + document.type?.code === DOCUMENT_TYPE.AUTHORIZATION_LETTER, + ); + if (authorizationLetters.length === 0) { + errors.push( + new ServiceValidationException( + `Notice of Intent has no authorization letters`, + ), + ); + } + } + + if (primaryOwner.type.code === OWNER_TYPE.AGENT || isGovernmentContact) { + if ( + !primaryOwner.firstName || + !primaryOwner.lastName || + !primaryOwner.phoneNumber || + !primaryOwner.email + ) { + errors.push( + new ServiceValidationException( + `Invalid Third Party Agent Information`, + ), + ); + } + } + + if (errors.length === 0) { + return primaryOwner; + } + return undefined; + } + + private async validateLocalGovernment( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + errors: Error[], + ) { + const localGovernments = await this.localGovernmentService.list(); + const matchingLg = localGovernments.find( + (lg) => lg.uuid === noticeOfIntentSubmission.localGovernmentUuid, + ); + if (!noticeOfIntentSubmission.localGovernmentUuid) { + errors.push( + new ServiceValidationException( + 'Notice of Intent has no local government', + ), + ); + } + + if (!matchingLg) { + errors.push( + new ServiceValidationException( + 'Cannot find local government set on Notice of Intent', + ), + ); + return; + } + + if (!matchingLg.bceidBusinessGuid) { + errors.push( + new ServiceValidationException( + `Selected local government is setup in portal ${matchingLg.name}`, + ), + ); + } + } + + private async validateLandUse( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + errors: Error[], + ) { + if ( + !noticeOfIntentSubmission.parcelsAgricultureDescription || + !noticeOfIntentSubmission.parcelsAgricultureImprovementDescription || + !noticeOfIntentSubmission.parcelsNonAgricultureUseDescription + ) { + errors.push(new ServiceValidationException(`Invalid Parcel Description`)); + } + + if ( + !noticeOfIntentSubmission.northLandUseType || + !noticeOfIntentSubmission.northLandUseTypeDescription || + !noticeOfIntentSubmission.eastLandUseType || + !noticeOfIntentSubmission.eastLandUseTypeDescription || + !noticeOfIntentSubmission.southLandUseType || + !noticeOfIntentSubmission.southLandUseTypeDescription || + !noticeOfIntentSubmission.westLandUseType || + !noticeOfIntentSubmission.westLandUseTypeDescription + ) { + errors.push(new ServiceValidationException(`Invalid Adjacent Parcels`)); + } + } + + private async validateOptionalDocuments( + applicantDocuments: NoticeOfIntentDocument[], + errors: Error[], + ) { + const untypedDocuments = applicantDocuments.filter( + (document) => !document.type, + ); + for (const document of untypedDocuments) { + errors.push( + new ServiceValidationException( + `Document ${document.uuid} missing type`, + ), + ); + } + + const optionalDocuments = applicantDocuments.filter((document) => + [ + DOCUMENT_TYPE.OTHER, + DOCUMENT_TYPE.PHOTOGRAPH, + DOCUMENT_TYPE.PROFESSIONAL_REPORT, + ].includes(document.type?.code as DOCUMENT_TYPE), + ); + for (const document of optionalDocuments) { + if (!document.description) { + errors.push( + new ServiceValidationException( + `Document ${document.uuid} missing description`, + ), + ); + } + } + } + + private async validateRosoProposal( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + applicantDocuments: NoticeOfIntentDocument[], + errors: Error[], + ) { + if (noticeOfIntentSubmission.soilTypeRemoved === null) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal incomplete`, + ), + ); + } + + if ( + noticeOfIntentSubmission.soilToRemoveVolume === null || + noticeOfIntentSubmission.soilToRemoveArea === null || + noticeOfIntentSubmission.soilToRemoveMaximumDepth === null || + noticeOfIntentSubmission.soilToRemoveAverageDepth === null || + noticeOfIntentSubmission.soilAlreadyRemovedVolume === null || + noticeOfIntentSubmission.soilAlreadyRemovedArea === null || + noticeOfIntentSubmission.soilAlreadyRemovedMaximumDepth === null || + noticeOfIntentSubmission.soilAlreadyRemovedAverageDepth === null + ) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} Soil Table Incomplete`, + ), + ); + } + this.runSharedSoilValidation( + noticeOfIntentSubmission, + errors, + applicantDocuments, + ); + } + + private async validatePofoProposal( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + applicantDocuments: NoticeOfIntentDocument[], + errors: Error[], + ) { + if ( + noticeOfIntentSubmission.soilFillTypeToPlace === null || + noticeOfIntentSubmission.soilAlternativeMeasures === null || + noticeOfIntentSubmission.soilReduceNegativeImpacts === null + ) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal incomplete`, + ), + ); + } + + this.runSharedSoilValidation( + noticeOfIntentSubmission, + errors, + applicantDocuments, + ); + + if ( + noticeOfIntentSubmission.soilToPlaceVolume === null || + noticeOfIntentSubmission.soilToPlaceArea === null || + noticeOfIntentSubmission.soilToPlaceMaximumDepth === null || + noticeOfIntentSubmission.soilToPlaceAverageDepth === null || + noticeOfIntentSubmission.soilAlreadyPlacedVolume === null || + noticeOfIntentSubmission.soilAlreadyPlacedArea === null || + noticeOfIntentSubmission.soilAlreadyPlacedMaximumDepth === null || + noticeOfIntentSubmission.soilAlreadyPlacedAverageDepth === null + ) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} Soil Table Incomplete`, + ), + ); + } + this.runSharedSoilValidation( + noticeOfIntentSubmission, + errors, + applicantDocuments, + ); + } + + private async validatePfrsProposal( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + applicantDocuments: NoticeOfIntentDocument[], + errors: Error[], + ) { + if (noticeOfIntentSubmission.soilIsExtractionOrMining === null) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing extraction/mining answer`, + ), + ); + } + + if (noticeOfIntentSubmission.soilIsExtractionOrMining) { + if (noticeOfIntentSubmission.soilHasSubmittedNotice === null) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing notice submitted answer`, + ), + ); + } + } + + const noticeOfWork = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.NOTICE_OF_WORK, + ); + if (noticeOfIntentSubmission.soilIsFollowUp && noticeOfWork.length === 0) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal has yes to notice of work but is not attached`, + ), + ); + } + } + + private runSharedSoilValidation( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + errors: Error[], + applicantDocuments: NoticeOfIntentDocument[], + ) { + if ( + noticeOfIntentSubmission.soilIsFollowUp === null || + noticeOfIntentSubmission.soilProjectDurationAmount === null || + noticeOfIntentSubmission.soilProjectDurationUnit === null + ) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing shared fields`, + ), + ); + } + + if ( + noticeOfIntentSubmission.soilIsFollowUp && + !noticeOfIntentSubmission.soilFollowUpIDs + ) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing Application or NOI IDs`, + ), + ); + } + + const proposalMaps = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.PROPOSAL_MAP, + ); + if (proposalMaps.length === 0) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing Proposal Map / Site Plan`, + ), + ); + } + + if (noticeOfIntentSubmission.soilIsExtractionOrMining) { + const crossSections = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.CROSS_SECTIONS, + ); + if (crossSections.length === 0) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing Cross Section Diagrams`, + ), + ); + } + + const reclamationPlans = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.RECLAMATION_PLAN, + ); + if (reclamationPlans.length === 0) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing Reclamation Plans`, + ), + ); + } + + if (noticeOfIntentSubmission.soilHasSubmittedNotice === null) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing work notice answer`, + ), + ); + } + + if (noticeOfIntentSubmission.soilHasSubmittedNotice) { + const noticeOfWorks = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.NOTICE_OF_WORK, + ); + if (noticeOfWorks.length === 0) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing Notice of Work`, + ), + ); + } + } + } + } + + private validateAdditionalInfo( + noticeOfIntentSubmission: NoticeOfIntentSubmission, + applicantDocuments: NoticeOfIntentDocument[], + errors: Error[], + ) { + if (noticeOfIntentSubmission.soilIsRemovingSoilForNewStructure === null) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing structure questions`, + ), + ); + } + + if (noticeOfIntentSubmission.soilIsRemovingSoilForNewStructure) { + if (noticeOfIntentSubmission.soilProposedStructures.length === 0) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing structures`, + ), + ); + } + + for (const structure of noticeOfIntentSubmission.soilProposedStructures) { + if (!structure.type || structure.area === null) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal incomplete structure`, + ), + ); + } + } + + const buildingPlans = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.BUILDING_PLAN, + ); + if (buildingPlans.length === 0) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing Building Plans`, + ), + ); + } + + const hasFarmStructure = + noticeOfIntentSubmission.soilProposedStructures.some( + (structure) => structure.type === 'Farm Structure', + ); + if (hasFarmStructure) { + if (!noticeOfIntentSubmission.soilStructureFarmUseReason) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing farm use reason`, + ), + ); + } + if (!noticeOfIntentSubmission.soilAgriParcelActivity) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing agricultural activity`, + ), + ); + } + } + + const hasResidentialStructure = + noticeOfIntentSubmission.soilProposedStructures.some((structure) => + [ + 'Residential - Principal Residence', + 'Residential - Additional Residence', + 'Residential - Accessory Structure', + ].includes(structure.type!), + ); + if ( + hasResidentialStructure && + !noticeOfIntentSubmission.soilStructureResidentialUseReason + ) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing residential use reason`, + ), + ); + } + } + + const hasAccessoryStructure = + noticeOfIntentSubmission.soilProposedStructures.find( + (structure) => structure.type === 'Residential - Accessory Structure', + ); + if ( + hasAccessoryStructure && + !noticeOfIntentSubmission.soilStructureResidentialAccessoryUseReason + ) { + errors.push( + new ServiceValidationException( + `${noticeOfIntentSubmission.typeCode} proposal missing accessory use reason`, + ), + ); + } + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts index a0052602b0..4bbc67fa74 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts @@ -12,6 +12,10 @@ import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; import { User } from '../../user/user.entity'; +import { + NoticeOfIntentSubmissionValidatorService, + ValidatedNoticeOfIntentSubmission, +} from './notice-of-intent-submission-validator.service'; import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; import { NoticeOfIntentSubmissionDetailedDto, @@ -25,6 +29,7 @@ describe('NoticeOfIntentSubmissionController', () => { let mockNoiSubmissionService: DeepMocked; let mockDocumentService: DeepMocked; let mockLgService: DeepMocked; + let mockNoiValidatorService: DeepMocked; const localGovernmentUuid = 'local-government'; const applicant = 'fake-applicant'; @@ -34,6 +39,7 @@ describe('NoticeOfIntentSubmissionController', () => { mockNoiSubmissionService = createMock(); mockDocumentService = createMock(); mockLgService = createMock(); + mockNoiValidatorService = createMock(); const module: TestingModule = await Test.createTestingModule({ controllers: [NoticeOfIntentSubmissionController], @@ -51,6 +57,10 @@ describe('NoticeOfIntentSubmissionController', () => { provide: LocalGovernmentService, useValue: mockLgService, }, + { + provide: NoticeOfIntentSubmissionValidatorService, + useValue: mockNoiValidatorService, + }, { provide: ClsService, useValue: {}, @@ -272,8 +282,14 @@ describe('NoticeOfIntentSubmissionController', () => { mockNoiSubmissionService.getIfCreatorByUuid.mockResolvedValue( new NoticeOfIntentSubmission(), ); - - mockNoiSubmissionService.updateStatus.mockResolvedValue(); + mockNoiValidatorService.validateSubmission.mockResolvedValue({ + noticeOfIntentSubmission: + new NoticeOfIntentSubmission() as ValidatedNoticeOfIntentSubmission, + errors: [], + }); + mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as NoticeOfIntentSubmissionDetailedDto, + ); await controller.submitAsApplicant(mockFileId, { user: { @@ -282,13 +298,10 @@ describe('NoticeOfIntentSubmissionController', () => { }); expect(mockNoiSubmissionService.verifyAccessByUuid).toHaveBeenCalledTimes( - 1, + 2, ); expect(mockNoiSubmissionService.submitToAlcs).toHaveBeenCalledTimes(1); - expect(mockNoiSubmissionService.updateStatus).toHaveBeenCalledTimes(1); - expect(mockNoiSubmissionService.updateStatus).toHaveBeenCalledWith( - undefined, - NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, - ); + expect(mockNoiValidatorService.validateSubmission).toHaveBeenCalledTimes(1); + expect(mockNoiSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(1); }); }); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts index 82e569cfa9..2e81391598 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Body, Controller, Get, @@ -9,10 +10,9 @@ import { Req, UseGuards, } from '@nestjs/common'; -import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; -import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; import { User } from '../../user/user.entity'; +import { NoticeOfIntentSubmissionValidatorService } from './notice-of-intent-submission-validator.service'; import { NoticeOfIntentSubmissionCreateDto, NoticeOfIntentSubmissionUpdateDto, @@ -26,6 +26,7 @@ export class NoticeOfIntentSubmissionController { constructor( private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private noticeOfIntentValidatorService: NoticeOfIntentSubmissionValidatorService, ) {} @Get() @@ -127,25 +128,30 @@ export class NoticeOfIntentSubmissionController { req.user.entity, ); - const validationResult = { - noticeOfIntentSubmission, - errors: [], - }; + const validationResult = + await this.noticeOfIntentValidatorService.validateSubmission( + noticeOfIntentSubmission, + ); - if (validationResult) { + if (validationResult.noticeOfIntentSubmission) { const validatedApplicationSubmission = validationResult.noticeOfIntentSubmission; + await this.noticeOfIntentSubmissionService.submitToAlcs( validatedApplicationSubmission, ); - return await this.noticeOfIntentSubmissionService.updateStatus( - noticeOfIntentSubmission.uuid, - NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, + + const finalSubmission = + await this.noticeOfIntentSubmissionService.verifyAccessByUuid( + uuid, + req.user.entity, + ); + return await this.noticeOfIntentSubmissionService.mapToDetailedDTO( + finalSubmission, ); } else { - //TODO: Uncomment when we add validation - //this.logger.debug(validationResult.errors); - //throw new BadRequestException('Invalid Application'); + this.logger.debug(validationResult.errors); + throw new BadRequestException('Invalid Notice of Intent'); } } } diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts index 33a9ac799f..8fe460df4c 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts @@ -18,6 +18,7 @@ import { NoticeOfIntentParcelOwnershipType } from './notice-of-intent-parcel/not import { NoticeOfIntentParcelController } from './notice-of-intent-parcel/notice-of-intent-parcel.controller'; import { NoticeOfIntentParcel } from './notice-of-intent-parcel/notice-of-intent-parcel.entity'; import { NoticeOfIntentParcelService } from './notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmissionValidatorService } from './notice-of-intent-submission-validator.service'; import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; @@ -48,6 +49,7 @@ import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.s NoticeOfIntentSubmissionService, NoticeOfIntentParcelService, NoticeOfIntentOwnerService, + NoticeOfIntentSubmissionValidatorService, NoticeOfIntentSubmissionProfile, NoticeOfIntentOwnerProfile, NoticeOfIntentParcelProfile, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts index 91967e708f..8c91c6e772 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -15,6 +15,7 @@ import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-int import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; +import { ValidatedNoticeOfIntentSubmission } from './notice-of-intent-submission-validator.service'; import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; @@ -196,7 +197,9 @@ describe('NoticeOfIntentSubmissionService', () => { mockNoiService.submit.mockRejectedValue(new Error()); await expect( - service.submitToAlcs(noticeOfIntentSubmission), + service.submitToAlcs( + noticeOfIntentSubmission as ValidatedNoticeOfIntentSubmission, + ), ).rejects.toMatchObject( new BaseServiceException( `Failed to submit notice of intent: ${fileNumber}`, @@ -223,7 +226,9 @@ describe('NoticeOfIntentSubmissionService', () => { ); mockNoiService.submit.mockResolvedValue(mockNoticeOfIntent); - await service.submitToAlcs(mockNoiSubmission); + await service.submitToAlcs( + mockNoiSubmission as ValidatedNoticeOfIntentSubmission, + ); expect(mockNoiService.submit).toBeCalledTimes(1); expect(mockNoiStatusService.setStatusDate).toHaveBeenCalledTimes(1); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index 2a808b8aa3..b0a81cca14 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -23,6 +23,7 @@ import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; import { filterUndefined } from '../../utils/undefined'; +import { ValidatedNoticeOfIntentSubmission } from './notice-of-intent-submission-validator.service'; import { NoticeOfIntentSubmissionDetailedDto, NoticeOfIntentSubmissionDto, @@ -549,18 +550,20 @@ export class NoticeOfIntentSubmissionService { }; } - async submitToAlcs(noticeOfIntent: NoticeOfIntentSubmission) { + async submitToAlcs( + noticeOfIntentSubmission: ValidatedNoticeOfIntentSubmission, + ) { try { const submittedNoi = await this.noticeOfIntentService.submit({ - fileNumber: noticeOfIntent.fileNumber, - applicant: noticeOfIntent.applicant!, //TODO: Remove ! once validation is implemented - localGovernmentUuid: noticeOfIntent.localGovernmentUuid!, - typeCode: noticeOfIntent.typeCode, + fileNumber: noticeOfIntentSubmission.fileNumber, + applicant: noticeOfIntentSubmission.applicant, + localGovernmentUuid: noticeOfIntentSubmission.localGovernmentUuid, + typeCode: noticeOfIntentSubmission.typeCode, dateSubmittedToAlc: new Date(), }); await this.noticeOfIntentSubmissionStatusService.setStatusDate( - submittedNoi.uuid, + noticeOfIntentSubmission.uuid, NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, submittedNoi.dateSubmittedToAlc, ); @@ -569,7 +572,7 @@ export class NoticeOfIntentSubmissionService { } catch (ex) { this.logger.error(ex); throw new BaseServiceException( - `Failed to submit notice of intent: ${noticeOfIntent.fileNumber}`, + `Failed to submit notice of intent: ${noticeOfIntentSubmission.fileNumber}`, ); } } From dd5610bc1ba9b06a24bf409bca07dfb90ad1b7fe Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Tue, 15 Aug 2023 13:02:27 -0700 Subject: [PATCH 253/954] Feature/alcs 980 step 6 on step 8 (#879) * step 6 on step 8 for roso and some fixes * unit test --- .../roso-additional-information.component.ts | 3 +- .../notice-of-intent-details.component.html | 43 +++----- .../notice-of-intent-details.component.scss | 8 ++ .../notice-of-intent-details.module.ts | 3 +- ...roso-additional-information.component.html | 103 ++++++++++++++++++ ...roso-additional-information.component.scss | 5 + ...o-additional-information.component.spec.ts | 31 ++++++ .../roso-additional-information.component.ts | 97 +++++++++++++++++ .../roso-details/roso-details.component.scss | 9 -- 9 files changed, 265 insertions(+), 37 deletions(-) create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.ts diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.ts index 1d694c58f3..ba72e75687 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component.ts @@ -25,7 +25,7 @@ export enum STRUCTURE_TYPES { type ProposedStructure = { type: STRUCTURE_TYPES | null; area: string | null }; -const RESIDENTIAL_STRUCTURE_TYPES = [ +export const RESIDENTIAL_STRUCTURE_TYPES = [ STRUCTURE_TYPES.ACCESSORY_STRUCTURE, STRUCTURE_TYPES.ADDITIONAL_RESIDENCE, STRUCTURE_TYPES.PRINCIPAL_RESIDENCE, @@ -215,6 +215,7 @@ export class RosoAdditionalInformationComponent extends FilesStepComponent imple this.confirmRemovalOfSoil = false; this.form.reset(); + this.form.markAsDirty(); this.form.controls.isRemovingSoilForNewStructure.setValue('false'); this.proposedStructures = []; this.structuresSource = new MatTableDataSource(this.proposedStructures); diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.html index d4ad467c8f..c8a9bf25fe 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.html @@ -112,10 +112,7 @@

Land Use of Parcel(s) under Notice of Intent

{{ noiSubmission.parcelsAgricultureDescription }} - +
Quantify and describe in detail all agricultural improvements made to the parcel(s). @@ -136,10 +133,7 @@

Land Use of Parcel(s) under Notice of Intent

{{ noiSubmission.parcelsNonAgricultureUseDescription }} - +

@@ -169,10 +163,7 @@

{{ noiSubmission.northLandUseTypeDescription }} - +
East
@@ -181,10 +172,7 @@

{{ noiSubmission.eastLandUseTypeDescription }} - +
South
@@ -193,10 +181,7 @@

{{ noiSubmission.southLandUseTypeDescription }} - +
West
@@ -205,10 +190,7 @@

{{ noiSubmission.westLandUseTypeDescription }} - +
@@ -229,8 +211,17 @@

5. Proposal

>
-

6. Additional Information

- TODO +

6. Additional Proposal Information

+ +

7. Optional Documents

diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.scss b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.scss index 8df832ca0f..7ef4dbb6b8 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.scss +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.component.scss @@ -58,6 +58,14 @@ overflow-x: auto; } + .soil-table { + display: grid; + grid-template-columns: max-content max-content; + overflow-x: auto; + grid-column-gap: rem(36); + grid-row-gap: rem(12); + } + .other-attachments { display: grid; grid-template-columns: max-content max-content max-content; diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts index 7fbf9b5cd3..9f59f389d8 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts @@ -5,9 +5,10 @@ import { SharedModule } from '../../../shared/shared.module'; import { NoticeOfIntentDetailsComponent } from './notice-of-intent-details.component'; import { ParcelComponent } from './parcel/parcel.component'; import { RosoDetailsComponent } from './roso-details/roso-details.component'; +import { RosoAdditionalInformationComponent } from './roso-details/roso-additional-information/roso-additional-information.component'; @NgModule({ - declarations: [ParcelComponent, RosoDetailsComponent, NoticeOfIntentDetailsComponent], + declarations: [ParcelComponent, RosoDetailsComponent, NoticeOfIntentDetailsComponent, RosoAdditionalInformationComponent], imports: [CommonModule, SharedModule, NgxMaskPipe], exports: [NoticeOfIntentDetailsComponent], }) diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.html new file mode 100644 index 0000000000..985e6732d8 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.html @@ -0,0 +1,103 @@ +
+
+ Are you removing soil in order to build a structure? + +
+
+ + {{ _noiSubmission.soilIsRemovingSoilForNewStructure ? 'Yes' : 'No' }} + + +
+ + +
+
#
+
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 }} + +
+
+ +
Detailed Building Plan(s)
+ +
+ +
+ +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.scss b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.scss new file mode 100644 index 0000000000..bfe0899632 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.scss @@ -0,0 +1,5 @@ +.multiple-documents { + a { + display: block; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.spec.ts new file mode 100644 index 0000000000..92b752ab0b --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; + +import { RosoAdditionalInformationComponent } from './roso-additional-information.component'; + +describe('RosoAdditionalInformationComponent', () => { + let component: RosoAdditionalInformationComponent; + let fixture: ComponentFixture; + let mockNoiDocumentService: DeepMocked; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RosoAdditionalInformationComponent], + providers: [ + { + provide: NoticeOfIntentDocumentService, + useValue: mockNoiDocumentService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(RosoAdditionalInformationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.ts new file mode 100644 index 0000000000..a2b65f427e --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.ts @@ -0,0 +1,97 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { NoticeOfIntentDocumentDto } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; +import { + RESIDENTIAL_STRUCTURE_TYPES, + STRUCTURE_TYPES, +} from '../../../edit-submission/additional-information/roso/roso-additional-information.component'; + +@Component({ + selector: 'app-roso-additional-information', + templateUrl: './roso-additional-information.component.html', + styleUrls: ['./roso-additional-information.component.scss'], +}) +export class RosoAdditionalInformationComponent { + @Input() showErrors = true; + @Input() showEdit = true; + @Input() draftMode = false; + @Input() updatedFields: string[] = []; + + _noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + + @Input() set noiSubmission(noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined) { + if (noiSubmission) { + this._noiSubmission = noiSubmission; + this.setVisibilityAndValidatorsForResidentialFields(); + this.setVisibilityAndValidatorsForAccessoryFields(); + this.setVisibilityAndValidatorsForFarmFields(); + } + } + + @Input() set noiDocuments(documents: NoticeOfIntentDocumentDto[]) { + this.buildingPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.BUILDING_PLAN); + } + + buildingPlans: NoticeOfIntentDocumentDto[] = []; + + isSoilStructureFarmUseReasonVisible = false; + isSoilStructureResidentialUseReasonVisible = false; + isSoilAgriParcelActivityVisible = false; + isSoilStructureResidentialAccessoryUseReasonVisible = false; + + constructor(private router: Router, private noticeOfIntentDocumentService: NoticeOfIntentDocumentService) {} + + private setVisibilityAndValidatorsForResidentialFields() { + if ( + this._noiSubmission?.soilProposedStructures.some( + (structure) => structure.type && RESIDENTIAL_STRUCTURE_TYPES.includes(structure.type) + ) + ) { + this.isSoilStructureResidentialUseReasonVisible = true; + } else { + this.isSoilStructureResidentialUseReasonVisible = false; + } + } + + private setVisibilityAndValidatorsForAccessoryFields() { + if ( + this._noiSubmission?.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE + ) + ) { + this.isSoilStructureResidentialAccessoryUseReasonVisible = true; + } else { + this.isSoilStructureResidentialAccessoryUseReasonVisible = false; + } + } + + private setVisibilityAndValidatorsForFarmFields() { + 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; + } + } + + async onEditSection(step: number) { + if (this.draftMode) { + await this.router.navigateByUrl( + `/alcs/notice-of-intent/${this._noiSubmission?.fileNumber}/edit/${step}?errors=t` + ); + } else { + await this.router.navigateByUrl(`notice-of-intent/${this._noiSubmission?.fileNumber}/edit/${step}?errors=t`); + } + } + + async openFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.scss b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.scss index 0c540ce5fb..e69de29bb2 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.scss +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.scss @@ -1,9 +0,0 @@ -@use '../../../../../styles/functions' as *; - -.soil-table { - display: grid; - grid-template-columns: max-content max-content; - overflow-x: auto; - grid-column-gap: rem(36); - grid-row-gap: rem(12); -} From 1d914f38c1311afd58b3da8abce31fa77407577b Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 15 Aug 2023 16:11:49 -0700 Subject: [PATCH 254/954] Add Proposal map to NFU Submissions * Add to Step 6 and Step 8 * Add to ALCS --- .../application-details.component.html | 1 + .../naru-details/naru-details.component.html | 8 +++-- .../naru-details/naru-details.component.ts | 2 +- .../nfu-details/nfu-details.component.html | 8 +++++ .../nfu-details/nfu-details.component.spec.ts | 12 ++++++-- .../nfu-details/nfu-details.component.ts | 17 ++++++++++- .../application-details.component.html | 6 ++-- .../naru-details.component.spec.ts | 7 ----- .../nfu-details/nfu-details.component.html | 7 +++++ .../nfu-details/nfu-details.component.spec.ts | 16 ++++++++-- .../nfu-details/nfu-details.component.ts | 15 +++++++++- .../edit-submission.component.html | 1 + .../nfu-proposal/nfu-proposal.component.html | 13 +++++++++ .../nfu-proposal.component.spec.ts | 12 ++++++++ .../nfu-proposal/nfu-proposal.component.ts | 29 ++++++++++++++----- ...pplication-submission-validator.service.ts | 18 +++++++++++- 16 files changed, 140 insertions(+), 32 deletions(-) diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html index 56625b34d5..1777a072b9 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.html @@ -149,6 +149,7 @@

Proposal

Proposal Map / Site Plan
diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.ts index 23d8f536c5..94a33a8f5a 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/naru-details/naru-details.component.ts @@ -26,7 +26,7 @@ export class NaruDetailsComponent { proposalMap: ApplicationDocumentDto[] = []; - constructor(private router: Router, private applicationDocumentService: ApplicationDocumentService) {} + constructor(private applicationDocumentService: ApplicationDocumentService) {} async openFile(file: ApplicationDocumentDto) { await this.applicationDocumentService.download(file.uuid, file.fileName); diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html index 97fbb7bff9..2e25f03054 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html @@ -18,6 +18,14 @@
{{ applicationSubmission.nfuAgricultureSupport }}
+
Proposal Map / Site Plan
+
Do you need to import any fill to construct or conduct the proposed Non-farm use? Fill is any material brought onto the property, including gravel for construction. diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.spec.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.spec.ts index c89c5c5448..e20167bde4 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.spec.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ApplicationDocumentService } from '../../../../../services/application/application-document/application-document.service'; import { NfuDetailsComponent } from './nfu-details.component'; @@ -8,9 +9,14 @@ describe('NfuDetailsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ NfuDetailsComponent ] - }) - .compileComponents(); + providers: [ + { + provide: ApplicationDocumentService, + useValue: {}, + }, + ], + declarations: [NfuDetailsComponent], + }).compileComponents(); fixture = TestBed.createComponent(NfuDetailsComponent); component = fixture.componentInstance; diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.ts index c7f3561504..6d878a899c 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.ts @@ -1,5 +1,8 @@ import { Component, Input } from '@angular/core'; +import { ApplicationDocumentDto } from '../../../../../services/application/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../../services/application/application-document/application-document.service'; import { ApplicationSubmissionDto } from '../../../../../services/application/application.dto'; +import { DOCUMENT_TYPE } from '../../../../../shared/document/document.dto'; @Component({ selector: 'app-nfu-details[applicationSubmission]', @@ -9,5 +12,17 @@ import { ApplicationSubmissionDto } from '../../../../../services/application/ap export class NfuDetailsComponent { @Input() applicationSubmission!: ApplicationSubmissionDto; - constructor() {} + @Input() set files(documents: ApplicationDocumentDto[] | undefined) { + if (documents) { + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + } + } + + proposalMap: ApplicationDocumentDto[] = []; + + constructor(private applicationDocumentService: ApplicationDocumentService) {} + + async openFile(file: ApplicationDocumentDto) { + await this.applicationDocumentService.download(file.uuid, file.fileName); + } } diff --git a/portal-frontend/src/app/features/applications/application-details/application-details.component.html b/portal-frontend/src/app/features/applications/application-details/application-details.component.html index de1ad9841b..20d8ac1cab 100644 --- a/portal-frontend/src/app/features/applications/application-details/application-details.component.html +++ b/portal-frontend/src/app/features/applications/application-details/application-details.component.html @@ -41,10 +41,7 @@

3. Primary Contact

Organization (optional) Ministry/Department Responsible @@ -242,6 +239,7 @@

6. Proposal

[showEdit]="showEdit" [draftMode]="draftMode" [applicationSubmission]="applicationSubmission" + [applicationDocuments]="appDocuments" [updatedFields]="updatedFields" > { let component: NaruDetailsComponent; let fixture: ComponentFixture; let mockAppDocumentService: DeepMocked; - let mockAppParcelService: DeepMocked; beforeEach(async () => { - mockAppParcelService = createMock(); mockAppDocumentService = createMock(); await TestBed.configureTestingModule({ @@ -22,10 +19,6 @@ describe('PofoDetailsComponent', () => { provide: ApplicationDocumentService, useValue: mockAppDocumentService, }, - { - provide: ApplicationParcelService, - useValue: mockAppParcelService, - }, ], }).compileComponents(); diff --git a/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html index 339409f445..5dcc330bc5 100644 --- a/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html +++ b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html @@ -32,6 +32,13 @@ {{ applicationSubmission.nfuAgricultureSupport }}
+
Proposal Map / Site Plan
+
Do you need to import any fill to construct or conduct the proposed Non-farm use? Fill is any material brought onto the property, including gravel for construction. diff --git a/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.spec.ts b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.spec.ts index c89c5c5448..0083d0c979 100644 --- a/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.spec.ts +++ b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.spec.ts @@ -1,16 +1,26 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { NfuDetailsComponent } from './nfu-details.component'; describe('NfuDetailsComponent', () => { let component: NfuDetailsComponent; let fixture: ComponentFixture; + let mockAppDocumentService: DeepMocked; beforeEach(async () => { + mockAppDocumentService = createMock(); + await TestBed.configureTestingModule({ - declarations: [ NfuDetailsComponent ] - }) - .compileComponents(); + declarations: [NfuDetailsComponent], + providers: [ + { + provide: ApplicationDocumentService, + useValue: mockAppDocumentService, + }, + ], + }).compileComponents(); fixture = TestBed.createComponent(NfuDetailsComponent); component = fixture.componentInstance; diff --git a/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.ts b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.ts index 5a4f0a54d4..2479a231f3 100644 --- a/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.ts +++ b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.ts @@ -1,6 +1,9 @@ import { Component, Input } from '@angular/core'; import { Router } from '@angular/router'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; @Component({ selector: 'app-nfu-details[applicationSubmission]', @@ -13,8 +16,13 @@ export class NfuDetailsComponent { @Input() showEdit = true; @Input() draftMode = false; @Input() updatedFields: string[] = []; + proposalMap: ApplicationDocumentDto[] = []; - constructor(private router: Router) {} + @Input() set applicationDocuments(documents: ApplicationDocumentDto[]) { + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + } + + constructor(private router: Router, private applicationDocumentService: ApplicationDocumentService) {} async onEditSection(step: number) { if (this.draftMode) { @@ -25,4 +33,9 @@ export class NfuDetailsComponent { await this.router.navigateByUrl(`application/${this.applicationSubmission?.fileNumber}/edit/${step}?errors=t`); } } + + async openFile(uuid: string) { + const res = await this.applicationDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.html index 704d34ef5f..269d5d3ebb 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/edit-submission.component.html @@ -96,6 +96,7 @@
Proposal
Characters left: {{ 4000 - agricultureSupportText.textLength }}
+
+ +
A visual representation of your proposal.
+ +
[noiDocuments]="appDocuments" [updatedFields]="updatedFields" > - +

7. Optional Documents

diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts index 9f59f389d8..f194f51c70 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module.ts @@ -4,11 +4,20 @@ import { NgxMaskPipe } from 'ngx-mask'; import { SharedModule } from '../../../shared/shared.module'; import { NoticeOfIntentDetailsComponent } from './notice-of-intent-details.component'; import { ParcelComponent } from './parcel/parcel.component'; +import { PfrsDetailsComponent } from './pfrs-details/pfrs-details.component'; +import { PofoDetailsComponent } from './pofo-details/pofo-details.component'; import { RosoDetailsComponent } from './roso-details/roso-details.component'; -import { RosoAdditionalInformationComponent } from './roso-details/roso-additional-information/roso-additional-information.component'; +import { AdditionalInformationComponent } from './additional-information/additional-information.component'; @NgModule({ - declarations: [ParcelComponent, RosoDetailsComponent, NoticeOfIntentDetailsComponent, RosoAdditionalInformationComponent], + declarations: [ + ParcelComponent, + RosoDetailsComponent, + PofoDetailsComponent, + PfrsDetailsComponent, + NoticeOfIntentDetailsComponent, + AdditionalInformationComponent, + ], imports: [CommonModule, SharedModule, NgxMaskPipe], exports: [NoticeOfIntentDetailsComponent], }) diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.html new file mode 100644 index 0000000000..8d6d6739a8 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.html @@ -0,0 +1,254 @@ +
+
+ Has the ALC previously received an application or Notice of Intent for this proposal? + +
+
+ + {{ _noiSubmission.soilIsFollowUp ? 'Yes' : 'No' }} + + +
+ +
+ Application or NOI ID + +
+
+ {{ _noiSubmission.soilFollowUpIDs }} + +
+ +
+ What is the purpose of the proposal? + +
+
+ {{ _noiSubmission.purpose }} + +
+ +
+ Describe the type of soil proposed to be removed. + +
+
+ {{ _noiSubmission.soilTypeRemoved }} + +
+ +
+ Describe the type, origin and quality of fill proposed to be placed. + +
+
+ {{ _noiSubmission.soilFillTypeToPlace }} + +
+ +
+
+
Soil to be Removed
+
Fill to be Placed
+ +
Volume
+
+ {{ _noiSubmission.soilToRemoveVolume }} + m3 + +
+
+ {{ _noiSubmission.soilToPlaceVolume }} + m3 + +
+ +
Area
+
+ {{ _noiSubmission.soilToRemoveArea }} + ha + +
+
+ {{ _noiSubmission.soilToPlaceArea }} + ha + +
+ +
Maximum Depth
+
+ {{ _noiSubmission.soilToRemoveMaximumDepth }} + m + +
+
+ {{ _noiSubmission.soilToPlaceMaximumDepth }} + m + +
+ +
Average Depth
+
+ {{ _noiSubmission.soilToRemoveAverageDepth }} + m + +
+
+ {{ _noiSubmission.soilToPlaceAverageDepth }} + m + +
+ +
+ +
+
Soil already Removed
+
Fill already Placed
+ +
Volume
+
+ {{ _noiSubmission.soilAlreadyRemovedVolume }} + m3 + +
+
+ {{ _noiSubmission.soilAlreadyPlacedVolume }} + m3 + +
+ +
Area
+
+ {{ _noiSubmission.soilAlreadyRemovedArea }} + ha + +
+
+ {{ _noiSubmission.soilAlreadyPlacedArea }} + ha + +
+ +
Maximum Depth
+
+ {{ _noiSubmission.soilAlreadyRemovedMaximumDepth }} + m + +
+
+ {{ _noiSubmission.soilAlreadyPlacedMaximumDepth }} + m + +
+ +
Average Depth
+
+ {{ _noiSubmission.soilAlreadyRemovedAverageDepth }} + m + +
+
+ {{ _noiSubmission.soilAlreadyPlacedAverageDepth }} + m + +
+
+ +
+

Project Duration

+
+
+ Duration +
+
+ {{ _noiSubmission.soilProjectDurationUnit }} + +
+
+ Length +
+
+ {{ _noiSubmission.soilProjectDurationAmount }} + +
+ +
Proposal Map / Site Plan
+ + +
Is your proposal for area-wide filling?
+
+ + {{ _noiSubmission.soilIsAreaWideFilling ? 'Yes' : 'No' }} + + +
+ +
Is your proposal for aggregate extraction or placer mining?
+
+ + {{ _noiSubmission.soilIsExtractionOrMining ? 'Yes' : 'No' }} + + +
+ + +
Cross Sections
+ + +
Reclamation Plan
+ + + +
+ Have you submitted a Notice of Work to the Ministry of Energy, Mines and Low Carbon Innovation (EMLI)? +
+
+ + {{ _noiSubmission.soilHasSubmittedNotice ? 'Yes' : 'No' }} + + +
+
+ + +
Notice of Work
+ +
+
+ +
+ +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.scss b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.scss new file mode 100644 index 0000000000..9cda769473 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.scss @@ -0,0 +1,10 @@ +@use '../../../../../styles/functions' as *; + +.soil-table { + grid-template-columns: max-content max-content max-content; + + .spacer-row { + grid-column: 1/4; + height: rem(16); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.spec.ts new file mode 100644 index 0000000000..36f0806422 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; + +import { PfrsDetailsComponent } from './pfrs-details.component'; + +describe('RosoDetailsComponent', () => { + let component: PfrsDetailsComponent; + let fixture: ComponentFixture; + let mockNoiDocumentService: DeepMocked; + let mockNoiParcelService: DeepMocked; + + beforeEach(async () => { + mockNoiParcelService = createMock(); + mockNoiDocumentService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [PfrsDetailsComponent], + providers: [ + { + provide: NoticeOfIntentDocumentService, + useValue: mockNoiDocumentService, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNoiParcelService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(PfrsDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.ts new file mode 100644 index 0000000000..7362317011 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pfrs-details/pfrs-details.component.ts @@ -0,0 +1,55 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; + +@Component({ + selector: 'app-pfrs-details[noiSubmission]', + templateUrl: './pfrs-details.component.html', + styleUrls: ['./pfrs-details.component.scss'], +}) +export class PfrsDetailsComponent { + @Input() showErrors = true; + @Input() showEdit = true; + @Input() draftMode = false; + @Input() updatedFields: string[] = []; + + _noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + + @Input() set noiSubmission(noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined) { + if (noiSubmission) { + this._noiSubmission = noiSubmission; + } + } + + @Input() set noiDocuments(documents: NoticeOfIntentDocumentDto[]) { + this.crossSections = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.CROSS_SECTIONS); + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.reclamationPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.RECLAMATION_PLAN); + this.noticeOfWorks = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.NOTICE_OF_WORK); + } + + crossSections: NoticeOfIntentDocumentDto[] = []; + proposalMap: NoticeOfIntentDocumentDto[] = []; + reclamationPlans: NoticeOfIntentDocumentDto[] = []; + noticeOfWorks: NoticeOfIntentDocumentDto[] = []; + + constructor(private router: Router, private noticeOfIntentDocumentService: NoticeOfIntentDocumentService) {} + + async onEditSection(step: number) { + if (this.draftMode) { + await this.router.navigateByUrl( + `/alcs/notice-of-intent/${this._noiSubmission?.fileNumber}/edit/${step}?errors=t` + ); + } else { + await this.router.navigateByUrl(`notice-of-intent/${this._noiSubmission?.fileNumber}/edit/${step}?errors=t`); + } + } + + async openFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.html new file mode 100644 index 0000000000..1fbf8a4fdd --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.html @@ -0,0 +1,166 @@ +
+
+ Has the ALC previously received an application or Notice of Intent for this proposal? + +
+
+ + {{ _noiSubmission.soilIsFollowUp ? 'Yes' : 'No' }} + + +
+ +
+ Application or NOI ID + +
+
+ {{ _noiSubmission.soilFollowUpIDs }} + +
+ +
+ What is the purpose of the proposal? + +
+
+ {{ _noiSubmission.purpose }} + +
+ +
+ Describe the type, origin and quality of fill proposed to be placed. + +
+
+ {{ _noiSubmission.soilFillTypeToPlace }} + +
+ +
+
+
+ Fill to be Placed + +
+
Volume
+
+ {{ _noiSubmission.soilToPlaceVolume }} + m3 + +
+
Area
+
+ {{ _noiSubmission.soilToPlaceArea }} ha + +
+
Maximum Depth
+
+ {{ _noiSubmission.soilToPlaceMaximumDepth }} + m + +
+
Average Depth
+
+ {{ _noiSubmission.soilToPlaceAverageDepth }} + m + +
+
+ +
+
+
+ Fill already Placed + +
+
Volume
+
+ {{ _noiSubmission.soilAlreadyPlacedVolume }} + m3 + +
+
Area
+
+ {{ _noiSubmission.soilAlreadyPlacedArea }} + ha + +
+
Maximum Depth
+
+ {{ _noiSubmission.soilAlreadyPlacedMaximumDepth }} + m + +
+
Average Depth
+
+ {{ _noiSubmission.soilAlreadyPlacedAverageDepth }} + m + +
+
+ +
+

Project Duration

+
+
+ Duration + +
+
+ {{ _noiSubmission.soilProjectDurationUnit }} + +
+
+ Length + +
+
+ {{ _noiSubmission.soilProjectDurationAmount }} + +
+ +
Proposal Map / Site Plan
+ + +
Is your proposal for area-wide filling?
+
+ + {{ _noiSubmission.soilIsAreaWideFilling ? 'Yes' : 'No' }} + + +
+ + +
Cross Sections
+ + +
Reclamation Plan
+ +
+ +
+ +
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.scss b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.scss new file mode 100644 index 0000000000..76605b2b82 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.scss @@ -0,0 +1,8 @@ +@use '../../../../../styles/functions' as *; + +.soil-table { + .spacer-row { + grid-column: 1/4; + height: rem(16); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.spec.ts new file mode 100644 index 0000000000..6bdcc5a467 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; + +import { PofoDetailsComponent } from './pofo-details.component'; + +describe('RosoDetailsComponent', () => { + let component: PofoDetailsComponent; + let fixture: ComponentFixture; + let mockNoiDocumentService: DeepMocked; + let mockNoiParcelService: DeepMocked; + + beforeEach(async () => { + mockNoiParcelService = createMock(); + mockNoiDocumentService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [PofoDetailsComponent], + providers: [ + { + provide: NoticeOfIntentDocumentService, + useValue: mockNoiDocumentService, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNoiParcelService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(PofoDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.ts b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.ts new file mode 100644 index 0000000000..ca7f7ed72c --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/pofo-details/pofo-details.component.ts @@ -0,0 +1,55 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; + +@Component({ + selector: 'app-pofo-details[noiSubmission]', + templateUrl: './pofo-details.component.html', + styleUrls: ['./pofo-details.component.scss'], +}) +export class PofoDetailsComponent { + @Input() showErrors = true; + @Input() showEdit = true; + @Input() draftMode = false; + @Input() updatedFields: string[] = []; + + _noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + + @Input() set noiSubmission(noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined) { + if (noiSubmission) { + this._noiSubmission = noiSubmission; + } + } + + @Input() set noiDocuments(documents: NoticeOfIntentDocumentDto[]) { + this.crossSections = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.CROSS_SECTIONS); + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.reclamationPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.RECLAMATION_PLAN); + this.noticeOfWorks = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.NOTICE_OF_WORK); + } + + crossSections: NoticeOfIntentDocumentDto[] = []; + proposalMap: NoticeOfIntentDocumentDto[] = []; + reclamationPlans: NoticeOfIntentDocumentDto[] = []; + noticeOfWorks: NoticeOfIntentDocumentDto[] = []; + + constructor(private router: Router, private noticeOfIntentDocumentService: NoticeOfIntentDocumentService) {} + + async onEditSection(step: number) { + if (this.draftMode) { + await this.router.navigateByUrl( + `/alcs/notice-of-intent/${this._noiSubmission?.fileNumber}/edit/${step}?errors=t` + ); + } else { + await this.router.navigateByUrl(`notice-of-intent/${this._noiSubmission?.fileNumber}/edit/${step}?errors=t`); + } + } + + async openFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html index 7cab8ff23b..f1d020d0a1 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/roso-details/roso-details.component.html @@ -42,7 +42,7 @@
Soil to be Removed - +
Volume
diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts index b2b363f816..c336e011b3 100644 --- a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts @@ -33,7 +33,7 @@ export class NoticeOfIntentDocumentService { file, documentType, source, - `${this.serviceUrl}/application/${fileNumber}/attachExternal` + `${this.serviceUrl}/notice-of-intent/${fileNumber}/attachExternal` ); this.toastService.showSuccessToast('Document uploaded'); return res; @@ -81,7 +81,9 @@ export class NoticeOfIntentDocumentService { async update(fileNumber: string | undefined, updateDtos: NoticeOfIntentDocumentUpdateDto[]) { try { - await firstValueFrom(this.httpClient.patch(`${this.serviceUrl}/application/${fileNumber}`, updateDtos)); + await firstValueFrom( + this.httpClient.patch(`${this.serviceUrl}/notice-of-intent/${fileNumber}`, updateDtos) + ); } catch (e) { console.error(e); this.toastService.showErrorToast('Failed to update documents, please try again'); @@ -92,7 +94,7 @@ export class NoticeOfIntentDocumentService { async getByFileId(fileNumber: string) { try { return await firstValueFrom( - this.httpClient.get(`${this.serviceUrl}/application/${fileNumber}`) + this.httpClient.get(`${this.serviceUrl}/notice-of-intent/${fileNumber}`) ); } catch (e) { console.error(e); diff --git a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts index 798c1ada78..94504765ee 100644 --- a/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/portal-frontend/src/app/services/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -1,4 +1,4 @@ -import { STRUCTURE_TYPES } from '../../features/notice-of-intents/edit-submission/additional-information/roso/roso-additional-information.component'; +import { STRUCTURE_TYPES } from '../../features/notice-of-intents/edit-submission/additional-information/additional-information.component'; import { BaseCodeDto } from '../../shared/dto/base.dto'; import { NoticeOfIntentOwnerDto } from '../notice-of-intent-owner/notice-of-intent-owner.dto'; @@ -77,16 +77,17 @@ export interface NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmi soilAlreadyPlacedMaximumDepth: number | null; soilAlreadyPlacedAverageDepth: number | null; soilProjectDurationAmount: number | null; - soilProjectDurationUnit?: string | null; - soilFillTypeToPlace?: string | null; - soilAlternativeMeasures?: string | null; - soilIsExtractionOrMining?: boolean; - soilHasSubmittedNotice?: boolean; + soilProjectDurationUnit: string | null; + soilFillTypeToPlace: string | null; + soilAlternativeMeasures: string | null; + soilIsExtractionOrMining: boolean | null; + soilIsAreaWideFilling: boolean | null; + soilHasSubmittedNotice: boolean | null; soilIsRemovingSoilForNewStructure: boolean | null; - soilStructureFarmUseReason?: string | null; - soilStructureResidentialUseReason?: string | null; - soilAgriParcelActivity?: string | null; - soilStructureResidentialAccessoryUseReason?: string | null; + soilStructureFarmUseReason: string | null; + soilStructureResidentialUseReason: string | null; + soilAgriParcelActivity: string | null; + soilStructureResidentialAccessoryUseReason: string | null; soilProposedStructures: ProposedStructure[]; } @@ -133,6 +134,7 @@ export interface NoticeOfIntentSubmissionUpdateDto { soilFillTypeToPlace?: string | null; soilAlternativeMeasures?: string | null; soilIsExtractionOrMining?: boolean | null; + soilIsAreaWideFilling?: boolean | null; soilHasSubmittedNotice?: boolean | null; soilIsRemovingSoilForNewStructure?: boolean | null; soilStructureFarmUseReason?: string | null; diff --git a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts index 5fb933f583..97ef994ac7 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-document/notice-of-intent-document.controller.ts @@ -41,7 +41,7 @@ export class NoticeOfIntentDocumentController { @InjectMapper() private mapper: Mapper, ) {} - @Get('/application/:fileNumber') + @Get('/notice-of-intent/:fileNumber') async listApplicantDocuments( @Param('fileNumber') fileNumber: string, @Param('documentType') documentType: DOCUMENT_TYPE | null, @@ -73,7 +73,7 @@ export class NoticeOfIntentDocumentController { return { url }; } - @Patch('/application/:fileNumber') + @Patch('/notice-of-intent/:fileNumber') async update( @Param('fileNumber') fileNumber: string, @Req() req, @@ -122,7 +122,7 @@ export class NoticeOfIntentDocumentController { return {}; } - @Post('/application/:uuid/attachExternal') + @Post('/notice-of-intent/:uuid/attachExternal') async attachExternalDocument( @Param('uuid') fileNumber: string, @Body() data: AttachExternalDocumentDto, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts index 30c3dd7f0f..0116f1f75e 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.dto.ts @@ -151,6 +151,9 @@ export class NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmissio @AutoMap(() => Boolean) soilIsExtractionOrMining?: boolean; + @AutoMap(() => Boolean) + soilIsAreaWideFilling?: boolean; + @AutoMap(() => Boolean) soilHasSubmittedNotice?: boolean; @@ -352,6 +355,10 @@ export class NoticeOfIntentSubmissionUpdateDto { @IsOptional() soilIsExtractionOrMining?: boolean; + @IsBoolean() + @IsOptional() + soilIsAreaWideFilling?: boolean; + @IsBoolean() @IsOptional() soilHasSubmittedNotice?: boolean; diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts index 515a813bdf..d37048bd35 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.entity.ts @@ -394,6 +394,10 @@ export class NoticeOfIntentSubmission extends Base { @Column({ type: 'boolean', nullable: true }) soilIsExtractionOrMining: boolean | null; + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + soilIsAreaWideFilling: boolean | null; + @AutoMap(() => Boolean) @Column({ type: 'boolean', nullable: true }) soilHasSubmittedNotice: boolean | null; diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index b0a81cca14..b1e7825f70 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -281,6 +281,11 @@ export class NoticeOfIntentSubmissionService { noticeOfIntentSubmission.soilIsExtractionOrMining, ); + noticeOfIntentSubmission.soilIsAreaWideFilling = filterUndefined( + updateDto.soilIsAreaWideFilling, + noticeOfIntentSubmission.soilIsAreaWideFilling, + ); + noticeOfIntentSubmission.soilHasSubmittedNotice = filterUndefined( updateDto.soilHasSubmittedNotice, noticeOfIntentSubmission.soilHasSubmittedNotice, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692207063498-add_area_wide_filling_to_noi_submission.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692207063498-add_area_wide_filling_to_noi_submission.ts new file mode 100644 index 0000000000..2a88e5ef04 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692207063498-add_area_wide_filling_to_noi_submission.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addAreaWideFillingToNoiSubmission1692207063498 + implements MigrationInterface +{ + name = 'addAreaWideFillingToNoiSubmission1692207063498'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" ADD "soil_is_area_wide_filling" boolean`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_submission" DROP COLUMN "soil_is_area_wide_filling"`, + ); + } +} From c462c370993645b2c0ae5b8865f7298cd536fa66 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Wed, 16 Aug 2023 15:22:24 -0700 Subject: [PATCH 257/954] Feature/alcs 803 roso applicant info (#883) ROSO NOI view in ALCS added "source" column to NOI --- .../applicant-info.component.ts | 6 +- .../application/application.component.ts | 4 +- .../decision/decision.component.ts | 4 +- .../application/intake/intake.component.ts | 4 +- .../proposal/proposal.component.ts | 4 +- .../application-dialog.component.spec.ts | 7 +- .../notice-of-intent-dialog.component.spec.ts | 4 +- .../applicant-info.component.html | 11 + .../applicant-info.component.scss | 10 + .../applicant-info.component.spec.ts | 41 +++ .../applicant-info.component.ts | 48 ++++ .../notice-of-intent-details.component.html | 192 +++++++++++++ .../notice-of-intent-details.component.scss | 126 ++++++++ ...notice-of-intent-details.component.spec.ts | 124 ++++++++ .../notice-of-intent-details.component.ts | 66 +++++ .../notice-of-intent-details.module.ts | 20 ++ .../parcel/parcel.component.html | 92 ++++++ .../parcel/parcel.component.scss | 24 ++ .../parcel/parcel.component.spec.ts | 54 ++++ .../parcel/parcel.component.ts | 77 +++++ ...roso-additional-information.component.html | 73 +++++ ...roso-additional-information.component.scss | 5 + ...o-additional-information.component.spec.ts | 32 +++ .../roso-additional-information.component.ts | 80 ++++++ .../roso-details/roso-details.component.html | 140 +++++++++ .../roso-details/roso-details.component.scss | 0 .../roso-details.component.spec.ts | 35 +++ .../roso-details/roso-details.component.ts | 40 +++ .../notice-of-intent.component.ts | 8 + .../notice-of-intent.module.ts | 5 +- .../application-parcel.service.spec.ts | 2 - .../services/application/application.dto.ts | 4 +- .../application/application.service.spec.ts | 4 +- .../notice-of-intent-parcel.service.spec.ts | 42 +++ .../notice-of-intent-parcel.service.ts | 40 +++ ...otice-of-intent-submission.service.spec.ts | 101 +++++++ .../notice-of-intent-submission.service.ts | 37 +++ .../notice-of-intent/notice-of-intent.dto.ts | 270 ++++++++++++++++++ .../src/app/shared/document/document.dto.ts | 3 + .../additional-information.component.html | 1 + ...notice-of-intent-parcel.controller.spec.ts | 52 ++++ .../notice-of-intent-parcel.controller.ts | 47 +++ ...ce-of-intent-submission.controller.spec.ts | 85 ++++++ .../notice-of-intent-submission.controller.ts | 52 ++++ ...otice-of-intent-submission.service.spec.ts | 157 ++++++++++ .../notice-of-intent-submission.service.ts | 85 ++++++ .../notice-of-intent/notice-of-intent.dto.ts | 12 + .../notice-of-intent.entity.ts | 19 +- .../notice-of-intent.module.ts | 15 + .../notice-of-intent.service.ts | 1 + ...of-intent-submission.automapper.profile.ts | 39 ++- .../notice-of-intent.automapper.profile.ts | 3 + .../notice-of-intent-owner.dto.ts | 2 +- .../notice-of-intent-submission.module.ts | 6 +- .../notice-of-intent-submission.service.ts | 1 + .../1692216006482-noi_source_column.ts | 23 ++ 56 files changed, 2412 insertions(+), 27 deletions(-) create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.module.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.controller.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692216006482-noi_source_column.ts diff --git a/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts b/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts index e32a3e3a3d..3ae87c4d62 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/applicant-info.component.ts @@ -1,14 +1,14 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subject, takeUntil } from 'rxjs'; -import { DOCUMENT_TYPE } from '../../../shared/document/document.dto'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; import { ApplicationSubmissionService } from '../../../services/application/application-submission/application-submission.service'; import { - APPLICATION_SYSTEM_SOURCE_TYPES, ApplicationDto, ApplicationSubmissionDto, SUBMISSION_STATUS, + SYSTEM_SOURCE_TYPES, } from '../../../services/application/application.dto'; +import { DOCUMENT_TYPE } from '../../../shared/document/document.dto'; @Component({ selector: 'app-applicant-info', @@ -37,7 +37,7 @@ export class ApplicantInfoComponent implements OnInit, OnDestroy { this.fileNumber = application.fileNumber; this.submission = await this.applicationSubmissionService.fetchSubmission(this.fileNumber); - const isApplicantSubmission = application.source === APPLICATION_SYSTEM_SOURCE_TYPES.APPLICANT; + const isApplicantSubmission = application.source === SYSTEM_SOURCE_TYPES.APPLICANT; this.isSubmittedToAlc = isApplicantSubmission ? !!application.dateSubmittedToAlc : true; diff --git a/alcs-frontend/src/app/features/application/application.component.ts b/alcs-frontend/src/app/features/application/application.component.ts index 001007eb8f..c0c35069e1 100644 --- a/alcs-frontend/src/app/features/application/application.component.ts +++ b/alcs-frontend/src/app/features/application/application.component.ts @@ -12,8 +12,8 @@ import { ApplicationSubmissionService } from '../../services/application/applica import { ApplicationDto, ApplicationSubmissionDto, - APPLICATION_SYSTEM_SOURCE_TYPES, SUBMISSION_STATUS, + SYSTEM_SOURCE_TYPES, } from '../../services/application/application.dto'; import { ApplicantInfoComponent } from './applicant-info/applicant-info.component'; import { ApplicationMeetingComponent } from './application-meeting/application-meeting.component'; @@ -200,7 +200,7 @@ export class ApplicationComponent implements OnInit, OnDestroy { this.reconsiderationService.fetchByApplication(application.fileNumber); this.modificationService.fetchByApplication(application.fileNumber); - this.isApplicantSubmission = application.source === APPLICATION_SYSTEM_SOURCE_TYPES.APPLICANT; + this.isApplicantSubmission = application.source === SYSTEM_SOURCE_TYPES.APPLICANT; let wasSubmittedToLfng = false; if (this.isApplicantSubmission) { diff --git a/alcs-frontend/src/app/features/application/decision/decision.component.ts b/alcs-frontend/src/app/features/application/decision/decision.component.ts index 5c66f837dd..f9f3a16678 100644 --- a/alcs-frontend/src/app/features/application/decision/decision.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Subject, takeUntil } from 'rxjs'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; -import { ApplicationDto, APPLICATION_SYSTEM_SOURCE_TYPES } from '../../../services/application/application.dto'; +import { ApplicationDto, SYSTEM_SOURCE_TYPES } from '../../../services/application/application.dto'; import { decisionChildRoutes } from './decision.module'; @Component({ @@ -13,7 +13,7 @@ export class DecisionComponent implements OnInit, OnDestroy { $destroy = new Subject(); childRoutes = decisionChildRoutes; - APPLICATION_SYSTEM_SOURCE_TYPES = APPLICATION_SYSTEM_SOURCE_TYPES; + APPLICATION_SYSTEM_SOURCE_TYPES = SYSTEM_SOURCE_TYPES; application: ApplicationDto | undefined; constructor(private applicationDetailService: ApplicationDetailService) {} diff --git a/alcs-frontend/src/app/features/application/intake/intake.component.ts b/alcs-frontend/src/app/features/application/intake/intake.component.ts index 79a378b256..c84f229787 100644 --- a/alcs-frontend/src/app/features/application/intake/intake.component.ts +++ b/alcs-frontend/src/app/features/application/intake/intake.component.ts @@ -3,8 +3,8 @@ import moment from 'moment'; import { environment } from '../../../../environments/environment'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; import { - APPLICATION_SYSTEM_SOURCE_TYPES, ApplicationDto, + SYSTEM_SOURCE_TYPES, UpdateApplicationDto, } from '../../../services/application/application.dto'; import { ToastService } from '../../../services/toast/toast.service'; @@ -17,7 +17,7 @@ import { ToastService } from '../../../services/toast/toast.service'; export class IntakeComponent implements OnInit { dateSubmittedToAlc?: string; application?: ApplicationDto; - APPLICATION_SYSTEM_SOURCE_TYPES = APPLICATION_SYSTEM_SOURCE_TYPES; + APPLICATION_SYSTEM_SOURCE_TYPES = SYSTEM_SOURCE_TYPES; constructor(private applicationDetailService: ApplicationDetailService, private toastService: ToastService) {} diff --git a/alcs-frontend/src/app/features/application/proposal/proposal.component.ts b/alcs-frontend/src/app/features/application/proposal/proposal.component.ts index cb5bbdb889..6ce20da5e5 100644 --- a/alcs-frontend/src/app/features/application/proposal/proposal.component.ts +++ b/alcs-frontend/src/app/features/application/proposal/proposal.component.ts @@ -1,8 +1,8 @@ import { Component, OnInit } from '@angular/core'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; import { - APPLICATION_SYSTEM_SOURCE_TYPES, ApplicationDto, + SYSTEM_SOURCE_TYPES, UpdateApplicationDto, } from '../../../services/application/application.dto'; import { ToastService } from '../../../services/toast/toast.service'; @@ -58,7 +58,7 @@ export class ProposalComponent implements OnInit { agCapSourceOptions = AG_CAP_SOURCE_OPTIONS; alrArea: string | undefined; staffObservations: string = ''; - APPLICATION_SYSTEM_SOURCE_TYPES = APPLICATION_SYSTEM_SOURCE_TYPES; + APPLICATION_SYSTEM_SOURCE_TYPES = SYSTEM_SOURCE_TYPES; constructor(private applicationDetailService: ApplicationDetailService, private toastService: ToastService) {} diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.spec.ts index cad7d8ca4a..30be9ca64a 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.spec.ts @@ -1,13 +1,12 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { RouterTestingModule } from '@angular/router/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ApplicationRegionDto, ApplicationTypeDto } from '../../../../services/application/application-code.dto'; -import { APPLICATION_SYSTEM_SOURCE_TYPES, ApplicationDto } from '../../../../services/application/application.dto'; -import { BoardDto } from '../../../../services/board/board.dto'; +import { ApplicationDto, SYSTEM_SOURCE_TYPES } from '../../../../services/application/application.dto'; import { BoardService } from '../../../../services/board/board.service'; import { CardDto } from '../../../../services/card/card.dto'; import { AssigneeDto } from '../../../../services/user/user.dto'; @@ -71,7 +70,7 @@ describe('ApplicationDialogComponent', () => { code: 'card-status', }, } as CardDto, - source: APPLICATION_SYSTEM_SOURCE_TYPES.ALCS, + source: SYSTEM_SOURCE_TYPES.ALCS, }; beforeEach(async () => { diff --git a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.spec.ts b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.spec.ts index 3347b68d7b..1e2829a64b 100644 --- a/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/board/dialogs/notice-of-intent/notice-of-intent-dialog.component.spec.ts @@ -12,7 +12,7 @@ import { AuthenticationService, ICurrentUser } from '../../../../services/authen import { BoardService, BoardWithFavourite } from '../../../../services/board/board.service'; import { CardDto } from '../../../../services/card/card.dto'; import { CardService } from '../../../../services/card/card.service'; -import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice-of-intent.dto'; +import { NoticeOfIntentDto, NoticeOfIntentTypeDto } from '../../../../services/notice-of-intent/notice-of-intent.dto'; import { NoticeOfIntentService } from '../../../../services/notice-of-intent/notice-of-intent.service'; import { ToastService } from '../../../../services/toast/toast.service'; import { AssigneeDto } from '../../../../services/user/user.dto'; @@ -54,6 +54,8 @@ describe('NoticeOfIntentDialogComponent', () => { uuid: '', retroactive: null, subtype: [], + type: {} as NoticeOfIntentTypeDto, + source: 'ALCS', }; beforeEach(async () => { diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.html b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.html new file mode 100644 index 0000000000..ca71ef9c2c --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.html @@ -0,0 +1,11 @@ +
+
+ +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.scss b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.scss new file mode 100644 index 0000000000..480cb3d0e2 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.scss @@ -0,0 +1,10 @@ +.applicant-info { + & > div { + margin: 32px 0; + } + } + + .subheading1 { + margin-bottom: 4px !important; + } + \ No newline at end of file diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.spec.ts new file mode 100644 index 0000000000..bd5f1a9b09 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.spec.ts @@ -0,0 +1,41 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDetailService } from '../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentDto } from '../../../services/notice-of-intent/notice-of-intent.dto'; +import { ToastService } from '../../../services/toast/toast.service'; + +import { ApplicantInfoComponent } from './applicant-info.component'; + +describe('ApplicantInfoComponent', () => { + let component: ApplicantInfoComponent; + let fixture: ComponentFixture; + let mockAppDetailService: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(async () => { + mockAppDetailService = createMock() + mockAppDetailService.$noticeOfIntent = new BehaviorSubject(undefined); + + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [ ApplicantInfoComponent ], + providers: [ + { provide: NoticeOfIntentDetailService, useValue: mockAppDetailService }, + { provide: ToastService, useValue: mockToastService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .compileComponents(); + + fixture = TestBed.createComponent(ApplicantInfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.ts new file mode 100644 index 0000000000..cf7aafffbf --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/applicant-info.component.ts @@ -0,0 +1,48 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Subject, takeUntil } from 'rxjs'; +import { SYSTEM_SOURCE_TYPES } from '../../../services/application/application.dto'; +import { NoticeOfIntentDetailService } from '../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; +import { + NoticeOfIntentDto, + NoticeOfIntentSubmissionDto, +} from '../../../services/notice-of-intent/notice-of-intent.dto'; +import { DOCUMENT_TYPE } from '../../../shared/document/document.dto'; + +@Component({ + selector: 'app-applicant-info', + templateUrl: './applicant-info.component.html', + styleUrls: ['./applicant-info.component.scss'], +}) +export class ApplicantInfoComponent implements OnInit, OnDestroy { + fileNumber: string = ''; + applicant: string = ''; + destroy = new Subject(); + DOCUMENT_TYPE = DOCUMENT_TYPE; + noticeOfIntent: NoticeOfIntentDto | undefined; + submission?: NoticeOfIntentSubmissionDto = undefined; + isSubmittedToAlc = false; + + constructor( + private noiDetailService: NoticeOfIntentDetailService, + private noiSubmissionService: NoticeOfIntentSubmissionService + ) {} + + ngOnInit(): void { + this.noiDetailService.$noticeOfIntent.pipe(takeUntil(this.destroy)).subscribe(async (noi) => { + if (noi) { + this.noticeOfIntent = noi; + this.fileNumber = noi.fileNumber; + + this.submission = await this.noiSubmissionService.fetchSubmission(this.fileNumber); + const isApplicantSubmission = noi.source === SYSTEM_SOURCE_TYPES.APPLICANT; + this.isSubmittedToAlc = isApplicantSubmission ? !!noi.dateSubmittedToAlc : true; + } + }); + } + + ngOnDestroy(): void { + this.destroy.next(); + this.destroy.complete(); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.html b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.html new file mode 100644 index 0000000000..7dc46b1a0e --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.html @@ -0,0 +1,192 @@ +
+ +
+
+ +
+
+
+
+

Primary Contact Information

+
+
Type
+
+ {{ submission.primaryContact?.type?.label }} + +
+
First Name
+
+ {{ submission.primaryContact?.firstName }} + +
+
Last Name
+
+ {{ submission.primaryContact?.lastName }} + +
+
+ Organization + Ministry/Department Responsible + Department +
+
+ {{ submission.primaryContact?.organizationName }} + +
+
Phone
+
+ {{ submission.primaryContact?.phoneNumber }} + +
+
Email
+
+ {{ submission.primaryContact?.email }} + +
+ +
Authorization Letter(s)
+ +
+
+ +
+
+
+
+

Land Use

+
+
+

Land Use of Parcel(s) under Application

+
+
+ Quantify and describe in detail all agriculture that currently takes place on the parcel(s). +
+
+ {{ submission.parcelsAgricultureDescription }} +
+
+ Quantify and describe in detail all agricultural improvements made to the parcel(s). +
+
+ {{ submission.parcelsAgricultureImprovementDescription }} +
+
+ Quantify and describe all non-agricultural uses that currently take place on the parcel(s). +
+
+ {{ submission.parcelsNonAgricultureUseDescription }} +
+
+

Land Use of Adjacent Parcels

+
+
+
+
Main Land Use Type
+
Specific Activity
+
North
+
+ {{ submission.northLandUseType }} +
+
+ {{ submission.northLandUseTypeDescription }} +
+
East
+
+ {{ submission.eastLandUseType }} +
+
+ {{ submission.eastLandUseTypeDescription }} +
+
South
+
+ {{ submission.southLandUseType }} +
+
+ {{ submission.southLandUseTypeDescription }} +
+
West
+
+ {{ submission.westLandUseType }} +
+
+ {{ submission.westLandUseTypeDescription }} +
+
+
+ +
+
+
+
+

Proposal

+ +
+
+ +
+
+
+
+

Additional Proposal Information

+ +
+
+ +
+
+
+
+

Optional Documents

+
+
+
Type
+
Description
+
File Name
+ +
+ {{ file.type?.label }} +
+
+ {{ file.description }} +
+ +
+
No optional attachments
+
+
+ +
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.scss b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.scss new file mode 100644 index 0000000000..9a90bc113d --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.scss @@ -0,0 +1,126 @@ +@use '../../../../../styles/colors'; + +:host::ng-deep { + .view-grid-item { + display: grid; + grid-template-columns: minmax(100px, 0.5fr) 1fr; + column-gap: 16px; + margin-bottom: 12px; + } + + .details-wrapper { + margin-top: 24px; + margin-bottom: 24px; + + .title { + margin-bottom: 16px !important; + } + } + + h3 .subtext { + margin: 0.5rem 0 !important; + } + + label { + font-weight: 600; + } + + .no-data-text { + text-align: center; + color: colors.$grey; + padding-top: rem(12); + + .error { + justify-content: center; + } + } + + .custom-mat-expansion-panel-header { + height: fit-content; + } + + .table-wrapper { + overflow-x: auto; + width: 100%; + } +} + +:host::ng-deep { + .soil-table { + display: grid; + grid-template-columns: max-content max-content; + overflow-x: auto; + grid-column-gap: 36px; + grid-row-gap: 12px; + overflow: unset; + } + + .scrollable { + overflow-x: auto; + } + + .other-attachments { + display: grid; + grid-template-columns: max-content max-content max-content; + overflow-x: auto; + overflow-y: hidden; + grid-column-gap: 36px; + grid-row-gap: 12px; + + .full-width { + grid-column: 1/3; + } + } + + .adjacent-parcels { + display: grid; + grid-template-columns: max-content max-content max-content; + overflow-x: auto; + grid-column-gap: 36px; + grid-row-gap: 12px; + + .full-width { + grid-column: 1/4; + } + } + + .review-table { + background-color: colors.$grey-light; + display: grid; + grid-row-gap: 24px; + grid-column-gap: 16px; + word-wrap: break-word; + hyphens: auto; + padding: 16px; + margin: 24px 0 40px 0; + grid-template-columns: minmax(60px, 1fr) minmax(60px, 1fr) minmax(60px, 1fr) minmax(60px, 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; + } + + .subheading2 { + margin-bottom: 4px !important; + } + } +} + +.edit-section { + margin-top: -40px; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.spec.ts new file mode 100644 index 0000000000..e50059e993 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.spec.ts @@ -0,0 +1,124 @@ +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 { NoiDocumentService } from '../../../../services/notice-of-intent/noi-document/noi-document.service'; +import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; +import { + NOI_SUBMISSION_STATUS, + NoticeOfIntentSubmissionStatusDto, +} from '../../../../services/notice-of-intent/notice-of-intent.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; + +import { NoticeOfIntentDetailsComponent } from './notice-of-intent-details.component'; + +describe('NoticeOfIntentDetailsComponent', () => { + let component: NoticeOfIntentDetailsComponent; + let fixture: ComponentFixture; + + let mockNoiDocumentService: DeepMocked; + let mockRouter: DeepMocked; + let mockToastService: DeepMocked; + let mockNoiSubmissionService: DeepMocked; + + beforeEach(async () => { + mockNoiDocumentService = createMock(); + mockRouter = createMock(); + mockNoiSubmissionService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: NoiDocumentService, + useValue: mockNoiDocumentService, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoiSubmissionService, + }, + ], + declarations: [NoticeOfIntentDetailsComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(NoticeOfIntentDetailsComponent); + component = fixture.componentInstance; + component.submission = { + fileNumber: '', + uuid: '', + createdAt: 1, + updatedAt: 1, + applicant: '', + localGovernmentUuid: '', + type: '', + typeCode: '', + status: { + code: NOI_SUBMISSION_STATUS.IN_PROGRESS, + portalBackgroundColor: '', + portalColor: '', + } as NoticeOfIntentSubmissionStatusDto, + submissionStatuses: [], + owners: [], + canEdit: false, + canView: false, + + purpose: '', + parcelsAgricultureDescription: '', + parcelsAgricultureImprovementDescription: '', + parcelsNonAgricultureUseDescription: '', + northLandUseType: '', + northLandUseTypeDescription: '', + eastLandUseType: '', + eastLandUseTypeDescription: '', + southLandUseType: '', + southLandUseTypeDescription: '', + westLandUseType: '', + westLandUseTypeDescription: '', + + primaryContactOwnerUuid: null, + primaryContact: undefined, + + //Soil Fields + soilIsRemovingSoilForNewStructure: null, + soilIsFollowUp: null, + soilFollowUpIDs: '', + soilTypeRemoved: '', + soilReduceNegativeImpacts: '', + soilToRemoveVolume: null, + soilToRemoveArea: null, + soilToRemoveMaximumDepth: null, + soilToRemoveAverageDepth: null, + soilAlreadyRemovedVolume: null, + soilAlreadyRemovedArea: null, + soilAlreadyRemovedMaximumDepth: null, + soilAlreadyRemovedAverageDepth: null, + soilToPlaceVolume: null, + soilToPlaceArea: null, + soilToPlaceMaximumDepth: null, + soilToPlaceAverageDepth: null, + soilAlreadyPlacedVolume: null, + soilAlreadyPlacedArea: null, + soilAlreadyPlacedMaximumDepth: null, + soilAlreadyPlacedAverageDepth: null, + soilProjectDurationAmount: null, + soilProjectDurationUnit: null, + soilFillTypeToPlace: null, + soilProposedStructures: [], + }; + component.noiType = 'ROSO'; + component.fileNumber = 'fake'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.ts new file mode 100644 index 0000000000..f9ca90ef74 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.ts @@ -0,0 +1,66 @@ +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { Subject } from 'rxjs'; +import { environment } from '../../../../../environments/environment'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent/noi-document/noi-document.dto'; +import { NoiDocumentService } from '../../../../services/notice-of-intent/noi-document/noi-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent/notice-of-intent.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/document/document.dto'; + +@Component({ + selector: 'app-noi-details', + templateUrl: './notice-of-intent-details.component.html', + styleUrls: ['./notice-of-intent-details.component.scss'], +}) +export class NoticeOfIntentDetailsComponent implements OnInit, OnChanges, OnDestroy { + $destroy = new Subject(); + + @Input() submission!: NoticeOfIntentSubmissionDetailedDto; + @Input() noiType!: string; + @Input() fileNumber!: string; + @Input() showEdit = false; + @Input() isSubmittedToAlc = true; + @Input() wasSubmittedToLfng = false; + + authorizationLetters: NoticeOfIntentDocumentDto[] = []; + otherFiles: NoticeOfIntentDocumentDto[] = []; + files: NoticeOfIntentDocumentDto[] | undefined; + disableEdit = false; + showFullApp = false; + + constructor(private noiDocumentService: NoiDocumentService) {} + + ngOnInit(): void { + this.loadDocuments(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.disableEdit = !this.isSubmittedToAlc; + this.showFullApp = this.isSubmittedToAlc; + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + onEdit(step: number) { + window.location.href = `${environment.portalUrl}/alcs/notice-of-intent/${this.fileNumber}/edit/${step}`; + } + + async openFile(uuid: string) { + await this.noiDocumentService.download(uuid, ''); + } + + private async loadDocuments() { + const documents = await this.noiDocumentService.getApplicantDocuments(this.fileNumber); + this.otherFiles = documents.filter( + (document) => + document.type && + [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.OTHER, DOCUMENT_TYPE.PROFESSIONAL_REPORT].includes(document.type.code) + ); + this.authorizationLetters = documents.filter( + (document) => document.type?.code === DOCUMENT_TYPE.AUTHORIZATION_LETTER + ); + this.files = documents; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.module.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.module.ts new file mode 100644 index 0000000000..5da882c43d --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.module.ts @@ -0,0 +1,20 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NgxMaskPipe } from 'ngx-mask'; +import { SharedModule } from '../../../../shared/shared.module'; +import { NoticeOfIntentDetailsComponent } from './notice-of-intent-details.component'; +import { ParcelComponent } from './parcel/parcel.component'; +import { RosoAdditionalInformationComponent } from './roso-details/roso-additional-information/roso-additional-information.component'; +import { RosoDetailsComponent } from './roso-details/roso-details.component'; + +@NgModule({ + declarations: [ + ParcelComponent, + RosoDetailsComponent, + NoticeOfIntentDetailsComponent, + RosoAdditionalInformationComponent, + ], + imports: [CommonModule, SharedModule, NgxMaskPipe], + exports: [NoticeOfIntentDetailsComponent], +}) +export class NoticeOfIntentDetailsModule {} diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.html b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.html new file mode 100644 index 0000000000..7ad420667a --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.html @@ -0,0 +1,92 @@ +

Notice of Intent Parcels

+ +
+ +
+

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

+
+
Parcel 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 }} +
+
+ +
Purchase Date
+
+ {{ parcel.purchasedDate | date }} + +
+
+
Farm Classification
+
+ {{ parcel.isFarm ? 'Yes' : 'No' }} + +
+
Civic Address
+
+ {{ parcel.civicAddress }} + +
+ +
Crown Selection
+
+ {{ parcel.crownLandOwnerType }} + +
+
+
Certificate Of Title
+ +
Owner Information
+
+
Type
+
Full Name
+
Organization
+
+ Ministry/ Department +
+
Phone
+
Email
+
Corporate Summary
+ +
{{ owner.type.label }}
+
{{ owner.displayName }}
+
{{ owner.organizationName }}
+
{{ owner.phoneNumber }}
+
{{ owner.email }}
+ +
+
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.scss b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.scss new file mode 100644 index 0000000000..e79cc4a08f --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.scss @@ -0,0 +1,24 @@ +.owner-information { + display: grid; + grid-template-columns: max-content max-content max-content max-content max-content max-content; + overflow-x: auto; + overflow-y: hidden; + grid-column-gap: 36px; + grid-row-gap: 12px; + + .full-width { + grid-column: 1/3; + } +} + +.review-table { + grid-template-columns: 1fr 1fr !important; + + .full-width { + grid-column: 1/5; + } +} + +.crown-land { + text-transform: capitalize; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.spec.ts new file mode 100644 index 0000000000..6ed8739d73 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.spec.ts @@ -0,0 +1,54 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Observable } from 'rxjs'; +import { NoiDocumentService } from '../../../../../services/notice-of-intent/noi-document/noi-document.service'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { ParcelComponent } from './parcel.component'; + +describe('ParcelComponent', () => { + let component: ParcelComponent; + let fixture: ComponentFixture; + + let mockNoiParcelService: DeepMocked; + let mockNoiDocService: DeepMocked; + let mockRoute: DeepMocked; + + beforeEach(async () => { + mockNoiParcelService = createMock(); + mockNoiDocService = createMock(); + mockRoute = createMock(); + mockRoute.fragment = new Observable(); + + await TestBed.configureTestingModule({ + declarations: [ParcelComponent], + providers: [ + { + provide: NoticeOfIntentParcelService, + useValue: mockNoiParcelService, + }, + { + provide: NoiDocumentService, + useValue: mockNoiDocService, + }, + { + provide: ActivatedRoute, + useValue: mockRoute, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelComponent); + component = fixture.componentInstance; + component.noticeOfIntent = { + parcels: [], + } as any; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.ts new file mode 100644 index 0000000000..ba2ca715a0 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/parcel/parcel.component.ts @@ -0,0 +1,77 @@ +import { AfterContentChecked, Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { PARCEL_OWNERSHIP_TYPE } from '../../../../../services/application/application.dto'; +import { NoticeOfIntentDocumentDto } from '../../../../../services/notice-of-intent/noi-document/noi-document.dto'; +import { NoiDocumentService } from '../../../../../services/notice-of-intent/noi-document/noi-document.service'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentSubmissionDto } from '../../../../../services/notice-of-intent/notice-of-intent.dto'; + +@Component({ + selector: 'app-parcel', + templateUrl: './parcel.component.html', + styleUrls: ['./parcel.component.scss'], +}) +export class ParcelComponent implements OnInit, OnChanges, OnDestroy, AfterContentChecked { + $destroy = new Subject(); + + @Input() noticeOfIntent!: NoticeOfIntentSubmissionDto; + @Input() files: NoticeOfIntentDocumentDto[] = []; + + pageTitle: string = 'Notice of Intent Parcels'; + showCertificateOfTitle: boolean = true; + + fileId: string = ''; + parcels: any[] = []; + + PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; + private anchorededParcelUuid: string | undefined; + + constructor( + private noiDocumentService: NoiDocumentService, + private parcelService: NoticeOfIntentParcelService, + private route: ActivatedRoute + ) {} + + ngOnInit(): void { + this.route.fragment.pipe(takeUntil(this.$destroy)).subscribe((fragment) => { + if (fragment) { + this.anchorededParcelUuid = fragment; + } + }); + } + + async onOpenFile(uuid: string) { + const file = this.files.find((file) => file.uuid === uuid); + if (file) { + await this.noiDocumentService.download(file.uuid, file.fileName); + } + } + + async loadParcels(fileNumber: string) { + this.parcels = await this.parcelService.fetchParcels(fileNumber); + } + + ngOnChanges(changes: SimpleChanges): void { + this.loadParcels(this.noticeOfIntent.fileNumber); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + ngAfterContentChecked(): void { + if (this.anchorededParcelUuid) { + const el = document.getElementById(this.anchorededParcelUuid); + if (el) { + this.anchorededParcelUuid = undefined; + el.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'start', + }); + } + } + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.html b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.html new file mode 100644 index 0000000000..e27996ed82 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.html @@ -0,0 +1,73 @@ +
+
Are you removing soil in order to build a structure?
+
+ + {{ _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 }} + +
+
+ +
Detailed Building Plan(s)
+ +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.scss b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.scss new file mode 100644 index 0000000000..bfe0899632 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.scss @@ -0,0 +1,5 @@ +.multiple-documents { + a { + display: block; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.spec.ts new file mode 100644 index 0000000000..301b528b84 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.spec.ts @@ -0,0 +1,32 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DeepMocked } from '@golevelup/ts-jest'; +import { NoiDocumentService } from '../../../../../../services/notice-of-intent/noi-document/noi-document.service'; +import { RosoAdditionalInformationComponent } from './roso-additional-information.component'; + +describe('RosoAdditionalInformationComponent', () => { + let component: RosoAdditionalInformationComponent; + let fixture: ComponentFixture; + let mockNoiDocumentService: DeepMocked; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RosoAdditionalInformationComponent], + providers: [ + { + provide: NoiDocumentService, + useValue: mockNoiDocumentService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(RosoAdditionalInformationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.ts new file mode 100644 index 0000000000..2086d214c5 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-additional-information/roso-additional-information.component.ts @@ -0,0 +1,80 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { NoticeOfIntentDocumentDto } from '../../../../../../services/notice-of-intent/noi-document/noi-document.dto'; +import { NoiDocumentService } from '../../../../../../services/notice-of-intent/noi-document/noi-document.service'; +import { + NoticeOfIntentSubmissionDetailedDto, + RESIDENTIAL_STRUCTURE_TYPES, + STRUCTURE_TYPES, +} from '../../../../../../services/notice-of-intent/notice-of-intent.dto'; +import { DOCUMENT_TYPE } from '../../../../../../shared/document/document.dto'; + +@Component({ + selector: 'app-roso-additional-information', + templateUrl: './roso-additional-information.component.html', + styleUrls: ['./roso-additional-information.component.scss'], +}) +export class RosoAdditionalInformationComponent { + _noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + + @Input() set noiSubmission(noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined) { + if (noiSubmission) { + this._noiSubmission = noiSubmission; + this.setVisibilityAndValidatorsForResidentialFields(); + this.setVisibilityAndValidatorsForAccessoryFields(); + this.setVisibilityAndValidatorsForFarmFields(); + } + } + + @Input() set files(documents: NoticeOfIntentDocumentDto[] | undefined) { + this.buildingPlans = documents?.filter((document) => document.type?.code === DOCUMENT_TYPE.BUILDING_PLAN) ?? []; + } + + buildingPlans: NoticeOfIntentDocumentDto[] = []; + + isSoilStructureFarmUseReasonVisible = false; + isSoilStructureResidentialUseReasonVisible = false; + isSoilAgriParcelActivityVisible = false; + isSoilStructureResidentialAccessoryUseReasonVisible = false; + + constructor(private router: Router, private noticeOfIntentDocumentService: NoiDocumentService) {} + + private setVisibilityAndValidatorsForResidentialFields() { + if ( + this._noiSubmission?.soilProposedStructures.some( + (structure) => structure.type && RESIDENTIAL_STRUCTURE_TYPES.includes(structure.type) + ) + ) { + this.isSoilStructureResidentialUseReasonVisible = true; + } else { + this.isSoilStructureResidentialUseReasonVisible = false; + } + } + + private setVisibilityAndValidatorsForAccessoryFields() { + if ( + this._noiSubmission?.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE + ) + ) { + this.isSoilStructureResidentialAccessoryUseReasonVisible = true; + } else { + this.isSoilStructureResidentialAccessoryUseReasonVisible = false; + } + } + + private setVisibilityAndValidatorsForFarmFields() { + 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; + } + } + async openFile(file: NoticeOfIntentDocumentDto) { + await this.noticeOfIntentDocumentService.download(file.uuid, file.fileName); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.html b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.html new file mode 100644 index 0000000000..2582a4f4b5 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.html @@ -0,0 +1,140 @@ +
+
+ Has the ALC previously received an application or Notice of Intent for this proposal? +
+
+ + {{ _noiSubmission.soilIsFollowUp ? 'Yes' : 'No' }} + +
+ +
Application or NOI ID
+
+ {{ _noiSubmission.soilFollowUpIDs }} +
+ +
What is the purpose of the proposal?
+
+ {{ _noiSubmission.purpose }} +
+ +
Describe the type of soil proposed to be removed.
+
+ {{ _noiSubmission.soilTypeRemoved }} +
+ +
+
+
+
+ Soil to be Removed +
+
Volume
+
+ {{ _noiSubmission.soilToRemoveVolume }} + m3 +
+
Area
+
+ {{ _noiSubmission.soilToRemoveArea }} ha +
+
Maximum Depth
+
+ {{ _noiSubmission.soilToRemoveMaximumDepth }} + m +
+
Average Depth
+
+ {{ _noiSubmission.soilToRemoveAverageDepth }} + m +
+
+ +
+
+
+
+ Soil already Removed +
+
Volume
+
+ {{ _noiSubmission.soilAlreadyRemovedVolume }} + m3 +
+
Area
+
+ {{ _noiSubmission.soilAlreadyRemovedArea }} + ha +
+
Maximum Depth
+
+ {{ _noiSubmission.soilAlreadyRemovedMaximumDepth }} + m +
+
Average Depth
+
+ {{ _noiSubmission.soilAlreadyRemovedAverageDepth }} + m +
+
+ +
+

Project Duration

+
+
Duration
+
+ {{ _noiSubmission.soilProjectDurationUnit }} +
+
Length
+
+ {{ _noiSubmission.soilProjectDurationAmount }} +
+ +
Proposal Map / Site Plan
+ + +
Is your proposal for aggregate extraction or placer mining?
+
+ + {{ _noiSubmission.soilIsExtractionOrMining ? 'Yes' : 'No' }} + +
+ + +
Cross Sections
+ + +
Reclamation Plan
+ + +
+ Have you submitted a Notice of Work to the Ministry of Energy, Mines and Low Carbon Innovation (EMLI)? +
+
+ + {{ _noiSubmission.soilHasSubmittedNotice ? 'Yes' : 'No' }} + +
+ + +
Notice of Work
+ +
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.scss b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.spec.ts new file mode 100644 index 0000000000..7462bad6c3 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.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 { NoiDocumentService } from '../../../../../services/notice-of-intent/noi-document/noi-document.service'; + +import { RosoDetailsComponent } from './roso-details.component'; + +describe('RosoDetailsComponent', () => { + let component: RosoDetailsComponent; + let fixture: ComponentFixture; + let mockNoiDocumentService: DeepMocked; + + beforeEach(async () => { + mockNoiDocumentService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [RosoDetailsComponent], + providers: [ + { + provide: NoiDocumentService, + useValue: mockNoiDocumentService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(RosoDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.ts b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.ts new file mode 100644 index 0000000000..308a3be588 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/roso-details/roso-details.component.ts @@ -0,0 +1,40 @@ +import { Component, Input } from '@angular/core'; +import { NoticeOfIntentDocumentDto } from '../../../../../services/notice-of-intent/noi-document/noi-document.dto'; +import { NoiDocumentService } from '../../../../../services/notice-of-intent/noi-document/noi-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../../services/notice-of-intent/notice-of-intent.dto'; +import { DOCUMENT_TYPE } from '../../../../../shared/document/document.dto'; + +@Component({ + selector: 'app-roso-details[noiSubmission]', + templateUrl: './roso-details.component.html', + styleUrls: ['./roso-details.component.scss'], +}) +export class RosoDetailsComponent { + _noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; + + @Input() set noiSubmission(noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined) { + if (noiSubmission) { + this._noiSubmission = noiSubmission; + } + } + + @Input() set files(documents: NoticeOfIntentDocumentDto[] | undefined) { + if (documents) { + this.crossSections = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.CROSS_SECTIONS); + this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); + this.reclamationPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.RECLAMATION_PLAN); + this.noticeOfWorks = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.NOTICE_OF_WORK); + } + } + + crossSections: NoticeOfIntentDocumentDto[] = []; + proposalMap: NoticeOfIntentDocumentDto[] = []; + reclamationPlans: NoticeOfIntentDocumentDto[] = []; + noticeOfWorks: NoticeOfIntentDocumentDto[] = []; + + constructor(private noticeOfIntentDocumentService: NoiDocumentService) {} + + async openFile(file: NoticeOfIntentDocumentDto) { + await this.noticeOfIntentDocumentService.download(file.uuid, file.fileName); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts index 0be8d680f2..214617e702 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts @@ -14,6 +14,7 @@ import { IntakeComponent } from './intake/intake.component'; import { OverviewComponent } from './overview/overview.component'; import { PostDecisionComponent } from './post-decision/post-decision.component'; import { PreparationComponent } from './preparation/preparation.component'; +import { ApplicantInfoComponent } from './applicant-info/applicant-info.component'; export const childRoutes = [ { @@ -22,6 +23,13 @@ export const childRoutes = [ icon: 'summarize', component: OverviewComponent, }, + { + path: 'applicant-info', + menuTitle: 'Applicant Info', + icon: 'persons', + component: ApplicantInfoComponent, + portalOnly: true, + }, { path: 'intake', menuTitle: 'ALC Intake', diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts index d9f93f256d..ce6d30256e 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts @@ -15,6 +15,8 @@ import { OverviewComponent } from './overview/overview.component'; import { EditModificationDialogComponent } from './post-decision/edit-modification-dialog/edit-modification-dialog.component'; import { PostDecisionComponent } from './post-decision/post-decision.component'; import { PreparationComponent } from './preparation/preparation.component'; +import { ApplicantInfoComponent } from './applicant-info/applicant-info.component'; +import { NoticeOfIntentDetailsModule } from './applicant-info/notice-of-intent-details/notice-of-intent-details.module'; const routes: Routes = [ { @@ -40,7 +42,8 @@ const routes: Routes = [ EditModificationDialogComponent, NoiDocumentsComponent, DocumentUploadDialogComponent, + ApplicantInfoComponent, ], - imports: [SharedModule.forRoot(), RouterModule.forChild(routes)], + imports: [SharedModule.forRoot(), RouterModule.forChild(routes), NoticeOfIntentDetailsModule], }) export class NoticeOfIntentModule {} diff --git a/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.spec.ts b/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.spec.ts index 7dbe4bebaf..0c1b0ef2ec 100644 --- a/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application-parcel/application-parcel.service.spec.ts @@ -3,8 +3,6 @@ import { TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { of } from 'rxjs'; import { ToastService } from '../../toast/toast.service'; -import { ApplicationSubmissionDto, SubmittedApplicationOwnerDto } from '../application.dto'; - import { ApplicationParcelService } from './application-parcel.service'; describe('ApplicationParcelService', () => { diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 7e350fc3e4..aee39f025d 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -5,7 +5,7 @@ import { ApplicationRegionDto, ApplicationTypeDto } from './application-code.dto import { ApplicationLocalGovernmentDto } from './application-local-government/application-local-government.dto'; import { ApplicationSubmissionToSubmissionStatusDto } from './application-submission-status/application-submission-status.dto'; -export enum APPLICATION_SYSTEM_SOURCE_TYPES { +export enum SYSTEM_SOURCE_TYPES { APPLICANT = 'APPLICANT', ALCS = 'ALCS', } @@ -249,7 +249,7 @@ export interface ApplicationDto { decisionMeetings: ApplicationDecisionMeetingDto[]; card?: CardDto; submittedApplication?: ApplicationSubmissionDto; - source: APPLICATION_SYSTEM_SOURCE_TYPES; + source: SYSTEM_SOURCE_TYPES; alrArea?: number; agCap?: string; agCapSource?: string; diff --git a/alcs-frontend/src/app/services/application/application.service.spec.ts b/alcs-frontend/src/app/services/application/application.service.spec.ts index 29a794768c..b4e509f0bd 100644 --- a/alcs-frontend/src/app/services/application/application.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application.service.spec.ts @@ -3,7 +3,7 @@ import { TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { firstValueFrom, of, throwError } from 'rxjs'; import { ToastService } from '../toast/toast.service'; -import { APPLICATION_SYSTEM_SOURCE_TYPES, ApplicationDto } from './application.dto'; +import { ApplicationDto, SYSTEM_SOURCE_TYPES } from './application.dto'; import { ApplicationService } from './application.service'; @@ -40,7 +40,7 @@ describe('ApplicationService', () => { backgroundColor: '', textColor: '', }, - source: APPLICATION_SYSTEM_SOURCE_TYPES.ALCS, + source: SYSTEM_SOURCE_TYPES.ALCS, }; beforeEach(() => { diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts new file mode 100644 index 0000000000..89e25f0b2a --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts @@ -0,0 +1,42 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ToastService } from '../../toast/toast.service'; +import { of } from 'rxjs'; + +import { NoticeOfIntentParcelService } from './notice-of-intent-parcel.service'; + +describe('NoticeOfIntentParcelService', () => { + let service: NoticeOfIntentParcelService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { provide: ToastService, useValue: mockToastService }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentParcelService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should successfully fetch parcels', async () => { + mockHttpClient.get.mockReturnValue(of([])); + + const result = await service.fetchParcels('1'); + + expect(result).toEqual([]); + expect(mockHttpClient.get).toBeCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.ts new file mode 100644 index 0000000000..a7764d1067 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.ts @@ -0,0 +1,40 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { ToastService } from '../../toast/toast.service'; +import { NoticeOfIntentParcelDto } from '../notice-of-intent.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentParcelService { + private baseUrl = `${environment.apiUrl}/notice-of-intent-parcel`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetchParcels(fileNumber: string): Promise { + try { + return firstValueFrom(this.http.get(`${this.baseUrl}/${fileNumber}`)); + } catch (e) { + this.toastService.showErrorToast('Failed to fetch Application Parcels'); + throw e; + } + } + + async setParcelArea(uuid: string, alrArea: number | null): Promise { + try { + const res = await firstValueFrom( + this.http.post(`${this.baseUrl}/${uuid}`, { + alrArea, + }) + ); + this.toastService.showSuccessToast('Notice of Intent updated'); + + return res; + } catch (e) { + this.toastService.showErrorToast('Failed to update Notice of Intent Parcel'); + throw e; + } + } +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts new file mode 100644 index 0000000000..380dd25362 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -0,0 +1,101 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of } from 'rxjs'; +import { ToastService } from '../../toast/toast.service'; +import { NOI_SUBMISSION_STATUS, NoticeOfIntentSubmissionDto } from '../notice-of-intent.dto'; + +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; + +describe('NoticeOfIntentSubmissionService', () => { + let service: NoticeOfIntentSubmissionService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + const mockSubmittedApplication: NoticeOfIntentSubmissionDto = { + fileNumber: '', + uuid: '', + createdAt: 0, + updatedAt: 0, + applicant: '', + localGovernmentUuid: '', + type: '', + typeCode: '', + status: { + code: NOI_SUBMISSION_STATUS.IN_PROGRESS, + portalBackgroundColor: '', + portalColor: '', + label: '', + description: '', + }, + submissionStatuses: [], + owners: [], + canEdit: false, + canView: false, + purpose: null, + parcelsAgricultureDescription: '', + parcelsAgricultureImprovementDescription: '', + parcelsNonAgricultureUseDescription: '', + northLandUseType: '', + northLandUseTypeDescription: '', + eastLandUseType: '', + eastLandUseTypeDescription: '', + southLandUseType: '', + southLandUseTypeDescription: '', + westLandUseType: '', + westLandUseTypeDescription: '', + primaryContactOwnerUuid: null, + soilIsFollowUp: null, + soilFollowUpIDs: null, + soilTypeRemoved: null, + soilReduceNegativeImpacts: null, + soilToRemoveVolume: null, + soilToRemoveArea: null, + soilToRemoveMaximumDepth: null, + soilToRemoveAverageDepth: null, + soilAlreadyRemovedVolume: null, + soilAlreadyRemovedArea: null, + soilAlreadyRemovedMaximumDepth: null, + soilAlreadyRemovedAverageDepth: null, + soilToPlaceVolume: null, + soilToPlaceArea: null, + soilToPlaceMaximumDepth: null, + soilToPlaceAverageDepth: null, + soilAlreadyPlacedVolume: null, + soilAlreadyPlacedArea: null, + soilAlreadyPlacedMaximumDepth: null, + soilAlreadyPlacedAverageDepth: null, + soilProjectDurationAmount: null, + soilIsRemovingSoilForNewStructure: null, + soilProposedStructures: [] + }; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { provide: ToastService, useValue: mockToastService }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentSubmissionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should successfully fetch application submission', async () => { + mockHttpClient.get.mockReturnValue(of(mockSubmittedApplication)); + + const result = await service.fetchSubmission('1'); + + expect(result).toEqual(mockSubmittedApplication); + expect(mockHttpClient.get).toBeCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.ts new file mode 100644 index 0000000000..eb5a37ef95 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -0,0 +1,37 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { ToastService } from '../../toast/toast.service'; +import { NoticeOfIntentSubmissionDto } from '../notice-of-intent.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentSubmissionService { + private baseUrl = `${environment.apiUrl}/notice-of-intent-submission`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetchSubmission(fileNumber: string): Promise { + try { + return firstValueFrom(this.http.get(`${this.baseUrl}/${fileNumber}`)); + } catch (e) { + this.toastService.showErrorToast('Failed to fetch Notice of Intent Submission'); + throw e; + } + } + + async setSubmissionStatus(fileNumber: string, statusCode: string): Promise { + try { + return firstValueFrom( + this.http.patch(`${this.baseUrl}/${fileNumber}/update-status`, { + statusCode, + }) + ); + } catch (e) { + this.toastService.showErrorToast('Failed to update Notice of Intent Submission Status'); + throw e; + } + } +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.dto.ts index 1f62f11780..b1a15ce775 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.dto.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.dto.ts @@ -16,6 +16,12 @@ export interface CreateNoticeOfIntentDto { dateSubmittedToAlc: number; } +export interface NoticeOfIntentTypeDto extends BaseCodeDto { + shortLabel: string; + backgroundColor: string; + textColor: string; +} + export interface NoticeOfIntentDto { uuid: string; fileNumber: string; @@ -23,6 +29,8 @@ export interface NoticeOfIntentDto { localGovernment: ApplicationLocalGovernmentDto; region: ApplicationRegionDto; applicant: string; + type: NoticeOfIntentTypeDto; + source: 'ALCS' | 'APPLICANT'; dateSubmittedToAlc?: number; feePaidDate?: number; @@ -55,3 +63,265 @@ export interface UpdateNoticeOfIntentDto { retroactive?: boolean; subtype?: string[]; } + +export enum NOI_SUBMISSION_STATUS { + IN_PROGRESS = 'PROG', + SUBMITTED_TO_ALC = 'SUBM', //Submitted to ALC + SUBMITTED_TO_ALC_INCOMPLETE = 'SUIN', //Submitted to ALC - Incomplete + RECEIVED_BY_ALC = 'RECA', //Received By ALC + ALC_DECISION = 'ALCD', //Decision Released + CANCELLED = 'CANC', +} + +export interface NoticeOfIntentSubmissionStatusDto extends BaseCodeDto { + code: NOI_SUBMISSION_STATUS; + portalBackgroundColor: string; + portalColor: string; +} + +export interface NoticeOfIntentSubmissionToSubmissionStatusDto { + submissionUuid: string; + effectiveDate: number | null; + statusTypeCode: string; + status: NoticeOfIntentSubmissionStatusDto; +} + +export interface NoticeOfIntentSubmissionDto { + fileNumber: string; + uuid: string; + createdAt: number; + updatedAt: number; + applicant: string; + localGovernmentUuid: string; + type: string; + typeCode: string; + status: NoticeOfIntentSubmissionStatusDto; + submissionStatuses: NoticeOfIntentSubmissionToSubmissionStatusDto[]; + owners: NoticeOfIntentOwnerDto[]; + canEdit: boolean; + canView: boolean; + + purpose: string | null; + parcelsAgricultureDescription: string; + parcelsAgricultureImprovementDescription: string; + parcelsNonAgricultureUseDescription: string; + northLandUseType: string; + northLandUseTypeDescription: string; + eastLandUseType: string; + eastLandUseTypeDescription: string; + southLandUseType: string; + southLandUseTypeDescription: string; + westLandUseType: string; + westLandUseTypeDescription: string; + + primaryContactOwnerUuid: string | null; + primaryContact?: SubmittedNoticeOfIntentOwnerDto; + + //Soil Fields + soilIsFollowUp: boolean | null; + soilFollowUpIDs: string | null; + soilTypeRemoved: string | null; + soilReduceNegativeImpacts: string | null; + soilToRemoveVolume: number | null; + soilToRemoveArea: number | null; + soilToRemoveMaximumDepth: number | null; + soilToRemoveAverageDepth: number | null; + soilAlreadyRemovedVolume: number | null; + soilAlreadyRemovedArea: number | null; + soilAlreadyRemovedMaximumDepth: number | null; + soilAlreadyRemovedAverageDepth: number | null; + soilToPlaceVolume: number | null; + soilToPlaceArea: number | null; + soilToPlaceMaximumDepth: number | null; + soilToPlaceAverageDepth: number | null; + soilAlreadyPlacedVolume: number | null; + soilAlreadyPlacedArea: number | null; + soilAlreadyPlacedMaximumDepth: number | null; + soilAlreadyPlacedAverageDepth: number | null; + soilProjectDurationAmount: number | null; + soilProjectDurationUnit?: string | null; + soilFillTypeToPlace?: string | null; + soilAlternativeMeasures?: string | null; + soilIsExtractionOrMining?: boolean; + soilHasSubmittedNotice?: boolean; + soilIsRemovingSoilForNewStructure: boolean | null; + soilStructureFarmUseReason?: string | null; + soilStructureResidentialUseReason?: string | null; + soilAgriParcelActivity?: string | null; + soilStructureResidentialAccessoryUseReason?: string | null; + soilProposedStructures: ProposedStructure[]; +} + +export interface NoticeOfIntentSubmissionDetailedDto extends NoticeOfIntentSubmissionDto { + purpose: string | null; + parcelsAgricultureDescription: string; + parcelsAgricultureImprovementDescription: string; + parcelsNonAgricultureUseDescription: string; + northLandUseType: string; + northLandUseTypeDescription: string; + eastLandUseType: string; + eastLandUseTypeDescription: string; + southLandUseType: string; + southLandUseTypeDescription: string; + westLandUseType: string; + westLandUseTypeDescription: string; + + primaryContactOwnerUuid: string | null; + primaryContact?: SubmittedNoticeOfIntentOwnerDto; + + //Soil Fields + soilIsFollowUp: boolean | null; + soilFollowUpIDs: string | null; + soilTypeRemoved: string | null; + soilReduceNegativeImpacts: string | null; + soilToRemoveVolume: number | null; + soilToRemoveArea: number | null; + soilToRemoveMaximumDepth: number | null; + soilToRemoveAverageDepth: number | null; + soilAlreadyRemovedVolume: number | null; + soilAlreadyRemovedArea: number | null; + soilAlreadyRemovedMaximumDepth: number | null; + soilAlreadyRemovedAverageDepth: number | null; + soilToPlaceVolume: number | null; + soilToPlaceArea: number | null; + soilToPlaceMaximumDepth: number | null; + soilToPlaceAverageDepth: number | null; + soilAlreadyPlacedVolume: number | null; + soilAlreadyPlacedArea: number | null; + soilAlreadyPlacedMaximumDepth: number | null; + soilAlreadyPlacedAverageDepth: number | null; + soilProjectDurationAmount: number | null; + soilProjectDurationUnit?: string | null; + soilFillTypeToPlace?: string | null; + soilAlternativeMeasures?: string | null; + soilIsExtractionOrMining?: boolean; + soilHasSubmittedNotice?: boolean; + soilIsRemovingSoilForNewStructure: boolean | null; + soilStructureFarmUseReason?: string | null; + soilStructureResidentialUseReason?: string | null; + soilAgriParcelActivity?: string | null; + soilStructureResidentialAccessoryUseReason?: string | null; + soilProposedStructures: ProposedStructure[]; +} + +export interface NoticeOfIntentSubmissionUpdateDto { + applicant?: string | null; + purpose?: string | null; + localGovernmentUuid?: string | null; + typeCode?: string | null; + parcelsAgricultureDescription?: string | null; + parcelsAgricultureImprovementDescription?: string | null; + parcelsNonAgricultureUseDescription?: string | null; + northLandUseType?: string | null; + northLandUseTypeDescription?: string | null; + eastLandUseType?: string | null; + eastLandUseTypeDescription?: string | null; + southLandUseType?: string | null; + southLandUseTypeDescription?: string | null; + westLandUseType?: string | null; + westLandUseTypeDescription?: string | null; + + //Soil Fields + soilIsFollowUp?: boolean | null; + soilFollowUpIDs?: string | null; + soilTypeRemoved?: string | null; + soilReduceNegativeImpacts?: string | null; + soilToRemoveVolume?: number | null; + soilToRemoveArea?: number | null; + soilToRemoveMaximumDepth?: number | null; + soilToRemoveAverageDepth?: number | null; + soilAlreadyRemovedVolume?: number | null; + soilAlreadyRemovedArea?: number | null; + soilAlreadyRemovedMaximumDepth?: number | null; + soilAlreadyRemovedAverageDepth?: number | null; + soilToPlaceVolume?: number | null; + soilToPlaceArea?: number | null; + soilToPlaceMaximumDepth?: number | null; + soilToPlaceAverageDepth?: number | null; + soilAlreadyPlacedVolume?: number | null; + soilAlreadyPlacedArea?: number | null; + soilAlreadyPlacedMaximumDepth?: number | null; + soilAlreadyPlacedAverageDepth?: number | null; + soilProjectDurationAmount?: number | null; + soilProjectDurationUnit?: string | null; + soilFillTypeToPlace?: string | null; + soilAlternativeMeasures?: string | null; + soilIsExtractionOrMining?: boolean | null; + soilHasSubmittedNotice?: boolean | null; + soilIsRemovingSoilForNewStructure?: boolean | null; + soilStructureFarmUseReason?: string | null; + soilStructureResidentialUseReason?: string | null; + soilAgriParcelActivity?: string | null; + soilStructureResidentialAccessoryUseReason?: string | null; + soilProposedStructures?: ProposedStructure[]; +} + +export interface ProposedStructure { + type: STRUCTURE_TYPES | null; + area: number | null; +} + +export enum STRUCTURE_TYPES { + FARM_STRUCTURE = 'Farm Structure', + PRINCIPAL_RESIDENCE = 'Residential - Principal Residence', + ADDITIONAL_RESIDENCE = 'Residential - Additional Residence', + ACCESSORY_STRUCTURE = 'Residential - Accessory Structure', +} + +export interface NoticeOfIntentOwnerDto { + uuid: string; + noticeOfIntentSubmissionUuid: string; + displayName: string; + firstName: string | null; + lastName: string | null; + organizationName: string | null; + phoneNumber: string | null; + email: string | null; + type: OwnerTypeDto; +} + +export enum OWNER_TYPE { + INDIVIDUAL = 'INDV', + ORGANIZATION = 'ORGZ', + AGENT = 'AGEN', + CROWN = 'CRWN', + GOVERNMENT = 'GOVR', +} + +export interface OwnerTypeDto extends BaseCodeDto { + code: OWNER_TYPE; +} + +export const RESIDENTIAL_STRUCTURE_TYPES = [ + STRUCTURE_TYPES.ACCESSORY_STRUCTURE, + STRUCTURE_TYPES.ADDITIONAL_RESIDENCE, + STRUCTURE_TYPES.PRINCIPAL_RESIDENCE, +]; + +export interface SubmittedNoticeOfIntentOwnerDto { + uuid: string; + displayName: string; + firstName: string; + lastName: string; + organizationName?: string; + phoneNumber: string; + email: string; + type: BaseCodeDto; + corporateSummaryUuid?: string; +} + +export interface NoticeOfIntentParcelDto { + uuid: string; + pid?: string; + pin?: string; + legalDescription: string; + mapAreaHectares: string; + purchasedDate?: number; + isFarm?: boolean; + ownershipType?: string; + crownLandOwnerType?: string; + parcelType?: string; + certificateOfTitleUuid?: string; + owners: SubmittedNoticeOfIntentOwnerDto[]; + alrArea: number; +} diff --git a/alcs-frontend/src/app/shared/document/document.dto.ts b/alcs-frontend/src/app/shared/document/document.dto.ts index 393e62c16f..e177053562 100644 --- a/alcs-frontend/src/app/shared/document/document.dto.ts +++ b/alcs-frontend/src/app/shared/document/document.dto.ts @@ -27,6 +27,9 @@ export enum DOCUMENT_TYPE { PROOF_OF_SIGNAGE = 'POSA', REPORT_OF_PUBLIC_HEARING = 'ROPH', PROOF_OF_ADVERTISING = 'POAA', + + // NOI Documents + BUILDING_PLAN = 'BLDP', } export enum DOCUMENT_SOURCE { diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/additional-information/additional-information.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/additional-information/additional-information.component.html index 1796e934c9..4c4ae5349f 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/additional-information/additional-information.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/additional-information/additional-information.component.html @@ -14,6 +14,7 @@
+
The total floor area (m2) of the proposed structure(s)
#
Type
diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts new file mode 100644 index 0000000000..0ab1923602 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.controller.spec.ts @@ -0,0 +1,52 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentParcelService } from '../../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NoticeOfIntentParcelController } from './notice-of-intent-parcel.controller'; + +describe('NoticeOfIntentParcelController', () => { + let controller: NoticeOfIntentParcelController; + let mockParcelService: DeepMocked; + + beforeEach(async () => { + mockParcelService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentParcelController], + providers: [ + { + provide: NoticeOfIntentParcelService, + useValue: mockParcelService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NoticeOfIntentParcelController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call through to service for get', async () => { + mockParcelService.fetchByFileId.mockResolvedValue([]); + + await controller.get(''); + expect(mockParcelService.fetchByFileId).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts new file mode 100644 index 0000000000..58f667084b --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.controller.ts @@ -0,0 +1,47 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { NoticeOfIntentParcelDto } from '../../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcel } from '../../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelService } from '../../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service'; + +@Controller('notice-of-intent-parcel') +export class NoticeOfIntentParcelController { + constructor( + private applicationParcelService: NoticeOfIntentParcelService, + @InjectMapper() private mapper: Mapper, + ) {} + + @UserRoles(...ANY_AUTH_ROLE) + @Get('/:fileNumber') + async get(@Param('fileNumber') fileNumber: string) { + const parcels = await this.applicationParcelService.fetchByFileId( + fileNumber, + ); + + return this.mapper.mapArray( + parcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + ); + } + + @UserRoles(...ANY_AUTH_ROLE) + @Post('/:uuid') + async update(@Param('uuid') uuid: string, @Body() body: { alrArea: number }) { + const parcels = await this.applicationParcelService.update([ + { + uuid, + alrArea: body.alrArea, + }, + ]); + + return this.mapper.mapArray( + parcels, + NoticeOfIntentParcel, + NoticeOfIntentParcelDto, + )[0]; + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts new file mode 100644 index 0000000000..04da3b69a3 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts @@ -0,0 +1,85 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { AlcsNoticeOfIntentSubmissionDto } from '../notice-of-intent.dto'; +import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; +import { Document } from '../../../document/document.entity'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; + +describe('NoticeOfIntentSubmissionController', () => { + let controller: NoticeOfIntentSubmissionController; + + let mockNoticeOfIntentSubmissionService: DeepMocked; + let mockDocumentService: DeepMocked; + + beforeEach(async () => { + mockNoticeOfIntentSubmissionService = createMock(); + mockDocumentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [NoticeOfIntentSubmissionController], + providers: [ + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoticeOfIntentSubmissionService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NoticeOfIntentSubmissionController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call NoticeOfIntentSubmissionService to get Notice of Intent Submission', async () => { + const fakeFileNumber = 'fake'; + + mockNoticeOfIntentSubmissionService.get.mockResolvedValue({ + fileNumber: fakeFileNumber, + } as NoticeOfIntentSubmission); + mockNoticeOfIntentSubmissionService.mapToDto.mockResolvedValue( + createMock(), + ); + + const result = await controller.get(fakeFileNumber); + + expect(mockNoticeOfIntentSubmissionService.get).toBeCalledTimes(1); + expect(mockNoticeOfIntentSubmissionService.mapToDto).toBeCalledTimes(1); + expect(result).toBeDefined(); + }); + + it('should call documentService to get inline download url Submission', async () => { + const fakeDownloadUrl = 'fake_download'; + const fakeUuid = 'fake'; + + mockDocumentService.getDocument.mockResolvedValue({} as Document); + mockDocumentService.getDownloadUrl.mockResolvedValue(fakeDownloadUrl); + + const result = await controller.downloadFile(fakeUuid); + + expect(mockDocumentService.getDocument).toBeCalledTimes(1); + expect(mockDocumentService.getDownloadUrl).toBeCalledTimes(1); + expect(mockDocumentService.getDocument).toBeCalledWith(fakeUuid); + expect(mockDocumentService.getDownloadUrl).toBeCalledWith( + {} as Document, + true, + ); + expect(result).toEqual({ url: fakeDownloadUrl }); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.controller.ts new file mode 100644 index 0000000000..7ec2b16fda --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.controller.ts @@ -0,0 +1,52 @@ +import { Body, Controller, Get, Param, Patch, UseGuards } from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { DocumentService } from '../../../document/document.service'; +import { NOI_SUBMISSION_STATUS } from '../notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(RolesGuard) +@Controller('notice-of-intent-submission') +export class NoticeOfIntentSubmissionController { + constructor( + private applicationSubmissionService: NoticeOfIntentSubmissionService, + private documentService: DocumentService, + ) {} + + @UserRoles(...ANY_AUTH_ROLE) + @Get('/:fileNumber') + async get(@Param('fileNumber') fileNumber: string) { + const submission = await this.applicationSubmissionService.get(fileNumber); + + return await this.applicationSubmissionService.mapToDto(submission); + } + + @UserRoles(...ANY_AUTH_ROLE) + @Get('/document/:uuid/open') + async downloadFile(@Param('uuid') uuid: string) { + const document = await this.documentService.getDocument(uuid); + const url = await this.documentService.getDownloadUrl(document, true); + return { url }; + } + + @UserRoles(...ANY_AUTH_ROLE) + @Patch('/:fileNumber/update-status') + async updateStatus( + @Param('fileNumber') fileNumber: string, + @Body('statusCode') status: string, + ) { + if (!fileNumber) { + throw new ServiceValidationException('File number is required'); + } + await this.applicationSubmissionService.updateStatus( + fileNumber, + status as NOI_SUBMISSION_STATUS, + ); + return this.get(fileNumber); + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts new file mode 100644 index 0000000000..2e992ca778 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -0,0 +1,157 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NoticeOfIntentOwnerProfile } from '../../../common/automapper/notice-of-intent-owner.automapper.profile'; +import { NoticeOfIntentSubmissionProfile } from '../../../common/automapper/notice-of-intent-submission.automapper.profile'; +import { NoticeOfIntentOwner } from '../../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionStatusType } from '../notice-of-intent-submission-status/notice-of-intent-status-type.entity'; +import { NOI_SUBMISSION_STATUS } from '../notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NoticeOfIntentSubmissionStatusService } from '../notice-of-intent-submission-status/notice-of-intent-submission-status.service'; +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; + +describe('NoticeOfIntentSubmissionService', () => { + let service: NoticeOfIntentSubmissionService; + + let mockNoticeOfIntentSubmissionRepository: DeepMocked< + Repository + >; + let mockNoticeOfIntentStatusRepository: DeepMocked< + Repository + >; + let mockNoticeOfIntentSubmissionStatusService: DeepMocked; + + beforeEach(async () => { + mockNoticeOfIntentSubmissionRepository = createMock(); + mockNoticeOfIntentStatusRepository = createMock(); + mockNoticeOfIntentSubmissionStatusService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + providers: [ + NoticeOfIntentSubmissionService, + NoticeOfIntentSubmissionProfile, + NoticeOfIntentOwnerProfile, + { + provide: getRepositoryToken(NoticeOfIntentSubmission), + useValue: mockNoticeOfIntentSubmissionRepository, + }, + { + provide: getRepositoryToken(NoticeOfIntentSubmissionStatusType), + useValue: mockNoticeOfIntentStatusRepository, + }, + { + provide: NoticeOfIntentSubmissionStatusService, + useValue: mockNoticeOfIntentSubmissionStatusService, + }, + ], + }).compile(); + + mockNoticeOfIntentSubmissionStatusService.setStatusDate.mockResolvedValue( + {} as any, + ); + + service = module.get( + NoticeOfIntentSubmissionService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should successfully find NoticeOfIntentSubmission', async () => { + const fakeFileNumber = 'fake'; + + mockNoticeOfIntentSubmissionRepository.findOneOrFail.mockResolvedValue( + {} as NoticeOfIntentSubmission, + ); + + const result = await service.get(fakeFileNumber); + + expect(result).toBeDefined(); + expect( + mockNoticeOfIntentSubmissionRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect(mockNoticeOfIntentSubmissionRepository.findOneOrFail).toBeCalledWith( + { + where: { fileNumber: fakeFileNumber, isDraft: false }, + relations: { + noticeOfIntent: { + documents: { + document: true, + }, + }, + owners: { + type: true, + }, + }, + }, + ); + }); + + it('should properly map to dto', async () => { + const fakeSubmission = createMock({ + primaryContactOwnerUuid: 'uuid', + }); + fakeSubmission.owners = [ + new NoticeOfIntentOwner({ + uuid: 'uuid', + }), + ]; + + const result = await service.mapToDto(fakeSubmission); + + expect(result).toBeDefined(); + expect(result.primaryContact).toBeDefined(); + }); + + it('should successfully retrieve status from repo', async () => { + mockNoticeOfIntentStatusRepository.findOneOrFail.mockResolvedValue( + {} as NoticeOfIntentSubmissionStatusType, + ); + + const result = await service.getStatus(NOI_SUBMISSION_STATUS.ALC_DECISION); + + expect(result).toBeDefined(); + expect(mockNoticeOfIntentStatusRepository.findOneOrFail).toBeCalledTimes(1); + expect(mockNoticeOfIntentStatusRepository.findOneOrFail).toBeCalledWith({ + where: { code: NOI_SUBMISSION_STATUS.ALC_DECISION }, + }); + }); + + it('should successfully update the status', async () => { + mockNoticeOfIntentStatusRepository.findOneOrFail.mockResolvedValue( + {} as NoticeOfIntentSubmissionStatusType, + ); + mockNoticeOfIntentSubmissionRepository.findOneOrFail.mockResolvedValue({ + uuid: 'fake', + } as NoticeOfIntentSubmission); + + await service.updateStatus('fake', NOI_SUBMISSION_STATUS.ALC_DECISION); + + expect( + mockNoticeOfIntentSubmissionRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect(mockNoticeOfIntentSubmissionRepository.findOneOrFail).toBeCalledWith( + { + where: { + fileNumber: 'fake', + }, + }, + ); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDate, + ).toBeCalledTimes(1); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDate, + ).toBeCalledWith('fake', NOI_SUBMISSION_STATUS.ALC_DECISION); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.ts new file mode 100644 index 0000000000..bf38c26422 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -0,0 +1,85 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NoticeOfIntentOwnerDto } from '../../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentOwner } from '../../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionStatusType } from '../notice-of-intent-submission-status/notice-of-intent-status-type.entity'; +import { NOI_SUBMISSION_STATUS } from '../notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NoticeOfIntentSubmissionStatusService } from '../notice-of-intent-submission-status/notice-of-intent-submission-status.service'; +import { AlcsNoticeOfIntentSubmissionDto } from '../notice-of-intent.dto'; + +@Injectable() +export class NoticeOfIntentSubmissionService { + constructor( + @InjectRepository(NoticeOfIntentSubmission) + private noiSubmissionRepository: Repository, + @InjectRepository(NoticeOfIntentSubmissionStatusType) + private noiStatusRepository: Repository, + @InjectMapper() private mapper: Mapper, + private noiSubmissionStatusService: NoticeOfIntentSubmissionStatusService, + ) {} + + async get(fileNumber: string) { + return await this.noiSubmissionRepository.findOneOrFail({ + where: { fileNumber, isDraft: false }, + relations: { + noticeOfIntent: { + documents: { + document: true, + }, + }, + owners: { + type: true, + }, + }, + }); + } + + async mapToDto(submission: NoticeOfIntentSubmission) { + const mappedSubmission = await this.mapper.mapAsync( + submission, + NoticeOfIntentSubmission, + AlcsNoticeOfIntentSubmissionDto, + ); + + const primaryContact = submission.owners.find( + (e) => e.uuid === submission.primaryContactOwnerUuid, + ); + + mappedSubmission.primaryContact = await this.mapper.mapAsync( + primaryContact, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDto, + ); + + return mappedSubmission; + } + + async getStatus(code: NOI_SUBMISSION_STATUS) { + return await this.noiStatusRepository.findOneOrFail({ + where: { + code, + }, + }); + } + + async updateStatus(fileNumber: string, statusCode: NOI_SUBMISSION_STATUS) { + const submission = await this.loadBarebonesSubmission(fileNumber); + await this.noiSubmissionStatusService.setStatusDate( + submission.uuid, + statusCode, + ); + } + + private loadBarebonesSubmission(fileNumber: string) { + //Load submission without relations to prevent save from crazy cascading + return this.noiSubmissionRepository.findOneOrFail({ + where: { + fileNumber, + }, + }); + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts index 6d4fff23da..987111de92 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts @@ -9,10 +9,16 @@ import { IsUUID, } from 'class-validator'; import { BaseCodeDto } from '../../common/dtos/base.dto'; +import { NoticeOfIntentOwnerDto } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.dto'; import { CardDto } from '../card/card.dto'; import { ApplicationRegionDto } from '../code/application-code/application-region/application-region.dto'; +import { NoticeOfIntentTypeDto } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.dto'; import { LocalGovernmentDto } from '../local-government/local-government.dto'; +export class AlcsNoticeOfIntentSubmissionDto extends NoticeOfIntentSubmissionDetailedDto { + primaryContact?: NoticeOfIntentOwnerDto; +} export class NoticeOfIntentSubtypeDto extends BaseCodeDto { @AutoMap() @IsBoolean() @@ -95,6 +101,12 @@ export class NoticeOfIntentDto { @AutoMap(() => [NoticeOfIntentSubtypeDto]) subtype: NoticeOfIntentSubtypeDto[]; + + @AutoMap(() => NoticeOfIntentTypeDto) + type: NoticeOfIntentTypeDto; + + @AutoMap() + source: 'ALCS' | 'APPLICANT'; } export class UpdateNoticeOfIntentDto { diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts index 3e3d233a1a..9108748bf2 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts @@ -8,14 +8,16 @@ import { JoinTable, ManyToMany, ManyToOne, + OneToMany, OneToOne, } from 'typeorm'; import { Base } from '../../common/entities/base.entity'; import { ColumnNumericTransformer } from '../../utils/column-numeric-transform'; -import { LocalGovernment } from '../local-government/local-government.entity'; import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { LocalGovernment } from '../local-government/local-government.entity'; +import { NoticeOfIntentDocument } from './notice-of-intent-document/notice-of-intent-document.entity'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; @Entity() @@ -129,6 +131,14 @@ export class NoticeOfIntent extends Base { }) dateAcknowledgedComplete: Date | null; + @AutoMap() + @Column({ + default: 'ALCS', + type: 'text', + comment: 'Determines where the NOI came from', + }) + source: 'ALCS' | 'APPLICANT'; + @AutoMap() @Column({ type: 'timestamptz', @@ -143,4 +153,11 @@ export class NoticeOfIntent extends Base { @Column() typeCode: string; + + @AutoMap() + @OneToMany( + () => NoticeOfIntentDocument, + (noiDocument) => noiDocument.noticeOfIntent, + ) + documents: NoticeOfIntentDocument[]; } diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts index ffad50cbda..48f54d1834 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts @@ -24,6 +24,14 @@ import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; import { NoticeOfIntent } from './notice-of-intent.entity'; import { NoticeOfIntentService } from './notice-of-intent.service'; +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission/notice-of-intent-submission.controller'; +import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionStatusType } from './notice-of-intent-submission-status/notice-of-intent-status-type.entity'; +import { NoticeOfIntentParcelController } from './notice-of-intent-parcel/notice-of-intent-parcel.controller'; +import { NoticeOfIntentSubmissionModule } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.module'; +import { NoticeOfIntentParcel } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { NoticeOfIntentParcelProfile } from '../../common/automapper/notice-of-intent-parcel.automapper.profile'; @Module({ imports: [ @@ -34,6 +42,8 @@ import { NoticeOfIntentService } from './notice-of-intent.service'; NoticeOfIntentType, NoticeOfIntentSubtype, NoticeOfIntentDocument, + NoticeOfIntentSubmission, + NoticeOfIntentSubmissionStatusType, DocumentCode, ]), forwardRef(() => BoardModule), @@ -43,17 +53,22 @@ import { NoticeOfIntentService } from './notice-of-intent.service'; CodeModule, LocalGovernmentModule, NoticeOfIntentSubmissionStatusModule, + forwardRef(() => NoticeOfIntentSubmissionModule), ], providers: [ NoticeOfIntentService, NoticeOfIntentProfile, NoticeOfIntentMeetingService, NoticeOfIntentDocumentService, + NoticeOfIntentSubmissionService, + NoticeOfIntentParcelProfile, ], controllers: [ NoticeOfIntentController, NoticeOfIntentMeetingController, NoticeOfIntentDocumentController, + NoticeOfIntentSubmissionController, + NoticeOfIntentParcelController, ], exports: [ NoticeOfIntentService, diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts index 459849bea5..5d3e76e411 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts @@ -52,6 +52,7 @@ export class NoticeOfIntentService { localGovernment: true, region: true, subtype: true, + type: true, }; constructor( diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts index f577977c4a..8b4e971899 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts @@ -7,7 +7,11 @@ import { NoticeOfIntentSubmissionToSubmissionStatusDto, } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.entity'; -import { NoticeOfIntentOwnerDto } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { AlcsNoticeOfIntentSubmissionDto } from '../../alcs/notice-of-intent/notice-of-intent.dto'; +import { + NoticeOfIntentOwnerDetailedDto, + NoticeOfIntentOwnerDto, +} from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; import { NoticeOfIntentSubmissionDetailedDto, @@ -106,6 +110,39 @@ export class NoticeOfIntentSubmissionProfile extends AutomapperProfile { }), ), ); + + createMap( + mapper, + NoticeOfIntentSubmission, + AlcsNoticeOfIntentSubmissionDto, + // TODO uncomment when working on statuses + // forMember( + // (a) => a.lastStatusUpdate, + // mapFrom((ad) => { + // return ad.status?.effectiveDate?.getTime(); + // }), + // ), + forMember( + (a) => a.status, + mapFrom((ad) => { + return ad.status.statusType; + }), + ), + forMember( + (a) => a.owners, + mapFrom((ad) => { + if (ad.owners) { + return this.mapper.mapArray( + ad.owners, + NoticeOfIntentOwner, + NoticeOfIntentOwnerDetailedDto, + ); + } else { + return []; + } + }), + ), + ); }; } } diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent.automapper.profile.ts index 786b0c1e7b..a1f6658a50 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent.automapper.profile.ts @@ -3,6 +3,8 @@ import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { ApplicationDocumentDto } from '../../alcs/application/application-document/application-document.dto'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; +import { NoticeOfIntentTypeDto } from '../../alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.dto'; +import { NoticeOfIntentType } from '../../alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; @@ -30,6 +32,7 @@ export class NoticeOfIntentProfile extends AutomapperProfile { override get profile() { return (mapper) => { createMap(mapper, NoticeOfIntentSubtype, NoticeOfIntentSubtypeDto); + createMap(mapper, NoticeOfIntentType, NoticeOfIntentTypeDto); createMap( mapper, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts index 08ab45b2ea..671de80829 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto.ts @@ -8,8 +8,8 @@ import { } from 'class-validator'; import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; import { - OWNER_TYPE, OwnerTypeDto, + OWNER_TYPE, } from '../../../common/owner-type/owner-type.entity'; import { emailRegex } from '../../../utils/email.helper'; import { NoticeOfIntentParcelDto } from '../notice-of-intent-parcel/notice-of-intent-parcel.dto'; diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts index 8fe460df4c..1b0fc2501a 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts @@ -54,6 +54,10 @@ import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.s NoticeOfIntentOwnerProfile, NoticeOfIntentParcelProfile, ], - exports: [NoticeOfIntentSubmissionService], + exports: [ + NoticeOfIntentSubmissionService, + NoticeOfIntentParcelService, + NoticeOfIntentParcelProfile, + ], }) export class NoticeOfIntentSubmissionModule {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index b1e7825f70..53276c81e0 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -95,6 +95,7 @@ export class NoticeOfIntentSubmissionService { fileNumber, applicant: 'Unknown', typeCode: type, + source: 'APPLICANT', }); const noiSubmission = new NoticeOfIntentSubmission({ diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692216006482-noi_source_column.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692216006482-noi_source_column.ts new file mode 100644 index 0000000000..be56225375 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692216006482-noi_source_column.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class noiSourceColumn1692216006482 implements MigrationInterface { + name = 'noiSourceColumn1692216006482'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" ADD "source" text NOT NULL DEFAULT 'ALCS'`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent"."source" IS 'Determines where the NOI came from'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent"."source" IS 'Determines where the NOI came from'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent" DROP COLUMN "source"`, + ); + } +} From 98a6d336beace51891c62df9b6ee55f2d05b2af3 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 16 Aug 2023 15:10:52 -0700 Subject: [PATCH 258/954] Add NOI View Page * Add NOI View Page * Remove navigation-wrapper and use default angular tabs (with bold headers) --- portal-frontend/src/app/app.module.ts | 2 + .../lfng-review/lfng-review.component.scss | 37 ---- ...view-application-submission.component.html | 117 +---------- ...view-application-submission.component.scss | 77 +++----- .../view-application-submission.component.ts | 26 +-- .../src/app/features/home/home.component.html | 2 +- .../src/app/features/home/home.component.scss | 7 +- .../edit-submission.component.scss | 1 + ...notice-of-intent-submission.component.html | 107 +++++++++- ...notice-of-intent-submission.component.scss | 183 ++++++++++++++++++ ...ice-of-intent-submission.component.spec.ts | 31 +++ ...w-notice-of-intent-submission.component.ts | 63 +++++- .../notice-of-intent-submission.dto.ts | 1 + .../src/styles/navigation-wrapper.scss | 76 -------- ...of-intent-submission.automapper.profile.ts | 12 ++ .../notice-of-intent-submission.dto.ts | 1 + 16 files changed, 440 insertions(+), 303 deletions(-) delete mode 100644 portal-frontend/src/styles/navigation-wrapper.scss diff --git a/portal-frontend/src/app/app.module.ts b/portal-frontend/src/app/app.module.ts index 2bf28c4feb..c0675920cf 100644 --- a/portal-frontend/src/app/app.module.ts +++ b/portal-frontend/src/app/app.module.ts @@ -20,6 +20,7 @@ import { AlcReviewComponent } from './features/applications/view-submission/alc- import { SubmissionDocumentsComponent } from './features/applications/view-submission/alc-review/submission-documents/submission-documents.component'; import { LfngReviewComponent } from './features/applications/view-submission/lfng-review/lfng-review.component'; import { ViewApplicationSubmissionComponent } from './features/applications/view-submission/view-application-submission.component'; +import { NoticeOfIntentDetailsModule } from './features/notice-of-intents/notice-of-intent-details/notice-of-intent-details.module'; import { ViewNoticeOfIntentSubmissionComponent } from './features/notice-of-intents/view-submission/view-notice-of-intent-submission.component'; import { AuthInterceptorService } from './services/authentication/auth-interceptor.service'; import { TokenRefreshService } from './services/authentication/token-refresh.service'; @@ -59,6 +60,7 @@ import { DecisionsComponent } from './features/applications/view-submission/alc- MatPaginatorModule, MatToolbarModule, ApplicationDetailsModule, + NoticeOfIntentDetailsModule, ], providers: [ ConfirmationDialogService, diff --git a/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.scss b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.scss index b2921ee5b8..8af4733989 100644 --- a/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.scss +++ b/portal-frontend/src/app/features/applications/view-submission/lfng-review/lfng-review.component.scss @@ -38,43 +38,6 @@ cursor: pointer; } -.header { - flex-direction: column; - align-items: start; - - h3 { - margin-bottom: rem(24) !important; - } - - .btns-wrapper { - display: flex; - flex-direction: column; - width: 100%; - - button { - width: 100%; - margin-bottom: rem(8) !important; - } - } - - @media screen and (min-width: $desktopBreakpoint) { - flex-direction: row; - - h3 { - max-width: 50%; - } - - .btns-wrapper { - flex-direction: row; - width: unset; - - button { - width: unset; - } - } - } -} - .review-table { padding: rem(8); margin: rem(12) 0 rem(20) 0; diff --git a/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.html b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.html index 3990a744a8..447eef32c7 100644 --- a/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.html +++ b/portal-frontend/src/app/features/applications/view-submission/view-application-submission.component.html @@ -1,6 +1,3 @@ -
- -
-
>
-
Ag Cap Source
+
Agricultural Capability Source
Proposal Components - {{ application?.type?.label }} >
-
Ag Cap Map
+
Agricultural Capability Mapsheet Reference
-
Ag Cap Consultant
+
Agricultural Capability Consultant
#
Type
Size (ha)
-
ALR Area (ha)
+
ALR Area Impacted (ha)
{{ i + 1 }} diff --git a/alcs-frontend/src/app/features/notice-of-intent/proposal/parcel-prep/parcel-prep.component.html b/alcs-frontend/src/app/features/notice-of-intent/proposal/parcel-prep/parcel-prep.component.html index 91cfbd81db..610596a4ac 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/proposal/parcel-prep/parcel-prep.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/proposal/parcel-prep/parcel-prep.component.html @@ -34,7 +34,7 @@ {{ row.mapAreaHectares }} - ALR Area (ha) + ALR Area Impacted (ha) diff --git a/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html b/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html index d384b46289..f57ffd73e6 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/proposal/proposal.component.html @@ -3,11 +3,11 @@

Notice of Intent Prep

Proposal Components - {{ noticeOfIntent?.type?.label }}
-
Proposal ALR Area (ha)
+
Proposal ALR Area Impacted (ha)
-
Ag Cap
+
Agricultural Capability
Proposal Components - {{ noticeOfIntent?.type?.label }} >
-
Ag Cap Source
+
Agricultural Capability Source
Proposal Components - {{ noticeOfIntent?.type?.label }} >
-
Ag Cap Map
+
Agricultural Capability Mapsheet Reference
-
Ag Cap Consultant
+
Agricultural Capability Consultant
-
+
Total Number of Proposed Lots Date: Mon, 21 Aug 2023 15:50:55 -0700 Subject: [PATCH 277/954] Feature/alcs 841 - legacy id (#901) added legacy id to application detailed page and detailed card view --- .../src/app/features/board/board.component.ts | 1 + .../application-dialog.component.html | 1 + .../services/application/application.dto.ts | 1 + .../application-legacy-id.component.html | 4 ++++ .../application-legacy-id.component.scss | 11 +++++++++ .../application-legacy-id.component.spec.ts | 23 +++++++++++++++++++ .../application-legacy-id.component.ts | 10 ++++++++ .../src/app/shared/card/card.component.ts | 1 + .../details-header.component.html | 1 + .../details-header.component.ts | 6 +++++ alcs-frontend/src/app/shared/shared.module.ts | 3 +++ .../src/alcs/application/application.dto.ts | 3 +++ .../alcs/application/application.entity.ts | 9 ++++++++ .../migrations/1692639176392-legacy_id.ts | 23 +++++++++++++++++++ 14 files changed, 97 insertions(+) create mode 100644 alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.html create mode 100644 alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.scss create mode 100644 alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.spec.ts create mode 100644 alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692639176392-legacy_id.ts diff --git a/alcs-frontend/src/app/features/board/board.component.ts b/alcs-frontend/src/app/features/board/board.component.ts index 526c511722..28e8d032f5 100644 --- a/alcs-frontend/src/app/features/board/board.component.ts +++ b/alcs-frontend/src/app/features/board/board.component.ts @@ -333,6 +333,7 @@ export class BoardComponent implements OnInit, OnDestroy { cardType: CardType.APP, cardUuid: application.card!.uuid, dateReceived: application.dateSubmittedToAlc, + legacyId: application.legacyId, }; } diff --git a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.html b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.html index 614288178e..d10de71d06 100644 --- a/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.html +++ b/alcs-frontend/src/app/features/board/dialogs/application/application-dialog.component.html @@ -9,6 +9,7 @@
Application

{{ cardTitle }} +

diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 2794b6f739..58f073bb2e 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -252,6 +252,7 @@ export interface ApplicationDto { inclExclApplicantType?: string; proposalEndDate?: number; proposalExpiryDate?: number; + legacyId?: string; } export interface UpdateApplicationDto { diff --git a/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.html b/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.html new file mode 100644 index 0000000000..72cf5592b1 --- /dev/null +++ b/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.html @@ -0,0 +1,4 @@ +
+ folder_open{{ legacyId }} +
diff --git a/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.scss b/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.scss new file mode 100644 index 0000000000..5c5ee50824 --- /dev/null +++ b/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.scss @@ -0,0 +1,11 @@ +.legacy-id { + display: inline-flex; + align-items: baseline; + font-size: 20px; + font-weight: 400; + + mat-icon { + vertical-align: sub; + } + } + \ No newline at end of file diff --git a/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.spec.ts b/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.spec.ts new file mode 100644 index 0000000000..5ac982f88e --- /dev/null +++ b/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ApplicationLegacyIdComponent } from './application-legacy-id.component'; + +describe('ApplicationLegacyIdComponent', () => { + let component: ApplicationLegacyIdComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ApplicationLegacyIdComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ApplicationLegacyIdComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.ts b/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.ts new file mode 100644 index 0000000000..f2b0f7d19e --- /dev/null +++ b/alcs-frontend/src/app/shared/application-legacy-id/application-legacy-id.component.ts @@ -0,0 +1,10 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-application-legacy-id', + templateUrl: './application-legacy-id.component.html', + styleUrls: ['./application-legacy-id.component.scss'], +}) +export class ApplicationLegacyIdComponent { + @Input() legacyId?: string; +} diff --git a/alcs-frontend/src/app/shared/card/card.component.ts b/alcs-frontend/src/app/shared/card/card.component.ts index 69e8e7a953..3779913ce1 100644 --- a/alcs-frontend/src/app/shared/card/card.component.ts +++ b/alcs-frontend/src/app/shared/card/card.component.ts @@ -24,6 +24,7 @@ export interface CardData { verticalOutBound?: boolean; dueDate?: Date; maxActiveDays?: number; + legacyId?: string; } export interface CardSelectedEvent { diff --git a/alcs-frontend/src/app/shared/details-header/details-header.component.html b/alcs-frontend/src/app/shared/details-header/details-header.component.html index f949f75c1d..c1880bd6a1 100644 --- a/alcs-frontend/src/app/shared/details-header/details-header.component.html +++ b/alcs-frontend/src/app/shared/details-header/details-header.component.html @@ -5,6 +5,7 @@
{{ _application.fileNumber }} ({{ _application.applicant }})
+
diff --git a/alcs-frontend/src/app/shared/details-header/details-header.component.ts b/alcs-frontend/src/app/shared/details-header/details-header.component.ts index 50b5fdb2a7..607858a36d 100644 --- a/alcs-frontend/src/app/shared/details-header/details-header.component.ts +++ b/alcs-frontend/src/app/shared/details-header/details-header.component.ts @@ -31,6 +31,8 @@ export class DetailsHeaderComponent { @Input() days = 'Calendar Days'; @Input() showStatus = false; + legacyId?: string; + _application: ApplicationDto | CommissionerApplicationDto | NoticeOfIntentDto | undefined; @Input() set application(application: ApplicationDto | CommissionerApplicationDto | NoticeOfIntentDto | undefined) { @@ -55,6 +57,10 @@ export class DetailsHeaderComponent { this.showRetroLabel = !!application.retroactive; } + if ('legacyId' in application) { + this.legacyId = application.legacyId; + } + if (this.showStatus) { this.submissionStatusService .fetchCurrentStatusByFileNumber(application.fileNumber, false) diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index f80164609a..6eef90b2d6 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -66,6 +66,7 @@ import { TimelineComponent } from './timeline/timeline.component'; import { DATE_FORMATS } from './utils/date-format'; import { ExtensionsDatepickerFormatter } from './utils/extensions-datepicker-formatter'; import { WarningBannerComponent } from './warning-banner/warning-banner.component'; +import { ApplicationLegacyIdComponent } from './application-legacy-id/application-legacy-id.component'; @NgModule({ declarations: [ @@ -100,6 +101,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen ErrorMessageComponent, LotsTableFormComponent, InlineNgSelectComponent, + ApplicationLegacyIdComponent, ], imports: [ CommonModule, @@ -190,6 +192,7 @@ import { WarningBannerComponent } from './warning-banner/warning-banner.componen WarningBannerComponent, ErrorMessageComponent, LotsTableFormComponent, + ApplicationLegacyIdComponent, ], }) export class SharedModule { diff --git a/services/apps/alcs/src/alcs/application/application.dto.ts b/services/apps/alcs/src/alcs/application/application.dto.ts index db902aeb43..9bb99437b6 100644 --- a/services/apps/alcs/src/alcs/application/application.dto.ts +++ b/services/apps/alcs/src/alcs/application/application.dto.ts @@ -252,6 +252,9 @@ export class ApplicationDto { @AutoMap(() => String) inclExclApplicantType?: string; + @AutoMap(() => String) + legacyId?: string; + proposalEndDate?: number; proposalExpiryDate?: number; } diff --git a/services/apps/alcs/src/alcs/application/application.entity.ts b/services/apps/alcs/src/alcs/application/application.entity.ts index 423c222b3d..ed48cc5671 100644 --- a/services/apps/alcs/src/alcs/application/application.entity.ts +++ b/services/apps/alcs/src/alcs/application/application.entity.ts @@ -247,6 +247,15 @@ export class Application extends Base { }) proposalExpiryDate?: Date | null; + @AutoMap(() => String) + @Column({ + type: 'text', + comment: + 'Application Id that is applicable only to paper version applications from 70s - 80s', + nullable: true, + }) + legacyId?: string | null; + @AutoMap() @OneToMany(() => ApplicationPaused, (appPaused) => appPaused.application) pauses: ApplicationPaused[]; diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692639176392-legacy_id.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692639176392-legacy_id.ts new file mode 100644 index 0000000000..b8eee01b76 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692639176392-legacy_id.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class legacyId1692639176392 implements MigrationInterface { + name = 'legacyId1692639176392'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application" ADD "legacy_id" text`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application"."legacy_id" IS 'Application Id that is applicable only to paper version applications from 70s - 80s'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application"."legacy_id" IS 'Application Id that is applicable only to paper version applications from 70s - 80s'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application" DROP COLUMN "legacy_id"`, + ); + } +} From f7549744acce2f5f676a508c8cc205f101f64d1d Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 21 Aug 2023 16:22:35 -0700 Subject: [PATCH 278/954] MR Feedback --- bin/migrate-oats-data/migrate.py | 2 +- bin/migrate-oats-data/submissions/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/migrate-oats-data/migrate.py b/bin/migrate-oats-data/migrate.py index 4d5d088f43..01501d78ad 100644 --- a/bin/migrate-oats-data/migrate.py +++ b/bin/migrate-oats-data/migrate.py @@ -324,7 +324,7 @@ def setup_menu_args_parser(import_batch_size): process_noi_documents(batch_size=import_batch_size) case "app-sub-import": - console.log("Beginning OATS -> ALCS app-prep import process") + console.log("Beginning OATS -> ALCS app-sub import process") with console.status( "[bold green]App submission import (application_submission table update in ALCS)..." ) as status: diff --git a/bin/migrate-oats-data/submissions/__init__.py b/bin/migrate-oats-data/submissions/__init__.py index 9d0f9dbe9c..6d8ed07ef9 100644 --- a/bin/migrate-oats-data/submissions/__init__.py +++ b/bin/migrate-oats-data/submissions/__init__.py @@ -1 +1 @@ -from .app_submissions import * \ No newline at end of file +from .app_submissions import process_alcs_app_submissions, clean_application_submission \ No newline at end of file From 415f3eb368b26e1ec63c8c6dd396912971ba907e Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 21 Aug 2023 16:30:12 -0700 Subject: [PATCH 279/954] updated function names and retested --- .../submissions/app_submissions.py | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 8eea34fa3b..ba4ef88cb8 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -66,7 +66,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): f"retrieved/updated items count: {applications_to_be_updated_count}; total successfully updated submissions so far {successful_updates_count}; last updated application_id: {last_application_id}" ) except Exception as e: - # this is NOT going to be caused by actual data update failure. This code is only executed when the code error appears or connection to DB is lost + # this is NOT going to be caused by actual data insert failure. This code is only executed when the code error appears or connection to DB is lost conn.rollback() str_err = str(e) trace_err = traceback.format_exc() @@ -82,7 +82,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): def update_app_sub_records(conn, batch_size, cursor, rows): """ - Function to update submission records in batches. + Function to insert submission records in batches. Args: conn (obj): Connection to the database. @@ -104,7 +104,7 @@ def update_app_sub_records(conn, batch_size, cursor, rows): if len(nfu_data_list) > 0: execute_batch( cursor, - get_update_query_for_nfu(), + get_insert_query_for_nfu(), nfu_data_list, page_size=batch_size, ) @@ -112,7 +112,7 @@ def update_app_sub_records(conn, batch_size, cursor, rows): if len(nar_data_list) > 0: execute_batch( cursor, - get_update_query_for_nar(), + get_insert_query_for_nar(), nar_data_list, page_size=batch_size, ) @@ -120,7 +120,7 @@ def update_app_sub_records(conn, batch_size, cursor, rows): if len(exc_data_list) > 0: execute_batch( cursor, - get_update_query_for_exc(), + get_insert_query_for_exc(), exc_data_list, page_size=batch_size, ) @@ -128,7 +128,7 @@ def update_app_sub_records(conn, batch_size, cursor, rows): if len(inc_data_list) > 0: execute_batch( cursor, - get_update_query_for_inc(), + get_insert_query_for_inc(), inc_data_list, page_size=batch_size, ) @@ -136,7 +136,7 @@ def update_app_sub_records(conn, batch_size, cursor, rows): if len(other_data_list) > 0: execute_batch( cursor, - get_update_query_for_other(), + get_insert_query_for_other(), other_data_list, page_size=batch_size, ) @@ -183,7 +183,7 @@ def prepare_app_sub_data(app_sub_raw_data_list): return nfu_data_list, nar_data_list, other_data_list, exc_data_list, inc_data_list -def get_update_query(unique_fields,unique_values): +def get_insert_query(unique_fields,unique_values): # unique_fields takes input from called function and appends to query query = """ INSERT INTO alcs.application_submission ( @@ -207,47 +207,47 @@ def get_update_query(unique_fields,unique_values): """ return query.format(unique_fields=unique_fields, unique_values=unique_values) -def get_update_query_for_nfu(): +def get_insert_query_for_nfu(): unique_fields = """, nfu_hectares """ unique_values = """, %(alr_area)s """ - return get_update_query(unique_fields,unique_values) + return get_insert_query(unique_fields,unique_values) -def get_update_query_for_nar(): +def get_insert_query_for_nar(): # naruSubtype is a part of submission, import there unique_fields = """""" unique_values = """""" - return get_update_query(unique_fields,unique_values) + return get_insert_query(unique_fields,unique_values) -def get_update_query_for_exc(): +def get_insert_query_for_exc(): unique_fields = """, incl_excl_hectares """ unique_values = """, %(alr_area)s """ - return get_update_query(unique_fields,unique_values) + return get_insert_query(unique_fields,unique_values) -def get_update_query_for_inc(): +def get_insert_query_for_inc(): unique_fields = """, incl_excl_hectares """ unique_values = """, %(alr_area)s """ - return get_update_query(unique_fields,unique_values) + return get_insert_query(unique_fields,unique_values) -def get_update_query_for_other(): +def get_insert_query_for_other(): # leaving blank insert for now unique_fields = """""" unique_values = """""" - return get_update_query(unique_fields,unique_values) + return get_insert_query(unique_fields,unique_values) From ef6813a3e4b35f0600325608139d225f7120b885 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 22 Aug 2023 09:10:49 -0700 Subject: [PATCH 280/954] changing string variable formatting --- .../submissions/app_submissions.py | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index ba4ef88cb8..02c9d7bf0f 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -208,45 +208,33 @@ def get_insert_query(unique_fields,unique_values): return query.format(unique_fields=unique_fields, unique_values=unique_values) def get_insert_query_for_nfu(): - unique_fields = """, - nfu_hectares - """ - unique_values = """, - %(alr_area)s - """ + unique_fields = ", nfu_hectares" + unique_values = ", %(alr_area)s" return get_insert_query(unique_fields,unique_values) def get_insert_query_for_nar(): # naruSubtype is a part of submission, import there - unique_fields = """""" - unique_values = """""" + unique_fields = "" + unique_values = "" return get_insert_query(unique_fields,unique_values) def get_insert_query_for_exc(): - unique_fields = """, - incl_excl_hectares - """ - unique_values = """, - %(alr_area)s - """ + unique_fields = ", incl_excl_hectares" + unique_values = ", %(alr_area)s" return get_insert_query(unique_fields,unique_values) def get_insert_query_for_inc(): - unique_fields = """, - incl_excl_hectares - """ - unique_values = """, - %(alr_area)s - """ + unique_fields = ", incl_excl_hectares" + unique_values = ", %(alr_area)s" return get_insert_query(unique_fields,unique_values) def get_insert_query_for_other(): # leaving blank insert for now - unique_fields = """""" - unique_values = """""" + unique_fields = "" + unique_values = "" return get_insert_query(unique_fields,unique_values) From 1ddde70a10aae8ab31ab05ddca0d4cb2531d8336 Mon Sep 17 00:00:00 2001 From: Mekhti Date: Tue, 22 Aug 2023 09:21:27 -0700 Subject: [PATCH 281/954] update search query for NOI --- .../notice-of-intent-submission.service.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index 729fcbe99e..7ebe9a7502 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -240,9 +240,8 @@ export class NoticeOfIntentSubmissionService { ...searchOptions, localGovernmentUuid: matchingLocalGovernment.uuid, isDraft: false, - submissionStatuses: { - effectiveDate: Not(IsNull()), - statusTypeCode: Not(NOI_SUBMISSION_STATUS.IN_PROGRESS), + noticeOfIntent: { + dateSubmittedToAlc: Not(IsNull()), }, }); } From 8b10b51adceb6c6f3016e9f4dd2b9d88259c1b73 Mon Sep 17 00:00:00 2001 From: Mekhti Date: Tue, 22 Aug 2023 09:22:42 -0700 Subject: [PATCH 282/954] Revert "update search query for NOI" This reverts commit 1ddde70a10aae8ab31ab05ddca0d4cb2531d8336. --- .../notice-of-intent-submission.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index 7ebe9a7502..729fcbe99e 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -240,8 +240,9 @@ export class NoticeOfIntentSubmissionService { ...searchOptions, localGovernmentUuid: matchingLocalGovernment.uuid, isDraft: false, - noticeOfIntent: { - dateSubmittedToAlc: Not(IsNull()), + submissionStatuses: { + effectiveDate: Not(IsNull()), + statusTypeCode: Not(NOI_SUBMISSION_STATUS.IN_PROGRESS), }, }); } From 9504c08d91f53899345d7340e7691fde581f4279 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 22 Aug 2023 09:23:43 -0700 Subject: [PATCH 283/954] Code Review Feedback Use ng-content to DRY up the code a bit --- .../incl-excl/incl-excl.component.html | 5 +---- .../incl-excl/incl-excl.component.ts | 5 ----- .../decision-component/naru/naru.component.html | 5 +---- .../decision-component/naru/naru.component.ts | 5 ----- .../decision-component/nfup/nfup.component.html | 16 +++++++--------- .../decision-component/nfup/nfup.component.ts | 5 ----- .../decision-component/pfrs/pfrs.component.html | 12 +++++------- .../decision-component/pfrs/pfrs.component.ts | 5 ----- .../decision-component/pofo/pofo.component.html | 2 +- .../decision-component/pofo/pofo.component.ts | 5 ----- .../decision-component/roso/roso.component.html | 2 +- .../decision-component/roso/roso.component.ts | 5 ----- .../decision-component/subd/subd.component.html | 3 ++- .../decision-component/subd/subd.component.ts | 5 ----- .../decision-component/turp/turp.component.html | 2 +- .../decision-component/turp/turp.component.ts | 5 ----- 16 files changed, 19 insertions(+), 68 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html index 5ef5d7b857..a594ecffa7 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.html @@ -1,7 +1,4 @@ -
- -
- +
Applicant Type
{{ component.inclExclApplicantType }} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts index ff8841afbe..d164dca1aa 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts @@ -8,9 +8,4 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec }) export class InclExclComponent { @Input() component!: DecisionComponentDto; - @Output() saveAlrArea = new EventEmitter(); - - onSaveAlrArea($event: string | null) { - this.saveAlrArea.emit($event); - } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.html index 0c54d2acdc..dafcee70ff 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.html @@ -1,7 +1,4 @@ -
- -
- +
Residential Use Type
{{ component.naruSubtype?.label }} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.ts index 974153c5fc..2160e20196 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.ts @@ -8,9 +8,4 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec }) export class NaruComponent { @Input() component!: DecisionComponentDto; - @Output() saveAlrArea = new EventEmitter(); - - onSaveAlrArea($event: string | null) { - this.saveAlrArea.emit($event); - } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.html index 999db02795..b689abbefd 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.html @@ -1,18 +1,16 @@ +
- -
-
-
Non-Farm Use Type
+
Non-Farm Use Type
{{ component.nfuType }} - +
-
Non-Farm Use Sub-Type
+
Non-Farm Use Sub-Type
{{ component.nfuSubType }} - +
-
Use End Date
+
Use End Date
{{ component.endDate | date }} - +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.ts index f864471d24..ac4b5cddfb 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.ts @@ -8,9 +8,4 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec }) export class NfupComponent { @Input() component!: DecisionComponentDto; - @Output() saveAlrArea = new EventEmitter(); - - onSaveAlrArea($event: string | null) { - this.saveAlrArea.emit($event); - } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html index 1aa8e4b62c..87b04a95bc 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.html @@ -1,10 +1,8 @@ -
- -
-
Use End Date
- {{ component.endDate | date }} - -
+ +
+
Use End Date
+ {{ component.endDate | date }} +
Type of soil approved to be removed
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.ts index 76569d4b9c..532eddb08e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.ts @@ -8,9 +8,4 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec }) export class PfrsComponent { @Input() component!: DecisionComponentDto; - @Output() saveAlrArea = new EventEmitter(); - - onSaveAlrArea($event: string | null) { - this.saveAlrArea.emit($event); - } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html index cfeb435400..f6960c3d2b 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html @@ -1,4 +1,4 @@ - +
Use End Date
{{ component.endDate | date }} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.ts index bc5fe00014..dcee185d01 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.ts @@ -8,9 +8,4 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec }) export class PofoComponent { @Input() component!: DecisionComponentDto; - @Output() saveAlrArea = new EventEmitter(); - - onSaveAlrArea($event: string | null) { - this.saveAlrArea.emit($event); - } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html index 1a282c6af9..52a9c85e0e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html @@ -1,4 +1,4 @@ - +
Use End Date
{{ component.endDate | date }} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.ts index b8532d53b7..f474f24ae0 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.ts @@ -8,9 +8,4 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec }) export class RosoComponent { @Input() component!: DecisionComponentDto; - @Output() saveAlrArea = new EventEmitter(); - - onSaveAlrArea($event: string | null) { - this.saveAlrArea.emit($event); - } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html index 2aa48cc956..525b2dba48 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.html @@ -1,10 +1,11 @@ - +
Total Number of Lots Approved
{{ component.lots?.length }}
+ Proposed Lot Areas
#
Type
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts index 82ddfa919e..3fb6ec788c 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts @@ -14,16 +14,11 @@ export class SubdComponent implements OnInit { constructor(private componentLotService: ApplicationDecisionComponentLotService) {} @Input() component!: DecisionComponentDto; - @Output() saveAlrArea = new EventEmitter(); ngOnInit(): void { this.component.lots = this.component.lots?.sort((a, b) => a.index - b.index) ?? undefined; } - onSaveAlrArea($event: string | null) { - this.saveAlrArea.emit($event); - } - async onSaveAlrParcelArea(lot: ProposedDecisionLotDto, alrArea: string | null) { if (lot.uuid) { lot.alrArea = alrArea ? parseFloat(alrArea) : null; diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.html index e9b2951536..a05daeca10 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.html @@ -1,4 +1,4 @@ - +
Expiry Date
{{ component.expiryDate | date }} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.ts index 9f518d3a8b..1fa3091fa7 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.ts @@ -8,9 +8,4 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec }) export class TurpComponent { @Input() component!: DecisionComponentDto; - @Output() saveAlrArea = new EventEmitter(); - - onSaveAlrArea($event: string | null) { - this.saveAlrArea.emit($event); - } } From f0283fd8cf679a93cd8c52662b9c17fccc65ddb7 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Tue, 22 Aug 2023 09:24:22 -0700 Subject: [PATCH 284/954] added legacy id to commissioner view (#902) --- alcs-frontend/src/app/services/commissioner/commissioner.dto.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/alcs-frontend/src/app/services/commissioner/commissioner.dto.ts b/alcs-frontend/src/app/services/commissioner/commissioner.dto.ts index ebacb86d20..dd94cc4646 100644 --- a/alcs-frontend/src/app/services/commissioner/commissioner.dto.ts +++ b/alcs-frontend/src/app/services/commissioner/commissioner.dto.ts @@ -12,4 +12,5 @@ export interface CommissionerApplicationDto { localGovernment: ApplicationLocalGovernmentDto; hasRecons: boolean; hasModifications: boolean; + legacyId?: string; } From 36af3951c66a742be6a74f7f1ac13e4ddbb28b21 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 22 Aug 2023 09:10:49 -0700 Subject: [PATCH 285/954] changing string variable formatting insert, not update --- .../submissions/app_submissions.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 02c9d7bf0f..37fdf32f1f 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -34,7 +34,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): print("- Total Application Submission data to insert: ", count_total) failed_inserts = 0 - successful_updates_count = 0 + successful_inserts_count = 0 last_application_id = 0 with open( @@ -53,17 +53,17 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): if not rows: break try: - applications_to_be_updated_count = len(rows) + applications_to_be_inserted_count = len(rows) - update_app_sub_records(conn, batch_size, cursor, rows) + insert_app_sub_records(conn, batch_size, cursor, rows) - successful_updates_count = ( - successful_updates_count + applications_to_be_updated_count + successful_inserts_count = ( + successful_inserts_count + applications_to_be_inserted_count ) last_application_id = dict(rows[-1])["alr_application_id"] print( - f"retrieved/updated items count: {applications_to_be_updated_count}; total successfully updated submissions so far {successful_updates_count}; last updated application_id: {last_application_id}" + f"retrieved/inserted items count: {applications_to_be_inserted_count}; total successfully inserted submissions so far {successful_inserts_count}; last inserted application_id: {last_application_id}" ) except Exception as e: # this is NOT going to be caused by actual data insert failure. This code is only executed when the code error appears or connection to DB is lost @@ -73,14 +73,14 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): print(str_err) print(trace_err) log_end(etl_name, str_err, trace_err) - failed_inserts = count_total - successful_updates_count + failed_inserts = count_total - successful_inserts_count last_application_id = last_application_id + 1 - print("Total amount of successful updates:", successful_updates_count) - print("Total failed updates:", failed_inserts) + print("Total amount of successful inserts:", successful_inserts_count) + print("Total failed inserts:", failed_inserts) log_end(etl_name) -def update_app_sub_records(conn, batch_size, cursor, rows): +def insert_app_sub_records(conn, batch_size, cursor, rows): """ Function to insert submission records in batches. @@ -88,7 +88,7 @@ def update_app_sub_records(conn, batch_size, cursor, rows): conn (obj): Connection to the database. batch_size (int): Number of rows to execute at one time. cursor (obj): Cursor object to execute queries. - rows (list): Rows of data to update in the database. + rows (list): Rows of data to insert in the database. Returns: None: Commits the changes to the database. From aa9d0a2e37e8fc6473c7924dd0802f2182a520de Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 22 Aug 2023 09:47:57 -0700 Subject: [PATCH 286/954] Re-organize and style Decisions V2 * Remove days to hide from public * Chair Review Required as Default --- .../application/application.component.ts | 2 +- .../pfrs/pfrs.component.spec.ts | 2 + .../pofo/pofo.component.spec.ts | 2 + .../roso/roso.component.spec.ts | 2 + .../subd/subd.component.scss | 8 + .../decision-documents.component.html | 4 - .../decision-documents.component.scss | 6 +- .../decision-component.component.html | 8 +- .../decision-component.component.ts | 5 + .../decision-components.component.html | 33 +- .../decision-components.component.scss | 8 + .../decision-components.component.ts | 4 +- .../decision-condition.component.html | 28 +- .../decision-condition.component.scss | 28 +- .../decision-condition.component.spec.ts | 7 + .../decision-condition.component.ts | 14 +- .../decision-conditions.component.html | 46 +- .../decision-conditions.component.scss | 7 +- .../decision-conditions.component.spec.ts | 11 + .../decision-conditions.component.ts | 6 +- .../decision-input-v2.component.html | 441 +++++++------- .../decision-input-v2.component.scss | 42 +- .../decision-input-v2.component.ts | 7 +- .../decision-v2/decision-v2.component.html | 544 +++++++++--------- .../decision-v2/decision-v2.component.scss | 249 ++++---- .../decision-v2/decision-v2.component.ts | 19 +- .../application/decision/decision.module.ts | 2 +- .../application-decision-v2.dto.ts | 2 - .../application-decision-v2.controller.ts | 4 - .../application-decision-v2.service.ts | 15 +- .../application-decision.dto.ts | 7 - .../application-decision.entity.ts | 9 - .../1692723526069-remove_days_hide.ts | 17 + 33 files changed, 804 insertions(+), 785 deletions(-) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692723526069-remove_days_hide.ts diff --git a/alcs-frontend/src/app/features/application/application.component.ts b/alcs-frontend/src/app/features/application/application.component.ts index 5073de3e65..d71435a940 100644 --- a/alcs-frontend/src/app/features/application/application.component.ts +++ b/alcs-frontend/src/app/features/application/application.component.ts @@ -134,7 +134,7 @@ export const appChildRoutes = [ }, { path: 'decision', - menuTitle: 'Decision', + menuTitle: 'Decisions', icon: 'gavel', module: DecisionModule, portalOnly: false, diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts index cfef8adbd3..5a691549ad 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts @@ -1,3 +1,4 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @@ -10,6 +11,7 @@ describe('PfrsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [PfrsComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(PfrsComponent); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts index d3b5977136..eeee8571e3 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts @@ -1,3 +1,4 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @@ -10,6 +11,7 @@ describe('PofoComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [PofoComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(PofoComponent); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.spec.ts index d70b4c57ef..cc4679d648 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.spec.ts @@ -1,3 +1,4 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @@ -10,6 +11,7 @@ describe('RosoComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [RosoComponent], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(RosoComponent); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.scss index e69de29bb2..1944097143 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.scss @@ -0,0 +1,8 @@ +.lot-table { + margin-top: 12px; + display: grid; + grid-template-columns: max-content max-content max-content 0.8fr; + grid-column-gap: 36px; + grid-row-gap: 12px; + align-items: center; +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html index 78377b19d1..cb3638941a 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html @@ -1,7 +1,3 @@ -
-
Decision Documents
-
Decision Documents *
-
+
+
{{ data.applicationDecisionComponentType?.label }}
+ +
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts index 177d0873cb..1bec2e48eb 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts @@ -27,6 +27,7 @@ export class DecisionComponentComponent implements OnInit { @Input() data!: DecisionComponentDto; @Input() codes!: DecisionCodesDto; @Output() dataChange = new EventEmitter(); + @Output() remove = new EventEmitter(); @ViewChild(SubdInputComponent) subdInputComponent?: SubdInputComponent; @@ -343,4 +344,8 @@ export class DecisionComponentComponent implements OnInit { expiryDate: this.expiryDate.value ? formatDateForApi(this.expiryDate.value) : null, }; } + + onRemove() { + this.remove.emit(); + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.html index b3a243ca19..e331336783 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.html @@ -1,31 +1,15 @@
-
-
Decision Components
-
- -
- -
- -
-
- -
+
+

Components

@@ -43,4 +27,13 @@
Decision Components
+ +
+ +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.scss index e69de29bb2..484d1ef16a 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.scss @@ -0,0 +1,8 @@ +@use '../../../../../../../styles/colors'; + +.component { + border-radius: 4px; + border: 1px solid colors.$grey; + padding: 16px; + margin-bottom: 48px; +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index 421541c4e6..f1d5b564a2 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -155,7 +155,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView this.patchInclExclFields(component); } - this.components.push(component); + this.components.unshift(component); break; case APPLICATION_DECISION_COMPONENT_TYPE.NFUP: case APPLICATION_DECISION_COMPONENT_TYPE.TURP: @@ -166,7 +166,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView case APPLICATION_DECISION_COMPONENT_TYPE.SUBD: case APPLICATION_DECISION_COMPONENT_TYPE.INCL: case APPLICATION_DECISION_COMPONENT_TYPE.EXCL: - this.components.push({ + this.components.unshift({ applicationDecisionComponentTypeCode: typeCode, applicationDecisionComponentType: this.decisionComponentTypes.find( (e) => e.code === typeCode && e.uiCode !== 'COPY' diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html index 9e13ef2f1c..ead69cd822 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html @@ -1,28 +1,20 @@ +
+
{{ data.type?.label }}
+ +
+
- - - + Component to Condition - + {{ component.label }} -
+
Approval Dependent* Yes @@ -30,7 +22,7 @@
- + Security Amount - + Administrative Fee Amount { fixture = TestBed.createComponent(DecisionConditionComponent); component = fixture.componentInstance; + component.data = { + type: { + code: '', + label: '', + description: '', + }, + }; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts index fc566f39e6..5b7a0808d3 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts @@ -1,6 +1,5 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { ApplicationDecisionConditionTypeDto } from '../../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { parseStringToBoolean } from '../../../../../../../shared/utils/boolean-helper'; import { SelectableComponent, TempApplicationDecisionConditionDto } from '../decision-conditions.component'; @@ -12,11 +11,10 @@ import { SelectableComponent, TempApplicationDecisionConditionDto } from '../dec export class DecisionConditionComponent implements OnInit, OnChanges { @Input() data!: TempApplicationDecisionConditionDto; @Output() dataChange = new EventEmitter(); + @Output() remove = new EventEmitter(); - @Input() codes!: ApplicationDecisionConditionTypeDto[]; @Input() selectableComponents: SelectableComponent[] = []; - type = new FormControl(null, [Validators.required]); componentsToCondition = new FormControl(null, [Validators.required]); approvalDependant = new FormControl(null, [Validators.required]); @@ -25,7 +23,6 @@ export class DecisionConditionComponent implements OnInit, OnChanges { description = new FormControl(null, [Validators.required]); form = new FormGroup({ - type: this.type, approvalDependant: this.approvalDependant, securityAmount: this.securityAmount, administrativeFee: this.administrativeFee, @@ -51,7 +48,6 @@ export class DecisionConditionComponent implements OnInit, OnChanges { this.componentsToCondition.setValue(selectedOptions.map((e) => e.tempId) ?? null); this.form.patchValue({ - type: this.data.type?.code ?? null, approvalDependant, securityAmount: this.data.securityAmount?.toString() ?? null, administrativeFee: this.data.administrativeFee?.toString() ?? null, @@ -60,8 +56,6 @@ export class DecisionConditionComponent implements OnInit, OnChanges { } this.form.valueChanges.subscribe((changes) => { - const matchingType = this.codes.find((code) => code.code === this.type.value); - const selectedOptions = this.selectableComponents .filter((component) => this.componentsToCondition.value?.includes(component.tempId)) .map((e) => ({ @@ -71,9 +65,9 @@ export class DecisionConditionComponent implements OnInit, OnChanges { })); this.dataChange.emit({ + type: this.data.type, tempUuid: this.data.tempUuid, uuid: this.data.uuid, - type: matchingType ?? null, approvalDependant: parseStringToBoolean(this.approvalDependant.value), securityAmount: this.securityAmount.value !== null ? parseFloat(this.securityAmount.value) : undefined, administrativeFee: this.administrativeFee.value !== null ? parseFloat(this.administrativeFee.value) : undefined, @@ -97,4 +91,8 @@ export class DecisionConditionComponent implements OnInit, OnChanges { } } } + + onRemove() { + this.remove.emit(); + } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html index 34dd8b0936..029df03dfa 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html @@ -1,26 +1,38 @@
-
Decision Conditions
+
+

Conditions

+
+ + +
+
+ + + + +
- -
-
- -
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss index e2dfd40275..a55dff748c 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss @@ -1,3 +1,5 @@ +@use '../../../../../../../styles/colors'; + section { width: 100%; margin: 24px 0 56px; @@ -8,7 +10,10 @@ section { } .condition { - margin: 48px 0; + border-radius: 4px; + border: 1px solid colors.$grey; + padding: 16px; + margin-bottom: 48px; &:first-of-type { margin-top: 12px; diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts index 7d5a8dabd0..f6e3fcb0a3 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts @@ -1,5 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatMenuModule } from '@angular/material/menu'; import { createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; import { @@ -21,6 +22,7 @@ describe('DecisionConditionComponent', () => { mockDecisionService.$decisions = new BehaviorSubject([]); await TestBed.configureTestingModule({ + imports: [MatMenuModule], providers: [ { provide: ApplicationDecisionV2Service, @@ -33,6 +35,15 @@ describe('DecisionConditionComponent', () => { fixture = TestBed.createComponent(DecisionConditionsComponent); component = fixture.componentInstance; + component.codes = { + ceoCriterion: [], + decisionComponentTypes: [], + decisionMakers: [], + linkedResolutionOutcomeTypes: [], + naruSubtypes: [], + outcomes: [], + decisionConditionTypes: [], + }; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 631a758310..66deb97f9a 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -86,8 +86,10 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy }); } - onAddNewCondition() { - this.mappedConditions.push({ + onAddNewCondition(typeCode: string) { + const matchingType = this.codes.decisionConditionTypes.find((code) => code.code === typeCode); + this.mappedConditions.unshift({ + type: matchingType, tempUuid: (Math.random() * 10000).toFixed(0), }); this.conditionsChange.emit({ diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html index d500be1a92..972f1f2ee3 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html @@ -1,252 +1,246 @@ -
-
-

Decisions > Edit Decision Draft

-
+
+

Decision #{{ index }} Draft

+
- -
-
- -
+

Resolution

+ +
+
+ +
-
-
+
+
+ + +
+
+
+ Res #{{ resolutionNumberControl.getRawValue() }} / {{ resolutionYearControl.getRawValue() }} - -
-
-
- Res #{{ resolutionNumberControl.getRawValue() }} / {{ resolutionYearControl.getRawValue() }} - -
+
- - Decision Date - - - - - - - - - + + Decision Date + + + + - + + - - - {{ item.number }} - {{ item.label }} - - - {{ item.number }} - {{ item.label }} - - - - Criterion 8 Modification - - Time Extension - Other - - + - + -
-
- Subject to Conditions* - - Yes - No - -
-
+ + + {{ item.number }} - {{ item.label }} + + + {{ item.number }} - {{ item.label }} + + + + Criterion 8 Modification + + Time Extension + Other + + -
- - Decision Description - - -
+ +
- Stats required* + Subject to Conditions* Yes No
+
- - Days to Hide from Public - +
+ + Decision Description + +
- - - Rescinded Date - - - - - +
+ Stats required* + + Yes + No +
- - Rescinded Comment - + + Rescinded Date + + + +
-
- -
+ + + Rescinded Comment + + + - - +
+

Documents

+ +
-
- -
+ + -
-

Audit and Chair Review Info

-
+
+ +
+
+

Audit and Chair Review

Audit Date @@ -297,16 +291,19 @@

Audit and Chair Review Info

+
+ -
- - -
- - -
-
- +
+
+ + +
+
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss index 2b74458b55..c751cae2c7 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss @@ -1,5 +1,25 @@ @use '../../../../../../styles/colors.scss'; +.bottom-scroller { + margin-top: 36px; + position: absolute; + bottom: 0; + left: 212px; + right: 0; + background-color: #fff; + padding: 16px; + z-index: 2; + border-top: 1px solid colors.$grey-light; + + button:not(:last-child) { + margin-right: 16px !important; + } +} + +form { + padding-bottom: 120px; +} + :host::ng-deep { .grid, .grid-2 { @@ -13,6 +33,11 @@ } } + h3 { + margin-top: 36px !important; + margin-bottom: 12px !important; + } + .resolution-number-wrapper { display: grid; grid-template-columns: 1fr 16px 0.7fr; @@ -88,14 +113,6 @@ .container-wide { max-width: 700px; } - .btn-row { - justify-content: space-between; - } - - .flex-right { - display: flex; - justify-content: right; - } .resolution-number-wrapper { display: flex; @@ -127,15 +144,6 @@ height: 100px !important; } - .subheading1 { - font-family: 'BCSans', 'Noto Sans', Verdana, Arial, sans-serif !important; - font-size: 19px !important; - font-weight: 400 !important; - line-height: 24px; - color: #565656 !important; - margin: 24px 0 !important; - } - .error-field-outlined.ng-invalid { border-color: colors.$error-color !important; diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index 15eaaee3f1..8cc68224b8 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -58,6 +58,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { showConditions = false; conditionsValid = true; componentsValid = true; + index = 1; fileNumber: string = ''; uuid: string = ''; @@ -98,7 +99,6 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { isSubjectToConditions: new FormControl(undefined, [Validators.required]), decisionDescription: new FormControl(undefined, [Validators.required]), isStatsRequired: new FormControl(undefined, [Validators.required]), - daysHideFromPublic: new FormControl('2', [Validators.required]), rescindedDate: new FormControl(null), rescindedComment: new FormControl(null), }); @@ -128,6 +128,8 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { private extractAndPopulateQueryParams() { const fileNumber = this.route.parent?.parent?.snapshot.paramMap.get('fileNumber'); const uuid = this.route.snapshot.paramMap.get('uuid'); + const index = this.route.snapshot.paramMap.get('index'); + this.index = index ? parseInt(index) : 1; if (uuid) { this.uuid = uuid; @@ -283,7 +285,6 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { isSubjectToConditions: parseBooleanToString(existingDecision.isSubjectToConditions), decisionDescription: existingDecision.decisionDescription, isStatsRequired: parseBooleanToString(existingDecision.isStatsRequired), - daysHideFromPublic: existingDecision.daysHideFromPublic?.toString() ?? '2', rescindedDate: existingDecision.rescindedDate ? new Date(existingDecision.rescindedDate) : undefined, rescindedComment: existingDecision.rescindedComment, linkedResolutionOutcome: existingDecision.linkedResolutionOutcome?.code, @@ -417,7 +418,6 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { isSubjectToConditions, decisionDescription, isStatsRequired, - daysHideFromPublic, rescindedDate, rescindedComment, linkedResolutionOutcome, @@ -445,7 +445,6 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { isSubjectToConditions: parseStringToBoolean(isSubjectToConditions), decisionDescription: decisionDescription, isStatsRequired: parseStringToBoolean(isStatsRequired), - daysHideFromPublic: daysHideFromPublic ? parseInt(daysHideFromPublic) : 2, rescindedDate: rescindedDate ? formatDateForApi(rescindedDate) : rescindedDate, rescindedComment: rescindedComment, decisionComponents: this.components, diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html index f660e2b9f5..841e2465ae 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.html @@ -1,135 +1,129 @@ - - - -

Decision

-
- -
-
- - -
-
-
-
- +
+

Decisions

+
+ +
+
+
+
No Decisions
+
+
+
+ +
+
+
+
+ Modified By:  + {{ decision.modifiedByResolutions?.join(', ') }} + N/A +
+
+ Reconsidered By:  + {{ decision.reconsideredByResolutions?.join(', ') }} + N/A
-
-
No Decisions
-
-
-
- + +
+
+
+

Decision #{{ decision.index }}

+
+ + calendar_month + {{ application.activeDays }} + + + pause + {{ application.pausedDays }} + +
+ + + + + + + +
Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }}
+ + + + + + +
+ + + + +
+ +
-
-
+ +

Resolution

+
+
- Modified By:  - {{ decision.modifiedByResolutions?.join(', ') }} - N/A +
Decision Date
+ {{ decision.date | momentFormat }} +
- Reconsidered By:  - {{ decision.reconsideredByResolutions?.join(', ') }} - N/A +
Decision Maker
+ {{ decision.decisionMaker?.label }} +
-
-
-
-
- Decision #{{ decision.index }} -
- - calendar_month - {{ application.activeDays }} - - - pause - {{ application.pausedDays }} - -
- - - - - - - - Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }} - - - - - - -
- - - - -
- -
+
+
Decision Outcome
+ {{ decision.outcome.label }} {{ decision.isSubjectToConditions ? '- Subject to Conditions' : '' }}
-
-
-
Decision Date
- {{ decision.date | momentFormat }} - -
-
-
Decision Maker
- {{ decision.decisionMaker?.label }} - -
- -
-
Decision Outcome
- {{ decision.outcome.label }} {{ decision.isSubjectToConditions ? '- Subject to Conditions' : '' }} -
- -
-
CEO Criterion
- {{ decision.ceoCriterion?.label }} - - - Time extension - , - Other -
+
+
CEO Criterion
+ {{ decision.ceoCriterion?.label }} + - + Time extension + , + Other +
-
-
- Modification Outcome - Reconsideration Outcome -
- {{ decision.linkedResolutionOutcome?.label }} - - {{ decision.modifies?.linkedResolutions?.join(', ') }} - {{ decision.reconsiders?.linkedResolutions?.join(', ') }} +
+
+ Modification Outcome + Reconsideration Outcome
+ {{ decision.linkedResolutionOutcome?.label }} + + {{ decision.modifies?.linkedResolutions?.join(', ') }} + {{ decision.reconsiders?.linkedResolutions?.join(', ') }}
-
+
Decision Description
{{ decision.decisionDescription }} Decision >
-
-
-
Days To Hide From Public
- {{ decision.daysHideFromPublic }} -
-
-
Stats Required
- -
+
+
Stats Required
+
-
+
Rescinded Date
{{ decision.rescindedDate | momentFormat }}
-
+ +
Rescinded Comment
{{ decision.rescindedComment }}
+
+
-
- -
+

Documents

+
+ +
- -
-
-
Decision Components and Conditions
-
- -
-
- {{ component.applicationDecisionComponentType?.label }} Component -
-
- + +

Components

+
+ + + +
{{ component.applicationDecisionComponentType?.label }}
+
+ + + > + - + + > - + + > - + + > - + + > - + + > - + + > - + + > -
-
-
Agricultural Capability
- {{ component.agCap }} - -
-
-
Agricultural Capability Source
- {{ component.agCapSource }} - -
+
+
+
Agricultural Capability
+ {{ component.agCap }} + +
+
+
Agricultural Capability Source
+ {{ component.agCapSource }} + +
-
-
Agricultural Capability Mapsheet Reference
- {{ component.agCapMap }} - -
-
-
Agricultural Capability Consultant
- {{ component.agCapConsultant }} - -
+
+
Agricultural Capability Mapsheet Reference
+ {{ component.agCapMap }} + +
+
+
Agricultural Capability Consultant
+ {{ component.agCapConsultant }} +
-
+
+
+
+
-
-

Conditions

- + +

Conditions

+
+ +
+
-
-
Audit and Chair Review Info
-
-
-
Audit Date
- {{ decision.auditDate | momentFormat }} - - - -
-
-
Chair Review Date
- - - - {{ decision.chairReviewDate | momentFormat }} -
-
-
Chair Review Outcome
- {{ decision.chairReviewOutcome.label }} - -
-
+

Audit and Chair Review

+
+
+
+
Audit Date
+ {{ decision.auditDate | momentFormat }} + + + +
+
+
Chair Review
+ {{ decision.chairReviewRequired ? 'Required' : 'Not Needed' }} +
+
+
Chair Review Date
+ + + + {{ decision.chairReviewDate | momentFormat }} +
+
+
Chair Review Outcome
+ {{ decision.chairReviewOutcome.label }} +
- - - -
+
+ +
+
-
-
- +
+
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss index 09b54fbebb..ed21e4ab3a 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.scss @@ -1,170 +1,146 @@ @use '../../../../../styles/colors'; -:host ::ng-deep { - section { - margin-bottom: 64px; - } +section { + margin-bottom: 64px; +} - .decision-container { - position: relative; - } +h4 { + margin-bottom: 12px !important; +} - .decision { - margin: 24px 0; - box-shadow: 0 2px 8px 1px rgba(0, 0, 0, 0.25); - } +hr { + margin: 36px 0; + stroke: colors.$grey; +} - .loading-overlay { - position: absolute; - z-index: 2; - background-color: colors.$grey; - opacity: 0.4; - width: 100%; - height: 100%; +.decision-container { + position: relative; +} + +.decision { + margin: 24px 0; + box-shadow: 0 2px 8px 1px rgba(0, 0, 0, 0.25); +} + +.decision.draft { + border: 2px solid colors.$secondary-color-light; +} + +.decision-section { + background: colors.$grey-light; + padding: 18px; +} + +.decision-section-no-title { + background: colors.$grey-light; + padding: 1px 18px; +} + +.header { + display: flex; + justify-content: space-between; + margin-bottom: 36px; + + .title { display: flex; align-items: center; - justify-content: center; + justify-content: space-between; + gap: 28px; + + .days { + display: inline-block; + margin-right: 6px; + margin-top: 4px; + + .mat-icon { + font-size: 19px !important; + line-height: 21px !important; + width: 19px; + vertical-align: middle; + } + } } +} - .post-decisions { - padding: 6px 32px; - background-color: colors.$grey-light; - grid-template-columns: 50% 50%; - display: grid; - min-height: 36px; - text-transform: uppercase; - border-radius: 4px 4px 0 0; - } +.loading-overlay { + position: absolute; + z-index: 2; + background-color: colors.$grey; + opacity: 0.4; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} - .decision-menu { - position: absolute; - top: 0; - right: 0; - height: 36px; - background: colors.$accent-color; - box-shadow: -1px 1px 4px rgba(0, 0, 0, 0.25); - border-radius: 0 4px 0 10px; - } +.post-decisions { + padding: 6px 32px; + background-color: colors.$grey-light; + grid-template-columns: 50% 50%; + display: grid; + min-height: 36px; + text-transform: uppercase; + border-radius: 4px 4px 0 0; +} - .decision-padding { - padding: 18px 32px 32px 32px; - } +.decision-menu { + position: absolute; + top: 0; + right: 0; + height: 36px; + background: colors.$accent-color; + box-shadow: -1px 1px 4px rgba(0, 0, 0, 0.25); + border-radius: 0 4px 0 10px; +} - .grid-2 { - display: grid; - grid-template-columns: 50% 50%; - grid-row-gap: 24px; - } +.decision-padding { + padding: 18px 32px; +} - .grid-3 { - display: grid; - grid-template-columns: 33% 33% 33%; - grid-row-gap: 24px; +.no-decisions { + margin-top: 16px; + display: flex; + align-items: center; + justify-content: center; + background-color: colors.$grey-light; + height: 72px; +} - .grid-2 { - margin-top: unset; - } - } +.conditions-link-icon { + position: absolute; +} - .grid-uneven { +:host ::ng-deep { + .grid-2 { + margin-top: 18px; + margin-bottom: 18px; display: grid; - grid-template-columns: 66% 33%; - grid-row-gap: 24px; + grid-template-columns: 50% 50%; + grid-row-gap: 18px; - .grid-2 { - margin-top: unset; + .full-width { + grid-column: 1/3; } } - .decision-content, - .grid-2 { - margin-top: 16px; - margin-bottom: 16px; - font-size: 16px; - } - - .components-wrapper, - .conditions-wrapper { - margin-bottom: 36px; + .mat-mdc-table { + background: colors.$grey-light; } .subheading2 { margin-bottom: 6px !important; } - .header { - display: flex; - justify-content: space-between; - - .title { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - - .days { - display: inline-block; - margin-right: 6px; - - .mat-icon { - font-size: 19px !important; - line-height: 21px !important; - width: 19px; - vertical-align: middle; - } - } - } - } - .row { margin: 16px 0; } - .conditions-link-icon { - position: absolute; - } - - .no-decisions { - margin-top: 16px; - display: flex; - align-items: center; - justify-content: center; - background-color: colors.$grey-light; - height: 72px; - } - - .decision-documents .subheading1 { - font-family: 'BCSans', 'Noto Sans', Verdana, Arial, sans-serif !important; - font-size: 19px !important; - font-weight: 400 !important; - margin: 0 !important; - line-height: 24px; - color: #565656 !important; - } - - .subheading1 { - color: colors.$grey-dark !important; - } - .application-pill-wrapper, .application-pill { margin-right: 0 !important; } - .decision-tabs { - .mdc-tab__text-label { - color: colors.$black !important; - } - - .mdc-tab { - margin-left: 8px; - } - } - - .full-width { - grid-column: 1/4; - } - table { border-spacing: 12px; margin-left: -12px; @@ -177,13 +153,4 @@ .pre-wrapped-text { white-space: pre-wrap; } - - .lot-table { - margin-top: 12px; - display: grid; - grid-template-columns: max-content max-content max-content 0.8fr; - grid-column-gap: 36px; - grid-row-gap: 12px; - align-items: center; - } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts index 3208b8c6ad..32a3904d71 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-v2.component.ts @@ -115,7 +115,7 @@ export class DecisionV2Component implements OnInit, OnDestroy { async onCreate() { const newDecision = await this.decisionService.create({ resolutionYear: new Date().getFullYear(), - chairReviewRequired: false, + chairReviewRequired: true, isDraft: true, date: Date.now(), applicationFileNumber: this.fileNumber, @@ -123,14 +123,23 @@ export class DecisionV2Component implements OnInit, OnDestroy { reconsidersUuid: null, }); - await this.router.navigate([`/application/${this.fileNumber}/decision/draft/${newDecision.uuid}/edit`]); + const index = this.decisions.length; + await this.router.navigate([ + `/application/${this.fileNumber}/decision/draft/${newDecision.uuid}/edit/${index + 1}`, + ]); } - async onEdit(decision: ApplicationDecisionWithLinkedResolutionDto) { - await this.router.navigate([`/application/${this.fileNumber}/decision/draft/${decision.uuid}/edit`]); + async onEdit(selectedDecision: ApplicationDecisionWithLinkedResolutionDto) { + const position = this.decisions.findIndex((decision) => decision.uuid === selectedDecision.uuid); + const index = this.decisions.length - position; + await this.router.navigate([ + `/application/${this.fileNumber}/decision/draft/${selectedDecision.uuid}/edit/${index}`, + ]); } async onRevertToDraft(uuid: string) { + const position = this.decisions.findIndex((decision) => decision.uuid === uuid); + const index = this.decisions.length - position; this.dialog .open(RevertToDraftDialogComponent, { data: { fileNumber: this.fileNumber }, @@ -143,7 +152,7 @@ export class DecisionV2Component implements OnInit, OnDestroy { }); await this.applicationDetailService.loadApplication(this.fileNumber); - await this.router.navigate([`/application/${this.fileNumber}/decision/draft/${uuid}/edit`]); + await this.router.navigate([`/application/${this.fileNumber}/decision/draft/${uuid}/edit/${index}`]); } }); } diff --git a/alcs-frontend/src/app/features/application/decision/decision.module.ts b/alcs-frontend/src/app/features/application/decision/decision.module.ts index 5e656899df..b5fd0f9ddc 100644 --- a/alcs-frontend/src/app/features/application/decision/decision.module.ts +++ b/alcs-frontend/src/app/features/application/decision/decision.module.ts @@ -57,7 +57,7 @@ export const decisionChildRoutes = [ portalOnly: false, }, { - path: 'draft/:uuid/edit', + path: 'draft/:uuid/edit/:index', menuTitle: 'Decision', component: DecisionInputV2Component, portalOnly: false, diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts index 758198262f..1a1be3a708 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts @@ -24,7 +24,6 @@ export interface UpdateApplicationDecisionDto { isSubjectToConditions?: boolean | null; decisionDescription?: string | null; isStatsRequired?: boolean | null; - daysHideFromPublic?: number | null; rescindedDate?: number | null; rescindedComment?: string | null; conditions?: UpdateApplicationDecisionConditionDto[]; @@ -67,7 +66,6 @@ export interface ApplicationDecisionDto { isSubjectToConditions?: boolean | null; decisionDescription?: string | null; isStatsRequired?: boolean | null; - daysHideFromPublic?: number | null; rescindedDate?: number | null; rescindedComment?: string | null; modifies?: LinkedResolutionDto; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts index 633a1b92c8..db5695690c 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts @@ -297,10 +297,6 @@ export class ApplicationDecisionV2Controller { const date = decision.date ? new Date(decision.date) : new Date(); - if (decision.daysHideFromPublic) { - date.setDate(date.getDate() + decision.daysHideFromPublic); - } - const options: Intl.DateTimeFormatOptions = { weekday: 'long', year: 'numeric', diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts index ad461c9d46..d555f09e30 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts @@ -6,8 +6,6 @@ import { ServiceNotFoundException, ServiceValidationException, } from '../../../../../../../libs/common/src/exceptions/base.exception'; -import { ApplicationSubmissionStatusService } from '../../../application/application-submission-status/application-submission-status.service'; -import { SUBMISSION_STATUS } from '../../../application/application-submission-status/submission-status.dto'; import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, @@ -16,6 +14,8 @@ import { DocumentService } from '../../../../document/document.service'; import { NaruSubtype } from '../../../../portal/application-submission/naru-subtype/naru-subtype.entity'; import { User } from '../../../../user/user.entity'; import { formatIncomingDate } from '../../../../utils/incoming-date.formatter'; +import { ApplicationSubmissionStatusService } from '../../../application/application-submission-status/application-submission-status.service'; +import { SUBMISSION_STATUS } from '../../../application/application-submission-status/submission-status.dto'; import { Application } from '../../../application/application.entity'; import { ApplicationService } from '../../../application/application.service'; import { ApplicationCeoCriterionCode } from '../../application-ceo-criterion/application-ceo-criterion.entity'; @@ -277,7 +277,6 @@ export class ApplicationDecisionV2Service { existingDecision.isSubjectToConditions = updateDto.isSubjectToConditions; existingDecision.decisionDescription = updateDto.decisionDescription; existingDecision.isStatsRequired = updateDto.isStatsRequired; - existingDecision.daysHideFromPublic = updateDto.daysHideFromPublic; existingDecision.isDraft = updateDto.isDraft; existingDecision.rescindedDate = formatIncomingDate( updateDto.rescindedDate, @@ -380,7 +379,6 @@ export class ApplicationDecisionV2Service { isSubjectToConditions: createDto.isSubjectToConditions, decisionDescription: createDto.decisionDescription, isStatsRequired: createDto.isStatsRequired, - daysHideFromPublic: createDto.daysHideFromPublic, rescindedDate: createDto.rescindedDate ? new Date(createDto.rescindedDate) : null, @@ -713,17 +711,10 @@ export class ApplicationDecisionV2Service { decisionDate: Date | null, applicationDecision: ApplicationDecision, ) { - const statusDate = decisionDate; - if (applicationDecision.daysHideFromPublic) { - statusDate?.setDate( - statusDate.getDate() + applicationDecision.daysHideFromPublic, - ); - } - await this.applicationSubmissionStatusService.setStatusDateByFileNumber( applicationDecision.application.fileNumber, SUBMISSION_STATUS.ALC_DECISION, - statusDate, + decisionDate, ); } diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts index c0e8132722..c4d50ee910 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision.dto.ts @@ -92,10 +92,6 @@ export class UpdateApplicationDecisionDto { @IsOptional() isStatsRequired?: boolean | null; - @IsNumber() - @IsOptional() - daysHideFromPublic?: number | null; - @IsNumber() @IsOptional() rescindedDate?: number | null; @@ -212,9 +208,6 @@ export class ApplicationDecisionDto { @AutoMap(() => Boolean) isStatsRequired?: boolean | null; - @AutoMap(() => Number) - daysHideFromPublic?: number | null; - @AutoMap(() => Number) rescindedDate?: number | null; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts index 8f26dd8e42..11f3051d0b 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts @@ -133,15 +133,6 @@ export class ApplicationDecision extends Base { }) isStatsRequired?: boolean | null; - @AutoMap(() => Number) - @Column({ - comment: - 'Indicates how long the decision should stay hidden from public in days from decision date', - nullable: true, - type: 'integer', - }) - daysHideFromPublic?: number | null; - @AutoMap(() => Date) @Column({ type: 'timestamptz', diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692723526069-remove_days_hide.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692723526069-remove_days_hide.ts new file mode 100644 index 0000000000..5c040bc22a --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692723526069-remove_days_hide.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class removeDaysHide1692723526069 implements MigrationInterface { + name = 'removeDaysHide1692723526069'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision" DROP COLUMN "days_hide_from_public"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_decision" ADD "days_hide_from_public" integer`, + ); + } +} From 7ad7b74e19d444505bf792f44582ad138bcd280c Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 22 Aug 2023 10:42:48 -0700 Subject: [PATCH 287/954] Add confirmation dialogs, fix error state * Error state had no border so was not showing red * Add confirmation dialogs for removing conditions / components. --- .../decision-components.component.spec.ts | 5 + .../decision-components.component.ts | 120 ++++++++++-------- .../decision-conditions.component.spec.ts | 5 + .../decision-conditions.component.ts | 18 ++- .../decision-input-v2.component.scss | 4 +- 5 files changed, 92 insertions(+), 60 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.spec.ts index 7e2540c161..1492dfe5c5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.spec.ts @@ -9,6 +9,7 @@ import { ApplicationDto } from '../../../../../../services/application/applicati import { ApplicationDecisionDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { ToastService } from '../../../../../../services/toast/toast.service'; +import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionComponentsComponent } from './decision-components.component'; @@ -50,6 +51,10 @@ describe('DecisionComponentsComponent', () => { provide: ApplicationSubmissionService, useValue: mockApplicationSubmissionService, }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index f1d5b564a2..3cc2e2e646 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -20,6 +20,7 @@ import { DecisionComponentTypeDto, } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ToastService } from '../../../../../../services/toast/toast.service'; +import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionComponentComponent } from './decision-component/decision-component.component'; export type DecisionComponentTypeMenuItem = DecisionComponentTypeDto & { isDisabled: boolean; uiCode: string }; @@ -49,7 +50,8 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView constructor( private toastService: ToastService, private applicationDetailService: ApplicationDetailService, - private submissionService: ApplicationSubmissionService + private submissionService: ApplicationSubmissionService, + private confirmationDialogService: ConfirmationDialogService ) {} ngOnInit(): void { @@ -79,30 +81,6 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView this.$destroy.complete(); } - private async prepareDecisionComponentTypes(codes: DecisionCodesDto) { - const decisionComponentTypes: DecisionComponentTypeMenuItem[] = codes.decisionComponentTypes.map((e) => ({ - ...e, - isDisabled: false, - uiCode: e.code, - })); - - const mappedProposalType = decisionComponentTypes.find((e) => e.code === this.application.type.code); - - if (mappedProposalType) { - const proposalDecisionType: DecisionComponentTypeMenuItem = { - isDisabled: false, - uiCode: 'COPY', - code: mappedProposalType!.code, - label: `Copy Proposal - ${mappedProposalType?.label}`, - description: mappedProposalType?.description ?? '', - }; - decisionComponentTypes.unshift(proposalDecisionType); - } - this.decisionComponentTypes = decisionComponentTypes; - - this.updateComponentsMenuItems(); - } - onAddNewComponent(uiCode: string, typeCode: string) { switch (uiCode) { case 'COPY': @@ -180,6 +158,68 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView this.updateComponentsMenuItems(); } + onRemove(index: number) { + this.confirmationDialogService + .openDialog({ + body: 'Are you sure you want to remove this component?', + }) + .subscribe((didConfirm) => { + if (didConfirm) { + this.components.splice(index, 1); + this.updateComponentsMenuItems(); + } + }); + } + + trackByFn(index: any, item: DecisionComponentDto) { + return item.applicationDecisionComponentTypeCode; + } + + onChange() { + const isValid = + this.components.length > 0 && (!this.childComponents || this.childComponents?.length < 1) + ? false + : this.childComponents.reduce((isValid, component) => isValid && component.form.valid, true); + + this.componentsChange.emit({ + components: this.components, + isValid, + }); + } + + onValidate() { + this.childComponents.forEach((component) => { + component.form.markAllAsTouched(); + if ('markTouched' in component) { + component.markTouched(); + } + }); + } + + private async prepareDecisionComponentTypes(codes: DecisionCodesDto) { + const decisionComponentTypes: DecisionComponentTypeMenuItem[] = codes.decisionComponentTypes.map((e) => ({ + ...e, + isDisabled: false, + uiCode: e.code, + })); + + const mappedProposalType = decisionComponentTypes.find((e) => e.code === this.application.type.code); + + if (mappedProposalType) { + const proposalDecisionType: DecisionComponentTypeMenuItem = { + isDisabled: false, + uiCode: 'COPY', + code: mappedProposalType!.code, + label: `Copy Proposal - ${mappedProposalType?.label}`, + description: mappedProposalType?.description ?? '', + }; + decisionComponentTypes.unshift(proposalDecisionType); + } + this.decisionComponentTypes = decisionComponentTypes; + + this.updateComponentsMenuItems(); + } + private patchNfuFields(component: DecisionComponentDto) { component.nfuType = this.application.nfuUseType; component.nfuSubType = this.application.nfuUseSubType; @@ -231,34 +271,4 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView isDisabled: this.components.some((c) => c.applicationDecisionComponentTypeCode === e.code), })); } - - onRemove(index: number) { - this.components.splice(index, 1); - this.updateComponentsMenuItems(); - } - - trackByFn(index: any, item: DecisionComponentDto) { - return item.applicationDecisionComponentTypeCode; - } - - onChange() { - const isValid = - this.components.length > 0 && (!this.childComponents || this.childComponents?.length < 1) - ? false - : this.childComponents.reduce((isValid, component) => isValid && component.form.valid, true); - - this.componentsChange.emit({ - components: this.components, - isValid, - }); - } - - onValidate() { - this.childComponents.forEach((component) => { - component.form.markAllAsTouched(); - if ('markTouched' in component) { - component.markTouched(); - } - }); - } } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts index f6e3fcb0a3..81eb022bc1 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts @@ -8,6 +8,7 @@ import { ApplicationDecisionWithLinkedResolutionDto, } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; +import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionConditionsComponent } from './decision-conditions.component'; @@ -28,6 +29,10 @@ describe('DecisionConditionComponent', () => { provide: ApplicationDecisionV2Service, useValue: mockDecisionService, }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, ], declarations: [DecisionConditionsComponent], schemas: [NO_ERRORS_SCHEMA], diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index 66deb97f9a..bf61badbd1 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -18,6 +18,7 @@ import { UpdateApplicationDecisionConditionDto, } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; +import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionConditionComponent } from './decision-condition/decision-condition.component'; export type TempApplicationDecisionConditionDto = UpdateApplicationDecisionConditionDto & { tempUuid?: string }; @@ -46,7 +47,10 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy mappedConditions: TempApplicationDecisionConditionDto[] = []; decision: ApplicationDecisionDto | undefined; - constructor(private decisionService: ApplicationDecisionV2Service) {} + constructor( + private decisionService: ApplicationDecisionV2Service, + private confirmationDialogService: ConfirmationDialogService + ) {} ngOnInit(): void { this.decisionService.$decision @@ -106,8 +110,16 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy } onRemoveCondition(index: number) { - this.mappedConditions.splice(index, 1); - this.onChanges(); + this.confirmationDialogService + .openDialog({ + body: 'Are you sure you want to remove this condition?', + }) + .subscribe((didConfirm) => { + if (didConfirm) { + this.mappedConditions.splice(index, 1); + this.onChanges(); + } + }); } ngOnChanges(changes: SimpleChanges): void { diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss index c751cae2c7..811eda701f 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss @@ -145,7 +145,7 @@ form { } .error-field-outlined.ng-invalid { - border-color: colors.$error-color !important; + border: 1px solid colors.$error-color !important; .mat-button-toggle { border-color: colors.$error-color; @@ -154,7 +154,7 @@ form { &.upload-button { border: 2px solid colors.$error-color; - margin-bottom: 0px !important; + margin-bottom: 0 !important; } } From 8c95d417c06c13a6647a141bb51c11157dfac43a Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 22 Aug 2023 10:57:58 -0700 Subject: [PATCH 288/954] cleaned up based on MR feedback --- .../submissions/app_submissions.py | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 37fdf32f1f..eb32f17f57 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -35,17 +35,17 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): failed_inserts = 0 successful_inserts_count = 0 - last_application_id = 0 + last_submission_id = 0 with open( "submissions/sql/app_submission.sql", "r", encoding="utf-8", ) as sql_file: - application_sql = sql_file.read() + submission_sql = sql_file.read() while True: cursor.execute( - f"{application_sql} WHERE acg.alr_application_id > {last_application_id} ORDER BY acg.alr_application_id;" + f"{submission_sql} WHERE acg.alr_application_id > {last_submission_id} ORDER BY acg.alr_application_id;" ) rows = cursor.fetchmany(batch_size) @@ -53,20 +53,19 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): if not rows: break try: - applications_to_be_inserted_count = len(rows) + submissions_to_be_inserted_count = len(rows) insert_app_sub_records(conn, batch_size, cursor, rows) successful_inserts_count = ( - successful_inserts_count + applications_to_be_inserted_count + successful_inserts_count + submissions_to_be_inserted_count ) - last_application_id = dict(rows[-1])["alr_application_id"] + last_submission_id = dict(rows[-1])["alr_application_id"] print( - f"retrieved/inserted items count: {applications_to_be_inserted_count}; total successfully inserted submissions so far {successful_inserts_count}; last inserted application_id: {last_application_id}" + f"retrieved/inserted items count: {submissions_to_be_inserted_count}; total successfully inserted submissions so far {successful_inserts_count}; last inserted application_id: {last_submission_id}" ) except Exception as e: - # this is NOT going to be caused by actual data insert failure. This code is only executed when the code error appears or connection to DB is lost conn.rollback() str_err = str(e) trace_err = traceback.format_exc() @@ -74,7 +73,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): print(trace_err) log_end(etl_name, str_err, trace_err) failed_inserts = count_total - successful_inserts_count - last_application_id = last_application_id + 1 + last_submission_id = last_submission_id + 1 print("Total amount of successful inserts:", successful_inserts_count) print("Total failed inserts:", failed_inserts) @@ -136,7 +135,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows): if len(other_data_list) > 0: execute_batch( cursor, - get_insert_query_for_other(), + get_insert_query("",""), other_data_list, page_size=batch_size, ) @@ -230,16 +229,6 @@ def get_insert_query_for_inc(): unique_values = ", %(alr_area)s" return get_insert_query(unique_fields,unique_values) - -def get_insert_query_for_other(): - # leaving blank insert for now - unique_fields = "" - unique_values = "" - return get_insert_query(unique_fields,unique_values) - - - - @inject_conn_pool def clean_application_submission(conn=None): print("Start application_submission cleaning") From e72462c057747be345221bb3d887060d94706d2f Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 23 Aug 2023 12:22:38 -0700 Subject: [PATCH 289/954] code is working commit before refactor --- .../submissions/app_submissions.py | 77 +++++++++++++++++-- 1 file changed, 70 insertions(+), 7 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index eb32f17f57..7c08900a58 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -53,9 +53,21 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): if not rows: break try: + application_ids = [dict(item)["alr_application_id"] for item in rows] + print(application_ids) + application_ids_string = ', '.join(str(item) for item in application_ids) + # print(application_ids_string) + + + adj_rows_query = f"""SELECT * from + oats.oats_adjacent_land_uses oalu + WHERE oalu.alr_application_id in ({application_ids_string}) + """ + cursor.execute(adj_rows_query) + adj_rows = cursor.fetchall() submissions_to_be_inserted_count = len(rows) - insert_app_sub_records(conn, batch_size, cursor, rows) + insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows) successful_inserts_count = ( successful_inserts_count + submissions_to_be_inserted_count @@ -79,7 +91,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): print("Total failed inserts:", failed_inserts) log_end(etl_name) -def insert_app_sub_records(conn, batch_size, cursor, rows): +def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows): """ Function to insert submission records in batches. @@ -98,7 +110,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows): other_data_list, exc_data_list, inc_data_list, - ) = prepare_app_sub_data(rows) + ) = prepare_app_sub_data(rows, adj_rows) if len(nfu_data_list) > 0: execute_batch( @@ -142,7 +154,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows): conn.commit() -def prepare_app_sub_data(app_sub_raw_data_list): +def prepare_app_sub_data(app_sub_raw_data_list, raw_dir_data_list): """ This function prepares different lists of data based on the 'alr_change_code' field of each data dict in 'app_sub_raw_data_list'. @@ -163,7 +175,10 @@ def prepare_app_sub_data(app_sub_raw_data_list): for row in app_sub_raw_data_list: data = dict(row) - # data = map_basic_field(data) + data = add_direction_field(data) + for adj_row in raw_dir_data_list: + dir_data = dict(adj_row) + data = map_direction_field(data, dir_data) if data["alr_change_code"] == ALRChangeCode.NFU.value: # data = mapOatsToAlcsAppPrep(data) @@ -191,7 +206,15 @@ def get_insert_query(unique_fields,unique_values): type_code, is_draft, audit_created_by, - applicant + applicant, + east_land_use_type_description, + west_land_use_type_description, + north_land_use_type_description, + south_land_use_type_description, + east_land_use_type, + west_land_use_type, + north_land_use_type, + south_land_use_type {unique_fields} ) VALUES ( @@ -200,7 +223,15 @@ def get_insert_query(unique_fields,unique_values): %(type_code)s, false, 'oats_etl', - %(applicant)s + %(applicant)s, + %(east_land_use_type_description)s, + %(west_land_use_type_description)s, + %(north_land_use_type_description)s, + %(south_land_use_type_description)s, + %(east_land_use_type)s, + %(west_land_use_type)s, + %(north_land_use_type)s, + %(south_land_use_type)s {unique_values} ) """ @@ -229,6 +260,38 @@ def get_insert_query_for_inc(): unique_values = ", %(alr_area)s" return get_insert_query(unique_fields,unique_values) +def map_direction_field(data, dir_data): + if data['alr_application_id'] == dir_data['alr_application_id']: + if dir_data['cardinal_direction'] == 'EAST': + data['east_land_use_type_description'] = dir_data['description'] + data['east_land_use_type'] = dir_data['nonfarm_use_type_code'] + if dir_data['cardinal_direction'] == 'WEST': + data['west_land_use_type_description'] = dir_data['description'] + data['west_land_use_type'] = dir_data['nonfarm_use_type_code'] + if dir_data['cardinal_direction'] == 'NORTH': + data['north_land_use_type_description'] = dir_data['description'] + data['north_land_use_type'] = dir_data['nonfarm_use_type_code'] + if dir_data['cardinal_direction'] == 'SOUTH': + data['south_land_use_type_description'] = dir_data['description'] + data['south_land_use_type'] = dir_data['nonfarm_use_type_code'] + else: + return data + + print(data) + print("direction_found") + return data + +def add_direction_field(data): + data['east_land_use_type_description'] = None + data['east_land_use_type'] = None + data['west_land_use_type_description'] = None + data['west_land_use_type'] = None + data['north_land_use_type_description'] = None + data['north_land_use_type'] = None + data['south_land_use_type_description'] = None + data['south_land_use_type'] = None + return data + @inject_conn_pool def clean_application_submission(conn=None): print("Start application_submission cleaning") From 3722579acf23a846158e84d673db054dea77802a Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 23 Aug 2023 12:52:27 -0700 Subject: [PATCH 290/954] moved direction mapping to new submap folder --- bin/migrate-oats-data/submap/__init__.py | 1 + .../submap/direction_mapping.py | 29 +++++++++++++ .../submissions/app_submissions.py | 43 +++---------------- 3 files changed, 36 insertions(+), 37 deletions(-) create mode 100644 bin/migrate-oats-data/submap/__init__.py create mode 100644 bin/migrate-oats-data/submap/direction_mapping.py diff --git a/bin/migrate-oats-data/submap/__init__.py b/bin/migrate-oats-data/submap/__init__.py new file mode 100644 index 0000000000..775acecbf2 --- /dev/null +++ b/bin/migrate-oats-data/submap/__init__.py @@ -0,0 +1 @@ +from .direction_mapping import * \ No newline at end of file diff --git a/bin/migrate-oats-data/submap/direction_mapping.py b/bin/migrate-oats-data/submap/direction_mapping.py new file mode 100644 index 0000000000..a661e11f66 --- /dev/null +++ b/bin/migrate-oats-data/submap/direction_mapping.py @@ -0,0 +1,29 @@ + +def add_direction_field(data): + data['east_land_use_type_description'] = None + data['east_land_use_type'] = None + data['west_land_use_type_description'] = None + data['west_land_use_type'] = None + data['north_land_use_type_description'] = None + data['north_land_use_type'] = None + data['south_land_use_type_description'] = None + data['south_land_use_type'] = None + return data + +def map_direction_field(data, dir_data): + if data['alr_application_id'] == dir_data['alr_application_id']: + if dir_data['cardinal_direction'] == 'EAST': + data['east_land_use_type_description'] = dir_data['description'] + data['east_land_use_type'] = dir_data['nonfarm_use_type_code'] + if dir_data['cardinal_direction'] == 'WEST': + data['west_land_use_type_description'] = dir_data['description'] + data['west_land_use_type'] = dir_data['nonfarm_use_type_code'] + if dir_data['cardinal_direction'] == 'NORTH': + data['north_land_use_type_description'] = dir_data['description'] + data['north_land_use_type'] = dir_data['nonfarm_use_type_code'] + if dir_data['cardinal_direction'] == 'SOUTH': + data['south_land_use_type_description'] = dir_data['description'] + data['south_land_use_type'] = dir_data['nonfarm_use_type_code'] + else: + return data + return data \ No newline at end of file diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 7c08900a58..1af764b5c2 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -3,6 +3,10 @@ log_end, log_start, ) +from submap import ( + add_direction_field, + map_direction_field, +) from db import inject_conn_pool from constants import BATCH_UPLOAD_SIZE from psycopg2.extras import execute_batch, RealDictCursor @@ -54,17 +58,14 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): break try: application_ids = [dict(item)["alr_application_id"] for item in rows] - print(application_ids) - application_ids_string = ', '.join(str(item) for item in application_ids) - # print(application_ids_string) - - + application_ids_string = ', '.join(str(item) for item in application_ids) adj_rows_query = f"""SELECT * from oats.oats_adjacent_land_uses oalu WHERE oalu.alr_application_id in ({application_ids_string}) """ cursor.execute(adj_rows_query) adj_rows = cursor.fetchall() + submissions_to_be_inserted_count = len(rows) insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows) @@ -260,38 +261,6 @@ def get_insert_query_for_inc(): unique_values = ", %(alr_area)s" return get_insert_query(unique_fields,unique_values) -def map_direction_field(data, dir_data): - if data['alr_application_id'] == dir_data['alr_application_id']: - if dir_data['cardinal_direction'] == 'EAST': - data['east_land_use_type_description'] = dir_data['description'] - data['east_land_use_type'] = dir_data['nonfarm_use_type_code'] - if dir_data['cardinal_direction'] == 'WEST': - data['west_land_use_type_description'] = dir_data['description'] - data['west_land_use_type'] = dir_data['nonfarm_use_type_code'] - if dir_data['cardinal_direction'] == 'NORTH': - data['north_land_use_type_description'] = dir_data['description'] - data['north_land_use_type'] = dir_data['nonfarm_use_type_code'] - if dir_data['cardinal_direction'] == 'SOUTH': - data['south_land_use_type_description'] = dir_data['description'] - data['south_land_use_type'] = dir_data['nonfarm_use_type_code'] - else: - return data - - print(data) - print("direction_found") - return data - -def add_direction_field(data): - data['east_land_use_type_description'] = None - data['east_land_use_type'] = None - data['west_land_use_type_description'] = None - data['west_land_use_type'] = None - data['north_land_use_type_description'] = None - data['north_land_use_type'] = None - data['south_land_use_type_description'] = None - data['south_land_use_type'] = None - return data - @inject_conn_pool def clean_application_submission(conn=None): print("Start application_submission cleaning") From d8a2fc10ab4ded88adaae44e9bce10fc891adcd5 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 23 Aug 2023 13:13:31 -0700 Subject: [PATCH 291/954] simplified code, removed unused/currently unnecessary functions --- .../submissions/app_submissions.py | 59 ++++--------------- 1 file changed, 11 insertions(+), 48 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 1af764b5c2..03db7279d8 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -107,10 +107,8 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows): """ ( nfu_data_list, - nar_data_list, other_data_list, - exc_data_list, - inc_data_list, + inc_exc_data_list, ) = prepare_app_sub_data(rows, adj_rows) if len(nfu_data_list) > 0: @@ -121,27 +119,11 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows): page_size=batch_size, ) - if len(nar_data_list) > 0: + if len(inc_exc_data_list) > 0: execute_batch( cursor, - get_insert_query_for_nar(), - nar_data_list, - page_size=batch_size, - ) - - if len(exc_data_list) > 0: - execute_batch( - cursor, - get_insert_query_for_exc(), - exc_data_list, - page_size=batch_size, - ) - - if len(inc_data_list) > 0: - execute_batch( - cursor, - get_insert_query_for_inc(), - inc_data_list, + get_insert_query_for_inc_exc(), + inc_exc_data_list, page_size=batch_size, ) @@ -169,9 +151,7 @@ def prepare_app_sub_data(app_sub_raw_data_list, raw_dir_data_list): - Returns the mapped lists """ nfu_data_list = [] - nar_data_list = [] - exc_data_list = [] - inc_data_list = [] + inc_exc_data_list = [] other_data_list = [] for row in app_sub_raw_data_list: @@ -180,22 +160,17 @@ def prepare_app_sub_data(app_sub_raw_data_list, raw_dir_data_list): for adj_row in raw_dir_data_list: dir_data = dict(adj_row) data = map_direction_field(data, dir_data) + # currently rather slow + # ToDo optimize, potentially give index for dir_data resume point if data["alr_change_code"] == ALRChangeCode.NFU.value: - # data = mapOatsToAlcsAppPrep(data) nfu_data_list.append(data) - elif data["alr_change_code"] == ALRChangeCode.NAR.value: - nar_data_list.append(data) - elif data["alr_change_code"] == ALRChangeCode.EXC.value: - # data = mapOatsToAlcsLegislationCode(data) - exc_data_list.append(data) - elif data["alr_change_code"] == ALRChangeCode.INC.value: - # data = mapOatsToAlcsLegislationCode(data) - inc_data_list.append(data) + elif data["alr_change_code"] == ALRChangeCode.EXC.value or data["alr_change_code"] == ALRChangeCode.INC.value: + inc_exc_data_list.append(data) else: other_data_list.append(data) - return nfu_data_list, nar_data_list, other_data_list, exc_data_list, inc_data_list + return nfu_data_list, other_data_list, inc_exc_data_list def get_insert_query(unique_fields,unique_values): @@ -243,24 +218,12 @@ def get_insert_query_for_nfu(): unique_values = ", %(alr_area)s" return get_insert_query(unique_fields,unique_values) -def get_insert_query_for_nar(): - # naruSubtype is a part of submission, import there - unique_fields = "" - unique_values = "" - return get_insert_query(unique_fields,unique_values) - - -def get_insert_query_for_exc(): +def get_insert_query_for_inc_exc(): unique_fields = ", incl_excl_hectares" unique_values = ", %(alr_area)s" return get_insert_query(unique_fields,unique_values) -def get_insert_query_for_inc(): - unique_fields = ", incl_excl_hectares" - unique_values = ", %(alr_area)s" - return get_insert_query(unique_fields,unique_values) - @inject_conn_pool def clean_application_submission(conn=None): print("Start application_submission cleaning") From f11216853081bfc0b20ae0cb25b80ad9e8d46b5f Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 22 Aug 2023 15:59:22 -0700 Subject: [PATCH 292/954] Setup V2 Decisions for NOIs * Create services/controllers/entities * Copy over front-end * Rename App -> NOI * Fix Tests * TODO: Make it work properly --- .../card-status-dialog.component.spec.ts | 2 +- .../card-status/card-status.component.spec.ts | 2 +- ...n-condition-types-dialog.component.spec.ts | 4 +- ...cision-condition-types-dialog.component.ts | 4 +- ...decision-condition-types.component.spec.ts | 6 +- .../decision-condition-types.component.ts | 4 +- .../decision-maker-dialog.component.spec.ts | 4 +- .../decision-maker-dialog.component.ts | 4 +- .../decision-maker.component.spec.ts | 4 +- .../decision-maker.component.ts | 4 +- .../condition/condition.component.ts | 4 +- .../conditions/conditions.component.ts | 4 +- .../basic/basic.component.spec.ts | 4 +- .../basic/basic.component.ts | 4 +- .../incl-excl/incl-excl.component.spec.ts | 4 +- .../incl-excl/incl-excl.component.ts | 4 +- .../naru/naru.component.spec.ts | 4 +- .../decision-component/naru/naru.component.ts | 4 +- .../nfup/nfup.component.spec.ts | 4 +- .../decision-component/nfup/nfup.component.ts | 4 +- .../pfrs/pfrs.component.spec.ts | 4 +- .../decision-component/pfrs/pfrs.component.ts | 4 +- .../pofo/pofo.component.spec.ts | 4 +- .../decision-component/pofo/pofo.component.ts | 4 +- .../roso/roso.component.spec.ts | 4 +- .../decision-component/roso/roso.component.ts | 4 +- .../subd/subd.component.spec.ts | 8 +- .../decision-component/subd/subd.component.ts | 4 +- .../turp/turp.component.spec.ts | 4 +- .../decision-component/turp/turp.component.ts | 4 +- .../decision-component.component.ts | 10 +- .../naru-input/naru-input.component.spec.ts | 4 +- .../naru-input/naru-input.component.ts | 4 +- .../decision-components.component.ts | 30 +- .../decision-conditions.component.ts | 10 +- .../decision-input-v2.component.ts | 10 +- .../condition/condition.component.html | 73 +++ .../condition/condition.component.scss | 3 + .../condition/condition.component.spec.ts | 48 ++ .../condition/condition.component.ts | 104 +++ .../conditions/conditions.component.html | 74 +++ .../conditions/conditions.component.scss | 135 ++++ .../conditions/conditions.component.spec.ts | 55 ++ .../conditions/conditions.component.ts | 167 +++++ .../decision-dialog.component.ts | 8 +- .../decision-v1/decision-v1.component.html | 105 +++ .../decision-v1/decision-v1.component.scss | 142 ++++ .../decision-v1/decision-v1.component.spec.ts | 64 ++ .../decision-v1/decision-v1.component.ts | 208 ++++++ .../basic/basic.component.html | 7 + .../basic/basic.component.scss | 0 .../basic/basic.component.spec.ts | 26 + .../basic/basic.component.ts | 19 + .../pfrs/pfrs.component.html | 112 ++++ .../pfrs/pfrs.component.scss | 0 .../pfrs/pfrs.component.spec.ts | 26 + .../decision-component/pfrs/pfrs.component.ts | 11 + .../pofo/pofo.component.html | 70 ++ .../pofo/pofo.component.scss | 0 .../pofo/pofo.component.spec.ts | 26 + .../decision-component/pofo/pofo.component.ts | 11 + .../roso/roso.component.html | 65 ++ .../roso/roso.component.scss | 0 .../roso/roso.component.spec.ts | 26 + .../decision-component/roso/roso.component.ts | 11 + .../decision-documents.component.html | 70 ++ .../decision-documents.component.scss | 37 ++ .../decision-documents.component.spec.ts | 52 ++ .../decision-documents.component.ts | 114 ++++ .../decision-component.component.html | 57 ++ .../decision-component.component.scss | 0 .../decision-component.component.spec.ts | 32 + .../decision-component.component.ts | 213 ++++++ .../pfrs-input/pfrs-input.component.html | 180 +++++ .../pfrs-input/pfrs-input.component.scss | 0 .../pfrs-input/pfrs-input.component.spec.ts | 24 + .../pfrs-input/pfrs-input.component.ts | 11 + .../pofo-input/pofo-input.component.html | 114 ++++ .../pofo-input/pofo-input.component.scss | 0 .../pofo-input/pofo-input.component.spec.ts | 24 + .../pofo-input/pofo-input.component.ts | 11 + .../roso-input/roso-input.component.html | 114 ++++ .../roso-input/roso-input.component.scss | 0 .../roso-input/roso-input.component.spec.ts | 24 + .../roso-input/roso-input.component.ts | 11 + .../decision-components.component.html | 39 ++ .../decision-components.component.scss | 8 + .../decision-components.component.spec.ts | 72 ++ .../decision-components.component.ts | 218 +++++++ .../decision-condition.component.html | 52 ++ .../decision-condition.component.scss | 14 + .../decision-condition.component.spec.ts | 31 + .../decision-condition.component.ts | 98 +++ .../decision-conditions.component.html | 38 ++ .../decision-conditions.component.scss | 25 + .../decision-conditions.component.spec.ts | 55 ++ .../decision-conditions.component.ts | 211 ++++++ ...sion-document-upload-dialog.component.html | 95 +++ ...sion-document-upload-dialog.component.scss | 43 ++ ...n-document-upload-dialog.component.spec.ts | 47 ++ ...cision-document-upload-dialog.component.ts | 103 +++ .../decision-input-v2.component.html | 228 +++++++ .../decision-input-v2.component.scss | 166 +++++ .../decision-input-v2.component.spec.ts | 79 +++ .../decision-input-v2.component.ts | 483 ++++++++++++++ .../decision-v2/decision-v2.component.html | 242 +++++++ .../decision-v2/decision-v2.component.scss | 156 +++++ .../decision-v2/decision-v2.component.spec.ts | 86 +++ .../decision-v2/decision-v2.component.ts | 237 +++++++ .../release-dialog.component.html | 28 + .../release-dialog.component.scss | 6 + .../release-dialog.component.spec.ts | 52 ++ .../release-dialog.component.ts | 56 ++ .../revert-to-draft-dialog.component.html | 30 + .../revert-to-draft-dialog.component.scss | 6 + .../revert-to-draft-dialog.component.spec.ts | 31 + .../revert-to-draft-dialog.component.ts | 49 ++ .../decision/decision.component.html | 108 +-- .../decision/decision.component.scss | 142 ---- .../decision/decision.component.spec.ts | 37 +- .../decision/decision.component.ts | 185 +----- .../decision/decision.module.ts | 82 +++ .../notice-of-intent.component.ts | 20 +- .../notice-of-intent.module.ts | 4 - ...-decision-condition-types.service.spec.ts} | 8 +- ...ation-decision-condition-types.service.ts} | 8 +- ...pplication-decision-maker.service.spec.ts} | 8 +- .../application-decision-maker.service.ts} | 8 +- .../application-decision-component.service.ts | 6 +- .../application-decision-v2.dto.ts | 10 +- .../application-decision-v2.service.ts | 6 +- ...-intent-decision-component.service.spec.ts | 66 ++ ...ce-of-intent-decision-component.service.ts | 35 + ...-intent-decision-condition.service.spec.ts | 66 ++ ...ce-of-intent-decision-condition.service.ts | 35 + ...tice-of-intent-decision-v2.service.spec.ts | 204 ++++++ .../notice-of-intent-decision-v2.service.ts | 168 +++++ .../decision/notice-of-intent-decision.dto.ts | 135 +++- .../notice-of-intent-decision.service.spec.ts | 22 +- .../noi-document/noi-document.service.spec.ts | 2 +- .../notice-of-intent-detail.service.spec.ts | 8 +- .../notice-of-intent-detail.service.ts | 2 +- ...ice-of-intent-modification.service.spec.ts | 2 +- .../notice-of-intent-parcel.service.ts | 2 +- .../notice-of-intent-submission-status.dto.ts | 32 + ...f-intent-submission-status.service.spec.ts | 98 +++ ...ice-of-intent-submission-status.service.ts | 49 ++ ...otice-of-intent-submission.service.spec.ts | 10 +- .../notice-of-intent.service.ts | 2 +- .../application-decision.entity.ts | 8 +- .../src/alcs/import/noi-import.service.ts | 9 +- ...f-intent-decision-component-type.entity.ts | 12 + ...tent-decision-component.controller.spec.ts | 84 +++ ...of-intent-decision-component.controller.ts | 40 ++ ...notice-of-intent-decision-component.dto.ts | 158 +++++ ...ice-of-intent-decision-component.entity.ts | 190 ++++++ ...-intent-decision-component.service.spec.ts | 234 +++++++ ...ce-of-intent-decision-component.service.ts | 228 +++++++ ...f-intent-decision-condition-code.entity.ts | 5 + ...tent-decision-condition.controller.spec.ts | 109 ++++ ...of-intent-decision-condition.controller.ts | 48 ++ ...notice-of-intent-decision-condition.dto.ts | 103 +++ ...ice-of-intent-decision-condition.entity.ts | 85 +++ ...-intent-decision-condition.service.spec.ts | 162 +++++ ...ce-of-intent-decision-condition.service.ts | 121 ++++ ...-of-intent-decision-v1.controller.spec.ts} | 40 +- ...otice-of-intent-decision-v1.controller.ts} | 28 +- ...ice-of-intent-decision-v1.service.spec.ts} | 30 +- .../notice-of-intent-decision-v1.service.ts} | 30 +- ...e-of-intent-decision-v2.controller.spec.ts | 250 +++++++ ...notice-of-intent-decision-v2.controller.ts | 227 +++++++ ...tice-of-intent-decision-v2.service.spec.ts | 510 +++++++++++++++ .../notice-of-intent-decision-v2.service.ts | 616 ++++++++++++++++++ .../notice-of-intent-decision.dto.ts | 94 ++- .../notice-of-intent-decision.entity.ts | 77 ++- .../notice-of-intent-decision.module.ts | 28 +- .../notice-of-intent-modification.dto.ts | 4 +- ...ice-of-intent-modification.service.spec.ts | 6 +- .../notice-of-intent-modification.service.ts | 4 +- ...lication-decision-v2.automapper.profile.ts | 11 +- ...e-of-intent-decision.automapper.profile.ts | 90 ++- .../1692812627565-noi_decisions_v2.ts | 185 ++++++ .../1692812692333-seed_noi_dec_v2_tables.ts | 48 ++ 183 files changed, 10669 insertions(+), 733 deletions(-) create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.html create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.scss create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.spec.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts create mode 100644 alcs-frontend/src/app/features/notice-of-intent/decision/decision.module.ts rename alcs-frontend/src/app/services/{decision-condition-types/decision-condition-types.service.spec.ts => application/application-decision-condition-types/application-decision-condition-types.service.spec.ts} (92%) rename alcs-frontend/src/app/services/{decision-condition-types/decision-condition-types.service.ts => application/application-decision-condition-types/application-decision-condition-types.service.ts} (84%) rename alcs-frontend/src/app/services/{decision-maker/decision-maker.service.spec.ts => application/application-decision-maker/application-decision-maker.service.spec.ts} (93%) rename alcs-frontend/src/app/services/{decision-maker/decision-maker.service.ts => application/application-decision-maker/application-decision-maker.service.ts} (84%) create mode 100644 alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.spec.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.dto.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.spec.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.controller.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.dto.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.entity.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts rename services/apps/alcs/src/alcs/notice-of-intent-decision/{notice-of-intent-decision.controller.spec.ts => notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller.spec.ts} (80%) rename services/apps/alcs/src/alcs/notice-of-intent-decision/{notice-of-intent-decision.controller.ts => notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller.ts} (83%) rename services/apps/alcs/src/alcs/notice-of-intent-decision/{notice-of-intent-decision.service.spec.ts => notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.spec.ts} (92%) rename services/apps/alcs/src/alcs/notice-of-intent-decision/{notice-of-intent-decision.service.ts => notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.ts} (90%) create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692812627565-noi_decisions_v2.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692812692333-seed_noi_dec_v2_tables.ts diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.spec.ts b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.spec.ts index ef7e3af3d4..89b844851c 100644 --- a/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/admin/card-status/card-status-dialog/card-status-dialog.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { CardStatusService } from '../../../../services/card/card-status/card-status.service'; -import { DecisionConditionTypesService } from '../../../../services/decision-condition-types/decision-condition-types.service'; +import { ApplicationDecisionConditionTypesService } from '../../../../services/application/application-decision-condition-types/application-decision-condition-types.service'; import { CardStatusDialogComponent } from './card-status-dialog.component'; diff --git a/alcs-frontend/src/app/features/admin/card-status/card-status.component.spec.ts b/alcs-frontend/src/app/features/admin/card-status/card-status.component.spec.ts index d038f664e1..addd5047d1 100644 --- a/alcs-frontend/src/app/features/admin/card-status/card-status.component.spec.ts +++ b/alcs-frontend/src/app/features/admin/card-status/card-status.component.spec.ts @@ -4,7 +4,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { CardStatusService } from '../../../services/card/card-status/card-status.service'; -import { DecisionConditionTypesService } from '../../../services/decision-condition-types/decision-condition-types.service'; +import { ApplicationDecisionConditionTypesService } from '../../../services/application/application-decision-condition-types/application-decision-condition-types.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { CardStatusComponent } from './card-status.component'; diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.spec.ts b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.spec.ts index f27ef9c36f..2d8259b43b 100644 --- a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { CeoCriterionService } from '../../../../services/ceo-criterion/ceo-criterion.service'; -import { DecisionConditionTypesService } from '../../../../services/decision-condition-types/decision-condition-types.service'; +import { ApplicationDecisionConditionTypesService } from '../../../../services/application/application-decision-condition-types/application-decision-condition-types.service'; import { DecisionConditionTypesDialogComponent } from './decision-condition-types-dialog.component'; @@ -19,7 +19,7 @@ describe('DecisionConditionTypesDialogComponent', () => { { provide: MAT_DIALOG_DATA, useValue: undefined }, { provide: MatDialogRef, useValue: {} }, { - provide: DecisionConditionTypesService, + provide: ApplicationDecisionConditionTypesService, useValue: {}, }, ], diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.ts b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.ts index 8cb9bdd4fd..3626659f6a 100644 --- a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.ts +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types-dialog/decision-condition-types-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { ApplicationDecisionConditionTypeDto } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; -import { DecisionConditionTypesService } from '../../../../services/decision-condition-types/decision-condition-types.service'; +import { ApplicationDecisionConditionTypesService } from '../../../../services/application/application-decision-condition-types/application-decision-condition-types.service'; @Component({ selector: 'app-decision-condition-types-dialog', @@ -19,7 +19,7 @@ export class DecisionConditionTypesDialogComponent { constructor( @Inject(MAT_DIALOG_DATA) public data: ApplicationDecisionConditionTypeDto | undefined, private dialogRef: MatDialogRef, - private decisionConditionTypesService: DecisionConditionTypesService + private decisionConditionTypesService: ApplicationDecisionConditionTypesService ) { if (data) { this.description = data.description; diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts index 68d15c3272..15dde6a26d 100644 --- a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.spec.ts @@ -3,7 +3,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { DecisionConditionTypesService } from '../../../services/decision-condition-types/decision-condition-types.service'; +import { ApplicationDecisionConditionTypesService } from '../../../services/application/application-decision-condition-types/application-decision-condition-types.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionConditionTypesComponent } from './decision-condition-types.component'; @@ -11,7 +11,7 @@ import { DecisionConditionTypesComponent } from './decision-condition-types.comp describe('DecisionConditionTypesComponent', () => { let component: DecisionConditionTypesComponent; let fixture: ComponentFixture; - let mockDecTypesService: DeepMocked; + let mockDecTypesService: DeepMocked; let mockDialog: DeepMocked; let mockConfirmationDialogService: DeepMocked; @@ -24,7 +24,7 @@ describe('DecisionConditionTypesComponent', () => { declarations: [DecisionConditionTypesComponent], providers: [ { - provide: DecisionConditionTypesService, + provide: ApplicationDecisionConditionTypesService, useValue: mockDecTypesService, }, { diff --git a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.ts b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.ts index d730203f41..46e9ffd90c 100644 --- a/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.ts +++ b/alcs-frontend/src/app/features/admin/decision-condition-types/decision-condition-types.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Subject } from 'rxjs'; import { ApplicationDecisionConditionTypeDto } from '../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; -import { DecisionConditionTypesService } from '../../../services/decision-condition-types/decision-condition-types.service'; +import { ApplicationDecisionConditionTypesService } from '../../../services/application/application-decision-condition-types/application-decision-condition-types.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionConditionTypesDialogComponent } from './decision-condition-types-dialog/decision-condition-types-dialog.component'; @@ -18,7 +18,7 @@ export class DecisionConditionTypesComponent implements OnInit { displayedColumns: string[] = ['label', 'description', 'code', 'actions']; constructor( - private decisionConditionTypesService: DecisionConditionTypesService, + private decisionConditionTypesService: ApplicationDecisionConditionTypesService, public dialog: MatDialog, private confirmationDialogService: ConfirmationDialogService ) {} diff --git a/alcs-frontend/src/app/features/admin/decision-maker/decision-maker-dialog/decision-maker-dialog.component.spec.ts b/alcs-frontend/src/app/features/admin/decision-maker/decision-maker-dialog/decision-maker-dialog.component.spec.ts index 09bbcff7aa..8a36a923ef 100644 --- a/alcs-frontend/src/app/features/admin/decision-maker/decision-maker-dialog/decision-maker-dialog.component.spec.ts +++ b/alcs-frontend/src/app/features/admin/decision-maker/decision-maker-dialog/decision-maker-dialog.component.spec.ts @@ -2,7 +2,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { DecisionMakerService } from '../../../../services/decision-maker/decision-maker.service'; +import { ApplicationDecisionMakerService } from '../../../../services/application/application-decision-maker/application-decision-maker.service'; import { DecisionMakerDialogComponent } from './decision-maker-dialog.component'; @@ -18,7 +18,7 @@ describe('DecisionMakerDialogComponent', () => { { provide: MAT_DIALOG_DATA, useValue: undefined }, { provide: MatDialogRef, useValue: {} }, { - provide: DecisionMakerService, + provide: ApplicationDecisionMakerService, useValue: {}, }, ], diff --git a/alcs-frontend/src/app/features/admin/decision-maker/decision-maker-dialog/decision-maker-dialog.component.ts b/alcs-frontend/src/app/features/admin/decision-maker/decision-maker-dialog/decision-maker-dialog.component.ts index 082c6a1d39..0d92afcd79 100644 --- a/alcs-frontend/src/app/features/admin/decision-maker/decision-maker-dialog/decision-maker-dialog.component.ts +++ b/alcs-frontend/src/app/features/admin/decision-maker/decision-maker-dialog/decision-maker-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { DecisionMakerDto } from '../../../../services/application/decision/application-decision-v1/application-decision.dto'; -import { DecisionMakerService } from '../../../../services/decision-maker/decision-maker.service'; +import { ApplicationDecisionMakerService } from '../../../../services/application/application-decision-maker/application-decision-maker.service'; @Component({ selector: 'app-decision-maker-dialog', @@ -20,7 +20,7 @@ export class DecisionMakerDialogComponent { constructor( @Inject(MAT_DIALOG_DATA) public data: DecisionMakerDto | undefined, private dialogRef: MatDialogRef, - private decisionMakerService: DecisionMakerService + private decisionMakerService: ApplicationDecisionMakerService ) { if (data) { this.description = data.description; diff --git a/alcs-frontend/src/app/features/admin/decision-maker/decision-maker.component.spec.ts b/alcs-frontend/src/app/features/admin/decision-maker/decision-maker.component.spec.ts index 547b37edf6..14ec61f2c3 100644 --- a/alcs-frontend/src/app/features/admin/decision-maker/decision-maker.component.spec.ts +++ b/alcs-frontend/src/app/features/admin/decision-maker/decision-maker.component.spec.ts @@ -4,7 +4,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { CeoCriterionService } from '../../../services/ceo-criterion/ceo-criterion.service'; -import { DecisionMakerService } from '../../../services/decision-maker/decision-maker.service'; +import { ApplicationDecisionMakerService } from '../../../services/application/application-decision-maker/application-decision-maker.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionMakerComponent } from './decision-maker.component'; @@ -25,7 +25,7 @@ describe('DecisionMakerComponent', () => { declarations: [DecisionMakerComponent], providers: [ { - provide: DecisionMakerService, + provide: ApplicationDecisionMakerService, useValue: mockCeoCriterionService, }, { diff --git a/alcs-frontend/src/app/features/admin/decision-maker/decision-maker.component.ts b/alcs-frontend/src/app/features/admin/decision-maker/decision-maker.component.ts index 579f465cee..e6c5dc27f8 100644 --- a/alcs-frontend/src/app/features/admin/decision-maker/decision-maker.component.ts +++ b/alcs-frontend/src/app/features/admin/decision-maker/decision-maker.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Subject } from 'rxjs'; import { DecisionMakerDto } from '../../../services/application/decision/application-decision-v1/application-decision.dto'; -import { DecisionMakerService } from '../../../services/decision-maker/decision-maker.service'; +import { ApplicationDecisionMakerService } from '../../../services/application/application-decision-maker/application-decision-maker.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionMakerDialogComponent } from './decision-maker-dialog/decision-maker-dialog.component'; @@ -18,7 +18,7 @@ export class DecisionMakerComponent implements OnInit { displayedColumns: string[] = ['label', 'description', 'code', 'isActive', 'actions']; constructor( - private decisionMakerService: DecisionMakerService, + private decisionMakerService: ApplicationDecisionMakerService, public dialog: MatDialog, private confirmationDialogService: ConfirmationDialogService ) {} diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts index 4e7e434d1f..c445216b08 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts @@ -5,7 +5,7 @@ import { ApplicationDecisionConditionService } from '../../../../../services/app import { ApplicationDecisionConditionToComponentPlanNumberDto, APPLICATION_DECISION_COMPONENT_TYPE, - DecisionComponentDto, + ApplicationDecisionComponentDto, UpdateApplicationDecisionConditionDto, } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { @@ -44,7 +44,7 @@ export class ConditionComponent implements OnInit, AfterViewInit { isReadMoreVisible = false; conditionStatus: string = ''; isRequireSurveyPlan = false; - subdComponent?: DecisionComponentDto; + subdComponent?: ApplicationDecisionComponentDto; planNumbers: ApplicationDecisionConditionToComponentPlanNumberDto[] = []; constructor( diff --git a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts index 09b076ed80..8b84edc196 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/conditions.component.ts @@ -8,7 +8,7 @@ import { ApplicationDecisionConditionDto, ApplicationDecisionDto, ApplicationDecisionWithLinkedResolutionDto, - DecisionCodesDto, + ApplicationDecisionCodesDto, } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { @@ -53,7 +53,7 @@ export class ConditionsComponent implements OnInit { decision!: ApplicationDecisionWithConditionComponentLabels; conditionDecision!: ApplicationDecisionDto; application: ApplicationDto | undefined; - codes!: DecisionCodesDto; + codes!: ApplicationDecisionCodesDto; today!: number; dratDecisionLabel = DRAFT_DECISION_TYPE_LABEL; diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.spec.ts index 3cd182197e..0b966955c4 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.spec.ts @@ -1,6 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { BasicComponent } from './basic.component'; @@ -16,7 +16,7 @@ describe('BasicComponent', () => { fixture = TestBed.createComponent(BasicComponent); component = fixture.componentInstance; - component.component = {} as DecisionComponentDto; + component.component = {} as ApplicationDecisionComponentDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.ts index bf635028a4..ab85edd3e9 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/basic/basic.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ selector: 'app-basic', @@ -7,7 +7,7 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec styleUrls: ['./basic.component.scss'], }) export class BasicComponent { - @Input() component!: DecisionComponentDto; + @Input() component!: ApplicationDecisionComponentDto; @Input() fillRow = false; @Output() saveAlrArea = new EventEmitter(); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.spec.ts index 9681c2d6ab..b7c34fd7e7 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.spec.ts @@ -1,6 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { InclExclComponent } from './incl-excl.component'; @@ -16,7 +16,7 @@ describe('NaruComponent', () => { fixture = TestBed.createComponent(InclExclComponent); component = fixture.componentInstance; - component.component = {} as DecisionComponentDto; + component.component = {} as ApplicationDecisionComponentDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts index d164dca1aa..04747ca1d3 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/incl-excl/incl-excl.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ selector: 'app-incl-excl', @@ -7,5 +7,5 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec styleUrls: ['./incl-excl.component.scss'], }) export class InclExclComponent { - @Input() component!: DecisionComponentDto; + @Input() component!: ApplicationDecisionComponentDto; } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.spec.ts index d8629453a1..f5eb9c2369 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.spec.ts @@ -1,6 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { NaruComponent } from './naru.component'; @@ -16,7 +16,7 @@ describe('NaruComponent', () => { fixture = TestBed.createComponent(NaruComponent); component = fixture.componentInstance; - component.component = {} as DecisionComponentDto; + component.component = {} as ApplicationDecisionComponentDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.ts index 2160e20196..1f4090aa51 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/naru/naru.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ selector: 'app-naru', @@ -7,5 +7,5 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec styleUrls: ['./naru.component.scss'], }) export class NaruComponent { - @Input() component!: DecisionComponentDto; + @Input() component!: ApplicationDecisionComponentDto; } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.spec.ts index 0409a7eca8..f0cf2bf7ef 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.spec.ts @@ -1,6 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { NfupComponent } from './nfup.component'; @@ -16,7 +16,7 @@ describe('NfupComponent', () => { fixture = TestBed.createComponent(NfupComponent); component = fixture.componentInstance; - component.component = {} as DecisionComponentDto; + component.component = {} as ApplicationDecisionComponentDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.ts index ac4b5cddfb..728bef3466 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/nfup/nfup.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ selector: 'app-nfup', @@ -7,5 +7,5 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec styleUrls: ['./nfup.component.scss'], }) export class NfupComponent { - @Input() component!: DecisionComponentDto; + @Input() component!: ApplicationDecisionComponentDto; } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts index 5a691549ad..23476b67a0 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts @@ -1,6 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { PfrsComponent } from './pfrs.component'; @@ -16,7 +16,7 @@ describe('PfrsComponent', () => { fixture = TestBed.createComponent(PfrsComponent); component = fixture.componentInstance; - component.component = {} as DecisionComponentDto; + component.component = {} as ApplicationDecisionComponentDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.ts index 532eddb08e..b8b3304d59 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pfrs/pfrs.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ selector: 'app-pfrs', @@ -7,5 +7,5 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec styleUrls: ['./pfrs.component.scss'], }) export class PfrsComponent { - @Input() component!: DecisionComponentDto; + @Input() component!: ApplicationDecisionComponentDto; } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts index eeee8571e3..21a5215761 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts @@ -1,6 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { PofoComponent } from './pofo.component'; @@ -16,7 +16,7 @@ describe('PofoComponent', () => { fixture = TestBed.createComponent(PofoComponent); component = fixture.componentInstance; - component.component = {} as DecisionComponentDto; + component.component = {} as ApplicationDecisionComponentDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.ts index dcee185d01..90b0ca0879 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ selector: 'app-pofo', @@ -7,5 +7,5 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec styleUrls: ['./pofo.component.scss'], }) export class PofoComponent { - @Input() component!: DecisionComponentDto; + @Input() component!: ApplicationDecisionComponentDto; } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.spec.ts index cc4679d648..c30b323ba4 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.spec.ts @@ -1,6 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { RosoComponent } from './roso.component'; @@ -16,7 +16,7 @@ describe('RosoComponent', () => { fixture = TestBed.createComponent(RosoComponent); component = fixture.componentInstance; - component.component = {} as DecisionComponentDto; + component.component = {} as ApplicationDecisionComponentDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.ts index f474f24ae0..a6caddf3df 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ selector: 'app-roso', @@ -7,5 +7,5 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec styleUrls: ['./roso.component.scss'], }) export class RosoComponent { - @Input() component!: DecisionComponentDto; + @Input() component!: ApplicationDecisionComponentDto; } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts index 614a7a2ec5..c1d3d73a7f 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ApplicationDecisionComponentLotService } from '../../../../../../services/application/decision/application-decision-v2/application-decision-component-lot/application-decision-component-lot.service'; import { ApplicationDecisionComponentService } from '../../../../../../services/application/decision/application-decision-v2/application-decision-component/application-decision-component.service'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { SubdComponent } from './subd.component'; @@ -20,15 +20,15 @@ describe('PfrsComponent', () => { providers: [ { provide: ApplicationDecisionComponentLotService, - useValue: mockComponentLotService - } + useValue: mockComponentLotService, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(SubdComponent); component = fixture.componentInstance; - component.component = {} as DecisionComponentDto; + component.component = {} as ApplicationDecisionComponentDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts index 3fb6ec788c..ab5cdd4253 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/subd/subd.component.ts @@ -1,7 +1,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ApplicationDecisionComponentLotService } from '../../../../../../services/application/decision/application-decision-v2/application-decision-component-lot/application-decision-component-lot.service'; import { - DecisionComponentDto, + ApplicationDecisionComponentDto, ProposedDecisionLotDto, } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @@ -13,7 +13,7 @@ import { export class SubdComponent implements OnInit { constructor(private componentLotService: ApplicationDecisionComponentLotService) {} - @Input() component!: DecisionComponentDto; + @Input() component!: ApplicationDecisionComponentDto; ngOnInit(): void { this.component.lots = this.component.lots?.sort((a, b) => a.index - b.index) ?? undefined; diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.spec.ts index 1ad0c0b3fc..12425f4802 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.spec.ts @@ -1,6 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { TurpComponent } from './turp.component'; @@ -16,7 +16,7 @@ describe('TurpComponent', () => { fixture = TestBed.createComponent(TurpComponent); component = fixture.componentInstance; - component.component = {} as DecisionComponentDto; + component.component = {} as ApplicationDecisionComponentDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.ts index 1fa3091fa7..c9030c3662 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/turp/turp.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { DecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ selector: 'app-turp', @@ -7,5 +7,5 @@ import { DecisionComponentDto } from '../../../../../../services/application/dec styleUrls: ['./turp.component.scss'], }) export class TurpComponent { - @Input() component!: DecisionComponentDto; + @Input() component!: ApplicationDecisionComponentDto; } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts index 1bec2e48eb..8446e128e5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts @@ -2,8 +2,8 @@ import { Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angu import { FormControl, FormGroup, Validators } from '@angular/forms'; import { APPLICATION_DECISION_COMPONENT_TYPE, - DecisionCodesDto, - DecisionComponentDto, + ApplicationDecisionCodesDto, + ApplicationDecisionComponentDto, DecisionComponentTypeDto, NaruDecisionComponentDto, NfuDecisionComponentDto, @@ -24,9 +24,9 @@ import { SubdInputComponent } from './subd-input/subd-input.component'; styleUrls: ['./decision-component.component.scss'], }) export class DecisionComponentComponent implements OnInit { - @Input() data!: DecisionComponentDto; - @Input() codes!: DecisionCodesDto; - @Output() dataChange = new EventEmitter(); + @Input() data!: ApplicationDecisionComponentDto; + @Input() codes!: ApplicationDecisionCodesDto; + @Output() dataChange = new EventEmitter(); @Output() remove = new EventEmitter(); @ViewChild(SubdInputComponent) subdInputComponent?: SubdInputComponent; diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component.spec.ts index f528c6a600..3d9638c340 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component.spec.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component.spec.ts @@ -1,6 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DecisionCodesDto } from '../../../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionCodesDto } from '../../../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { NaruInputComponent } from './naru-input.component'; @@ -16,7 +16,7 @@ describe('NaruInputComponent', () => { fixture = TestBed.createComponent(NaruInputComponent); component = fixture.componentInstance; - component.codes = {} as DecisionCodesDto; + component.codes = {} as ApplicationDecisionCodesDto; fixture.detectChanges(); }); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component.ts index d52f03423e..ee5a8f01ee 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/naru-input/naru-input.component.ts @@ -1,6 +1,6 @@ import { Component, Input } from '@angular/core'; import { FormGroup } from '@angular/forms'; -import { DecisionCodesDto } from '../../../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionCodesDto } from '../../../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; @Component({ selector: 'app-naru-input', @@ -9,5 +9,5 @@ import { DecisionCodesDto } from '../../../../../../../../services/application/d }) export class NaruInputComponent { @Input() form!: FormGroup; - @Input() codes!: DecisionCodesDto; + @Input() codes!: ApplicationDecisionCodesDto; } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index 3cc2e2e646..6b4ef02f93 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -15,8 +15,8 @@ import { ApplicationSubmissionService } from '../../../../../../services/applica import { ApplicationDto } from '../../../../../../services/application/application.dto'; import { APPLICATION_DECISION_COMPONENT_TYPE, - DecisionCodesDto, - DecisionComponentDto, + ApplicationDecisionCodesDto, + ApplicationDecisionComponentDto, DecisionComponentTypeDto, } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ToastService } from '../../../../../../services/toast/toast.service'; @@ -33,13 +33,13 @@ export type DecisionComponentTypeMenuItem = DecisionComponentTypeDto & { isDisab export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterViewInit { $destroy = new Subject(); - @Input() codes!: DecisionCodesDto; + @Input() codes!: ApplicationDecisionCodesDto; @Input() fileNumber!: string; @Input() showError = false; - @Input() components: DecisionComponentDto[] = []; + @Input() components: ApplicationDecisionComponentDto[] = []; @Output() componentsChange = new EventEmitter<{ - components: DecisionComponentDto[]; + components: ApplicationDecisionComponentDto[]; isValid: boolean; }>(); @ViewChildren(DecisionComponentComponent) childComponents!: QueryList; @@ -84,7 +84,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView onAddNewComponent(uiCode: string, typeCode: string) { switch (uiCode) { case 'COPY': - const component: DecisionComponentDto = { + const component: ApplicationDecisionComponentDto = { applicationDecisionComponentTypeCode: typeCode, alrArea: this.application.alrArea, agCap: this.application.agCap, @@ -149,7 +149,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView applicationDecisionComponentType: this.decisionComponentTypes.find( (e) => e.code === typeCode && e.uiCode !== 'COPY' ), - } as DecisionComponentDto); + } as ApplicationDecisionComponentDto); break; default: this.toastService.showErrorToast(`Failed to create component ${typeCode}`); @@ -171,7 +171,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView }); } - trackByFn(index: any, item: DecisionComponentDto) { + trackByFn(index: any, item: ApplicationDecisionComponentDto) { return item.applicationDecisionComponentTypeCode; } @@ -196,7 +196,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView }); } - private async prepareDecisionComponentTypes(codes: DecisionCodesDto) { + private async prepareDecisionComponentTypes(codes: ApplicationDecisionCodesDto) { const decisionComponentTypes: DecisionComponentTypeMenuItem[] = codes.decisionComponentTypes.map((e) => ({ ...e, isDisabled: false, @@ -220,17 +220,17 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView this.updateComponentsMenuItems(); } - private patchNfuFields(component: DecisionComponentDto) { + private patchNfuFields(component: ApplicationDecisionComponentDto) { component.nfuType = this.application.nfuUseType; component.nfuSubType = this.application.nfuUseSubType; component.endDate = this.application.proposalEndDate; } - private patchTurpFields(component: DecisionComponentDto) { + private patchTurpFields(component: ApplicationDecisionComponentDto) { component.expiryDate = this.application.proposalExpiryDate; } - private patchPofoFields(component: DecisionComponentDto) { + private patchPofoFields(component: ApplicationDecisionComponentDto) { component.endDate = this.application.proposalEndDate; component.soilFillTypeToPlace = this.application.submittedApplication?.soilFillTypeToPlace; component.soilToPlaceVolume = this.application.submittedApplication?.soilToPlaceVolume; @@ -239,7 +239,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView component.soilToPlaceAverageDepth = this.application.submittedApplication?.soilToPlaceAverageDepth; } - private patchRosoFields(component: DecisionComponentDto) { + private patchRosoFields(component: ApplicationDecisionComponentDto) { component.endDate = this.application.proposalEndDate; component.soilTypeRemoved = this.application.submittedApplication?.soilTypeRemoved; component.soilToRemoveVolume = this.application.submittedApplication?.soilToRemoveVolume; @@ -248,13 +248,13 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView component.soilToRemoveAverageDepth = this.application.submittedApplication?.soilToRemoveAverageDepth; } - private patchNaruFields(component: DecisionComponentDto) { + private patchNaruFields(component: ApplicationDecisionComponentDto) { component.endDate = this.application.proposalEndDate; component.expiryDate = this.application.proposalExpiryDate; component.naruSubtypeCode = this.application.submittedApplication?.naruSubtype?.code; } - private patchInclExclFields(component: DecisionComponentDto) { + private patchInclExclFields(component: ApplicationDecisionComponentDto) { if (this.application.inclExclApplicantType) { component.inclExclApplicantType = this.application.inclExclApplicantType; } else { diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts index bf61badbd1..21682af88e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -13,8 +13,8 @@ import { combineLatestWith, Subject, takeUntil } from 'rxjs'; import { ApplicationDecisionConditionDto, ApplicationDecisionDto, - DecisionCodesDto, - DecisionComponentDto, + ApplicationDecisionCodesDto, + ApplicationDecisionComponentDto, UpdateApplicationDecisionConditionDto, } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; @@ -32,8 +32,8 @@ export type SelectableComponent = { uuid?: string; tempId: string; decisionUuid: export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy { $destroy = new Subject(); - @Input() codes!: DecisionCodesDto; - @Input() components: DecisionComponentDto[] = []; + @Input() codes!: ApplicationDecisionCodesDto; + @Input() components: ApplicationDecisionComponentDto[] = []; @Input() conditions: ApplicationDecisionConditionDto[] = []; @Input() showError = false; @ViewChildren(DecisionConditionComponent) conditionComponents: DecisionConditionComponent[] = []; @@ -172,7 +172,7 @@ export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy mapComponents( decisionUuid: string, - components: DecisionComponentDto[], + components: ApplicationDecisionComponentDto[], decisionNumber: number | null, decisionYear: number | null ) { diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index 8cc68224b8..7173b2f6aa 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -16,8 +16,8 @@ import { CeoCriterion, CeoCriterionDto, CreateApplicationDecisionDto, - DecisionCodesDto, - DecisionComponentDto, + ApplicationDecisionCodesDto, + ApplicationDecisionComponentDto, DecisionMaker, DecisionMakerDto, DecisionOutcomeCodeDto, @@ -70,12 +70,12 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { resolutionYears: number[] = []; postDecisions: MappedPostDecision[] = []; existingDecision: ApplicationDecisionDto | undefined; - codes?: DecisionCodesDto; + codes?: ApplicationDecisionCodesDto; resolutionNumberControl = new FormControl(null, [Validators.required]); resolutionYearControl = new FormControl(null, [Validators.required]); - components: DecisionComponentDto[] = []; + components: ApplicationDecisionComponentDto[] = []; conditions: ApplicationDecisionConditionDto[] = []; conditionUpdates: UpdateApplicationDecisionConditionDto[] = []; @@ -589,7 +589,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { } } - onComponentChange($event: { components: DecisionComponentDto[]; isValid: boolean }) { + onComponentChange($event: { components: ApplicationDecisionComponentDto[]; isValid: boolean }) { this.components = Array.from($event.components); this.componentsValid = $event.isValid; } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html new file mode 100644 index 0000000000..e0c39fdff4 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.html @@ -0,0 +1,73 @@ +
+

{{ condition.type.label }}

+ + + + + + + + + +
+
+
+
Component to Condition
+ {{ condition.componentLabelsStr }} + +
+ +
+
Approval Dependent
+ {{ condition.approvalDependant | booleanToString }} + +
+ +
+
Security Amount
+ {{ condition.securityAmount }} + +
+ +
+
Admin Fee
+ {{ condition.administrativeFee }} + +
+ +
+
Completion Date
+ +
+ +
+
Superseded Date
+ +
+ +
+
Description
+ {{ + condition.description + }} + + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss new file mode 100644 index 0000000000..b1976e0872 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.scss @@ -0,0 +1,3 @@ +.component-labels { + white-space: pre-wrap; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.spec.ts new file mode 100644 index 0000000000..11279144f7 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.spec.ts @@ -0,0 +1,48 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentDecisionConditionService } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; +import { SharedModule } from '../../../../../shared/shared.module'; +import { ConditionComponentLabels, DecisionConditionWithStatus } from '../conditions.component'; + +import { ConditionComponent } from './condition.component'; + +describe('ConditionComponent', () => { + let component: ConditionComponent; + let fixture: ComponentFixture; + let mockNOIDecisionConditionService: DeepMocked; + + beforeEach(async () => { + mockNOIDecisionConditionService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ConditionComponent], + providers: [ + { + provide: NoticeOfIntentDecisionConditionService, + useValue: mockNOIDecisionConditionService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + imports: [SharedModule, BrowserAnimationsModule], + }).compileComponents(); + + fixture = TestBed.createComponent(ConditionComponent); + component = fixture.componentInstance; + + component.condition = createMock< + DecisionConditionWithStatus & { + componentLabelsStr?: string; + componentLabels?: ConditionComponentLabels[]; + } + >(); + component.condition.conditionComponentsLabels = []; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts new file mode 100644 index 0000000000..99927ef81c --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts @@ -0,0 +1,104 @@ +import { AfterViewInit, Component, Input, OnInit } from '@angular/core'; +import moment from 'moment'; +import { UpdateApplicationDecisionConditionDto } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { NoticeOfIntentDecisionConditionService } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; +import { UpdateNoticeOfIntentDecisionConditionDto } from '../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { + DECISION_CONDITION_COMPLETE_LABEL, + DECISION_CONDITION_INCOMPLETE_LABEL, + DECISION_CONDITION_SUPERSEDED_LABEL, +} from '../../../../../shared/application-type-pill/application-type-pill.constants'; +import { CONDITION_STATUS, ConditionComponentLabels, DecisionConditionWithStatus } from '../conditions.component'; + +type Condition = DecisionConditionWithStatus & { + componentLabelsStr?: string; + componentLabels?: ConditionComponentLabels[]; +}; + +@Component({ + selector: 'app-condition', + templateUrl: './condition.component.html', + styleUrls: ['./condition.component.scss'], +}) +export class ConditionComponent implements OnInit, AfterViewInit { + @Input() condition!: Condition; + @Input() isDraftDecision!: boolean; + @Input() fileNumber!: string; + + incompleteLabel = DECISION_CONDITION_INCOMPLETE_LABEL; + completeLabel = DECISION_CONDITION_COMPLETE_LABEL; + supersededLabel = DECISION_CONDITION_SUPERSEDED_LABEL; + + CONDITION_STATUS = CONDITION_STATUS; + + isReadMoreClicked = false; + isReadMoreVisible = false; + conditionStatus: string = ''; + isRequireSurveyPlan = false; + + constructor(private conditionService: NoticeOfIntentDecisionConditionService) {} + + ngOnInit() { + this.updateStatus(); + if (this.condition) { + this.condition = { + ...this.condition, + componentLabelsStr: this.condition.conditionComponentsLabels?.flatMap((e) => e.label).join(';\n'), + }; + + this.isRequireSurveyPlan = this.condition.type?.code === 'RSPL'; + } + } + + ngAfterViewInit(): void { + setTimeout(() => (this.isReadMoreVisible = this.checkIfReadMoreVisible())); + } + + async onUpdateCondition( + field: keyof UpdateNoticeOfIntentDecisionConditionDto, + value: string[] | string | number | null + ) { + const condition = this.condition; + + if (condition) { + const update = await this.conditionService.update(condition.uuid, { + [field]: value, + }); + + const labels = this.condition.componentLabels; + this.condition = { ...update, componentLabels: labels } as Condition; + + this.updateStatus(); + } + } + + onToggleReadMore() { + this.isReadMoreClicked = !this.isReadMoreClicked; + } + + isEllipsisActive(e: string): boolean { + const el = document.getElementById(e); + // + 2 required as adjustment to height + return el ? el.clientHeight + 2 < el.scrollHeight : false; + } + + checkIfReadMoreVisible(): boolean { + return this.isReadMoreClicked || this.isEllipsisActive(this.condition.uuid + 'Description'); + } + + updateStatus() { + const today = moment().startOf('day').toDate().getTime(); + + if (this.condition.supersededDate && this.condition.supersededDate <= today) { + this.conditionStatus = CONDITION_STATUS.SUPERSEDED; + } else if (this.condition.completionDate && this.condition.completionDate <= today) { + this.conditionStatus = CONDITION_STATUS.COMPLETE; + } else { + this.conditionStatus = CONDITION_STATUS.INCOMPLETE; + } + } + + getComponentLabel(componentUuid: string) { + return this.condition.conditionComponentsLabels?.find((e) => e.componentUuid === componentUuid)?.label; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html new file mode 100644 index 0000000000..f4927fbbe4 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.html @@ -0,0 +1,74 @@ +
+
+

View Conditions

+ +
+
+
+
+
+ Modified By:  + {{ decision.modifiedByResolutions?.join(', ') }} + N/A +
+
+
+
+
Decision #{{ decision.index }}
+
+ + calendar_month + {{ noticeOfIntent.activeDays }} + + + pause + {{ noticeOfIntent.pausedDays }} + +
+ + + + Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }} + + + + +
+
+
+ +
+
+ +
+
+
+ +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss new file mode 100644 index 0000000000..d216bf2f2b --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.scss @@ -0,0 +1,135 @@ +@use '../../../../../styles/colors.scss'; + +h3, +div, +span, +p { + color: colors.$black; +} + +.header, +.footer { + display: flex; + padding-left: 16px; +} + +.header { + justify-content: space-between; +} + +.footer { + justify-content: flex-end; +} + +:host ::ng-deep { + .display-none { + display: none !important; + } + + .read-more { + display: flex; + justify-content: flex-end; + } + + .decision-container { + padding: 28px 0; + + .post-decisions { + padding: 8px 16px; + background-color: colors.$grey-light; + grid-template-columns: 50% 50%; + display: grid; + min-height: 36px; + text-transform: uppercase; + border-radius: 4px 4px 0 0; + } + + .header { + margin: 0; + display: flex; + justify-content: space-between; + padding: 0 16px; + + .title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + } + } + + .even-condition { + background-color: colors.$grey-light; + } + + .condition-container { + display: block; + padding: 24px 16px; + + .header { + display: flex; + padding: 0px; + gap: 16px; + padding-bottom: 16px; + + color: colors.$black; + justify-content: flex-start; + } + + .grid-3 { + display: grid; + grid-template-columns: 31% 31% 31%; + grid-row-gap: 24px; + grid-column-gap: 16px; + + .full-width { + grid-column: 1/4; + } + } + } + + .no-conditions { + margin: 16px 16px; + color: colors.$grey-dark; + } + } + + .lot-table { + margin-top: 12px; + display: grid; + grid-template-columns: max-content max-content max-content 0.8fr; + grid-column-gap: 36px; + grid-row-gap: 12px; + align-items: center; + } + + .container { + margin: 32px 0; + } + + .subheading2 { + display: flex; + gap: 4px; + } + + .icon { + height: 20px; + width: 20px; + font-size: 20px; + } + + .component-labels { + white-space: pre-wrap; + } + + .inline-plan-numbers { + width: 100%; + + .editing { + width: 100%; + .mat-mdc-form-field { + width: 100%; + } + } + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.spec.ts new file mode 100644 index 0000000000..5f991f7f3b --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.spec.ts @@ -0,0 +1,55 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, convertToParamMap } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDecisionV2Service } from '../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentDetailService } from '../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice-of-intent.dto'; + +import { ConditionsComponent } from './conditions.component'; + +describe('ConditionsComponent', () => { + let component: ConditionsComponent; + let fixture: ComponentFixture; + let mockNOIDetailService: DeepMocked; + let mockNOIV2DecisionService: DeepMocked; + + beforeEach(async () => { + mockNOIDetailService = createMock(); + mockNOIDetailService.$noticeOfIntent = new BehaviorSubject(undefined); + + mockNOIV2DecisionService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ConditionsComponent], + providers: [ + { + provide: NoticeOfIntentDetailService, + useValue: mockNOIDetailService, + }, + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockNOIV2DecisionService, + }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({ uuid: 'fake' }), + }, + }, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ConditionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts new file mode 100644 index 0000000000..0d64ea2980 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts @@ -0,0 +1,167 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import moment from 'moment'; +import { Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentDecisionV2Service } from '../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { + NoticeOfIntentDecisionCodesDto, + NoticeOfIntentDecisionConditionDto, + NoticeOfIntentDecisionWithLinkedResolutionDto, +} from '../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentDetailService } from '../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice-of-intent.dto'; +import { + DRAFT_DECISION_TYPE_LABEL, + MODIFICATION_TYPE_LABEL, + RECON_TYPE_LABEL, + RELEASED_DECISION_TYPE_LABEL, +} from '../../../../shared/application-type-pill/application-type-pill.constants'; + +export type ConditionComponentLabels = { + label: string[]; + componentUuid: string; + conditionUuid: string; +}; + +export type DecisionConditionWithStatus = NoticeOfIntentDecisionConditionDto & { + conditionComponentsLabels?: ConditionComponentLabels[]; + status: string; +}; + +export type DecisionWithConditionComponentLabels = NoticeOfIntentDecisionWithLinkedResolutionDto & { + conditions: DecisionConditionWithStatus[]; +}; + +export const CONDITION_STATUS = { + INCOMPLETE: 'incomplete', + COMPLETE: 'complete', + SUPERSEDED: 'superseded', +}; + +@Component({ + selector: 'app-conditions', + templateUrl: './conditions.component.html', + styleUrls: ['./conditions.component.scss'], +}) +export class ConditionsComponent implements OnInit { + $destroy = new Subject(); + + decisionUuid: string = ''; + fileNumber: string = ''; + decisions: DecisionWithConditionComponentLabels[] = []; + decision!: DecisionWithConditionComponentLabels; + noticeOfIntent: NoticeOfIntentDto | undefined; + codes!: NoticeOfIntentDecisionCodesDto; + today!: number; + + dratDecisionLabel = DRAFT_DECISION_TYPE_LABEL; + releasedDecisionLabel = RELEASED_DECISION_TYPE_LABEL; + reconLabel = RECON_TYPE_LABEL; + modificationLabel = MODIFICATION_TYPE_LABEL; + + constructor( + private noticeOfIntentDetailService: NoticeOfIntentDetailService, + private decisionService: NoticeOfIntentDecisionV2Service, + private activatedRouter: ActivatedRoute + ) { + this.today = moment().startOf('day').toDate().getTime(); + } + + ngOnInit(): void { + this.fileNumber = this.activatedRouter.parent?.parent?.snapshot.paramMap.get('fileNumber')!; + this.decisionUuid = this.activatedRouter.snapshot.paramMap.get('uuid')!; + + this.noticeOfIntentDetailService.$noticeOfIntent.pipe(takeUntil(this.$destroy)).subscribe((noticeOfIntent) => { + if (noticeOfIntent) { + this.noticeOfIntent = noticeOfIntent; + this.loadDecisions(noticeOfIntent.fileNumber); + } + }); + } + + async loadDecisions(fileNumber: string) { + this.codes = await this.decisionService.fetchCodes(); + this.decisionService.$decisions.pipe(takeUntil(this.$destroy)).subscribe((decisions) => { + this.decisions = decisions.map((decision) => { + if (decision.uuid === this.decisionUuid) { + const conditions = this.mapConditions(decision, decisions); + + this.sortConditions(decision, conditions); + + this.decision = decision as DecisionWithConditionComponentLabels; + } + + return decision as DecisionWithConditionComponentLabels; + }); + }); + + this.decisionService.loadDecisions(fileNumber); + } + + private sortConditions( + decision: NoticeOfIntentDecisionWithLinkedResolutionDto, + conditions: DecisionConditionWithStatus[] + ) { + decision.conditions = conditions.sort((a, b) => { + const order = [CONDITION_STATUS.INCOMPLETE, CONDITION_STATUS.COMPLETE, CONDITION_STATUS.SUPERSEDED]; + if (a.status === b.status) { + if (a.type && b.type) { + return a.type?.label.localeCompare(b.type.label); + } else { + return -1; + } + } else { + return order.indexOf(a.status) - order.indexOf(b.status); + } + }); + } + + private mapConditions( + decision: NoticeOfIntentDecisionWithLinkedResolutionDto, + decisions: NoticeOfIntentDecisionWithLinkedResolutionDto[] + ) { + return decision.conditions.map((condition) => { + const status = this.getStatus(condition, decision); + + return { + ...condition, + status, + conditionComponentsLabels: condition.components?.map((c) => { + const matchingType = this.codes.decisionComponentTypes.find( + (type) => type.code === c.noticeOfIntentDecisionComponentTypeCode + ); + + const componentsDecision = decisions.find((d) => d.uuid === c.noticeOfIntentDecisionUuid); + + if (componentsDecision) { + decision = componentsDecision; + } + + const label = + decision.resolutionNumber && decision.resolutionYear + ? `#${decision.resolutionNumber}/${decision.resolutionYear} ${matchingType?.label}` + : `Draft ${matchingType?.label}`; + + return { label, conditionUuid: condition.uuid, componentUuid: c.uuid }; + }), + } as DecisionConditionWithStatus; + }); + } + + private getStatus( + condition: NoticeOfIntentDecisionConditionDto, + decision: NoticeOfIntentDecisionWithLinkedResolutionDto + ) { + let status = ''; + if (condition.supersededDate && condition.supersededDate <= this.today) { + status = CONDITION_STATUS.SUPERSEDED; + } else if (condition.completionDate && condition.completionDate <= this.today) { + status = CONDITION_STATUS.COMPLETE; + } else if (!decision.isDraft) { + status = CONDITION_STATUS.INCOMPLETE; + } else { + status = ''; + } + return status; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-dialog/decision-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-dialog/decision-dialog.component.ts index 64224ba858..21aaa1d4fa 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-dialog/decision-dialog.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-dialog/decision-dialog.component.ts @@ -118,18 +118,16 @@ export class DecisionDialogComponent implements OnInit { outcomeCode: outcome!, decisionMaker: decisionMaker!, decisionMakerName: decisionMakerName!, - applicationFileNumber: this.data.fileNumber, + fileNumber: this.data.fileNumber, modifiesUuid: postDecision ?? undefined, + isDraft: false, }; try { if (this.data.existingDecision) { await this.decisionService.update(this.data.existingDecision.uuid, data); } else { - await this.decisionService.create({ - ...data, - applicationFileNumber: this.data.fileNumber, - }); + await this.decisionService.create(data); } this.dialogRef.close(true); } finally { diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.html new file mode 100644 index 0000000000..6992915501 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.html @@ -0,0 +1,105 @@ +
+

Decision

+
+ +
+
+
+
No Decisions
+
+
+
+ +
+
+
+
+ Modified By:  + {{ decision.modifiedByResolutions.join(', ') }} + N/A +
+
+ +
+ + + +
+
+
+ Decision #{{ decisions.length - i }} + + + calendar_month + {{ noticeOfIntent.activeDays >= MAX_ACTIVE_DAYS ? '61+' : noticeOfIntent.activeDays }} + + + + + + Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }} +
+
+
+
Decision Date
+ {{ decision.date | momentFormat }} +
+
+
Decision Outcome
+ {{ decision.outcome.label }} +
+
+
Decision Maker
+ {{ decision.decisionMaker }} +
+
+
Decision Maker Name
+ {{ decision.decisionMakerName ?? 'No Data' }} +
+
+
Audit Date
+ {{ decision.auditDate | momentFormat }} + + + +
+
+
Documents
+
+
File Name
+
File Upload Date
+
File Actions
+
No Documents
+ + +
{{ document.uploadedAt | momentFormat }}
+
+ + +
+
+
+
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.scss new file mode 100644 index 0000000000..f80d0c776b --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.scss @@ -0,0 +1,142 @@ +@use '../../../../../styles/colors'; + +h3 { + margin-bottom: 24px !important; +} + +section { + margin-bottom: 64px; +} + +.decision-container { + position: relative; +} + +.decision { + margin: 24px 0; + box-shadow: 0 2px 8px 1px rgba(0, 0, 0, 0.25); +} + +.loading-overlay { + position: absolute; + z-index: 2; + background-color: colors.$grey; + opacity: 0.4; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.days { + display: inline-block; + margin-right: 16px; + margin-left: 16px; + + .mat-icon { + font-size: 19px !important; + line-height: 21px !important; + width: 19px; + vertical-align: middle; + } +} + +.post-decisions { + padding: 6px 32px; + background-color: colors.$grey-light; + grid-template-columns: 1fr 1fr; + display: grid; + min-height: 36px; + text-transform: uppercase; + border-radius: 4px 4px 0 0; + box-shadow: -1px 1px 4px rgba(0, 0, 0, 0.25); +} + +.decision-menu { + position: absolute; + top: 0; + right: 0; + height: 36px; + background: colors.$accent-color; + box-shadow: -1px 1px 4px rgba(0, 0, 0, 0.25); + border-radius: 0 4px 0 10px; + + button { + color: colors.$white; + width: 36px; + height: 36px; + line-height: 36px; + } + + mat-icon { + position: absolute; + top: 8px; + left: 8px; + font-size: 21px; + width: 20px; + height: 36px; + } +} + +.decision-padding { + padding: 18px 32px 32px 32px; +} + +.decision-content { + margin-top: 16px; + margin-bottom: 32px; + margin-right: 100px; + display: grid; + grid-template-columns: 1fr 1fr; + grid-row-gap: 24px; + + .subheading2 { + margin-bottom: 6px !important; + } + + & > div { + font-size: 16px; + } +} + +.decision-documents { + margin-top: 4px; + display: grid; + grid-template-columns: 1fr 1fr 100px; + grid-row-gap: 4px; + + div { + display: flex; + align-items: center; + font-size: 16px !important; + } +} + +.file-actions { + margin-left: -12px; +} + +.delete-file-icon { + color: colors.$error-color; +} + +.no-decisions { + margin-top: 16px; + display: flex; + align-items: center; + justify-content: center; + background-color: colors.$grey-light; + height: 72px; +} + +.no-files { + grid-column: 1/4; + margin-top: 16px; + padding: 16px; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + background-color: colors.$grey-light; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.spec.ts new file mode 100644 index 0000000000..900a5dcadc --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.spec.ts @@ -0,0 +1,64 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDecisionService } from '../../../../services/notice-of-intent/decision/notice-of-intent-decision.service'; +import { NoticeOfIntentDetailService } from '../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice-of-intent.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { DecisionV1Component } from './decision-v1.component'; + +describe('DecisionComponent', () => { + let component: DecisionV1Component; + let fixture: ComponentFixture; + let mockNOIDetailService: DeepMocked; + + beforeEach(async () => { + mockNOIDetailService = createMock(); + mockNOIDetailService.$noticeOfIntent = new BehaviorSubject(undefined); + + await TestBed.configureTestingModule({ + imports: [MatSnackBarModule], + declarations: [DecisionV1Component], + providers: [ + { + provide: NoticeOfIntentDetailService, + useValue: mockNOIDetailService, + }, + { + provide: NoticeOfIntentDecisionService, + useValue: {}, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, + { + provide: ToastService, + useValue: {}, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionV1Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.ts new file mode 100644 index 0000000000..22c087f4c1 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.ts @@ -0,0 +1,208 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Subject, takeUntil } from 'rxjs'; +import { + NoticeOfIntentDecisionDto, + NoticeOfIntentDecisionOutcomeCodeDto, +} from '../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentDecisionService } from '../../../../services/notice-of-intent/decision/notice-of-intent-decision.service'; +import { NoticeOfIntentDetailService } from '../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice-of-intent.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { MODIFICATION_TYPE_LABEL } from '../../../../shared/application-type-pill/application-type-pill.constants'; +import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { formatDateForApi } from '../../../../shared/utils/api-date-formatter'; +import { DecisionDialogComponent } from '../decision-dialog/decision-dialog.component'; + +type LoadingDecision = NoticeOfIntentDecisionDto & { + modifiedByResolutions: string[]; + loading: boolean; +}; + +@Component({ + selector: 'app-noi-decision-v1', + templateUrl: './decision-v1.component.html', + styleUrls: ['./decision-v1.component.scss'], +}) +export class DecisionV1Component implements OnInit, OnDestroy { + $destroy = new Subject(); + fileNumber: string = ''; + decisionDate: number | undefined; + decisions: LoadingDecision[] = []; + outcomes: NoticeOfIntentDecisionOutcomeCodeDto[] = []; + isPaused = true; + MAX_ACTIVE_DAYS = 61; + + noticeOfIntent: NoticeOfIntentDto | undefined; + modificationLabel = MODIFICATION_TYPE_LABEL; + + constructor( + public dialog: MatDialog, + private noticeOfIntentDetailService: NoticeOfIntentDetailService, + private decisionService: NoticeOfIntentDecisionService, + private toastService: ToastService, + private confirmationDialogService: ConfirmationDialogService + ) {} + + ngOnInit(): void { + this.noticeOfIntentDetailService.$noticeOfIntent.pipe(takeUntil(this.$destroy)).subscribe((noticeOfIntent) => { + if (noticeOfIntent) { + this.fileNumber = noticeOfIntent.fileNumber; + this.decisionDate = noticeOfIntent.decisionDate; + this.isPaused = noticeOfIntent.paused; + this.loadDecisions(noticeOfIntent.fileNumber); + this.noticeOfIntent = noticeOfIntent; + } + }); + } + + async loadDecisions(fileNumber: string) { + const codes = await this.decisionService.fetchCodes(); + this.outcomes = codes.outcomes; + + const loadedDecision = await this.decisionService.fetchByFileNumber(fileNumber); + + this.decisions = loadedDecision.map((decision) => ({ + ...decision, + loading: false, + modifiedByResolutions: decision.modifiedBy?.flatMap((r) => r.linkedResolutions) || [], + })); + } + + onCreate() { + let minDate = new Date(0); + if (this.decisions.length > 0) { + minDate = new Date(this.decisions[this.decisions.length - 1].date); + } + + this.dialog + .open(DecisionDialogComponent, { + minWidth: '600px', + maxWidth: '900px', + maxHeight: '80vh', + width: '90%', + autoFocus: false, + data: { + isFirstDecision: this.decisions.length === 0, + minDate, + fileNumber: this.fileNumber, + outcomes: this.outcomes, + }, + }) + .afterClosed() + .subscribe((didCreate) => { + if (didCreate) { + this.noticeOfIntentDetailService.load(this.fileNumber); + } + }); + } + + onEdit(decision: LoadingDecision) { + const decisionIndex = this.decisions.indexOf(decision); + let minDate = new Date(0); + if (decisionIndex !== this.decisions.length - 1) { + minDate = new Date(this.decisions[this.decisions.length - 1].date); + } + this.dialog + .open(DecisionDialogComponent, { + minWidth: '600px', + maxWidth: '900px', + maxHeight: '80vh', + width: '90%', + autoFocus: false, + data: { + isFirstDecision: decisionIndex === this.decisions.length - 1, + minDate, + fileNumber: this.fileNumber, + outcomes: this.outcomes, + existingDecision: decision, + }, + }) + .afterClosed() + .subscribe((didModify) => { + if (didModify) { + this.noticeOfIntentDetailService.load(this.fileNumber); + } + }); + } + + async deleteDecision(uuid: string) { + this.confirmationDialogService + .openDialog({ + body: 'Are you sure you want to delete the selected decision?', + }) + .subscribe(async (confirmed) => { + if (confirmed) { + this.decisions = this.decisions.map((decision) => { + return { + ...decision, + loading: decision.uuid === uuid, + }; + }); + await this.decisionService.delete(uuid); + await this.noticeOfIntentDetailService.load(this.fileNumber); + this.toastService.showSuccessToast('Decision deleted'); + } + }); + } + + async attachFile(decisionUuid: string, event: Event) { + this.decisions = this.decisions.map((decision) => { + return { + ...decision, + loading: decision.uuid === decisionUuid, + }; + }); + const element = event.target as HTMLInputElement; + const fileList = element.files; + if (fileList && fileList.length > 0) { + const file: File = fileList[0]; + const uploadedFile = await this.decisionService.uploadFile(decisionUuid, file); + if (uploadedFile) { + await this.loadDecisions(this.fileNumber); + } + } + } + + async downloadFile(decisionUuid: string, decisionDocumentUuid: string, fileName: string) { + await this.decisionService.downloadFile(decisionUuid, decisionDocumentUuid, fileName, false); + } + + async openFile(decisionUuid: string, decisionDocumentUuid: string, fileName: string) { + await this.decisionService.downloadFile(decisionUuid, decisionDocumentUuid, fileName); + } + + async deleteFile(decisionUuid: string, decisionDocumentUuid: string, fileName: string) { + this.confirmationDialogService + .openDialog({ + body: `Are you sure you want to delete the file ${fileName}?`, + }) + .subscribe(async (confirmed) => { + if (confirmed) { + this.decisions = this.decisions.map((decision) => { + return { + ...decision, + loading: decision.uuid === decisionUuid, + }; + }); + + await this.decisionService.deleteFile(decisionUuid, decisionDocumentUuid); + await this.loadDecisions(this.fileNumber); + this.toastService.showSuccessToast('File deleted'); + } + }); + } + + async onSaveAuditDate(decisionUuid: string, auditReviewDate: number) { + await this.decisionService.update(decisionUuid, { + isDraft: false, + auditDate: formatDateForApi(auditReviewDate), + }); + await this.loadDecisions(this.fileNumber); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.html new file mode 100644 index 0000000000..995cbe4c53 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.html @@ -0,0 +1,7 @@ +
+
ALR Area Impacted (ha)
+ +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.spec.ts new file mode 100644 index 0000000000..f31dc65a8a --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.spec.ts @@ -0,0 +1,26 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoticeOfIntentDecisionComponentDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; + +import { BasicComponent } from './basic.component'; + +describe('BasicComponent', () => { + let component: BasicComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [BasicComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(BasicComponent); + component = fixture.componentInstance; + component.component = {} as NoticeOfIntentDecisionComponentDto; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.ts new file mode 100644 index 0000000000..d642d55165 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/basic/basic.component.ts @@ -0,0 +1,19 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { NoticeOfIntentDecisionComponentDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; + +@Component({ + selector: 'app-basic', + templateUrl: './basic.component.html', + styleUrls: ['./basic.component.scss'], +}) +export class BasicComponent { + @Input() component!: NoticeOfIntentDecisionComponentDto; + @Input() fillRow = false; + @Output() saveAlrArea = new EventEmitter(); + + constructor() {} + + async onSaveAlrArea(alrArea: string | null) { + this.saveAlrArea.emit(alrArea); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.html new file mode 100644 index 0000000000..87b04a95bc --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.html @@ -0,0 +1,112 @@ + +
+
Use End Date
+ {{ component.endDate | date }} + +
+
+
Type of soil approved to be removed
+
{{ component.soilTypeRemoved }}
+ +
+
+
Type, origin and quality of fill proposed to be placed
+
{{ component.soilFillTypeToPlace }}
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Soil to be RemovedFill to be Placed
Volume + {{ component.soilToRemoveVolume }} m3 + + + {{ component.soilToPlaceVolume }} m3 + +
+ Area +
Note: 0.01 ha is 100m2
+
+ + {{ component.soilToRemoveArea }} ha + + + {{ component.soilToPlaceArea }} ha + +
Maximum Depth + {{ component.soilToRemoveMaximumDepth }} m + + + {{ component.soilToPlaceMaximumDepth }} m + +
Average Depth + {{ component.soilToRemoveAverageDepth }} m + + + + {{ component.soilToPlaceAverageDepth }} m + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts new file mode 100644 index 0000000000..c1a21e1f61 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.spec.ts @@ -0,0 +1,26 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoticeOfIntentDecisionComponentDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; + +import { PfrsComponent } from './pfrs.component'; + +describe('PfrsComponent', () => { + let component: PfrsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PfrsComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PfrsComponent); + component = fixture.componentInstance; + component.component = {} as NoticeOfIntentDecisionComponentDto; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.ts new file mode 100644 index 0000000000..b97670a565 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pfrs/pfrs.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { NoticeOfIntentDecisionComponentDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; + +@Component({ + selector: 'app-pfrs', + templateUrl: './pfrs.component.html', + styleUrls: ['./pfrs.component.scss'], +}) +export class PfrsComponent { + @Input() component!: NoticeOfIntentDecisionComponentDto; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.html new file mode 100644 index 0000000000..f6960c3d2b --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.html @@ -0,0 +1,70 @@ + +
+
Use End Date
+ {{ component.endDate | date }} + +
+
+
Type, origin and quality of fill proposed to be placed
+
{{ component.soilFillTypeToPlace }}
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Fill to be Placed
Volume + + {{ component.soilToPlaceVolume }} m3 + + +
+ Area +
Note: 0.01 ha is 100m2
+
+ + {{ component.soilToPlaceArea }} ha + + +
Maximum Depth + + {{ component.soilToPlaceMaximumDepth }} m + + +
Average Depth + {{ component.soilToPlaceAverageDepth }} m + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts new file mode 100644 index 0000000000..f0e65a2b36 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.spec.ts @@ -0,0 +1,26 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoticeOfIntentDecisionComponentDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; + +import { PofoComponent } from './pofo.component'; + +describe('PofoComponent', () => { + let component: PofoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PofoComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PofoComponent); + component = fixture.componentInstance; + component.component = {} as NoticeOfIntentDecisionComponentDto; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.ts new file mode 100644 index 0000000000..d8082846da --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { NoticeOfIntentDecisionComponentDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; + +@Component({ + selector: 'app-pofo', + templateUrl: './pofo.component.html', + styleUrls: ['./pofo.component.scss'], +}) +export class PofoComponent { + @Input() component!: NoticeOfIntentDecisionComponentDto; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.html new file mode 100644 index 0000000000..52a9c85e0e --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.html @@ -0,0 +1,65 @@ + +
+
Use End Date
+ {{ component.endDate | date }} + +
+
+
Type of soil approved to be removed
+
{{ component.soilTypeRemoved }}
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Soil to be Removed
Volume + {{ component.soilToRemoveVolume }} m3 + +
Area + + {{ component.soilToRemoveArea }} ha + + +
Maximum Depth + {{ component.soilToRemoveMaximumDepth }} m + +
Average Depth + {{ component.soilToRemoveAverageDepth }} m + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.spec.ts new file mode 100644 index 0000000000..d89da3b470 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.spec.ts @@ -0,0 +1,26 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NoticeOfIntentDecisionComponentDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; + +import { RosoComponent } from './roso.component'; + +describe('RosoComponent', () => { + let component: RosoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RosoComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(RosoComponent); + component = fixture.componentInstance; + component.component = {} as NoticeOfIntentDecisionComponentDto; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.ts new file mode 100644 index 0000000000..773423f87f --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { NoticeOfIntentDecisionComponentDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; + +@Component({ + selector: 'app-roso', + templateUrl: './roso.component.html', + styleUrls: ['./roso.component.scss'], +}) +export class RosoComponent { + @Input() component!: NoticeOfIntentDecisionComponentDto; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.html new file mode 100644 index 0000000000..cb3638941a --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.html @@ -0,0 +1,70 @@ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDECPACKDocument Name + {{ element.fileName }} + SourceALC + Visibility +
* = Pending
+
+ A*, + C*, + G*, + P* + Upload Date{{ element.uploadedAt | date }}File Actions + + + +
No Documents
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.scss new file mode 100644 index 0000000000..dbb564d787 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.scss @@ -0,0 +1,37 @@ +@use '../../../../../../styles/colors'; + +.documents { + margin-top: 12px; + border-collapse: collapse; +} + +.upload-button { + width: 100%; + margin: 4px 0 12px !important; +} + +.mat-mdc-no-data-row { + height: 56px; + color: colors.$grey-dark; +} + +a { + word-break: break-all; +} + +table { + box-shadow: none; + + th { + font-weight: 700; + padding-bottom: 16px; + position: relative; + + .subheading { + font-size: 11px; + line-height: 16px; + font-weight: 400; + position: absolute; + } + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.spec.ts new file mode 100644 index 0000000000..2eb09e08dc --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.spec.ts @@ -0,0 +1,52 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentDecisionDto } from '../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { ToastService } from '../../../../../services/toast/toast.service'; + +import { DecisionDocumentsComponent } from './decision-documents.component'; + +describe('DecisionDocumentsComponent', () => { + let component: DecisionDocumentsComponent; + let fixture: ComponentFixture; + let mockNOIDecService: DeepMocked; + let mockDialog: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(async () => { + mockNOIDecService = createMock(); + mockDialog = createMock(); + mockToastService = createMock(); + mockNOIDecService.$decision = new BehaviorSubject(undefined); + + await TestBed.configureTestingModule({ + declarations: [DecisionDocumentsComponent], + providers: [ + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockNOIDecService, + }, + { + provide: MatDialog, + useValue: mockDialog, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionDocumentsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts new file mode 100644 index 0000000000..70aa70d30d --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts @@ -0,0 +1,114 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { Subject, takeUntil } from 'rxjs'; +import { DecisionDocumentDto } from '../../../../../services/application/decision/application-decision-v1/application-decision.dto'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentDecisionDto } from '../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { ToastService } from '../../../../../services/toast/toast.service'; +import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { DecisionDocumentUploadDialogComponent } from '../decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component'; + +@Component({ + selector: 'app-decision-documents', + templateUrl: './decision-documents.component.html', + styleUrls: ['./decision-documents.component.scss'], +}) +export class DecisionDocumentsComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + + @Input() editable = true; + @Input() loadData = true; + @Input() decision: NoticeOfIntentDecisionDto | undefined; + @Input() showError = false; + @Output() beforeDocumentUpload = new EventEmitter(); + + displayedColumns: string[] = ['type', 'fileName', 'source', 'visibilityFlags', 'uploadedAt', 'actions']; + documents: DecisionDocumentDto[] = []; + private fileId = ''; + + @ViewChild(MatSort) sort!: MatSort; + dataSource: MatTableDataSource = new MatTableDataSource(); + + constructor( + private decisionService: NoticeOfIntentDecisionV2Service, + private dialog: MatDialog, + private toastService: ToastService, + private confirmationDialogService: ConfirmationDialogService + ) {} + + ngOnInit(): void { + if (this.decision && !this.loadData) { + this.dataSource = new MatTableDataSource(this.decision.documents); + } + this.decisionService.$decision.pipe(takeUntil(this.$destroy)).subscribe((decision) => { + if (decision) { + this.dataSource = new MatTableDataSource(decision.documents); + this.decision = decision; + } + }); + } + + async openFile(fileUuid: string, fileName: string) { + if (this.decision) { + await this.decisionService.downloadFile(this.decision.uuid, fileUuid, fileName); + } + } + + async downloadFile(fileUuid: string, fileName: string) { + if (this.decision) { + await this.decisionService.downloadFile(this.decision.uuid, fileUuid, fileName, false); + } + } + + async onUploadFile() { + this.beforeDocumentUpload.emit(); + this.openFileDialog(); + } + + onEditFile(element: DecisionDocumentDto) { + this.openFileDialog(element); + } + + private openFileDialog(existingDocument?: DecisionDocumentDto) { + if (this.decision) { + this.dialog + .open(DecisionDocumentUploadDialogComponent, { + minWidth: '600px', + maxWidth: '800px', + width: '70%', + data: { + fileId: this.fileId, + decisionUuid: this.decision?.uuid, + existingDocument: existingDocument, + }, + }) + .beforeClosed() + .subscribe((isDirty: boolean) => { + if (isDirty && this.decision) { + this.decisionService.loadDecision(this.decision.uuid); + } + }); + } + } + + onDeleteFile(element: DecisionDocumentDto) { + this.confirmationDialogService + .openDialog({ + body: 'Are you sure you want to delete the selected file?', + }) + .subscribe(async (accepted) => { + if (accepted && this.decision) { + await this.decisionService.deleteFile(this.decision.uuid, element.uuid); + await this.decisionService.loadDecision(this.decision.uuid); + this.toastService.showSuccessToast('Document deleted'); + } + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html new file mode 100644 index 0000000000..f8601a7b5a --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html @@ -0,0 +1,57 @@ +
+
{{ data.noticeOfIntentDecisionComponentType?.label }}
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+ + + Ag Cap Map + + + + + Ag Cap Consultant + + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.spec.ts new file mode 100644 index 0000000000..a8d6f1024d --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.spec.ts @@ -0,0 +1,32 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ToastService } from '../../../../../../../services/toast/toast.service'; + +import { DecisionComponentComponent } from './decision-component.component'; + +describe('DecisionComponentComponent', () => { + let component: DecisionComponentComponent; + let fixture: ComponentFixture; + let mockToastService: DeepMocked; + + beforeEach(async () => { + mockToastService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [DecisionComponentComponent], + providers: [{ provide: ToastService, useValue: mockToastService }], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionComponentComponent); + component = fixture.componentInstance; + component.data = { noticeOfIntentDecisionComponentTypeCode: '' }; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts new file mode 100644 index 0000000000..4d6e98e313 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.ts @@ -0,0 +1,213 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { + NOI_DECISION_COMPONENT_TYPE, + NoticeOfIntentDecisionCodesDto, + NoticeOfIntentDecisionComponentDto, + NoticeOfIntentDecisionComponentTypeDto, + PofoDecisionComponentDto, + RosoDecisionComponentDto, +} from '../../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { ToastService } from '../../../../../../../services/toast/toast.service'; +import { AG_CAP_OPTIONS, AG_CAP_SOURCE_OPTIONS } from '../../../../../../../shared/dto/ag-cap.types.dto'; +import { formatDateForApi } from '../../../../../../../shared/utils/api-date-formatter'; + +@Component({ + selector: 'app-decision-component', + templateUrl: './decision-component.component.html', + styleUrls: ['./decision-component.component.scss'], +}) +export class DecisionComponentComponent implements OnInit { + @Input() data!: NoticeOfIntentDecisionComponentDto; + @Input() codes!: NoticeOfIntentDecisionCodesDto; + @Output() dataChange = new EventEmitter(); + @Output() remove = new EventEmitter(); + + COMPONENT_TYPE = NOI_DECISION_COMPONENT_TYPE; + + agCapOptions = AG_CAP_OPTIONS; + agCapSourceOptions = AG_CAP_SOURCE_OPTIONS; + + // pofo, pfrs + fillTypeToPlace = new FormControl(null, [Validators.required]); + volumeToPlace = new FormControl(null, [Validators.required]); + areaToPlace = new FormControl(null, [Validators.required]); + maximumDepthToPlace = new FormControl(null, [Validators.required]); + averageDepthToPlace = new FormControl(null, [Validators.required]); + + // roso, pfrs + soilTypeRemoved = new FormControl(null, [Validators.required]); + volumeToRemove = new FormControl(null, [Validators.required]); + areaToRemove = new FormControl(null, [Validators.required]); + maximumDepthToRemove = new FormControl(null, [Validators.required]); + averageDepthToRemove = new FormControl(null, [Validators.required]); + + // general + endDate = new FormControl(null, [Validators.required]); + alrArea = new FormControl(null, [Validators.required]); + agCap = new FormControl(null, [Validators.required]); + agCapSource = new FormControl(null, [Validators.required]); + agCapMap = new FormControl(null); + agCapConsultant = new FormControl(null); + + form: FormGroup = new FormGroup({ + alrArea: this.alrArea, + agCap: this.agCap, + agCapSource: this.agCapSource, + agCapMap: this.agCapMap, + agCapConsultant: this.agCapConsultant, + }); + + constructor(private toastService: ToastService) {} + + ngOnInit(): void { + if (this.data) { + this.alrArea.setValue(this.data.alrArea ? this.data.alrArea : null); + this.agCap.setValue(this.data.agCap ? this.data.agCap : null); + this.agCapSource.setValue(this.data.agCapSource ? this.data.agCapSource : null); + this.agCapMap.setValue(this.data.agCapMap ? this.data.agCapMap : null); + this.agCapConsultant.setValue(this.data.agCapConsultant ? this.data.agCapConsultant : null); + + switch (this.data.noticeOfIntentDecisionComponentTypeCode) { + case NOI_DECISION_COMPONENT_TYPE.POFO: + this.patchPofoFields(); + break; + case NOI_DECISION_COMPONENT_TYPE.ROSO: + this.patchRosoFields(); + break; + case NOI_DECISION_COMPONENT_TYPE.PFRS: + this.patchPofoFields(); + this.patchRosoFields(); + break; + default: + this.toastService.showErrorToast('Wrong decision component type'); + break; + } + } + + this.onFormValueChanges(); + } + + private onFormValueChanges() { + this.form.valueChanges.subscribe((changes) => { + let dataChange = { + alrArea: this.alrArea.value ? this.alrArea.value : null, + agCap: this.agCap.value ? this.agCap.value : null, + agCapSource: this.agCapSource.value ? this.agCapSource.value : null, + agCapMap: this.agCapMap.value ? this.agCapMap.value : null, + agCapConsultant: this.agCapConsultant.value ? this.agCapConsultant.value : null, + noticeOfIntentDecisionComponentTypeCode: this.data.noticeOfIntentDecisionComponentTypeCode, + noticeOfIntentDecisionComponentType: this.codes.decisionComponentTypes.find( + (e) => e.code === this.data.noticeOfIntentDecisionComponentTypeCode + )!, + noticeOfIntentDecisionUuid: this.data.uuid, + uuid: this.data.uuid, + }; + + const componentData = this.getComponentData(dataChange); + this.dataChange.emit(componentData); + }); + } + + private getComponentData(dataChange: { + alrArea: number | null; + agCap: string | null; + agCapSource: string | null; + agCapMap: string | null; + agCapConsultant: string | null; + noticeOfIntentDecisionComponentTypeCode: string; + noticeOfIntentDecisionComponentType: NoticeOfIntentDecisionComponentTypeDto; + noticeOfIntentDecisionUuid: string | undefined; + uuid: string | undefined; + }): NoticeOfIntentDecisionComponentDto { + switch (dataChange.noticeOfIntentDecisionComponentTypeCode) { + case NOI_DECISION_COMPONENT_TYPE.POFO: + dataChange = { ...dataChange, ...this.getPofoDataChange() }; + break; + case NOI_DECISION_COMPONENT_TYPE.ROSO: + dataChange = { ...dataChange, ...this.getRosoDataChange() }; + break; + case NOI_DECISION_COMPONENT_TYPE.PFRS: + dataChange = { ...dataChange, ...this.getPfrsDataChange() }; + break; + default: + this.toastService.showErrorToast('Wrong decision component type'); + break; + } + return dataChange; + } + + private patchPofoFields() { + this.form.addControl('endDate', this.endDate); + this.form.addControl('fillTypeToPlace', this.fillTypeToPlace); + this.form.addControl('areaToPlace', this.areaToPlace); + this.form.addControl('volumeToPlace', this.volumeToPlace); + this.form.addControl('maximumDepthToPlace', this.maximumDepthToPlace); + this.form.addControl('averageDepthToPlace', this.averageDepthToPlace); + + this.endDate.setValue(this.data.endDate ? new Date(this.data.endDate) : null); + this.fillTypeToPlace.setValue(this.data.soilFillTypeToPlace ?? null); + this.areaToPlace.setValue(this.data.soilToPlaceArea ?? null); + this.volumeToPlace.setValue(this.data.soilToPlaceVolume ?? null); + this.maximumDepthToPlace.setValue(this.data.soilToPlaceMaximumDepth ?? null); + this.averageDepthToPlace.setValue(this.data.soilToPlaceAverageDepth ?? null); + } + + private patchRosoFields() { + this.form.addControl('endDate', this.endDate); + this.form.addControl('soilTypeRemoved', this.soilTypeRemoved); + this.form.addControl('areaToRemove', this.areaToRemove); + this.form.addControl('volumeToRemove', this.volumeToRemove); + this.form.addControl('maximumDepthToRemove', this.maximumDepthToRemove); + this.form.addControl('averageDepthToRemove', this.averageDepthToRemove); + + this.endDate.setValue(this.data.endDate ? new Date(this.data.endDate) : null); + this.soilTypeRemoved.setValue(this.data.soilTypeRemoved ?? null); + this.areaToRemove.setValue(this.data.soilToRemoveArea ?? null); + this.volumeToRemove.setValue(this.data.soilToRemoveVolume ?? null); + this.maximumDepthToRemove.setValue(this.data.soilToRemoveMaximumDepth ?? null); + this.averageDepthToRemove.setValue(this.data.soilToRemoveAverageDepth ?? null); + } + + private getPofoDataChange(): PofoDecisionComponentDto { + return { + endDate: this.endDate.value ? formatDateForApi(this.endDate.value) : null, + soilFillTypeToPlace: this.fillTypeToPlace.value ?? null, + soilToPlaceArea: this.areaToPlace.value ?? null, + soilToPlaceVolume: this.volumeToPlace.value ?? null, + soilToPlaceMaximumDepth: this.maximumDepthToPlace.value ?? null, + soilToPlaceAverageDepth: this.averageDepthToPlace.value ?? null, + }; + } + + private getRosoDataChange(): RosoDecisionComponentDto { + return { + endDate: this.endDate.value ? formatDateForApi(this.endDate.value) : null, + soilTypeRemoved: this.soilTypeRemoved.value ?? null, + soilToRemoveArea: this.areaToRemove.value ?? null, + soilToRemoveVolume: this.volumeToRemove.value ?? null, + soilToRemoveMaximumDepth: this.maximumDepthToRemove.value ?? null, + soilToRemoveAverageDepth: this.averageDepthToRemove.value ?? null, + }; + } + + private getPfrsDataChange(): RosoDecisionComponentDto & PofoDecisionComponentDto { + return { + endDate: this.endDate.value ? formatDateForApi(this.endDate.value) : null, + soilTypeRemoved: this.soilTypeRemoved.value ?? null, + soilToRemoveArea: this.areaToRemove.value ?? null, + soilToRemoveVolume: this.volumeToRemove.value ?? null, + soilToRemoveMaximumDepth: this.maximumDepthToRemove.value ?? null, + soilToRemoveAverageDepth: this.averageDepthToRemove.value ?? null, + soilFillTypeToPlace: this.fillTypeToPlace.value ?? null, + soilToPlaceArea: this.areaToPlace.value ?? null, + soilToPlaceVolume: this.volumeToPlace.value ?? null, + soilToPlaceMaximumDepth: this.maximumDepthToPlace.value ?? null, + soilToPlaceAverageDepth: this.averageDepthToPlace.value ?? null, + }; + } + + onRemove() { + this.remove.emit(); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html new file mode 100644 index 0000000000..5469971cd0 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html @@ -0,0 +1,180 @@ +
+
+ + ALR Area Impacted (ha) + + + + + Use End Date + + + + +
+ + + + + + + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+ + + m3 + +
+
+ + + m3 + +
+
+ +
Note: 0.01 ha is 100m2
+
+
+ + + ha + +
+
+ + + ha + +
+
+ +
+
+ + + m + +
+
+ + + m + +
+
+ +
+
+ + + m + +
+
+ + + m + +
+
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.spec.ts new file mode 100644 index 0000000000..2d8c4a3e16 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PfrsInputComponent } from './pfrs-input.component'; + +describe('PfrsInputComponent', () => { + let component: PfrsInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PfrsInputComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PfrsInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.ts new file mode 100644 index 0000000000..b1f91bb0b9 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-pfrs-input', + templateUrl: './pfrs-input.component.html', + styleUrls: ['./pfrs-input.component.scss'], +}) +export class PfrsInputComponent { + @Input() form!: FormGroup; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html new file mode 100644 index 0000000000..7f4a43207b --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html @@ -0,0 +1,114 @@ +
+
+ + ALR Area Impacted (ha) + + + + + Use End Date + + + + +
+ + + + + +
+
+
+
+ +
+
+ +
+
+ + + m3 + +
+
+ +
Note: 0.01 ha is 100m2
+
+
+ + + ha + +
+
+ +
+
+ + + m + +
+
+ +
+
+ + + m + +
+
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.spec.ts new file mode 100644 index 0000000000..bdc4bf380e --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PofoInputComponent } from './pofo-input.component'; + +describe('PofoInputComponent', () => { + let component: PofoInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PofoInputComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PofoInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.ts new file mode 100644 index 0000000000..dc8f525fdc --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-pofo-input', + templateUrl: './pofo-input.component.html', + styleUrls: ['./pofo-input.component.scss'], +}) +export class PofoInputComponent { + @Input() form!: FormGroup; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html new file mode 100644 index 0000000000..f175fb8b59 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html @@ -0,0 +1,114 @@ +
+
+ + ALR Area Impacted (ha) + + + + + Use End Date + + + + +
+ + + + + +
+
+
+
+ +
+
+ +
+
+ + + m3 + +
+
+ +
Note: 0.01 ha is 100m2
+
+
+ + + ha + +
+
+ +
+
+ + + m + +
+
+ +
+
+ + + m + +
+
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.spec.ts new file mode 100644 index 0000000000..1aa4815305 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.spec.ts @@ -0,0 +1,24 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RosoInputComponent } from './roso-input.component'; + +describe('RosoInputComponent', () => { + let component: RosoInputComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RosoInputComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(RosoInputComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.ts new file mode 100644 index 0000000000..cb18c63a8e --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'app-roso-input', + templateUrl: './roso-input.component.html', + styleUrls: ['./roso-input.component.scss'], +}) +export class RosoInputComponent { + @Input() form!: FormGroup; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.html new file mode 100644 index 0000000000..e331336783 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.html @@ -0,0 +1,39 @@ +
+
+

Components

+
+ + +
+
+ + + + + +
+ +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.scss new file mode 100644 index 0000000000..484d1ef16a --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.scss @@ -0,0 +1,8 @@ +@use '../../../../../../../styles/colors'; + +.component { + border-radius: 4px; + border: 1px solid colors.$grey; + padding: 16px; + margin-bottom: 48px; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.spec.ts new file mode 100644 index 0000000000..f76e8a6cbd --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.spec.ts @@ -0,0 +1,72 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatMenuModule } from '@angular/material/menu'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentDecisionDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentDetailService } from '../../../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentSubmissionService } from '../../../../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentDto } from '../../../../../../services/notice-of-intent/notice-of-intent.dto'; +import { ToastService } from '../../../../../../services/toast/toast.service'; +import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { DecisionComponentsComponent } from './decision-components.component'; + +describe('DecisionComponentsComponent', () => { + let component: DecisionComponentsComponent; + let fixture: ComponentFixture; + let mockNoticeOfIntentDecisionV2Service: DeepMocked; + let mockToastService: DeepMocked; + let mockNoticeOfIntentDetailService: DeepMocked; + let mockNoticeOfIntentSubmissionService: DeepMocked; + + beforeEach(async () => { + mockNoticeOfIntentDecisionV2Service = createMock(); + mockNoticeOfIntentDecisionV2Service.$decision = new BehaviorSubject( + undefined + ); + + mockToastService = createMock(); + mockNoticeOfIntentDetailService = createMock(); + mockNoticeOfIntentDetailService.$noticeOfIntent = new BehaviorSubject(undefined); + + mockNoticeOfIntentSubmissionService = createMock(); + + await TestBed.configureTestingModule({ + imports: [MatMenuModule], + declarations: [DecisionComponentsComponent], + providers: [ + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockNoticeOfIntentDecisionV2Service, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: NoticeOfIntentDetailService, + useValue: mockNoticeOfIntentDetailService, + }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoticeOfIntentSubmissionService, + }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionComponentsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts new file mode 100644 index 0000000000..57efe1cd61 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -0,0 +1,218 @@ +import { + AfterViewInit, + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + QueryList, + ViewChildren, +} from '@angular/core'; +import { Subject, takeUntil } from 'rxjs'; +import { + NOI_DECISION_COMPONENT_TYPE, + NoticeOfIntentDecisionCodesDto, + NoticeOfIntentDecisionComponentDto, + NoticeOfIntentDecisionComponentTypeDto, +} from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentDetailService } from '../../../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentSubmissionService } from '../../../../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; +import { + NoticeOfIntentDto, + NoticeOfIntentSubmissionDto, +} from '../../../../../../services/notice-of-intent/notice-of-intent.dto'; +import { ToastService } from '../../../../../../services/toast/toast.service'; +import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { DecisionComponentComponent } from './decision-component/decision-component.component'; + +export type DecisionComponentTypeMenuItem = NoticeOfIntentDecisionComponentTypeDto & { + isDisabled: boolean; + uiCode: string; +}; + +@Component({ + selector: 'app-decision-components', + templateUrl: './decision-components.component.html', + styleUrls: ['./decision-components.component.scss'], +}) +export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterViewInit { + $destroy = new Subject(); + + @Input() codes!: NoticeOfIntentDecisionCodesDto; + @Input() fileNumber!: string; + @Input() showError = false; + + @Input() components: NoticeOfIntentDecisionComponentDto[] = []; + @Output() componentsChange = new EventEmitter<{ + components: NoticeOfIntentDecisionComponentDto[]; + isValid: boolean; + }>(); + @ViewChildren(DecisionComponentComponent) childComponents!: QueryList; + + noticeOfIntent!: NoticeOfIntentDto; + noticeOfIntentSubmission!: NoticeOfIntentSubmissionDto; + decisionComponentTypes: DecisionComponentTypeMenuItem[] = []; + + constructor( + private toastService: ToastService, + private noticeOfIntentDetailService: NoticeOfIntentDetailService, + private submissionService: NoticeOfIntentSubmissionService, + private confirmationDialogService: ConfirmationDialogService + ) {} + + ngOnInit(): void { + this.noticeOfIntentDetailService.$noticeOfIntent + .pipe(takeUntil(this.$destroy)) + .subscribe(async (noticeOfIntent) => { + if (noticeOfIntent) { + this.noticeOfIntent = noticeOfIntent; + + //TODO:What? + //this.noticeOfIntent.submittedApplication = await this.submissionService.fetchSubmission(noticeOfIntent.fileNumber); + } + }); + + this.prepareDecisionComponentTypes(this.codes); + + // validate components on load + setTimeout(() => this.onChange(), 0); + } + + ngAfterViewInit(): void { + // subscribes to child components and triggers validation on add/delete. NOTE: this is NOT getting called on first page load + this.childComponents.changes.subscribe(() => { + // setTimeout is required to ensure that current code is executed after the current change detection cycle completes, avoiding the ExpressionChangedAfterItHasBeenCheckedError + setTimeout(() => this.onChange(), 0); + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + onAddNewComponent(uiCode: string, typeCode: string) { + switch (uiCode) { + case 'COPY': + const component: NoticeOfIntentDecisionComponentDto = { + noticeOfIntentDecisionComponentTypeCode: typeCode, + alrArea: this.noticeOfIntent.alrArea, + agCap: this.noticeOfIntent.agCap, + agCapSource: this.noticeOfIntent.agCapSource, + agCapMap: this.noticeOfIntent.agCapMap, + agCapConsultant: this.noticeOfIntent.agCapConsultant, + noticeOfIntentDecisionComponentType: this.decisionComponentTypes.find( + (e) => e.code === typeCode && e.uiCode !== 'COPY' + )!, + }; + + if (typeCode === NOI_DECISION_COMPONENT_TYPE.POFO) { + this.patchPofoFields(component); + } + + if (typeCode === NOI_DECISION_COMPONENT_TYPE.ROSO) { + this.patchRosoFields(component); + } + + if (typeCode === NOI_DECISION_COMPONENT_TYPE.PFRS) { + this.patchPofoFields(component); + this.patchRosoFields(component); + } + + this.components.unshift(component); + break; + case NOI_DECISION_COMPONENT_TYPE.POFO: + case NOI_DECISION_COMPONENT_TYPE.ROSO: + case NOI_DECISION_COMPONENT_TYPE.PFRS: + const componentType = this.decisionComponentTypes.find((e) => e.code === typeCode && e.uiCode !== 'COPY'); + this.components.unshift({ + noticeOfIntentDecisionComponentTypeCode: typeCode, + noticeOfIntentDecisionComponentType: componentType, + } as unknown as NoticeOfIntentDecisionComponentDto); + break; + default: + this.toastService.showErrorToast(`Failed to create component ${typeCode}`); + } + + this.updateComponentsMenuItems(); + } + + onRemove(index: number) { + this.confirmationDialogService + .openDialog({ + body: 'Are you sure you want to remove this component?', + }) + .subscribe((didConfirm) => { + if (didConfirm) { + this.components.splice(index, 1); + this.updateComponentsMenuItems(); + } + }); + } + + trackByFn(index: any, item: NoticeOfIntentDecisionComponentDto) { + return item.noticeOfIntentDecisionComponentTypeCode; + } + + onChange() { + const isValid = + this.components.length > 0 && (!this.childComponents || this.childComponents?.length < 1) + ? false + : this.childComponents.reduce((isValid, component) => isValid && component.form.valid, true); + + this.componentsChange.emit({ + components: this.components, + isValid, + }); + } + + private async prepareDecisionComponentTypes(codes: NoticeOfIntentDecisionCodesDto) { + const decisionComponentTypes: DecisionComponentTypeMenuItem[] = codes.decisionComponentTypes.map((e) => ({ + ...e, + isDisabled: false, + uiCode: e.code, + })); + + const mappedProposalType = decisionComponentTypes.find((e) => e.code === this.noticeOfIntent.type.code); + + if (mappedProposalType) { + const proposalDecisionType: DecisionComponentTypeMenuItem = { + isDisabled: false, + uiCode: 'COPY', + code: mappedProposalType!.code, + label: `Copy Proposal - ${mappedProposalType?.label}`, + description: mappedProposalType?.description ?? '', + }; + decisionComponentTypes.unshift(proposalDecisionType); + } + this.decisionComponentTypes = decisionComponentTypes; + + this.updateComponentsMenuItems(); + } + + private patchPofoFields(component: NoticeOfIntentDecisionComponentDto) { + component.endDate = this.noticeOfIntent.proposalEndDate; + component.soilFillTypeToPlace = this.noticeOfIntentSubmission.soilFillTypeToPlace; + component.soilToPlaceVolume = this.noticeOfIntentSubmission.soilToPlaceVolume; + component.soilToPlaceArea = this.noticeOfIntentSubmission.soilToPlaceArea; + component.soilToPlaceMaximumDepth = this.noticeOfIntentSubmission.soilToPlaceMaximumDepth; + component.soilToPlaceAverageDepth = this.noticeOfIntentSubmission.soilToPlaceAverageDepth; + } + + private patchRosoFields(component: NoticeOfIntentDecisionComponentDto) { + component.endDate = this.noticeOfIntent.proposalEndDate; + component.soilTypeRemoved = this.noticeOfIntentSubmission.soilTypeRemoved; + component.soilToRemoveVolume = this.noticeOfIntentSubmission.soilToRemoveVolume; + component.soilToRemoveArea = this.noticeOfIntentSubmission.soilToRemoveArea; + component.soilToRemoveMaximumDepth = this.noticeOfIntentSubmission.soilToRemoveMaximumDepth; + component.soilToRemoveAverageDepth = this.noticeOfIntentSubmission.soilToRemoveAverageDepth; + } + + private updateComponentsMenuItems() { + this.decisionComponentTypes = this.decisionComponentTypes.map((e) => ({ + ...e, + isDisabled: this.components.some((c) => c.noticeOfIntentDecisionComponentTypeCode === e.code), + })); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html new file mode 100644 index 0000000000..ead69cd822 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.html @@ -0,0 +1,52 @@ +
+
{{ data.type?.label }}
+ +
+ +
+
+ + Component to Condition + + {{ + component.label + }} + + + +
+ Approval Dependent* + + Yes + No + +
+ + + Security Amount + + + + + Administrative Fee Amount + + + + + Description + + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.scss new file mode 100644 index 0000000000..3b4c9e86c7 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.scss @@ -0,0 +1,14 @@ +form { + margin-top: 12px; +} + +.condition { + display: grid; + grid-template-columns: calc(50% - 12px) calc(50% - 12px); + grid-column-gap: 24px; + grid-row-gap: 24px; + + .condition-full-width { + grid-column: 1/3; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.spec.ts new file mode 100644 index 0000000000..7468fb24c3 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.spec.ts @@ -0,0 +1,31 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DecisionConditionComponent } from './decision-condition.component'; + +describe('DecisionConditionComponent', () => { + let component: DecisionConditionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DecisionConditionComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionConditionComponent); + component = fixture.componentInstance; + component.data = { + type: { + code: '', + label: '', + description: '', + }, + }; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts new file mode 100644 index 0000000000..d2c9d59f81 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component.ts @@ -0,0 +1,98 @@ +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { parseStringToBoolean } from '../../../../../../../shared/utils/boolean-helper'; +import { SelectableComponent, TempNoticeOfIntentDecisionConditionDto } from '../decision-conditions.component'; + +@Component({ + selector: 'app-decision-condition', + templateUrl: './decision-condition.component.html', + styleUrls: ['./decision-condition.component.scss'], +}) +export class DecisionConditionComponent implements OnInit, OnChanges { + @Input() data!: TempNoticeOfIntentDecisionConditionDto; + @Output() dataChange = new EventEmitter(); + @Output() remove = new EventEmitter(); + + @Input() selectableComponents: SelectableComponent[] = []; + + componentsToCondition = new FormControl(null, [Validators.required]); + approvalDependant = new FormControl(null, [Validators.required]); + + securityAmount = new FormControl(null); + administrativeFee = new FormControl(null); + description = new FormControl(null, [Validators.required]); + + form = new FormGroup({ + approvalDependant: this.approvalDependant, + securityAmount: this.securityAmount, + administrativeFee: this.administrativeFee, + description: this.description, + componentsToCondition: this.componentsToCondition, + }); + + ngOnInit(): void { + if (this.data) { + let approvalDependant = null; + if (this.data.approvalDependant !== null) { + approvalDependant = this.data.approvalDependant ? 'true' : 'false'; + } + + const selectedOptions = this.selectableComponents + .filter((component) => this.data.componentsToCondition?.map((e) => e.tempId)?.includes(component.tempId)) + .map((e) => ({ + componentDecisionUuid: e.decisionUuid, + componentToConditionType: e.code, + tempId: e.tempId, + })); + + this.componentsToCondition.setValue(selectedOptions.map((e) => e.tempId) ?? null); + + this.form.patchValue({ + approvalDependant, + securityAmount: this.data.securityAmount?.toString() ?? null, + administrativeFee: this.data.administrativeFee?.toString() ?? null, + description: this.data.description ?? null, + }); + } + + this.form.valueChanges.subscribe((changes) => { + const selectedOptions = this.selectableComponents + .filter((component) => this.componentsToCondition.value?.includes(component.tempId)) + .map((e) => ({ + componentDecisionUuid: e.decisionUuid, + componentToConditionType: e.code, + tempId: e.tempId, + })); + + this.dataChange.emit({ + type: this.data.type, + tempUuid: this.data.tempUuid, + uuid: this.data.uuid, + approvalDependant: parseStringToBoolean(this.approvalDependant.value), + securityAmount: this.securityAmount.value !== null ? parseFloat(this.securityAmount.value) : undefined, + administrativeFee: this.administrativeFee.value !== null ? parseFloat(this.administrativeFee.value) : undefined, + description: this.description.value ?? undefined, + componentsToCondition: selectedOptions, + }); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['selectableComponents']) { + const selectedOptions = this.selectableComponents + .filter((component) => this.componentsToCondition.value?.includes(component.tempId)) + .map((e) => ({ + componentDecisionUuid: e.decisionUuid, + componentToConditionType: e.code, + })); + + if (selectedOptions && selectedOptions.length < 1) { + this.componentsToCondition.setValue(null); + } + } + } + + onRemove() { + this.remove.emit(); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html new file mode 100644 index 0000000000..029df03dfa --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.html @@ -0,0 +1,38 @@ +
+
+

Conditions

+
+ + +
+
+ + + + + +
+ +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss new file mode 100644 index 0000000000..a55dff748c --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.scss @@ -0,0 +1,25 @@ +@use '../../../../../../../styles/colors'; + +section { + width: 100%; + margin: 24px 0 56px; +} + +.buttons { + margin-top: 28px; +} + +.condition { + border-radius: 4px; + border: 1px solid colors.$grey; + padding: 16px; + margin-bottom: 48px; + + &:first-of-type { + margin-top: 12px; + } +} + +.remove-button { + margin-top: 18px !important; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts new file mode 100644 index 0000000000..a0015d4867 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts @@ -0,0 +1,55 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatMenuModule } from '@angular/material/menu'; +import { createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { + NoticeOfIntentDecisionDto, + NoticeOfIntentDecisionWithLinkedResolutionDto, +} from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { DecisionConditionsComponent } from './decision-conditions.component'; + +describe('DecisionConditionComponent', () => { + let component: DecisionConditionsComponent; + let fixture: ComponentFixture; + let mockDecisionService: NoticeOfIntentDecisionV2Service; + + beforeEach(async () => { + mockDecisionService = createMock(); + mockDecisionService.$decision = new BehaviorSubject(undefined); + mockDecisionService.$decisions = new BehaviorSubject([]); + + await TestBed.configureTestingModule({ + imports: [MatMenuModule], + providers: [ + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockDecisionService, + }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, + ], + declarations: [DecisionConditionsComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionConditionsComponent); + component = fixture.componentInstance; + component.codes = { + decisionComponentTypes: [], + outcomes: [], + decisionConditionTypes: [], + }; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts new file mode 100644 index 0000000000..0addc94d27 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.ts @@ -0,0 +1,211 @@ +import { + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, + ViewChildren, +} from '@angular/core'; +import { combineLatestWith, Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { + NoticeOfIntentDecisionCodesDto, + NoticeOfIntentDecisionComponentDto, + NoticeOfIntentDecisionConditionDto, + NoticeOfIntentDecisionDto, + UpdateNoticeOfIntentDecisionConditionDto, +} from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { ConfirmationDialogService } from '../../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { DecisionConditionComponent } from './decision-condition/decision-condition.component'; + +export type TempNoticeOfIntentDecisionConditionDto = UpdateNoticeOfIntentDecisionConditionDto & { tempUuid?: string }; +export type SelectableComponent = { uuid?: string; tempId: string; decisionUuid: string; code: string; label: string }; + +@Component({ + selector: 'app-decision-conditions', + templateUrl: './decision-conditions.component.html', + styleUrls: ['./decision-conditions.component.scss'], +}) +export class DecisionConditionsComponent implements OnInit, OnChanges, OnDestroy { + $destroy = new Subject(); + + @Input() codes!: NoticeOfIntentDecisionCodesDto; + @Input() components: NoticeOfIntentDecisionComponentDto[] = []; + @Input() conditions: NoticeOfIntentDecisionConditionDto[] = []; + @Input() showError = false; + @ViewChildren(DecisionConditionComponent) conditionComponents: DecisionConditionComponent[] = []; + + @Output() conditionsChange = new EventEmitter<{ + conditions: UpdateNoticeOfIntentDecisionConditionDto[]; + isValid: boolean; + }>(); + selectableComponents: SelectableComponent[] = []; + private allComponents: SelectableComponent[] = []; + mappedConditions: TempNoticeOfIntentDecisionConditionDto[] = []; + decision: NoticeOfIntentDecisionDto | undefined; + + constructor( + private decisionService: NoticeOfIntentDecisionV2Service, + private confirmationDialogService: ConfirmationDialogService + ) {} + + ngOnInit(): void { + this.decisionService.$decision + .pipe(takeUntil(this.$destroy)) + .pipe(combineLatestWith(this.decisionService.$decisions)) + .subscribe(([selectedDecision, decisions]) => { + const otherDecisionComponents = []; + for (const decision of decisions) { + const mappedComponents = this.mapComponents( + decision.uuid, + decision.components, + decision.resolutionNumber, + decision.resolutionYear + ); + const otherDecisionsComponents = mappedComponents.filter( + (component) => component.decisionUuid !== selectedDecision?.uuid + ); + otherDecisionComponents.push(...otherDecisionsComponents); + } + this.allComponents = otherDecisionComponents; + this.selectableComponents = [...this.allComponents]; + + if (selectedDecision) { + const updatedComponents = this.mapComponents( + selectedDecision.uuid, + this.components, + selectedDecision.resolutionNumber, + selectedDecision.resolutionYear + ); + this.selectableComponents = [...this.allComponents, ...updatedComponents]; + + this.decision = selectedDecision; + + this.populateConditions(selectedDecision.conditions); + this.onChanges(); + } + }); + } + + onAddNewCondition(typeCode: string) { + const matchingType = this.codes.decisionConditionTypes.find((code) => code.code === typeCode); + this.mappedConditions.unshift({ + type: matchingType, + tempUuid: (Math.random() * 10000).toFixed(0), + }); + this.conditionsChange.emit({ + conditions: this.mappedConditions, + isValid: false, + }); + } + + trackByFn(index: any, item: TempNoticeOfIntentDecisionConditionDto) { + if (item.uuid) { + return item.uuid; + } + return item.tempUuid; + } + + onRemoveCondition(index: number) { + this.confirmationDialogService + .openDialog({ + body: 'Are you sure you want to remove this condition?', + }) + .subscribe((didConfirm) => { + if (didConfirm) { + this.mappedConditions.splice(index, 1); + this.onChanges(); + } + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (this.decision && changes['components']) { + const updatedComponents = this.mapComponents( + this.decision.uuid, + this.components, + this.decision.resolutionNumber, + this.decision.resolutionYear + ); + this.selectableComponents = [...this.allComponents, ...updatedComponents]; + const validComponentIds = this.selectableComponents.map((component) => component.tempId); + + this.mappedConditions = this.mappedConditions.map((condition) => { + if (condition.componentsToCondition) { + condition.componentsToCondition = condition.componentsToCondition.filter((component) => + validComponentIds.includes(component.tempId) + ); + } + return condition; + }); + this.onChanges(); + } + + if (changes['conditions']) { + this.populateConditions(this.conditions); + this.onChanges(); + } + } + + private populateConditions(conditions: NoticeOfIntentDecisionConditionDto[]) { + this.mappedConditions = conditions.map((condition) => { + const selectedComponents = this.selectableComponents + .filter((component) => + condition.components?.map((conditionComponent) => conditionComponent.uuid).includes(component.uuid) + ) + .map((e) => ({ + componentDecisionUuid: e.decisionUuid, + componentToConditionType: e.code, + tempId: e.tempId, + uuid: e.uuid, + })); + + return { + ...condition, + componentsToCondition: selectedComponents, + }; + }); + } + + mapComponents( + decisionUuid: string, + components: NoticeOfIntentDecisionComponentDto[], + decisionNumber: number | null, + decisionYear: number | null + ) { + return components.map((component) => { + const matchingType = this.codes.decisionComponentTypes.find( + (type) => type.code === component.noticeOfIntentDecisionComponentTypeCode + ); + return { + uuid: component.uuid, + decisionUuid: decisionUuid, + code: component.noticeOfIntentDecisionComponentTypeCode, + label: + decisionNumber && decisionYear + ? `#${decisionNumber}/${decisionYear} ${matchingType?.label}` + : `Draft ${matchingType?.label}`, + tempId: `${decisionUuid}_${component.noticeOfIntentDecisionComponentTypeCode}`, + }; + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + onChanges() { + this.conditionsChange.emit({ + conditions: this.mappedConditions, + isValid: this.conditionComponents.reduce((isValid, component) => isValid && component.form.valid, true), + }); + } + + onValidate() { + this.conditionComponents.forEach((component) => component.form.markAllAsTouched()); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html new file mode 100644 index 0000000000..517e5c27f5 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html @@ -0,0 +1,95 @@ +
+

{{ title }} Document

+
+
+
+
+
+ Document Upload* +
+ + +
+
+ {{ pendingFile.name }} +  ({{ pendingFile.size | filesize }}) +
+ +
+
+ + +
+
+ +
+ + Document Name + + +
+ +
+ + +
+
+ + Source + + {{ source }} + + +
+
+ Visible To: +
+ Applicant, L/FNG, and Commissioner +
+
+ Public +
+
+
+ + +
+ + + +
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss new file mode 100644 index 0000000000..99825bd974 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss @@ -0,0 +1,43 @@ +.form { + display: grid; + grid-template-columns: 1fr 1fr; + row-gap: 32px; + column-gap: 32px; + + .double { + grid-column: 1/3; + } +} + +.full-width { + width: 100%; +} + +a { + word-break: break-all; +} + +.file { + border: 1px solid #000; + border-radius: 8px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; +} + +.upload-button { + margin-top: 6px !important; +} + +.spinner { + display: inline-block; + margin-right: 4px; +} + +:host::ng-deep { + .mdc-button__label { + display: flex; + align-items: center; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts new file mode 100644 index 0000000000..2c662b6be9 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts @@ -0,0 +1,47 @@ +import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; + +import { DecisionDocumentUploadDialogComponent } from './decision-document-upload-dialog.component'; + +describe('DecisionDocumentUploadDialogComponent', () => { + let component: DecisionDocumentUploadDialogComponent; + let fixture: ComponentFixture; + + let mockNOIDecService: DeepMocked; + + beforeEach(async () => { + mockNOIDecService = createMock(); + + const mockDialogRef = { + close: jest.fn(), + afterClosed: jest.fn(), + subscribe: jest.fn(), + backdropClick: () => new EventEmitter(), + }; + + await TestBed.configureTestingModule({ + declarations: [DecisionDocumentUploadDialogComponent], + providers: [ + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockNOIDecService, + }, + { provide: MatDialogRef, useValue: mockDialogRef }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + imports: [MatDialogModule], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionDocumentUploadDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts new file mode 100644 index 0000000000..ecb07ad94d --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts @@ -0,0 +1,103 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { DecisionDocumentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { DOCUMENT_SOURCE } from '../../../../../../shared/document/document.dto'; + +@Component({ + selector: 'app-decision-document-upload-dialog', + templateUrl: './decision-document-upload-dialog.component.html', + styleUrls: ['./decision-document-upload-dialog.component.scss'], +}) +export class DecisionDocumentUploadDialogComponent implements OnInit { + title = 'Create'; + isDirty = false; + isSaving = false; + allowsFileEdit = true; + documentType = 'Decision Package'; + + name = new FormControl('', [Validators.required]); + type = new FormControl({ disabled: true, value: undefined }, [Validators.required]); + source = new FormControl({ disabled: true, value: DOCUMENT_SOURCE.ALC }, [Validators.required]); + + visibleToInternal = new FormControl({ disabled: true, value: true }, [Validators.required]); + visibleToPublic = new FormControl({ disabled: true, value: true }, [Validators.required]); + + documentSources = Object.values(DOCUMENT_SOURCE); + + form = new FormGroup({ + name: this.name, + type: this.type, + source: this.source, + visibleToInternal: this.visibleToInternal, + visibleToPublic: this.visibleToPublic, + }); + + pendingFile: File | undefined; + existingFile: string | undefined; + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: { fileId: string; decisionUuid: string; existingDocument?: DecisionDocumentDto }, + protected dialog: MatDialogRef, + private decisionService: NoticeOfIntentDecisionV2Service + ) {} + + ngOnInit(): void { + if (this.data.existingDocument) { + const document = this.data.existingDocument; + this.title = 'Edit'; + this.form.patchValue({ + name: document.fileName, + }); + this.existingFile = document.fileName; + } + } + + async onSubmit() { + const file = this.pendingFile; + if (file) { + const renamedFile = new File([file], this.name.getRawValue() ?? file.name); + this.isSaving = true; + if (this.data.existingDocument) { + await this.decisionService.deleteFile(this.data.decisionUuid, this.data.existingDocument.uuid); + } + await this.decisionService.uploadFile(this.data.decisionUuid, renamedFile); + + this.dialog.close(true); + this.isSaving = false; + } + } + + uploadFile(event: Event) { + const element = event.target as HTMLInputElement; + const selectedFiles = element.files; + if (selectedFiles && selectedFiles[0]) { + this.pendingFile = selectedFiles[0]; + this.name.setValue(selectedFiles[0].name); + } + } + + onRemoveFile() { + this.pendingFile = undefined; + this.existingFile = undefined; + } + + openFile() { + if (this.pendingFile) { + const fileURL = URL.createObjectURL(this.pendingFile); + window.open(fileURL, '_blank'); + } + } + + async openExistingFile() { + if (this.data.existingDocument) { + await this.decisionService.downloadFile( + this.data.decisionUuid, + this.data.existingDocument.uuid, + this.data.existingDocument.fileName + ); + } + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html new file mode 100644 index 0000000000..17a889722c --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html @@ -0,0 +1,228 @@ +
+

Decision #{{ index }} Draft

+
+ +

Resolution

+
+
+
+ +
+ +
+
+ + +
+
+
+ Res #{{ resolutionNumberControl.getRawValue() }} / {{ resolutionYearControl.getRawValue() }} + +
+
+
+ + + Decision Date + + + + + + + + +
+ + Decision Maker + + +
+ + + +
+
+ Subject to Conditions* + + Yes + No + +
+
+ +
+ + Decision Description + + +
+ +
+ Stats required* + + Yes + No + +
+ + + + Rescinded Date + + + + + +
+ + + + Rescinded Comment + + + + +
+

Documents

+ +
+ + + + +
+ +
+ +
+

Audit and Chair Review

+
+ + Audit Date + + + + +
+
+
+ +
+
+ +
+
+
+ + +
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.scss new file mode 100644 index 0000000000..811eda701f --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.scss @@ -0,0 +1,166 @@ +@use '../../../../../../styles/colors.scss'; + +.bottom-scroller { + margin-top: 36px; + position: absolute; + bottom: 0; + left: 212px; + right: 0; + background-color: #fff; + padding: 16px; + z-index: 2; + border-top: 1px solid colors.$grey-light; + + button:not(:last-child) { + margin-right: 16px !important; + } +} + +form { + padding-bottom: 120px; +} + +:host::ng-deep { + .grid, + .grid-2 { + display: grid; + grid-template-columns: calc(50% - 12px) calc(50% - 12px); + grid-column-gap: 24px; + grid-row-gap: 32px; + + .full-width { + grid-column: 1/3; + } + } + + h3 { + margin-top: 36px !important; + margin-bottom: 12px !important; + } + + .resolution-number-wrapper { + display: grid; + grid-template-columns: 1fr 16px 0.7fr; + .resolution-number-btn-wrapper { + width: 100%; + } + } + + .row-no-flex, + .row, + .btn-row { + width: 100%; + margin: 24px 0; + } + + .row, + .btn-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + + .column { + display: flex; + flex-direction: column; + flex-basis: 100%; + flex: 1; + } + + .release-decision-btn { + width: 50%; + } + } + + .container, + .container-wide { + border: 1px solid colors.$grey-dark; + max-width: 438px; + + .soil-table { + padding: 16px; + display: grid; + grid-template-columns: 39% 59%; + grid-row-gap: 28px; + padding: 10px; + + &.double-table { + grid-template-columns: 22% 37% 37%; + column-gap: 10px; + } + + .full-width { + width: 100%; + } + } + + .label { + display: flex; + flex-direction: column; + justify-content: center; + margin-bottom: 0; + + .subtext { + margin-bottom: -8px !important; + } + } + + input { + text-align: right; + padding-right: 8px; + } + } + + .container-wide { + max-width: 700px; + } + + .resolution-number-wrapper { + display: flex; + align-items: center; + + .generate-number-btn { + width: 100%; + } + + .resolution-number { + display: flex; + align-items: center; + font-size: 20px; + color: colors.$grey-dark; + + .delete-icon { + color: colors.$error-color; + } + } + } + + .decision-component-form { + .grid { + margin-bottom: 24px; + } + } + + .soil-text-area { + height: 100px !important; + } + + .error-field-outlined.ng-invalid { + border: 1px solid colors.$error-color !important; + + .mat-button-toggle { + border-color: colors.$error-color; + color: colors.$error-color !important; + } + + &.upload-button { + border: 2px solid colors.$error-color; + margin-bottom: 0 !important; + } + } + + .toggle-label { + color: colors.$grey-dark; + display: block; + margin-bottom: 3px; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.spec.ts new file mode 100644 index 0000000000..1c744f2665 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.spec.ts @@ -0,0 +1,79 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentDetailService } from '../../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentModificationService } from '../../../../../services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.service'; +import { ToastService } from '../../../../../services/toast/toast.service'; +import { StartOfDayPipe } from '../../../../../shared/pipes/startOfDay.pipe'; + +import { DecisionInputV2Component } from './decision-input-v2.component'; + +describe('DecisionInputComponent', () => { + let component: DecisionInputV2Component; + let fixture: ComponentFixture; + let mockNoticeOfIntentDecisionV2Service: DeepMocked; + let mockNoticeOfIntentModificationService: DeepMocked; + let mockNoticeOfIntentDetailService: DeepMocked; + let mockRouter: DeepMocked; + let mockToastService: DeepMocked; + let mockMatDialog: DeepMocked; + + beforeEach(async () => { + mockNoticeOfIntentDecisionV2Service = createMock(); + mockNoticeOfIntentModificationService = createMock(); + mockToastService = createMock(); + mockRouter = createMock(); + mockRouter.navigateByUrl.mockResolvedValue(true); + mockMatDialog = createMock(); + mockNoticeOfIntentDetailService = createMock(); + + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + declarations: [DecisionInputV2Component, StartOfDayPipe], + providers: [ + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockNoticeOfIntentDecisionV2Service, + }, + { + provide: NoticeOfIntentModificationService, + useValue: mockNoticeOfIntentModificationService, + }, + { provide: NoticeOfIntentDetailService, useValue: mockNoticeOfIntentDetailService }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({ uuid: 'fake' }), + }, + }, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: MatDialog, + useValue: mockMatDialog, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionInputV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts new file mode 100644 index 0000000000..87425ac561 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -0,0 +1,483 @@ +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import moment from 'moment'; +import { combineLatestWith, Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { + CreateNoticeOfIntentDecisionDto, + NoticeOfIntentDecisionCodesDto, + NoticeOfIntentDecisionComponentDto, + NoticeOfIntentDecisionConditionDto, + NoticeOfIntentDecisionDto, + NoticeOfIntentDecisionOutcomeCodeDto, + UpdateNoticeOfIntentDecisionConditionDto, +} from '../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentDetailService } from '../../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentModificationDto } from '../../../../../services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.dto'; +import { NoticeOfIntentModificationService } from '../../../../../services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.service'; +import { ToastService } from '../../../../../services/toast/toast.service'; +import { formatDateForApi } from '../../../../../shared/utils/api-date-formatter'; +import { parseBooleanToString, parseStringToBoolean } from '../../../../../shared/utils/boolean-helper'; +import { ReleaseDialogComponent } from '../release-dialog/release-dialog.component'; +import { DecisionComponentsComponent } from './decision-components/decision-components.component'; +import { DecisionConditionsComponent } from './decision-conditions/decision-conditions.component'; + +type MappedPostDecision = { + label: string; + uuid: string; +}; + +@Component({ + selector: 'app-decision-input', + templateUrl: './decision-input-v2.component.html', + styleUrls: ['./decision-input-v2.component.scss'], +}) +export class DecisionInputV2Component implements OnInit, OnDestroy { + $destroy = new Subject(); + isLoading = false; + minDate = new Date(0); + isFirstDecision = false; + showComponents = false; + requireComponents = false; + showConditions = false; + conditionsValid = true; + componentsValid = true; + index = 1; + + fileNumber: string = ''; + uuid: string = ''; + outcomes: NoticeOfIntentDecisionOutcomeCodeDto[] = []; + + resolutionYears: number[] = []; + postDecisions: MappedPostDecision[] = []; + existingDecision: NoticeOfIntentDecisionDto | undefined; + codes?: NoticeOfIntentDecisionCodesDto; + + resolutionNumberControl = new FormControl(null, [Validators.required]); + resolutionYearControl = new FormControl(null, [Validators.required]); + + components: NoticeOfIntentDecisionComponentDto[] = []; + conditions: NoticeOfIntentDecisionConditionDto[] = []; + conditionUpdates: UpdateNoticeOfIntentDecisionConditionDto[] = []; + + @ViewChild(DecisionComponentsComponent) decisionComponentsComponent?: DecisionComponentsComponent; + @ViewChild(DecisionConditionsComponent) decisionConditionsComponent?: DecisionConditionsComponent; + + form = new FormGroup({ + outcome: new FormControl(null, [Validators.required]), + date: new FormControl(undefined, [Validators.required]), + decisionMaker: new FormControl(null, [Validators.required]), + postDecision: new FormControl(null), + resolutionNumber: this.resolutionNumberControl, + resolutionYear: this.resolutionYearControl, + auditDate: new FormControl(null), + isSubjectToConditions: new FormControl(undefined, [Validators.required]), + decisionDescription: new FormControl(undefined, [Validators.required]), + isStatsRequired: new FormControl(undefined, [Validators.required]), + rescindedDate: new FormControl(null), + rescindedComment: new FormControl(null), + }); + + constructor( + private decisionService: NoticeOfIntentDecisionV2Service, + private modificationService: NoticeOfIntentModificationService, + public router: Router, + private route: ActivatedRoute, + private toastService: ToastService, + private noticeOfIntentDetailService: NoticeOfIntentDetailService, + public dialog: MatDialog + ) {} + + ngOnInit(): void { + this.resolutionYearControl.disable(); + this.setYear(); + + this.extractAndPopulateQueryParams(); + + if (this.fileNumber) { + this.loadData(); + } + } + + private extractAndPopulateQueryParams() { + const fileNumber = this.route.parent?.parent?.snapshot.paramMap.get('fileNumber'); + const uuid = this.route.snapshot.paramMap.get('uuid'); + const index = this.route.snapshot.paramMap.get('index'); + this.index = index ? parseInt(index) : 1; + + if (uuid) { + this.uuid = uuid; + } + + if (fileNumber) { + this.fileNumber = fileNumber; + } + } + + private setYear() { + const year = moment('1974'); + const currentYear = moment().year(); + while (year.year() <= currentYear) { + this.resolutionYears.push(year.year()); + year.add(1, 'year'); + } + this.resolutionYears.reverse(); + } + + ngOnDestroy(): void { + this.decisionService.clearDecision(); + this.decisionService.clearDecisions(); + this.$destroy.next(); + this.$destroy.complete(); + } + + async loadData() { + if (this.uuid) { + await this.decisionService.loadDecision(this.uuid); + } + + await this.decisionService.loadDecisions(this.fileNumber); + + this.codes = await this.decisionService.fetchCodes(); + this.outcomes = this.codes.outcomes; + + await this.prepareDataForEdit(); + } + + private async prepareDataForEdit() { + this.decisionService.$decision + .pipe(takeUntil(this.$destroy)) + .pipe(combineLatestWith(this.modificationService.$modifications, this.decisionService.$decisions)) + .subscribe(([decision, modifications, decisions]) => { + if (decision) { + this.existingDecision = decision; + this.uuid = decision.uuid; + } + + this.mapPostDecisionsToControls(modifications, this.existingDecision); + + if (this.existingDecision) { + this.patchFormWithExistingData(this.existingDecision); + + if (decisions.length > 0) { + let minDate = null; + this.isFirstDecision = true; + + for (const decision of decisions) { + if (!minDate && decision.date) { + minDate = decision.date; + } + + if (minDate && decision.date && minDate > decision.date) { + minDate = decision.date; + } + + if ( + this.existingDecision.createdAt > decision.createdAt && + this.existingDecision.uuid !== decision.uuid + ) { + this.isFirstDecision = false; + } + } + + if (minDate) { + this.minDate = new Date(minDate); + } + + if (!this.isFirstDecision) { + this.form.controls.postDecision.addValidators([Validators.required]); + this.form.controls.decisionMaker.disable(); + } + } else { + this.isFirstDecision = true; + } + } else { + this.resolutionYearControl.enable(); + } + }); + } + + private mapPostDecisionsToControls( + modifications: NoticeOfIntentModificationDto[], + existingDecision?: NoticeOfIntentDecisionDto + ) { + //TOOD: Clean this up + const mappedModifications = modifications + .filter( + (modification) => + (existingDecision && existingDecision.modifies?.uuid === modification.uuid) || + (modification.reviewOutcome.code === 'PRC' && !modification.resultingDecision) + ) + .map((modification, index) => ({ + label: `Modification Request #${modifications.length - index} - ${modification.modifiesDecisions + .map((dec) => `#${dec.resolutionNumber}/${dec.resolutionYear}`) + .join(', ')}`, + uuid: modification.uuid, + })); + + this.postDecisions = [...mappedModifications]; + } + + private patchFormWithExistingData(existingDecision: NoticeOfIntentDecisionDto) { + this.form.patchValue({ + outcome: existingDecision.outcome.code, + decisionMaker: existingDecision.decisionMaker, + date: existingDecision.date ? new Date(existingDecision.date) : undefined, + resolutionYear: existingDecision.resolutionYear, + resolutionNumber: existingDecision.resolutionNumber?.toString(10) || undefined, + auditDate: existingDecision.auditDate ? new Date(existingDecision.auditDate) : undefined, + postDecision: existingDecision.modifies?.uuid, + isSubjectToConditions: parseBooleanToString(existingDecision.isSubjectToConditions), + decisionDescription: existingDecision.decisionDescription, + isStatsRequired: parseBooleanToString(existingDecision.isStatsRequired), + rescindedDate: existingDecision.rescindedDate ? new Date(existingDecision.rescindedDate) : undefined, + rescindedComment: existingDecision.rescindedComment, + }); + + this.conditions = existingDecision.conditions; + + if (existingDecision.isSubjectToConditions) { + this.showConditions = true; + } + + if (!existingDecision.resolutionNumber) { + this.resolutionYearControl.enable(); + } + + if (existingDecision?.components) { + this.components = existingDecision.components; + } + + this.requireComponents = ['APPR', 'APPA'].includes(existingDecision.outcome.code); + + if (['APPR', 'APPA', 'RESC'].includes(existingDecision.outcome.code)) { + this.showComponents = true; + } else { + this.showComponents = false; + this.form.controls.isSubjectToConditions.disable(); + } + + if (existingDecision.outcome.code === 'RESC') { + this.form.controls.rescindedComment.setValidators([Validators.required]); + this.form.controls.rescindedDate.setValidators([Validators.required]); + } + } + + async onSubmit(isStayOnPage: boolean = false, isDraft: boolean = true) { + this.isLoading = true; + + try { + await this.saveDecision(isDraft); + } finally { + if (!isStayOnPage) { + this.onCancel(); + } else { + await this.loadData(); + } + + this.isLoading = false; + } + } + + async saveDecision(isDraft: boolean = true) { + const data = this.mapDecisionDataForSave(isDraft); + + if (this.uuid) { + await this.decisionService.update(this.uuid, data); + } else { + const createdDecision = await this.decisionService.create({ + ...data, + fileNumber: this.fileNumber, + }); + this.uuid = createdDecision.uuid; + } + } + + private mapDecisionDataForSave(isDraft: boolean) { + const { + date, + outcome, + decisionMaker, + resolutionNumber, + resolutionYear, + auditDate, + postDecision, + isSubjectToConditions, + decisionDescription, + isStatsRequired, + rescindedDate, + rescindedComment, + } = this.form.getRawValue(); + + const data: CreateNoticeOfIntentDecisionDto = { + date: formatDateForApi(date!), + resolutionNumber: parseInt(resolutionNumber!), + resolutionYear: resolutionYear!, + auditDate: auditDate ? formatDateForApi(auditDate) : auditDate, + outcomeCode: outcome!, + fileNumber: this.fileNumber, + modifiesUuid: postDecision ?? undefined, + isDraft, + isSubjectToConditions: parseStringToBoolean(isSubjectToConditions), + decisionDescription: decisionDescription, + isStatsRequired: parseStringToBoolean(isStatsRequired), + rescindedDate: rescindedDate ? formatDateForApi(rescindedDate) : rescindedDate, + rescindedComment: rescindedComment, + decisionComponents: this.components, + conditions: this.conditionUpdates, + decisionMaker: decisionMaker ?? undefined, + }; + + return data; + } + + onCancel() { + this.router.navigate([`notice-of-intent/${this.fileNumber}/decision`]); + } + + async onGenerateResolutionNumber() { + const selectedYear = this.form.controls.resolutionYear.getRawValue(); + if (selectedYear) { + const number = await this.decisionService.getNextAvailableResolutionNumber(selectedYear); + if (number) { + this.setResolutionNumber(number); + } else { + this.toastService.showErrorToast('Failed to retrieve resolution number.'); + } + } else { + this.toastService.showErrorToast('Resolution year is not selected. Select a resolution year first.'); + } + } + + private async setResolutionNumber(number: number) { + try { + this.resolutionYearControl.disable(); + this.form.controls.resolutionNumber.setValue(number.toString()); + await this.onSubmit(true); + } catch { + this.resolutionYearControl.enable(); + } + } + + async onDeleteResolutionNumber() { + this.resolutionNumberControl.setValue(null); + await this.onSubmit(true); + this.resolutionYearControl.enable(); + } + + private runValidation() { + this.form.markAllAsTouched(); + const requiresConditions = this.showConditions; + const requiresComponents = this.showComponents && this.requireComponents; + + if (this.decisionConditionsComponent) { + this.decisionConditionsComponent.onValidate(); + } + + if ( + !this.form.valid || + !this.conditionsValid || + !this.componentsValid || + (this.components.length === 0 && requiresComponents) || + (this.conditionUpdates.length === 0 && requiresConditions) + ) { + this.form.controls.decisionMaker.markAsDirty(); + this.toastService.showErrorToast('Please correct all errors before submitting the form'); + + // this will ensure that error rendering complete + setTimeout(() => this.scrollToError()); + + return false; + } else { + return true; + } + } + + private scrollToError() { + let elements = document.getElementsByClassName('ng-invalid'); + let elArray = Array.from(elements).filter((el) => el.nodeName !== 'FORM'); + + elArray[0]?.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + } + + async onRelease() { + if (this.runValidation()) { + this.dialog + .open(ReleaseDialogComponent, { + minWidth: '600px', + maxWidth: '900px', + maxHeight: '80vh', + width: '90%', + autoFocus: false, + }) + .afterClosed() + .subscribe(async (didAccept) => { + if (didAccept) { + await this.onSubmit(false, false); + await this.noticeOfIntentDetailService.load(this.fileNumber); + } + }); + } + } + + onComponentChange($event: { components: NoticeOfIntentDecisionComponentDto[]; isValid: boolean }) { + this.components = Array.from($event.components); + this.componentsValid = $event.isValid; + } + + onConditionsChange($event: { conditions: UpdateNoticeOfIntentDecisionConditionDto[]; isValid: boolean }) { + this.conditionUpdates = Array.from($event.conditions); + this.conditionsValid = $event.isValid; + } + + onChangeDecisionOutcome(selectedOutcome: NoticeOfIntentDecisionOutcomeCodeDto) { + if (['APPR', 'APPA', 'RESC'].includes(selectedOutcome.code)) { + if (this.form.controls.isSubjectToConditions.disabled) { + this.showComponents = true; + this.form.controls.isSubjectToConditions.enable(); + this.form.patchValue({ + isSubjectToConditions: null, + }); + this.showConditions = false; + } + } else if (this.form.controls.isSubjectToConditions.enabled) { + this.showComponents = false; + this.components = []; + this.conditions = []; + this.form.controls.isSubjectToConditions.disable(); + this.form.patchValue({ + isSubjectToConditions: 'false', + }); + } + + if (selectedOutcome.code === 'RESC') { + this.form.controls.rescindedComment.setValidators([Validators.required]); + this.form.controls.rescindedDate.setValidators([Validators.required]); + this.form.controls.rescindedComment.updateValueAndValidity(); + this.form.controls.rescindedDate.updateValueAndValidity(); + } else if (this.form.controls.rescindedComment.enabled) { + this.form.controls.rescindedComment.setValue(null); + this.form.controls.rescindedDate.setValue(null); + this.form.controls.rescindedComment.setValidators([]); + this.form.controls.rescindedDate.setValidators([]); + this.form.controls.rescindedComment.updateValueAndValidity(); + this.form.controls.rescindedDate.updateValueAndValidity(); + } + } + + onChangeSubjectToConditions($event: MatButtonToggleChange) { + if ($event.value === 'true') { + this.showConditions = true; + } else { + this.conditions = []; + this.conditionUpdates = []; + this.showConditions = false; + } + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html new file mode 100644 index 0000000000..7dbb975abb --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html @@ -0,0 +1,242 @@ +
+

Decisions

+
+ +
+
+
+
No Decisions
+
+
+
+ +
+
+
+
+ Modified By:  + {{ decision.modifiedBy?.join(', ') }} + N/A +
+
+ +
+
+
+

Decision #{{ i }}

+
+ + calendar_month + {{ noticeOfIntent.activeDays }} + + + pause + {{ noticeOfIntent.pausedDays }} + +
+ + + + +
Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }}
+ + + + + + +
+ + + + +
+ +
+
+ +

Resolution

+
+
+
+
Decision Date
+ {{ decision.date | momentFormat }} + +
+
+
Decision Maker
+ {{ decision.decisionMaker }} + +
+ +
+
Decision Outcome
+ {{ decision.outcome.label }} {{ decision.isSubjectToConditions ? '- Subject to Conditions' : '' }} +
+ +
+
Decision Description
+ {{ decision.decisionDescription }} + +
+ +
+
Stats Required
+ +
+ +
+
Rescinded Date
+ {{ decision.rescindedDate | momentFormat }} + +
+ +
+
Rescinded Comment
+ {{ decision.rescindedComment }} + +
+
+
+ +

Documents

+ + + + + + +

Components

+
+ + + +
{{ component.noticeOfIntentDecisionComponentType?.label }}
+
+ + + + + + + + + +
+
+
Agricultural Capability
+ {{ component.agCap }} + +
+
+
Agricultural Capability Source
+ {{ component.agCapSource }} + +
+ +
+
Agricultural Capability Mapsheet Reference
+ {{ component.agCapMap }} + +
+
+
Agricultural Capability Consultant
+ {{ component.agCapConsultant }} + +
+
+
+
+
+
+
+ + +

Conditions

+
+ +
+
+ +

Audit

+
+
+
+
Audit Date
+ {{ decision.auditDate | momentFormat }} + + + +
+
+
+ +
+ +
+
+
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss new file mode 100644 index 0000000000..ed21e4ab3a --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.scss @@ -0,0 +1,156 @@ +@use '../../../../../styles/colors'; + +section { + margin-bottom: 64px; +} + +h4 { + margin-bottom: 12px !important; +} + +hr { + margin: 36px 0; + stroke: colors.$grey; +} + +.decision-container { + position: relative; +} + +.decision { + margin: 24px 0; + box-shadow: 0 2px 8px 1px rgba(0, 0, 0, 0.25); +} + +.decision.draft { + border: 2px solid colors.$secondary-color-light; +} + +.decision-section { + background: colors.$grey-light; + padding: 18px; +} + +.decision-section-no-title { + background: colors.$grey-light; + padding: 1px 18px; +} + +.header { + display: flex; + justify-content: space-between; + margin-bottom: 36px; + + .title { + display: flex; + align-items: center; + justify-content: space-between; + gap: 28px; + + .days { + display: inline-block; + margin-right: 6px; + margin-top: 4px; + + .mat-icon { + font-size: 19px !important; + line-height: 21px !important; + width: 19px; + vertical-align: middle; + } + } + } +} + +.loading-overlay { + position: absolute; + z-index: 2; + background-color: colors.$grey; + opacity: 0.4; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.post-decisions { + padding: 6px 32px; + background-color: colors.$grey-light; + grid-template-columns: 50% 50%; + display: grid; + min-height: 36px; + text-transform: uppercase; + border-radius: 4px 4px 0 0; +} + +.decision-menu { + position: absolute; + top: 0; + right: 0; + height: 36px; + background: colors.$accent-color; + box-shadow: -1px 1px 4px rgba(0, 0, 0, 0.25); + border-radius: 0 4px 0 10px; +} + +.decision-padding { + padding: 18px 32px; +} + +.no-decisions { + margin-top: 16px; + display: flex; + align-items: center; + justify-content: center; + background-color: colors.$grey-light; + height: 72px; +} + +.conditions-link-icon { + position: absolute; +} + +:host ::ng-deep { + .grid-2 { + margin-top: 18px; + margin-bottom: 18px; + display: grid; + grid-template-columns: 50% 50%; + grid-row-gap: 18px; + + .full-width { + grid-column: 1/3; + } + } + + .mat-mdc-table { + background: colors.$grey-light; + } + + .subheading2 { + margin-bottom: 6px !important; + } + + .row { + margin: 16px 0; + } + + .application-pill-wrapper, + .application-pill { + margin-right: 0 !important; + } + + table { + border-spacing: 12px; + margin-left: -12px; + } + + .soil-table-label { + font-weight: 700; + } + + .pre-wrapped-text { + white-space: pre-wrap; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.spec.ts new file mode 100644 index 0000000000..9ed928895f --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.spec.ts @@ -0,0 +1,86 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; +import { ActivatedRoute } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDecisionComponentService } from '../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service'; +import { NoticeOfIntentDecisionV2Service } from '../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { + NoticeOfIntentDecisionDto, + NoticeOfIntentDecisionWithLinkedResolutionDto, +} from '../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentDetailService } from '../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice-of-intent.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { DecisionV2Component } from './decision-v2.component'; + +describe('DecisionV2Component', () => { + let component: DecisionV2Component; + let fixture: ComponentFixture; + let mockNOIDecisionService: DeepMocked; + let mockNOIDetailService: DeepMocked; + let mockNOIDecisionComponentService: DeepMocked; + + beforeEach(async () => { + mockNOIDecisionService = createMock(); + mockNOIDecisionService.$decision = new BehaviorSubject(undefined); + mockNOIDecisionService.$decisions = new BehaviorSubject([]); + + mockNOIDetailService = createMock(); + mockNOIDetailService.$noticeOfIntent = new BehaviorSubject(undefined); + + mockNOIDecisionComponentService = createMock(); + + await TestBed.configureTestingModule({ + imports: [MatSnackBarModule], + declarations: [DecisionV2Component], + providers: [ + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockNOIDecisionService, + }, + { + provide: NoticeOfIntentDetailService, + useValue: mockNOIDetailService, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, + { + provide: ToastService, + useValue: {}, + }, + { + provide: MatDialog, + useValue: {}, + }, + { + provide: NoticeOfIntentDecisionComponentService, + useValue: mockNOIDecisionComponentService, + }, + { + provide: ActivatedRoute, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts new file mode 100644 index 0000000000..f275aae97c --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts @@ -0,0 +1,237 @@ +import { Component, ElementRef, OnDestroy, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentDecisionComponentService } from '../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service'; +import { NoticeOfIntentDecisionV2Service } from '../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { + NOI_DECISION_COMPONENT_TYPE, + NoticeOfIntentDecisionDto, + NoticeOfIntentDecisionOutcomeCodeDto, +} from '../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentDetailService } from '../../../../services/notice-of-intent/notice-of-intent-detail.service'; +import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice-of-intent.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { + DRAFT_DECISION_TYPE_LABEL, + MODIFICATION_TYPE_LABEL, + RELEASED_DECISION_TYPE_LABEL, +} from '../../../../shared/application-type-pill/application-type-pill.constants'; +import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { formatDateForApi } from '../../../../shared/utils/api-date-formatter'; +import { decisionChildRoutes } from '../decision.module'; +import { RevertToDraftDialogComponent } from './revert-to-draft-dialog/revert-to-draft-dialog.component'; + +type LoadingDecision = NoticeOfIntentDecisionDto & { + loading: boolean; +}; + +@Component({ + selector: 'app-noi-decision-v2', + templateUrl: './decision-v2.component.html', + styleUrls: ['./decision-v2.component.scss'], +}) +export class DecisionV2Component implements OnInit, OnDestroy { + $destroy = new Subject(); + createDecision = decisionChildRoutes.find((e) => e.path === 'create')!; + isDraftExists = true; + disabledCreateBtnTooltip = ''; + + fileNumber: string = ''; + decisionDate: number | undefined; + decisions: LoadingDecision[] = []; + outcomes: NoticeOfIntentDecisionOutcomeCodeDto[] = []; + isPaused = true; + + modificationLabel = MODIFICATION_TYPE_LABEL; + noticeOfIntent: NoticeOfIntentDto | undefined; + dratDecisionLabel = DRAFT_DECISION_TYPE_LABEL; + releasedDecisionLabel = RELEASED_DECISION_TYPE_LABEL; + + COMPONENT_TYPE = NOI_DECISION_COMPONENT_TYPE; + + constructor( + public dialog: MatDialog, + private noticeOfIntentDetailService: NoticeOfIntentDetailService, + private decisionService: NoticeOfIntentDecisionV2Service, + private toastService: ToastService, + private confirmationDialogService: ConfirmationDialogService, + private noticeOfIntentDecisionComponentService: NoticeOfIntentDecisionComponentService, + private router: Router, + private activatedRouter: ActivatedRoute, + private elementRef: ElementRef + ) {} + + ngOnInit(): void { + this.noticeOfIntentDetailService.$noticeOfIntent.pipe(takeUntil(this.$destroy)).subscribe((noticeOfIntent) => { + if (noticeOfIntent) { + this.fileNumber = noticeOfIntent.fileNumber; + this.decisionDate = noticeOfIntent.decisionDate; + this.isPaused = noticeOfIntent.paused; + this.loadDecisions(noticeOfIntent.fileNumber); + this.noticeOfIntent = noticeOfIntent; + } + }); + } + + async loadDecisions(fileNumber: string) { + const codes = await this.decisionService.fetchCodes(); + this.outcomes = codes.outcomes; + this.decisionService.loadDecisions(fileNumber); + + this.decisionService.$decisions.pipe(takeUntil(this.$destroy)).subscribe((decisions) => { + this.decisions = decisions.map((decision) => ({ + ...decision, + loading: false, + })); + + this.scrollToDecision(); + + this.isDraftExists = this.decisions.some((d) => d.isDraft); + this.disabledCreateBtnTooltip = this.isPaused + ? 'This notice of intent is currently paused. Only active notice of intents can have decisions.' + : 'A notice of intent can only have one decision draft at a time. Please release or delete the existing decision draft to continue.'; + }); + } + + scrollToDecision() { + const decisionUuid = this.activatedRouter.snapshot.queryParamMap.get('uuid'); + + setTimeout(() => { + if (this.decisions.length > 0 && decisionUuid) { + this.scrollToElement(decisionUuid); + } + }); + } + + async onCreate() { + const newDecision = await this.decisionService.create({ + resolutionYear: new Date().getFullYear(), + isDraft: true, + date: Date.now(), + fileNumber: this.fileNumber, + }); + + const index = this.decisions.length; + await this.router.navigate([ + `/notice-of-intent/${this.fileNumber}/decision/draft/${newDecision.uuid}/edit/${index + 1}`, + ]); + } + + async onEdit(selectedDecision: NoticeOfIntentDecisionDto) { + const position = this.decisions.findIndex((decision) => decision.uuid === selectedDecision.uuid); + const index = this.decisions.length - position; + await this.router.navigate([ + `/notice-of-intent/${this.fileNumber}/decision/draft/${selectedDecision.uuid}/edit/${index}`, + ]); + } + + async onRevertToDraft(uuid: string) { + const position = this.decisions.findIndex((decision) => decision.uuid === uuid); + const index = this.decisions.length - position; + this.dialog + .open(RevertToDraftDialogComponent, { + data: { fileNumber: this.fileNumber }, + }) + .beforeClosed() + .subscribe(async (didConfirm) => { + if (didConfirm) { + await this.decisionService.update(uuid, { + isDraft: true, + }); + await this.noticeOfIntentDetailService.load(this.fileNumber); + + await this.router.navigate([`/notice-of-intent/${this.fileNumber}/decision/draft/${uuid}/edit/${index}`]); + } + }); + } + + async deleteDecision(uuid: string) { + this.confirmationDialogService + .openDialog({ + body: 'Are you sure you want to delete the selected decision?', + }) + .subscribe(async (confirmed) => { + if (confirmed) { + this.decisions = this.decisions.map((decision) => { + return { + ...decision, + loading: decision.uuid === uuid, + }; + }); + await this.decisionService.delete(uuid); + await this.noticeOfIntentDetailService.load(this.fileNumber); + this.toastService.showSuccessToast('Decision deleted'); + } + }); + } + + async attachFile(decisionUuid: string, event: Event) { + this.decisions = this.decisions.map((decision) => { + return { + ...decision, + loading: decision.uuid === decisionUuid, + }; + }); + const element = event.target as HTMLInputElement; + const fileList = element.files; + if (fileList && fileList.length > 0) { + const file: File = fileList[0]; + const uploadedFile = await this.decisionService.uploadFile(decisionUuid, file); + if (uploadedFile) { + await this.loadDecisions(this.fileNumber); + } + } + } + + async onSaveAuditDate(decisionUuid: string, auditReviewDate: number) { + await this.decisionService.update(decisionUuid, { + auditDate: formatDateForApi(auditReviewDate), + isDraft: this.decisions.find((e) => e.uuid === decisionUuid)!.isDraft, + }); + await this.loadDecisions(this.fileNumber); + } + + async onSaveAlrArea(decisionUuid: string, componentUuid: string | undefined, value?: any) { + const decision = this.decisions.find((e) => e.uuid === decisionUuid); + const component = decision?.components.find((e) => e.uuid === componentUuid); + if (componentUuid && component) { + await this.noticeOfIntentDecisionComponentService.update(componentUuid, { + uuid: componentUuid, + noticeOfIntentDecisionComponentTypeCode: component.noticeOfIntentDecisionComponentTypeCode, + alrArea: value ? value : null, + }); + } else { + this.toastService.showErrorToast('Unable to update the Alr Area. Please reload the page and try again.'); + } + + await this.loadDecisions(this.fileNumber); + } + + async onStatsRequiredUpdate(decisionUuid: string, value: boolean) { + await this.decisionService.update(decisionUuid, { + isStatsRequired: value, + isDraft: this.decisions.find((e) => e.uuid === decisionUuid)!.isDraft, + }); + await this.loadDecisions(this.fileNumber); + } + + ngOnDestroy(): void { + this.decisionService.clearDecisions(); + this.$destroy.next(); + this.$destroy.complete(); + } + + scrollToElement(elementId: string) { + const id = `#${CSS.escape(elementId)}`; + const element = this.elementRef.nativeElement.querySelector(id); + + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'start', + inline: 'start', + }); + } + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.html new file mode 100644 index 0000000000..3db6231291 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.html @@ -0,0 +1,28 @@ +
+

Confirm Release Decision

+
+ +
+

+ This decision and document will be immediately visible to the Applicant and Local/First Nation Government. The + decision will be visible to the Public after the prescribed amount of days is exhausted. +

+

+ Upon releasing the decision, the application will be updated to the status: + +

+ If there is more than one decision, the status will not change +

+ The Applicant and Local/First Nation Government will not receive an auto-email notification. Please complete any + notification manually. +

+

The Applicant and Local/First Nation Government will receive an auto-email notification.

+
+
+ + +
+ + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.scss new file mode 100644 index 0000000000..756d26e192 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.scss @@ -0,0 +1,6 @@ +.grid { + display: grid; + grid-template-columns: 1fr; + grid-column-gap: 24px; + grid-row-gap: 24px; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.spec.ts new file mode 100644 index 0000000000..4ff7b1b86f --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.spec.ts @@ -0,0 +1,52 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationService } from '../../../../../services/application/application.service'; +import { ApplicationDecisionV2Service } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentDecisionDto } from '../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentService } from '../../../../../services/notice-of-intent/notice-of-intent.service'; +import { ReleaseDialogComponent } from './release-dialog.component'; + +describe('ReleaseDialogComponent', () => { + let component: ReleaseDialogComponent; + let fixture: ComponentFixture; + let mockNOIService: DeepMocked; + let mockNOIDecisionV2Service: DeepMocked; + + beforeEach(async () => { + mockNOIService = createMock(); + mockNOIDecisionV2Service = createMock(); + mockNOIDecisionV2Service.$decision = new BehaviorSubject(undefined); + + await TestBed.configureTestingModule({ + declarations: [ReleaseDialogComponent], + providers: [ + { + provide: NoticeOfIntentService, + useValue: mockNOIService, + }, + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockNOIDecisionV2Service, + }, + { provide: MatDialogRef, useValue: {} }, + { + provide: MAT_DIALOG_DATA, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ReleaseDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.ts new file mode 100644 index 0000000000..9411c72448 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.ts @@ -0,0 +1,56 @@ +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentDecisionV2Service } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentService } from '../../../../../services/notice-of-intent/notice-of-intent.service'; +import { ApplicationPill } from '../../../../../shared/application-type-pill/application-type-pill.component'; + +@Component({ + selector: 'app-release-dialog', + templateUrl: './release-dialog.component.html', + styleUrls: ['./release-dialog.component.scss'], +}) +export class ReleaseDialogComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + mappedType?: ApplicationPill; + wasReleased = false; + + constructor( + private noticeOfIntentService: NoticeOfIntentService, + private decisionService: NoticeOfIntentDecisionV2Service, + public matDialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) data: any + ) {} + + ngOnInit(): void { + // this.applicationService.$applicationStatuses.pipe(takeUntil(this.$destroy)).subscribe((statuses) => { + // if (statuses) { + // const releasedStatus = statuses.find((status) => status.code === SUBMISSION_STATUS.ALC_DECISION); + // if (releasedStatus) { + // this.mappedType = { + // label: releasedStatus.label, + // backgroundColor: releasedStatus.alcsBackgroundColor, + // borderColor: releasedStatus.alcsBackgroundColor, + // textColor: releasedStatus.alcsColor, + // shortLabel: releasedStatus.label, + // }; + // } + // } + // }); + + this.decisionService.$decision.pipe(takeUntil(this.$destroy)).subscribe((decision) => { + if (decision) { + this.wasReleased = decision.wasReleased; + } + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + onRelease() { + this.matDialogRef.close(true); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.html new file mode 100644 index 0000000000..af89c6851d --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.html @@ -0,0 +1,30 @@ +
+

Confirm Revert to Draft

+
+ +
+

+ This decision and document will be reverted into draft form, providing you with full editing capabilities but + hiding it from the Applicant, Local/First Nation Government, and the Public. +

+

+ Upon reverting the decision to draft, the application will be updated to the status: + +

+ If there is more than one decision, the status will not change +

+ The Applicant and Local/First Nation Government will not receive an auto-email notification. Please complete any + notification manually. +

+
+
+ + +
+ + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.scss new file mode 100644 index 0000000000..756d26e192 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.scss @@ -0,0 +1,6 @@ +.grid { + display: grid; + grid-template-columns: 1fr; + grid-column-gap: 24px; + grid-row-gap: 24px; +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.spec.ts new file mode 100644 index 0000000000..dfa843d347 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.spec.ts @@ -0,0 +1,31 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { NoticeOfIntentSubmissionStatusService } from '../../../../../services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service'; + +import { RevertToDraftDialogComponent } from './revert-to-draft-dialog.component'; + +describe('RevertToDraftDialogComponent', () => { + let component: RevertToDraftDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RevertToDraftDialogComponent], + providers: [ + { provide: MatDialogRef, useValue: {} }, + { provide: NoticeOfIntentSubmissionStatusService, useValue: {} }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(RevertToDraftDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts new file mode 100644 index 0000000000..faec1414a0 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component.ts @@ -0,0 +1,49 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { SUBMISSION_STATUS } from '../../../../../services/application/application.dto'; +import { NoticeOfIntentSubmissionStatusService } from '../../../../../services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service'; +import { ApplicationSubmissionStatusPill } from '../../../../../shared/application-submission-status-type-pill/application-submission-status-type-pill.component'; + +@Component({ + selector: 'app-revert-to-draft-dialog', + templateUrl: './revert-to-draft-dialog.component.html', + styleUrls: ['./revert-to-draft-dialog.component.scss'], +}) +export class RevertToDraftDialogComponent { + mappedType?: ApplicationSubmissionStatusPill; + + constructor( + public matDialogRef: MatDialogRef, + private noticeOfIntentSubmissionStatusService: NoticeOfIntentSubmissionStatusService, + @Inject(MAT_DIALOG_DATA) data: { fileNumber: string } + ) { + this.calculateStatusChange(data.fileNumber); + } + + async calculateStatusChange(fileNumber: string) { + const statusHistory = await this.noticeOfIntentSubmissionStatusService.fetchSubmissionStatusesByFileNumber( + fileNumber + ); + const validStatuses = statusHistory + .filter( + (status) => + status.effectiveDate && + status.statusTypeCode !== SUBMISSION_STATUS.ALC_DECISION && + status.effectiveDate < Date.now() + ) + .sort((a, b) => b.status.weight! - a.status.weight!); + + if (validStatuses && validStatuses.length > 0) { + const newStatus = validStatuses[0].status; + this.mappedType = { + label: newStatus.label, + backgroundColor: newStatus.alcsBackgroundColor, + textColor: newStatus.alcsColor, + }; + } + } + + onConfirm() { + this.matDialogRef.close(true); + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.html index 6992915501..93d6cc768d 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.html @@ -1,105 +1,7 @@ -
-

Decision

-
- -
-
-
-
No Decisions
-
-
-
- -
-
-
-
- Modified By:  - {{ decision.modifiedByResolutions.join(', ') }} - N/A -
-
- -
- - - -
-
-
- Decision #{{ decisions.length - i }} - - - calendar_month - {{ noticeOfIntent.activeDays >= MAX_ACTIVE_DAYS ? '61+' : noticeOfIntent.activeDays }} - - - - - - Res #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }} -
-
-
-
Decision Date
- {{ decision.date | momentFormat }} -
-
-
Decision Outcome
- {{ decision.outcome.label }} -
-
-
Decision Maker
- {{ decision.decisionMaker }} -
-
-
Decision Maker Name
- {{ decision.decisionMakerName ?? 'No Data' }} -
-
-
Audit Date
- {{ decision.auditDate | momentFormat }} - - - -
-
-
Documents
-
-
File Name
-
File Upload Date
-
File Actions
-
No Documents
- - -
{{ document.uploadedAt | momentFormat }}
-
- - -
-
-
-
+
+ + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.scss index 4913228a68..e69de29bb2 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.scss +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.scss @@ -1,142 +0,0 @@ -@use '../../../../styles/colors'; - -h3 { - margin-bottom: 24px !important; -} - -section { - margin-bottom: 64px; -} - -.decision-container { - position: relative; -} - -.decision { - margin: 24px 0; - box-shadow: 0 2px 8px 1px rgba(0, 0, 0, 0.25); -} - -.loading-overlay { - position: absolute; - z-index: 2; - background-color: colors.$grey; - opacity: 0.4; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.days { - display: inline-block; - margin-right: 16px; - margin-left: 16px; - - .mat-icon { - font-size: 19px !important; - line-height: 21px !important; - width: 19px; - vertical-align: middle; - } -} - -.post-decisions { - padding: 6px 32px; - background-color: colors.$grey-light; - grid-template-columns: 1fr 1fr; - display: grid; - min-height: 36px; - text-transform: uppercase; - border-radius: 4px 4px 0 0; - box-shadow: -1px 1px 4px rgba(0, 0, 0, 0.25); -} - -.decision-menu { - position: absolute; - top: 0; - right: 0; - height: 36px; - background: colors.$accent-color; - box-shadow: -1px 1px 4px rgba(0, 0, 0, 0.25); - border-radius: 0 4px 0 10px; - - button { - color: colors.$white; - width: 36px; - height: 36px; - line-height: 36px; - } - - mat-icon { - position: absolute; - top: 8px; - left: 8px; - font-size: 21px; - width: 20px; - height: 36px; - } -} - -.decision-padding { - padding: 18px 32px 32px 32px; -} - -.decision-content { - margin-top: 16px; - margin-bottom: 32px; - margin-right: 100px; - display: grid; - grid-template-columns: 1fr 1fr; - grid-row-gap: 24px; - - .subheading2 { - margin-bottom: 6px !important; - } - - & > div { - font-size: 16px; - } -} - -.decision-documents { - margin-top: 4px; - display: grid; - grid-template-columns: 1fr 1fr 100px; - grid-row-gap: 4px; - - div { - display: flex; - align-items: center; - font-size: 16px !important; - } -} - -.file-actions { - margin-left: -12px; -} - -.delete-file-icon { - color: colors.$error-color; -} - -.no-decisions { - margin-top: 16px; - display: flex; - align-items: center; - justify-content: center; - background-color: colors.$grey-light; - height: 72px; -} - -.no-files { - grid-column: 1/4; - margin-top: 16px; - padding: 16px; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - background-color: colors.$grey-light; -} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.spec.ts index aca299f23d..8e5c05ab6d 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.spec.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.spec.ts @@ -1,58 +1,33 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatDialog, MatDialogRef } from '@angular/material/dialog'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { NoticeOfIntentDecisionService } from '../../../services/notice-of-intent/decision/notice-of-intent-decision.service'; import { NoticeOfIntentDetailService } from '../../../services/notice-of-intent/notice-of-intent-detail.service'; import { NoticeOfIntentDto } from '../../../services/notice-of-intent/notice-of-intent.dto'; -import { ToastService } from '../../../services/toast/toast.service'; -import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionComponent } from './decision.component'; describe('DecisionComponent', () => { let component: DecisionComponent; let fixture: ComponentFixture; - let mockNOIDetailService: DeepMocked; + let mockNoticeOfIntentDetailService: DeepMocked; beforeEach(async () => { - mockNOIDetailService = createMock(); - mockNOIDetailService.$noticeOfIntent = new BehaviorSubject(undefined); + mockNoticeOfIntentDetailService = createMock(); await TestBed.configureTestingModule({ - imports: [MatSnackBarModule], - declarations: [DecisionComponent], providers: [ { provide: NoticeOfIntentDetailService, - useValue: mockNOIDetailService, - }, - { - provide: NoticeOfIntentDecisionService, - useValue: {}, - }, - { - provide: MatDialogRef, - useValue: {}, - }, - { - provide: ConfirmationDialogService, - useValue: {}, - }, - { - provide: ToastService, - useValue: {}, - }, - { - provide: MatDialog, - useValue: {}, + useValue: mockNoticeOfIntentDetailService, }, ], + declarations: [DecisionComponent], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); + mockNoticeOfIntentDetailService.$noticeOfIntent = new BehaviorSubject(undefined); + fixture = TestBed.createComponent(DecisionComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.ts index 2d0bc788fc..b66c07d007 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.component.ts @@ -1,205 +1,32 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; import { Subject, takeUntil } from 'rxjs'; -import { - NoticeOfIntentDecisionDto, - NoticeOfIntentDecisionOutcomeCodeDto, -} from '../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; -import { NoticeOfIntentDecisionService } from '../../../services/notice-of-intent/decision/notice-of-intent-decision.service'; import { NoticeOfIntentDetailService } from '../../../services/notice-of-intent/notice-of-intent-detail.service'; import { NoticeOfIntentDto } from '../../../services/notice-of-intent/notice-of-intent.dto'; -import { ToastService } from '../../../services/toast/toast.service'; -import { MODIFICATION_TYPE_LABEL } from '../../../shared/application-type-pill/application-type-pill.constants'; -import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { formatDateForApi } from '../../../shared/utils/api-date-formatter'; -import { DecisionDialogComponent } from './decision-dialog/decision-dialog.component'; - -type LoadingDecision = NoticeOfIntentDecisionDto & { - modifiedByResolutions: string[]; - loading: boolean; -}; +import { SYSTEM_SOURCE_TYPES } from '../../../shared/dto/system-source.types.dto'; @Component({ - selector: 'app-noi-decision', + selector: 'app-decision', templateUrl: './decision.component.html', styleUrls: ['./decision.component.scss'], }) export class DecisionComponent implements OnInit, OnDestroy { $destroy = new Subject(); - fileNumber: string = ''; - decisionDate: number | undefined; - decisions: LoadingDecision[] = []; - outcomes: NoticeOfIntentDecisionOutcomeCodeDto[] = []; - isPaused = true; - MAX_ACTIVE_DAYS = 61; + SYSTEM_SOURCE_TYPES = SYSTEM_SOURCE_TYPES; noticeOfIntent: NoticeOfIntentDto | undefined; - modificationLabel = MODIFICATION_TYPE_LABEL; - constructor( - public dialog: MatDialog, - private noticeOfIntentDetailService: NoticeOfIntentDetailService, - private decisionService: NoticeOfIntentDecisionService, - private toastService: ToastService, - private confirmationDialogService: ConfirmationDialogService - ) {} + constructor(private noticeOfIntentDetailService: NoticeOfIntentDetailService) {} ngOnInit(): void { this.noticeOfIntentDetailService.$noticeOfIntent.pipe(takeUntil(this.$destroy)).subscribe((noticeOfIntent) => { if (noticeOfIntent) { - this.fileNumber = noticeOfIntent.fileNumber; - this.decisionDate = noticeOfIntent.decisionDate; - this.isPaused = noticeOfIntent.paused; - this.loadDecisions(noticeOfIntent.fileNumber); this.noticeOfIntent = noticeOfIntent; + } else { + this.noticeOfIntent = undefined; } }); } - async loadDecisions(fileNumber: string) { - const codes = await this.decisionService.fetchCodes(); - this.outcomes = codes.outcomes; - - const loadedDecision = await this.decisionService.fetchByFileNumber(fileNumber); - - this.decisions = loadedDecision.map((decision) => ({ - ...decision, - loading: false, - modifiedByResolutions: decision.modifiedBy?.flatMap((r) => r.linkedResolutions) || [], - })); - } - - onCreate() { - let minDate = new Date(0); - if (this.decisions.length > 0) { - minDate = new Date(this.decisions[this.decisions.length - 1].date); - } - - this.dialog - .open(DecisionDialogComponent, { - minWidth: '600px', - maxWidth: '900px', - maxHeight: '80vh', - width: '90%', - autoFocus: false, - data: { - isFirstDecision: this.decisions.length === 0, - minDate, - fileNumber: this.fileNumber, - outcomes: this.outcomes, - }, - }) - .afterClosed() - .subscribe((didCreate) => { - if (didCreate) { - this.noticeOfIntentDetailService.load(this.fileNumber); - } - }); - } - - onEdit(decision: LoadingDecision) { - const decisionIndex = this.decisions.indexOf(decision); - let minDate = new Date(0); - if (decisionIndex !== this.decisions.length - 1) { - minDate = new Date(this.decisions[this.decisions.length - 1].date); - } - this.dialog - .open(DecisionDialogComponent, { - minWidth: '600px', - maxWidth: '900px', - maxHeight: '80vh', - width: '90%', - autoFocus: false, - data: { - isFirstDecision: decisionIndex === this.decisions.length - 1, - minDate, - fileNumber: this.fileNumber, - outcomes: this.outcomes, - existingDecision: decision, - }, - }) - .afterClosed() - .subscribe((didModify) => { - if (didModify) { - this.noticeOfIntentDetailService.load(this.fileNumber); - } - }); - } - - async deleteDecision(uuid: string) { - this.confirmationDialogService - .openDialog({ - body: 'Are you sure you want to delete the selected decision?', - }) - .subscribe(async (confirmed) => { - if (confirmed) { - this.decisions = this.decisions.map((decision) => { - return { - ...decision, - loading: decision.uuid === uuid, - }; - }); - await this.decisionService.delete(uuid); - await this.noticeOfIntentDetailService.load(this.fileNumber); - this.toastService.showSuccessToast('Decision deleted'); - } - }); - } - - async attachFile(decisionUuid: string, event: Event) { - this.decisions = this.decisions.map((decision) => { - return { - ...decision, - loading: decision.uuid === decisionUuid, - }; - }); - const element = event.target as HTMLInputElement; - const fileList = element.files; - if (fileList && fileList.length > 0) { - const file: File = fileList[0]; - const uploadedFile = await this.decisionService.uploadFile(decisionUuid, file); - if (uploadedFile) { - await this.loadDecisions(this.fileNumber); - } - } - } - - async downloadFile(decisionUuid: string, decisionDocumentUuid: string, fileName: string) { - await this.decisionService.downloadFile(decisionUuid, decisionDocumentUuid, fileName, false); - } - - async openFile(decisionUuid: string, decisionDocumentUuid: string, fileName: string) { - await this.decisionService.downloadFile(decisionUuid, decisionDocumentUuid, fileName); - } - - async deleteFile(decisionUuid: string, decisionDocumentUuid: string, fileName: string) { - this.confirmationDialogService - .openDialog({ - body: `Are you sure you want to delete the file ${fileName}?`, - }) - .subscribe(async (confirmed) => { - if (confirmed) { - this.decisions = this.decisions.map((decision) => { - return { - ...decision, - loading: decision.uuid === decisionUuid, - }; - }); - - await this.decisionService.deleteFile(decisionUuid, decisionDocumentUuid); - await this.loadDecisions(this.fileNumber); - this.toastService.showSuccessToast('File deleted'); - } - }); - } - - async onSaveAuditDate(decisionUuid: string, auditReviewDate: number) { - await this.decisionService.update(decisionUuid, { - auditDate: formatDateForApi(auditReviewDate), - }); - await this.loadDecisions(this.fileNumber); - } - ngOnDestroy(): void { this.$destroy.next(); this.$destroy.complete(); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.module.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.module.ts new file mode 100644 index 0000000000..af64437137 --- /dev/null +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.module.ts @@ -0,0 +1,82 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { NgxMaskDirective } from 'ngx-mask'; +import { SharedModule } from '../../../shared/shared.module'; +import { ConditionComponent } from './conditions/condition/condition.component'; +import { ConditionsComponent } from './conditions/conditions.component'; +import { DecisionDialogComponent } from './decision-dialog/decision-dialog.component'; +import { DecisionV1Component } from './decision-v1/decision-v1.component'; +import { BasicComponent } from './decision-v2/decision-component/basic/basic.component'; +import { PfrsComponent } from './decision-v2/decision-component/pfrs/pfrs.component'; +import { PofoComponent } from './decision-v2/decision-component/pofo/pofo.component'; +import { RosoComponent } from './decision-v2/decision-component/roso/roso.component'; +import { DecisionDocumentsComponent } from './decision-v2/decision-documents/decision-documents.component'; +import { DecisionComponentComponent } from './decision-v2/decision-input/decision-components/decision-component/decision-component.component'; +import { PfrsInputComponent } from './decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component'; +import { PofoInputComponent } from './decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component'; +import { RosoInputComponent } from './decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component'; +import { DecisionComponentsComponent } from './decision-v2/decision-input/decision-components/decision-components.component'; +import { DecisionConditionComponent } from './decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component'; +import { DecisionConditionsComponent } from './decision-v2/decision-input/decision-conditions/decision-conditions.component'; +import { DecisionDocumentUploadDialogComponent } from './decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component'; +import { DecisionInputV2Component } from './decision-v2/decision-input/decision-input-v2.component'; +import { DecisionV2Component } from './decision-v2/decision-v2.component'; +import { ReleaseDialogComponent } from './decision-v2/release-dialog/release-dialog.component'; +import { RevertToDraftDialogComponent } from './decision-v2/revert-to-draft-dialog/revert-to-draft-dialog.component'; +import { DecisionComponent } from './decision.component'; + +export const decisionChildRoutes = [ + { + path: '', + menuTitle: 'Decision', + component: DecisionComponent, + portalOnly: false, + }, + { + path: 'create', + menuTitle: 'Decision', + component: DecisionInputV2Component, + portalOnly: false, + }, + { + path: 'draft/:uuid/edit/:index', + menuTitle: 'Decision', + component: DecisionInputV2Component, + portalOnly: false, + }, + { + path: 'conditions/:uuid', + menuTitle: 'Conditions', + component: ConditionsComponent, + portalOnly: false, + }, +]; + +@NgModule({ + declarations: [ + DecisionComponent, + DecisionV2Component, + DecisionInputV2Component, + DecisionV1Component, + DecisionDialogComponent, + ReleaseDialogComponent, + DecisionComponentComponent, + DecisionComponentsComponent, + DecisionDocumentUploadDialogComponent, + RevertToDraftDialogComponent, + DecisionDocumentsComponent, + DecisionConditionComponent, + DecisionConditionsComponent, + PofoInputComponent, + PofoComponent, + RosoComponent, + RosoInputComponent, + PfrsInputComponent, + PfrsComponent, + ConditionsComponent, + ConditionComponent, + BasicComponent, + ], + imports: [SharedModule.forRoot(), RouterModule.forChild(decisionChildRoutes), NgxMaskDirective], +}) +export class DecisionModule {} diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts index fea65ebd34..f344756e9a 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts @@ -8,7 +8,7 @@ import { NoticeOfIntentModificationDto } from '../../services/notice-of-intent/n import { NoticeOfIntentModificationService } from '../../services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.service'; import { NoticeOfIntentDto } from '../../services/notice-of-intent/notice-of-intent.dto'; import { ApplicantInfoComponent } from './applicant-info/applicant-info.component'; -import { DecisionComponent } from './decision/decision.component'; +import { decisionChildRoutes, DecisionModule } from './decision/decision.module'; import { NoiDocumentsComponent } from './documents/documents.component'; import { InfoRequestsComponent } from './info-requests/info-requests.component'; import { IntakeComponent } from './intake/intake.component'; @@ -50,9 +50,11 @@ export const childRoutes = [ }, { path: 'decision', - menuTitle: 'Decision', + menuTitle: 'Decisions', icon: 'gavel', - component: DecisionComponent, + module: DecisionModule, + portalOnly: false, + children: decisionChildRoutes, }, { path: 'post-decision', @@ -91,10 +93,12 @@ export class NoticeOfIntentComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { + this.fileNumber = '100135'; + this.load(); + this.route.params.pipe(takeUntil(this.destroy)).subscribe(async (routeParams) => { const { fileNumber } = routeParams; this.fileNumber = fileNumber; - this.load(); }); this.noticeOfIntentDetailService.$noticeOfIntent.pipe(takeUntil(this.destroy)).subscribe((noticeOfIntent) => { @@ -113,14 +117,14 @@ export class NoticeOfIntentComponent implements OnInit, OnDestroy { }); } + async load() { + await this.noticeOfIntentDetailService.load(this.fileNumber!); + } + ngOnDestroy(): void { this.noticeOfIntentDetailService.clear(); this.noticeOfIntentModificationService.clearModifications(); this.destroy.next(); this.destroy.complete(); } - - async load() { - await this.noticeOfIntentDetailService.load(this.fileNumber!); - } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts index 8c43fd5ed7..c82a7d121d 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts @@ -4,8 +4,6 @@ import { NoticeOfIntentDetailService } from '../../services/notice-of-intent/not import { SharedModule } from '../../shared/shared.module'; import { ApplicantInfoComponent } from './applicant-info/applicant-info.component'; import { NoticeOfIntentDetailsModule } from './applicant-info/notice-of-intent-details/notice-of-intent-details.module'; -import { DecisionDialogComponent } from './decision/decision-dialog/decision-dialog.component'; -import { DecisionComponent } from './decision/decision.component'; import { DocumentUploadDialogComponent } from './documents/document-upload-dialog/document-upload-dialog.component'; import { NoiDocumentsComponent } from './documents/documents.component'; import { InfoRequestDialogComponent } from './info-requests/info-request-dialog/info-request-dialog.component'; @@ -39,8 +37,6 @@ const routes: Routes = [ InfoRequestDialogComponent, PreparationComponent, NoticeOfIntentMeetingDialogComponent, - DecisionComponent, - DecisionDialogComponent, PostDecisionComponent, EditModificationDialogComponent, NoiDocumentsComponent, diff --git a/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.spec.ts b/alcs-frontend/src/app/services/application/application-decision-condition-types/application-decision-condition-types.service.spec.ts similarity index 92% rename from alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.spec.ts rename to alcs-frontend/src/app/services/application/application-decision-condition-types/application-decision-condition-types.service.spec.ts index 0d6ff26077..fcb4539053 100644 --- a/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application-decision-condition-types/application-decision-condition-types.service.spec.ts @@ -2,11 +2,11 @@ import { HttpClient } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { of, throwError } from 'rxjs'; -import { ToastService } from '../toast/toast.service'; -import { DecisionConditionTypesService } from './decision-condition-types.service'; +import { ToastService } from '../../toast/toast.service'; +import { ApplicationDecisionConditionTypesService } from './application-decision-condition-types.service'; describe('DecisionConditionTypesService', () => { - let service: DecisionConditionTypesService; + let service: ApplicationDecisionConditionTypesService; let mockHttpClient: DeepMocked; let mockToastService: DeepMocked; @@ -26,7 +26,7 @@ describe('DecisionConditionTypesService', () => { }, ], }); - service = TestBed.inject(DecisionConditionTypesService); + service = TestBed.inject(ApplicationDecisionConditionTypesService); }); it('should be created', () => { diff --git a/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.ts b/alcs-frontend/src/app/services/application/application-decision-condition-types/application-decision-condition-types.service.ts similarity index 84% rename from alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.ts rename to alcs-frontend/src/app/services/application/application-decision-condition-types/application-decision-condition-types.service.ts index 094e6edc15..5d6698a78c 100644 --- a/alcs-frontend/src/app/services/decision-condition-types/decision-condition-types.service.ts +++ b/alcs-frontend/src/app/services/application/application-decision-condition-types/application-decision-condition-types.service.ts @@ -1,14 +1,14 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; -import { environment } from '../../../environments/environment'; -import { ApplicationDecisionConditionTypeDto } from '../application/decision/application-decision-v2/application-decision-v2.dto'; -import { ToastService } from '../toast/toast.service'; +import { environment } from '../../../../environments/environment'; +import { ApplicationDecisionConditionTypeDto } from '../decision/application-decision-v2/application-decision-v2.dto'; +import { ToastService } from '../../toast/toast.service'; @Injectable({ providedIn: 'root', }) -export class DecisionConditionTypesService { +export class ApplicationDecisionConditionTypesService { private url = `${environment.apiUrl}/decision-condition-types`; constructor(private http: HttpClient, private toastService: ToastService) {} diff --git a/alcs-frontend/src/app/services/decision-maker/decision-maker.service.spec.ts b/alcs-frontend/src/app/services/application/application-decision-maker/application-decision-maker.service.spec.ts similarity index 93% rename from alcs-frontend/src/app/services/decision-maker/decision-maker.service.spec.ts rename to alcs-frontend/src/app/services/application/application-decision-maker/application-decision-maker.service.spec.ts index 28b1532158..a8477fcbf7 100644 --- a/alcs-frontend/src/app/services/decision-maker/decision-maker.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application-decision-maker/application-decision-maker.service.spec.ts @@ -2,11 +2,11 @@ import { HttpClient } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { of, throwError } from 'rxjs'; -import { ToastService } from '../toast/toast.service'; -import { DecisionMakerService } from './decision-maker.service'; +import { ToastService } from '../../toast/toast.service'; +import { ApplicationDecisionMakerService } from './application-decision-maker.service'; describe('DecisionMakerService', () => { - let service: DecisionMakerService; + let service: ApplicationDecisionMakerService; let mockHttpClient: DeepMocked; let mockToastService: DeepMocked; @@ -26,7 +26,7 @@ describe('DecisionMakerService', () => { }, ], }); - service = TestBed.inject(DecisionMakerService); + service = TestBed.inject(ApplicationDecisionMakerService); }); it('should be created', () => { diff --git a/alcs-frontend/src/app/services/decision-maker/decision-maker.service.ts b/alcs-frontend/src/app/services/application/application-decision-maker/application-decision-maker.service.ts similarity index 84% rename from alcs-frontend/src/app/services/decision-maker/decision-maker.service.ts rename to alcs-frontend/src/app/services/application/application-decision-maker/application-decision-maker.service.ts index efb97f784f..891a034e32 100644 --- a/alcs-frontend/src/app/services/decision-maker/decision-maker.service.ts +++ b/alcs-frontend/src/app/services/application/application-decision-maker/application-decision-maker.service.ts @@ -1,14 +1,14 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; -import { environment } from '../../../environments/environment'; -import { DecisionMakerDto } from '../application/decision/application-decision-v1/application-decision.dto'; -import { ToastService } from '../toast/toast.service'; +import { environment } from '../../../../environments/environment'; +import { DecisionMakerDto } from '../decision/application-decision-v1/application-decision.dto'; +import { ToastService } from '../../toast/toast.service'; @Injectable({ providedIn: 'root', }) -export class DecisionMakerService { +export class ApplicationDecisionMakerService { private url = `${environment.apiUrl}/decision-maker`; constructor(private http: HttpClient, private toastService: ToastService) {} diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-component/application-decision-component.service.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-component/application-decision-component.service.ts index 26e589c5fd..843d7d1299 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-component/application-decision-component.service.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-component/application-decision-component.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../../../../environments/environment'; import { ToastService } from '../../../../toast/toast.service'; -import { DecisionComponentDto } from '../application-decision-v2.dto'; +import { ApplicationDecisionComponentDto } from '../application-decision-v2.dto'; @Injectable({ providedIn: 'root', @@ -13,9 +13,9 @@ export class ApplicationDecisionComponentService { constructor(private http: HttpClient, private toastService: ToastService) {} - async update(uuid: string, data: DecisionComponentDto) { + async update(uuid: string, data: ApplicationDecisionComponentDto) { try { - const res = await firstValueFrom(this.http.patch(`${this.url}/${uuid}`, data)); + const res = await firstValueFrom(this.http.patch(`${this.url}/${uuid}`, data)); this.toastService.showSuccessToast('Decision updated'); return res; } catch (e) { diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts index 1a1be3a708..903e0e5bf0 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts @@ -41,7 +41,7 @@ export interface CreateApplicationDecisionDto extends UpdateApplicationDecisionD modifiesUuid: string | null; reconsidersUuid: string | null; isDraft: boolean; - decisionComponents?: DecisionComponentDto[]; + decisionComponents?: ApplicationDecisionComponentDto[]; } export interface ApplicationDecisionDto { @@ -72,7 +72,7 @@ export interface ApplicationDecisionDto { reconsiders?: LinkedResolutionDto; reconsideredBy?: LinkedResolutionDto[]; modifiedBy?: LinkedResolutionDto[]; - components: DecisionComponentDto[]; + components: ApplicationDecisionComponentDto[]; conditions: ApplicationDecisionConditionDto[]; wasReleased: boolean; } @@ -170,7 +170,7 @@ export interface InclExclDecisionComponentDto { inclExclApplicantType?: string | null; } -export interface DecisionComponentDto +export interface ApplicationDecisionComponentDto extends NfuDecisionComponentDto, TurpDecisionComponentDto, PofoDecisionComponentDto, @@ -190,7 +190,7 @@ export interface DecisionComponentDto conditionComponentsLabels?: string; } -export interface DecisionCodesDto { +export interface ApplicationDecisionCodesDto { outcomes: DecisionOutcomeCodeDto[]; decisionMakers: DecisionMakerDto[]; ceoCriterion: CeoCriterionDto[]; @@ -226,7 +226,7 @@ export interface ApplicationDecisionConditionDto { completionDate?: number | null; supersededDate?: number | null; type?: ApplicationDecisionConditionTypeDto | null; - components?: DecisionComponentDto[] | null; + components?: ApplicationDecisionComponentDto[] | null; } export interface ComponentToCondition { diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts index 2d2891b35a..5fc5d2b385 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts @@ -9,7 +9,7 @@ import { ApplicationDecisionDto, ApplicationDecisionWithLinkedResolutionDto, CreateApplicationDecisionDto, - DecisionCodesDto, + ApplicationDecisionCodesDto, UpdateApplicationDecisionDto, } from './application-decision-v2.dto'; @@ -37,9 +37,9 @@ export class ApplicationDecisionV2Service { return decisions; } - async fetchCodes(): Promise { + async fetchCodes(): Promise { try { - return await firstValueFrom(this.http.get(`${this.url}/codes`)); + return await firstValueFrom(this.http.get(`${this.url}/codes`)); } catch (err) { this.toastService.showErrorToast('Failed to fetch decisions'); } diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts new file mode 100644 index 0000000000..d5df6385b1 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts @@ -0,0 +1,66 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../../../toast/toast.service'; + +import { NoticeOfIntentDecisionComponentService } from './notice-of-intent-decision-component.service'; + +describe('NoticeOfIntentDecisionComponentService', () => { + let service: NoticeOfIntentDecisionComponentService; + let httpClient: DeepMocked; + let toastService: DeepMocked; + + beforeEach(() => { + httpClient = createMock(); + toastService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: httpClient, + }, + { + provide: ToastService, + useValue: toastService, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentDecisionComponentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make an http patch and show a success toast when updating', async () => { + httpClient.patch.mockReturnValue( + of({ + fileNumber: '1', + }) + ); + + await service.update('1', { noticeOfIntentDecisionComponentTypeCode: 'fake' }); + + expect(httpClient.patch).toHaveBeenCalledTimes(1); + expect(toastService.showSuccessToast).toHaveBeenCalledTimes(1); + }); + + it('should show a toast message if update fails', async () => { + httpClient.patch.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + try { + await service.update('1', { noticeOfIntentDecisionComponentTypeCode: 'fake' }); + } catch (e) { + //OM NOM NOM + } + + expect(httpClient.patch).toHaveBeenCalledTimes(1); + expect(toastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service.ts new file mode 100644 index 0000000000..76b9081acc --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-component/notice-of-intent-decision-component.service.ts @@ -0,0 +1,35 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../../environments/environment'; +import { ToastService } from '../../../toast/toast.service'; +import { + NoticeOfIntentDecisionComponentDto, + UpdateNoticeOfIntentDecisionComponentDto, +} from '../../decision/notice-of-intent-decision.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentDecisionComponentService { + private url = `${environment.apiUrl}/notice-of-intent-decision-component`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async update(uuid: string, data: UpdateNoticeOfIntentDecisionComponentDto) { + try { + const res = await firstValueFrom( + this.http.patch(`${this.url}/${uuid}`, data) + ); + this.toastService.showSuccessToast('Decision updated'); + return res; + } catch (e) { + if (e instanceof HttpErrorResponse && e.status === 400 && e.error?.message) { + this.toastService.showErrorToast(e.error.message); + } else { + this.toastService.showErrorToast('Failed to update decision'); + } + throw e; + } + } +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts new file mode 100644 index 0000000000..e33ee680de --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts @@ -0,0 +1,66 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../../../toast/toast.service'; + +import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition.service'; + +describe('NoticeOfIntentDecisionConditionService', () => { + let service: NoticeOfIntentDecisionConditionService; + let mockHttpClient: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(() => { + mockHttpClient = createMock(); + mockToastService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentDecisionConditionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make an http patch and show a success toast when updating', async () => { + mockHttpClient.patch.mockReturnValue( + of({ + fileNumber: '1', + }) + ); + + await service.update('1', {}); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showSuccessToast).toHaveBeenCalledTimes(1); + }); + + it('should show a toast message if update fails', async () => { + mockHttpClient.patch.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + try { + await service.update('1', {}); + } catch (e) { + //OM NOM NOM + } + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts new file mode 100644 index 0000000000..a719e47fb8 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts @@ -0,0 +1,35 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../../environments/environment'; +import { ToastService } from '../../../toast/toast.service'; +import { + NoticeOfIntentDecisionConditionDto, + UpdateNoticeOfIntentDecisionConditionDto, +} from '../../decision/notice-of-intent-decision.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentDecisionConditionService { + private url = `${environment.apiUrl}/notice-of-intent-decision-condition`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async update(uuid: string, data: UpdateNoticeOfIntentDecisionConditionDto) { + try { + const res = await firstValueFrom( + this.http.patch(`${this.url}/${uuid}`, data) + ); + this.toastService.showSuccessToast('Condition updated'); + return res; + } catch (e) { + if (e instanceof HttpErrorResponse && e.status === 400 && e.error?.message) { + this.toastService.showErrorToast(e.error.message); + } else { + this.toastService.showErrorToast('Failed to update condition'); + } + throw e; + } + } +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.spec.ts new file mode 100644 index 0000000000..bdc6ee227c --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.spec.ts @@ -0,0 +1,204 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { ToastService } from '../../toast/toast.service'; +import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2.service'; + +describe('NoticeOfIntentDecisionV2Service', () => { + let service: NoticeOfIntentDecisionV2Service; + let httpClient: DeepMocked; + let toastService: DeepMocked; + + beforeEach(() => { + httpClient = createMock(); + toastService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: httpClient, + }, + { + provide: ToastService, + useValue: toastService, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentDecisionV2Service); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should fetch and return an noiDto', async () => { + httpClient.get.mockReturnValue( + of([ + { + fileNumber: '1', + }, + ]) + ); + + const res = await service.fetchByFileNumber('1'); + + expect(res.length).toEqual(1); + expect(res[0].fileNumber).toEqual('1'); + }); + + it('should show a toast message if fetch fails', async () => { + httpClient.get.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.fetchByFileNumber('1'); + + expect(res.length).toEqual(0); + expect(toastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make an http patch and show a success toast when updating', async () => { + httpClient.patch.mockReturnValue( + of({ + fileNumber: '1', + }) + ); + + await service.update('1', { + isDraft: false, + }); + + expect(httpClient.patch).toHaveBeenCalledTimes(1); + expect(toastService.showSuccessToast).toHaveBeenCalledTimes(1); + }); + + it('should show a toast message if update fails', async () => { + httpClient.patch.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + try { + await service.update('1', { + isDraft: false, + }); + } catch (e) { + //OM NOM NOM + } + + expect(httpClient.patch).toHaveBeenCalledTimes(1); + expect(toastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make an http post and show a success toast when creating', async () => { + httpClient.post.mockReturnValue( + of({ + fileNumber: '1', + }) + ); + + await service.create({ + fileNumber: '', + date: 0, + modifiesUuid: '', + outcomeCode: '', + resolutionNumber: 0, + resolutionYear: 0, + isDraft: true, + }); + + expect(httpClient.post).toHaveBeenCalledTimes(1); + expect(toastService.showSuccessToast).toHaveBeenCalledTimes(1); + }); + + it('should show a toast message if create fails', async () => { + httpClient.post.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + try { + await service.create({ + fileNumber: '', + date: 0, + modifiesUuid: '', + outcomeCode: '', + resolutionNumber: 0, + resolutionYear: 0, + isDraft: true, + }); + } catch (e) { + //OM NOM NOM + } + + expect(httpClient.post).toHaveBeenCalledTimes(1); + expect(toastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make an http delete and show a success toast', async () => { + httpClient.delete.mockReturnValue( + of({ + fileNumber: '1', + }) + ); + + await service.delete(''); + + expect(httpClient.delete).toHaveBeenCalledTimes(1); + expect(toastService.showSuccessToast).toHaveBeenCalledTimes(1); + }); + + it('should show a toast message if delete fails', async () => { + httpClient.delete.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + try { + await service.delete(''); + } catch (e) { + //OM NOM NOM + } + + expect(httpClient.delete).toHaveBeenCalledTimes(1); + expect(toastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should show a toast warning when uploading a file thats too large', async () => { + const file = createMock(); + Object.defineProperty(file, 'size', { value: environment.maxFileSize + 1 }); + + await service.uploadFile('', file); + + expect(toastService.showWarningToast).toHaveBeenCalledTimes(1); + expect(httpClient.post).toHaveBeenCalledTimes(0); + }); + + it('should make an http delete when deleting a file', async () => { + httpClient.delete.mockReturnValue( + of({ + fileNumber: '1', + }) + ); + + await service.deleteFile('', ''); + + expect(httpClient.delete).toHaveBeenCalledTimes(1); + }); + + it('should make an http get when requesting a new resolution number', async () => { + httpClient.get.mockReturnValue(of(1)); + + await service.getNextAvailableResolutionNumber(2023); + + expect(httpClient.get).toHaveBeenCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.ts new file mode 100644 index 0000000000..86b38bb0a6 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.ts @@ -0,0 +1,168 @@ +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { BehaviorSubject, firstValueFrom } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { downloadFileFromUrl, openFileInline } from '../../../shared/utils/file'; +import { verifyFileSize } from '../../../shared/utils/file-size-checker'; +import { ToastService } from '../../toast/toast.service'; +import { + CreateNoticeOfIntentDecisionDto, + NoticeOfIntentDecisionCodesDto, + NoticeOfIntentDecisionDto, + NoticeOfIntentDecisionWithLinkedResolutionDto, + UpdateNoticeOfIntentDecisionDto, +} from '../decision/notice-of-intent-decision.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentDecisionV2Service { + private url = `${environment.apiUrl}/notice-of-intent-decision/v2`; + private decision: NoticeOfIntentDecisionDto | undefined; + private decisions: NoticeOfIntentDecisionWithLinkedResolutionDto[] = []; + $decision = new BehaviorSubject(undefined); + $decisions = new BehaviorSubject([]); + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetchByFileNumber(fileNumber: string) { + let decisions: NoticeOfIntentDecisionDto[] = []; + try { + decisions = await firstValueFrom( + this.http.get(`${this.url}/notice-of-intent/${fileNumber}`) + ); + } catch (err) { + this.toastService.showErrorToast('Failed to fetch decisions'); + } + return decisions; + } + + async fetchCodes(): Promise { + try { + return await firstValueFrom(this.http.get(`${this.url}/codes`)); + } catch (err) { + this.toastService.showErrorToast('Failed to fetch decisions'); + } + return { + outcomes: [], + decisionComponentTypes: [], + decisionConditionTypes: [], + }; + } + + async update(uuid: string, data: UpdateNoticeOfIntentDecisionDto) { + try { + const res = await firstValueFrom(this.http.patch(`${this.url}/${uuid}`, data)); + this.toastService.showSuccessToast('Decision updated'); + return res; + } catch (e) { + if (e instanceof HttpErrorResponse && e.status === 400 && e.error?.message) { + this.toastService.showErrorToast(e.error.message); + } else { + this.toastService.showErrorToast('Failed to update decision'); + } + throw e; + } + } + + async create(decision: CreateNoticeOfIntentDecisionDto) { + try { + const res = await firstValueFrom(this.http.post(`${this.url}`, decision)); + this.toastService.showSuccessToast('Decision created'); + return res; + } catch (e) { + if (e instanceof HttpErrorResponse && e.status === 400 && e.error?.message) { + this.toastService.showErrorToast(e.error.message); + } else { + this.toastService.showErrorToast(`Failed to create decision`); + } + throw e; + } + } + + async delete(uuid: string) { + try { + await firstValueFrom(this.http.delete(`${this.url}/${uuid}`)); + this.toastService.showSuccessToast('Decision deleted'); + } catch (err) { + this.toastService.showErrorToast('Failed to delete meeting'); + } + } + + async uploadFile(decisionUuid: string, file: File) { + const isValidSize = verifyFileSize(file, this.toastService); + if (!isValidSize) { + return; + } + + let formData: FormData = new FormData(); + formData.append('file', file, file.name); + const res = await firstValueFrom(this.http.post(`${this.url}/${decisionUuid}/file`, formData)); + this.toastService.showSuccessToast('Document uploaded'); + return res; + } + + async downloadFile(decisionUuid: string, documentUuid: string, fileName: string, isInline = true) { + const url = `${this.url}/${decisionUuid}/file/${documentUuid}`; + const finalUrl = isInline ? `${url}/open` : `${url}/download`; + const data = await firstValueFrom(this.http.get<{ url: string }>(finalUrl)); + if (isInline) { + openFileInline(data.url, fileName); + } else { + downloadFileFromUrl(data.url, fileName); + } + } + + async deleteFile(decisionUuid: string, documentUuid: string) { + const url = `${this.url}/${decisionUuid}/file/${documentUuid}`; + return await firstValueFrom(this.http.delete<{ url: string }>(url)); + } + + async getByUuid(uuid: string) { + let decision: NoticeOfIntentDecisionDto | undefined; + try { + decision = await firstValueFrom(this.http.get(`${this.url}/${uuid}`)); + } catch (err) { + this.toastService.showErrorToast('Failed to fetch decision'); + } + return decision; + } + + async loadDecision(uuid: string) { + this.clearDecision(); + this.decision = await this.getByUuid(uuid); + this.$decision.next(this.decision); + } + + async loadDecisions(fileNumber: string) { + this.clearDecisions(); + const decisions = await this.fetchByFileNumber(fileNumber); + const decisionsLength = decisions.length; + + this.decisions = decisions.map((decision, ind) => ({ + ...decision, + modifiedByResolutions: decision.modifiedBy?.flatMap((r) => r.linkedResolutions) || [], + index: decisionsLength - ind, + })); + + this.$decisions.next(this.decisions); + } + + clearDecision() { + this.$decision.next(undefined); + } + + clearDecisions() { + this.$decisions.next([]); + } + + async getNextAvailableResolutionNumber(resolutionYear: number) { + let result: number | undefined = undefined; + try { + result = await firstValueFrom(this.http.get(`${this.url}/next-resolution-number/${resolutionYear}`)); + } catch (err) { + this.toastService.showErrorToast('Failed to fetch resolutionNumber'); + } + return result; + } +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.dto.ts index 0065b7ddd7..e508c61b64 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.dto.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.dto.ts @@ -1,21 +1,31 @@ import { BaseCodeDto } from '../../../shared/dto/base.dto'; -import { NoticeOfIntentModificationDto } from '../notice-of-intent-modification/notice-of-intent-modification.dto'; export interface UpdateNoticeOfIntentDecisionDto { + resolutionNumber?: number; + resolutionYear?: number; date?: number; outcomeCode?: string; auditDate?: number | null; decisionMaker?: string | null; decisionMakerName?: string | null; + modifiesUuid?: string | null; + isSubjectToConditions?: boolean | null; + decisionDescription?: string | null; + isStatsRequired?: boolean | null; + rescindedDate?: number | null; + rescindedComment?: string | null; + isDraft?: boolean; + decisionComponents?: NoticeOfIntentDecisionComponentDto[]; + conditions?: UpdateNoticeOfIntentDecisionConditionDto[]; } export interface CreateNoticeOfIntentDecisionDto extends UpdateNoticeOfIntentDecisionDto { date: number; - outcomeCode: string; - resolutionNumber: number; + outcomeCode?: string; + resolutionNumber?: number; resolutionYear: number; - applicationFileNumber: string; - decisionMaker: string; + fileNumber: string; + decisionMaker?: string; decisionMakerName?: string; auditDate?: number | null; modifiesUuid?: string; @@ -29,16 +39,26 @@ export interface LinkedResolutionDto { export interface NoticeOfIntentDecisionDto { uuid: string; date: number; + createdAt: Date; outcome: NoticeOfIntentDecisionOutcomeCodeDto; resolutionNumber: number; resolutionYear: number; auditDate?: number | null; decisionMaker: string; decisionMakerName?: string; - noticeOfIntentFileNumber: string; + isSubjectToConditions: boolean | null; + isDraft: boolean; + wasReleased: boolean; + decisionDescription?: string | null; + isStatsRequired?: boolean | null; + rescindedDate?: number | null; + rescindedComment?: string | null; + fileNumber: string; documents: NoticeOfIntentDecisionDocumentDto[]; modifies?: LinkedResolutionDto; modifiedBy?: LinkedResolutionDto[]; + components: NoticeOfIntentDecisionComponentDto[]; + conditions: NoticeOfIntentDecisionConditionDto[]; } export interface NoticeOfIntentDecisionDocumentDto { @@ -50,3 +70,106 @@ export interface NoticeOfIntentDecisionDocumentDto { } export interface NoticeOfIntentDecisionOutcomeCodeDto extends BaseCodeDto {} + +export interface NoticeOfIntentDecisionConditionTypeDto extends BaseCodeDto {} +export interface NoticeOfIntentDecisionConditionDto { + uuid: string; + approvalDependant: boolean | null; + securityAmount: number | null; + administrativeFee: number | null; + description: string | null; + type: NoticeOfIntentDecisionConditionTypeDto; + componentUuid: string | null; + completionDate?: number; + supersededDate?: number; + components?: NoticeOfIntentDecisionComponentDto[]; +} + +export interface ComponentToCondition { + componentDecisionUuid?: string; + componentToConditionType?: string; + tempId: string; +} + +export interface UpdateNoticeOfIntentDecisionConditionDto { + uuid?: string; + componentsToCondition?: ComponentToCondition[]; + approvalDependant?: boolean | null; + securityAmount?: number | null; + administrativeFee?: number | null; + description?: string | null; + type?: NoticeOfIntentDecisionConditionTypeDto; + completionDate?: number | null; + supersededDate?: number | null; +} + +export interface NoticeOfIntentDecisionComponentTypeDto extends BaseCodeDto {} + +export interface UpdateNoticeOfIntentDecisionComponentDto { + uuid?: string; + alrArea?: number; + agCap?: string; + agCapSource?: string; + agCapMap?: string; + agCapConsultant?: string; + noticeOfIntentDecisionComponentTypeCode: string; + endDate?: number; + expiryDate?: number; + soilFillTypeToPlace?: string; + soilToPlaceVolume?: number | null; + soilToPlaceArea?: number | null; + soilToPlaceMaximumDepth?: number | null; + soilToPlaceAverageDepth?: number | null; + soilTypeRemoved?: string; + soilToRemoveVolume?: number | null; + soilToRemoveArea?: number | null; + soilToRemoveMaximumDepth?: number | null; + soilToRemoveAverageDepth?: number | null; +} + +export interface NoticeOfIntentDecisionComponentDto extends PofoDecisionComponentDto, RosoDecisionComponentDto { + uuid?: string; + alrArea?: number | null; + agCap?: string | null; + agCapSource?: string | null; + agCapMap?: string | null; + agCapConsultant?: string | null; + noticeOfIntentDecisionUuid?: string; + noticeOfIntentDecisionComponentTypeCode: string; + noticeOfIntentDecisionComponentType?: NoticeOfIntentDecisionComponentTypeDto; +} + +export interface PofoDecisionComponentDto { + endDate?: number | null; + soilFillTypeToPlace?: string | null; + soilToPlaceArea?: number | null; + soilToPlaceVolume?: number | null; + soilToPlaceMaximumDepth?: number | null; + soilToPlaceAverageDepth?: number | null; +} + +export interface RosoDecisionComponentDto { + endDate?: number | null; + soilTypeRemoved?: string | null; + soilToRemoveVolume?: number | null; + soilToRemoveArea?: number | null; + soilToRemoveMaximumDepth?: number | null; + soilToRemoveAverageDepth?: number | null; +} + +export interface NoticeOfIntentDecisionCodesDto { + outcomes: NoticeOfIntentDecisionOutcomeCodeDto[]; + decisionComponentTypes: NoticeOfIntentDecisionComponentTypeDto[]; + decisionConditionTypes: NoticeOfIntentDecisionConditionTypeDto[]; +} + +export interface NoticeOfIntentDecisionWithLinkedResolutionDto extends NoticeOfIntentDecisionDto { + modifiedByResolutions?: string[]; + index: number; +} + +export enum NOI_DECISION_COMPONENT_TYPE { + POFO = 'POFO', + ROSO = 'ROSO', + PFRS = 'PFRS', +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.service.spec.ts index 64ebbb5d5f..41499dc576 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.service.spec.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.service.spec.ts @@ -6,7 +6,7 @@ import { environment } from '../../../../environments/environment'; import { ToastService } from '../../toast/toast.service'; import { NoticeOfIntentDecisionService } from './notice-of-intent-decision.service'; -describe('ApplicationMeetingService', () => { +describe('NoticeOfIntentDecisionService', () => { let service: NoticeOfIntentDecisionService; let httpClient: DeepMocked; let toastService: DeepMocked; @@ -38,7 +38,7 @@ describe('ApplicationMeetingService', () => { httpClient.get.mockReturnValue( of([ { - noticeOfIntentFileNumber: '1', + fileNumber: '1', }, ]) ); @@ -46,7 +46,7 @@ describe('ApplicationMeetingService', () => { const res = await service.fetchByFileNumber('1'); expect(res.length).toEqual(1); - expect(res[0].noticeOfIntentFileNumber).toEqual('1'); + expect(res[0].fileNumber).toEqual('1'); }); it('should show a toast message if fetch fails', async () => { @@ -65,11 +65,13 @@ describe('ApplicationMeetingService', () => { it('should make an http patch and show a success toast when updating', async () => { httpClient.patch.mockReturnValue( of({ - applicationFileNumber: '1', + fileNumber: '1', }) ); - await service.update('1', {}); + await service.update('1', { + isDraft: false, + }); expect(httpClient.patch).toHaveBeenCalledTimes(1); expect(toastService.showSuccessToast).toHaveBeenCalledTimes(1); @@ -95,12 +97,12 @@ describe('ApplicationMeetingService', () => { it('should make an http post and show a success toast when creating', async () => { httpClient.post.mockReturnValue( of({ - applicationFileNumber: '1', + fileNumber: '1', }) ); await service.create({ - applicationFileNumber: '', + fileNumber: '', date: 0, outcomeCode: '', resolutionNumber: 0, @@ -121,7 +123,7 @@ describe('ApplicationMeetingService', () => { try { await service.create({ - applicationFileNumber: '', + fileNumber: '', date: 0, outcomeCode: '', resolutionNumber: 0, @@ -139,7 +141,7 @@ describe('ApplicationMeetingService', () => { it('should make an http delete and show a success toast', async () => { httpClient.delete.mockReturnValue( of({ - applicationFileNumber: '1', + fileNumber: '1', }) ); @@ -179,7 +181,7 @@ describe('ApplicationMeetingService', () => { it('should make an http delete when deleting a file', async () => { httpClient.delete.mockReturnValue( of({ - applicationFileNumber: '1', + fileNumber: '1', }) ); diff --git a/alcs-frontend/src/app/services/notice-of-intent/noi-document/noi-document.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/noi-document/noi-document.service.spec.ts index 07321c1818..c5afd1260c 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/noi-document/noi-document.service.spec.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/noi-document/noi-document.service.spec.ts @@ -7,7 +7,7 @@ import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../shared/document/documen import { ToastService } from '../../toast/toast.service'; import { NoiDocumentService } from './noi-document.service'; -describe('ApplicationDocumentService', () => { +describe('NoiDocumentService', () => { let service: NoiDocumentService; let httpClient: DeepMocked; let toastService: DeepMocked; diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-detail.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-detail.service.spec.ts index a51030f696..f6e8840842 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-detail.service.spec.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-detail.service.spec.ts @@ -1,9 +1,9 @@ import { TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { firstValueFrom, of } from 'rxjs'; +import { firstValueFrom } from 'rxjs'; import { ToastService } from '../toast/toast.service'; import { NoticeOfIntentDetailService } from './notice-of-intent-detail.service'; -import { NoticeOfIntentDto, UpdateNoticeOfIntentDto } from './notice-of-intent.dto'; +import { NoticeOfIntentDto } from './notice-of-intent.dto'; import { NoticeOfIntentService } from './notice-of-intent.service'; describe('NoticeOfIntentDetailService', () => { @@ -33,7 +33,7 @@ describe('NoticeOfIntentDetailService', () => { expect(service).toBeTruthy(); }); - it('should publish the loaded application', async () => { + it('should publish the loaded noi', async () => { noticeOfIntentService.fetchByFileNumber.mockResolvedValue({ fileNumber: '1', } as NoticeOfIntentDto); @@ -46,7 +46,7 @@ describe('NoticeOfIntentDetailService', () => { expect(res!.fileNumber).toEqual('1'); }); - it('should publish the updated application for update', async () => { + it('should publish the updated noi for update', async () => { noticeOfIntentService.update.mockResolvedValue({ fileNumber: '1', } as NoticeOfIntentDto); diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-detail.service.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-detail.service.ts index b0b6d1ad6d..d8ac8bfa61 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-detail.service.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-detail.service.ts @@ -15,7 +15,7 @@ export class NoticeOfIntentDetailService { this.$noticeOfIntent.next(noticeOfIntent); } - async clear() { + clear() { this.$noticeOfIntent.next(undefined); } diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.service.spec.ts index 77cd8e6913..8a5b69fda3 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.service.spec.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.service.spec.ts @@ -6,7 +6,7 @@ import { ToastService } from '../../toast/toast.service'; import { NoticeOfIntentModificationService } from './notice-of-intent-modification.service'; -describe('ApplicationReconsiderationService', () => { +describe('NoticeOfIntentModificationService', () => { let service: NoticeOfIntentModificationService; let httpClient: DeepMocked; let toastService: DeepMocked; diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.ts index a7764d1067..8b93c26cfa 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service.ts @@ -17,7 +17,7 @@ export class NoticeOfIntentParcelService { try { return firstValueFrom(this.http.get(`${this.baseUrl}/${fileNumber}`)); } catch (e) { - this.toastService.showErrorToast('Failed to fetch Application Parcels'); + this.toastService.showErrorToast('Failed to fetch Notice of Intent Parcels'); throw e; } } diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.dto.ts new file mode 100644 index 0000000000..47a7868033 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.dto.ts @@ -0,0 +1,32 @@ +import { BaseCodeDto } from '../../../shared/dto/base.dto'; +import { NOI_SUBMISSION_STATUS } from '../notice-of-intent.dto'; + +export interface NoticeOfIntentStatusDto extends BaseCodeDto { + alcsBackgroundColor: string; + + alcsColor: string; + + portalBackgroundColor: string; + + portalColor: string; + + code: NOI_SUBMISSION_STATUS; + + weight: number; +} + +export interface NoticeOfIntentSubmissionToSubmissionStatusDto { + submissionUuid: string; + + effectiveDate: number | null; + + statusTypeCode: string; + + status: NoticeOfIntentStatusDto; +} + +export const DEFAULT_NO_STATUS = { + backgroundColor: '#929292', + textColor: '#EFEFEF', + label: 'No Status', +}; diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.spec.ts new file mode 100644 index 0000000000..d55254c01d --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.spec.ts @@ -0,0 +1,98 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../../toast/toast.service'; +import { NoticeOfIntentSubmissionStatusService } from './notice-of-intent-submission-status.service'; + +describe('NoticeOfIntentSubmissionStatusService', () => { + let service: NoticeOfIntentSubmissionStatusService; + let mockHttpClient: DeepMocked; + let mockToastService: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentSubmissionStatusService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should fetch statuses by fileNumber', async () => { + mockHttpClient.get.mockReturnValue( + of([ + { + submissionUuid: 'fake', + }, + ]) + ); + + const res = await service.fetchSubmissionStatusesByFileNumber('1'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(res.length).toEqual(1); + expect(res[0].submissionUuid).toEqual('fake'); + }); + + it('should show a toast message if fetch statuses by fileNumber fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + try { + await service.fetchSubmissionStatusesByFileNumber('1'); + } catch { + // suppress error message + } + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should fetch current status by fileNumber', async () => { + mockHttpClient.get.mockReturnValue( + of({ + submissionUuid: 'fake', + }) + ); + + const res = await service.fetchCurrentStatusByFileNumber('1'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(res.submissionUuid).toEqual('fake'); + }); + + it('should show a toast message if fetch current status by fileNumber fails', async () => { + mockHttpClient.get.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + try { + await service.fetchCurrentStatusByFileNumber('1'); + } catch { + // suppress error message + } + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.ts new file mode 100644 index 0000000000..8e814dba64 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service.ts @@ -0,0 +1,49 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { ToastService } from '../../toast/toast.service'; +import { NoticeOfIntentSubmissionToSubmissionStatusDto } from './notice-of-intent-submission-status.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentSubmissionStatusService { + private baseUrl = `${environment.apiUrl}/notice-of-intent-submission-status`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetchSubmissionStatusesByFileNumber( + fileNumber: string, + showErrorToast = true + ): Promise { + try { + const result = await firstValueFrom( + this.http.get(`${this.baseUrl}/${fileNumber}`) + ); + return result; + } catch (e) { + if (showErrorToast) { + this.toastService.showErrorToast('Failed to fetch NOI Submission Statuses'); + } + throw e; + } + } + + async fetchCurrentStatusByFileNumber( + fileNumber: string, + showErrorToast = true + ): Promise { + try { + const result = await firstValueFrom( + this.http.get(`${this.baseUrl}/current-status/${fileNumber}`) + ); + return result; + } catch (e) { + if (showErrorToast) { + this.toastService.showErrorToast('Failed to fetch NOI Submission Status'); + } + throw e; + } + } +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts index 668511eef1..d98b15bfc6 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -12,7 +12,7 @@ describe('NoticeOfIntentSubmissionService', () => { let mockToastService: DeepMocked; let mockHttpClient: DeepMocked; - const mockSubmittedApplication: NoticeOfIntentSubmissionDto = { + const mockSubmittedNOI: NoticeOfIntentSubmissionDto = { fileNumber: '', uuid: '', createdAt: 0, @@ -66,7 +66,7 @@ describe('NoticeOfIntentSubmissionService', () => { soilAlreadyPlacedAverageDepth: null, soilProjectDurationAmount: null, soilIsRemovingSoilForNewStructure: null, - soilProposedStructures: [] + soilProposedStructures: [], }; beforeEach(() => { @@ -89,12 +89,12 @@ describe('NoticeOfIntentSubmissionService', () => { expect(service).toBeTruthy(); }); - it('should successfully fetch application submission', async () => { - mockHttpClient.get.mockReturnValue(of(mockSubmittedApplication)); + it('should successfully fetch noi submission', async () => { + mockHttpClient.get.mockReturnValue(of(mockSubmittedNOI)); const result = await service.fetchSubmission('1'); - expect(result).toEqual(mockSubmittedApplication); + expect(result).toEqual(mockSubmittedNOI); expect(mockHttpClient.get).toBeCalledTimes(1); }); }); diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.service.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.service.ts index 823cf9f663..74db344959 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.service.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.service.ts @@ -56,7 +56,7 @@ export class NoticeOfIntentService { async fetchByFileNumber(fileNumber: string) { try { - return await firstValueFrom(this.http.get(`${this.url}/${fileNumber}`)); + return firstValueFrom(this.http.get(`${this.url}/${fileNumber}`)); } catch (e) { console.error(e); this.toastService.showErrorToast('Failed to fetch Notice of Intent'); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts b/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts index 11f3051d0b..45e1858502 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision.entity.ts @@ -66,10 +66,6 @@ export class ApplicationDecision extends Base { @Column() outcomeCode: string; - @AutoMap() - @ManyToOne(() => Application) - application: Application; - @AutoMap() @Column({ type: 'int4', nullable: true }) resolutionNumber: number; @@ -178,6 +174,10 @@ export class ApplicationDecision extends Base { @Column({ nullable: true, type: 'text' }) linkedResolutionOutcomeCode: string | null; + @AutoMap() + @ManyToOne(() => Application) + application: Application; + @AutoMap() @Column({ type: 'uuid' }) applicationUuid: string; diff --git a/services/apps/alcs/src/alcs/import/noi-import.service.ts b/services/apps/alcs/src/alcs/import/noi-import.service.ts index 79f674fff6..47619d4728 100644 --- a/services/apps/alcs/src/alcs/import/noi-import.service.ts +++ b/services/apps/alcs/src/alcs/import/noi-import.service.ts @@ -6,10 +6,10 @@ import * as utc from 'dayjs/plugin/utc'; import * as fs from 'fs'; import * as path from 'path'; import { FALLBACK_APPLICANT_NAME } from '../../utils/owner.constants'; -import { LocalGovernmentService } from '../local-government/local-government.service'; import { BoardService } from '../board/board.service'; import { CardService } from '../card/card.service'; -import { NoticeOfIntentDecisionService } from '../notice-of-intent-decision/notice-of-intent-decision.service'; +import { LocalGovernmentService } from '../local-government/local-government.service'; +import { NoticeOfIntentDecisionV1Service } from '../notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service'; import { NoticeOfIntentMeetingService } from '../notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.service'; import { NoticeOfIntentSubtype } from '../notice-of-intent/notice-of-intent-subtype.entity'; import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; @@ -82,7 +82,7 @@ export class NoticeOfIntentImportService { private boardService: BoardService, private localGovernmentService: LocalGovernmentService, private cardService: CardService, - private noticeOfIntentDecisionService: NoticeOfIntentDecisionService, + private noticeOfIntentDecisionService: NoticeOfIntentDecisionV1Service, ) {} importNoiCsv() { @@ -363,11 +363,12 @@ export class NoticeOfIntentImportService { { date: mappedRow.decisionReleased.getTime(), decisionMaker: 'CEO Delegate', - applicationFileNumber: mappedRow.fileNumber, + fileNumber: mappedRow.fileNumber, outcomeCode: mappedRow.outcome === 'Approved' ? 'APPR' : 'ONTP', resolutionNumber: resolutionNumber, resolutionYear: resolutionYear, auditDate: mappedRow.auditDate?.getTime(), + isDraft: false, }, updatedApp, undefined, diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity.ts new file mode 100644 index 0000000000..77f3e40f43 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity.ts @@ -0,0 +1,12 @@ +import { Entity } from 'typeorm'; +import { BaseCodeEntity } from '../../../common/entities/base.code.entity'; + +@Entity() +export class NoticeOfIntentDecisionComponentType extends BaseCodeEntity { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.controller.spec.ts new file mode 100644 index 0000000000..6418968627 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.controller.spec.ts @@ -0,0 +1,84 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentDecisionProfile } from '../../../common/automapper/notice-of-intent-decision.automapper.profile'; +import { NoticeOfIntentDecisionComponentController } from './notice-of-intent-decision-component.controller'; +import { CreateNoticeOfIntentDecisionComponentDto } from './notice-of-intent-decision-component.dto'; +import { NoticeOfIntentDecisionComponent } from './notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecisionComponentService } from './notice-of-intent-decision-component.service'; + +describe('NoticeOfIntentDecisionComponentController', () => { + let controller: NoticeOfIntentDecisionComponentController; + let mockApplicationDecisionComponentService: DeepMocked; + + beforeEach(async () => { + mockApplicationDecisionComponentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentDecisionComponentController], + providers: [ + ...mockKeyCloakProviders, + NoticeOfIntentDecisionProfile, + { + provide: ClsService, + useValue: {}, + }, + { + provide: NoticeOfIntentDecisionComponentService, + useValue: mockApplicationDecisionComponentService, + }, + ], + }).compile(); + + controller = module.get( + NoticeOfIntentDecisionComponentController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call update of ApplicationDecisionComponentService', async () => { + mockApplicationDecisionComponentService.createOrUpdate.mockResolvedValue([ + new NoticeOfIntentDecisionComponent(), + ]); + mockApplicationDecisionComponentService.getOneOrFail.mockResolvedValue( + new NoticeOfIntentDecisionComponent(), + ); + const updates = { + uuid: 'fake_uuid', + alrArea: 10, + noticeOfIntentDecisionComponentTypeCode: 'fake', + } as CreateNoticeOfIntentDecisionComponentDto; + + await controller.update('fake_uuid', updates); + + expect( + mockApplicationDecisionComponentService.getOneOrFail, + ).toBeCalledTimes(1); + expect(mockApplicationDecisionComponentService.getOneOrFail).toBeCalledWith( + 'fake_uuid', + ); + expect( + mockApplicationDecisionComponentService.createOrUpdate, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionComponentService.createOrUpdate, + ).toBeCalledWith([ + { + uuid: 'fake_uuid', + alrArea: 10, + noticeOfIntentDecisionComponentTypeCode: 'fake', + }, + ]); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.controller.ts new file mode 100644 index 0000000000..cbc8b7cf6d --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.controller.ts @@ -0,0 +1,40 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Param, Patch } from '@nestjs/common'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { + NoticeOfIntentDecisionComponentDto, + UpdateNoticeOfIntentDecisionComponentDto, +} from './notice-of-intent-decision-component.dto'; +import { NoticeOfIntentDecisionComponent } from './notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecisionComponentService } from './notice-of-intent-decision-component.service'; + +@Controller('notice-of-intent-decision-component') +export class NoticeOfIntentDecisionComponentController { + constructor( + private noticeOfIntentDecisionComponentService: NoticeOfIntentDecisionComponentService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Patch('/:uuid') + @UserRoles(...ANY_AUTH_ROLE) + async update( + @Param('uuid') uuid: string, + @Body() updateDto: UpdateNoticeOfIntentDecisionComponentDto, + ): Promise { + await this.noticeOfIntentDecisionComponentService.getOneOrFail(uuid); + + const updatedComponent = + await this.noticeOfIntentDecisionComponentService.createOrUpdate([ + updateDto, + ]); + return ( + await this.mapper.mapArrayAsync( + updatedComponent, + NoticeOfIntentDecisionComponent, + NoticeOfIntentDecisionComponentDto, + ) + )[0]; + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.dto.ts new file mode 100644 index 0000000000..70ad3a57b8 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.dto.ts @@ -0,0 +1,158 @@ +import { AutoMap } from '@automapper/classes'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { BaseCodeDto } from '../../../common/dtos/base.dto'; + +export class NoticeOfIntentDecisionComponentTypeDto extends BaseCodeDto {} + +export class UpdateNoticeOfIntentDecisionComponentDto { + @IsString() + uuid: string; + + @IsOptional() + @IsString() + alrArea?: number; + + @IsOptional() + @IsString() + agCap?: string; + + @IsOptional() + @IsString() + agCapSource?: string; + + @IsOptional() + @IsString() + agCapMap?: string; + + @IsOptional() + @IsString() + agCapConsultant?: string; + + @IsString() + noticeOfIntentDecisionComponentTypeCode: string; + + @IsOptional() + @IsNumber() + endDate?: number; + + @IsOptional() + @IsNumber() + expiryDate?: number; + + @IsOptional() + @IsString() + soilFillTypeToPlace?: string; + + @IsNumber() + @IsOptional() + soilToPlaceVolume?: number | null; + + @IsNumber() + @IsOptional() + soilToPlaceArea?: number | null; + + @IsNumber() + @IsOptional() + soilToPlaceMaximumDepth?: number | null; + + @IsNumber() + @IsOptional() + soilToPlaceAverageDepth?: number | null; + + @IsOptional() + @IsString() + soilTypeRemoved?: string; + + @IsNumber() + @IsOptional() + soilToRemoveVolume?: number | null; + + @IsNumber() + @IsOptional() + soilToRemoveArea?: number | null; + + @IsNumber() + @IsOptional() + soilToRemoveMaximumDepth?: number | null; + + @IsNumber() + @IsOptional() + soilToRemoveAverageDepth?: number | null; +} + +export class CreateNoticeOfIntentDecisionComponentDto extends UpdateNoticeOfIntentDecisionComponentDto { + @IsOptional() + @IsString() + override uuid: string; +} + +export class NoticeOfIntentDecisionComponentDto { + @AutoMap() + uuid: string; + + @AutoMap() + alrArea?: number; + + @AutoMap() + agCap?: string; + + @AutoMap() + agCapSource?: string; + + @AutoMap() + agCapMap?: string; + + @AutoMap() + agCapConsultant?: string; + + @AutoMap() + endDate?: number; + + @AutoMap() + expiryDate?: number; + + @AutoMap() + noticeOfIntentDecisionUuid: string; + + @AutoMap() + soilFillTypeToPlace?: string; + + @AutoMap() + soilToPlaceVolume?: number; + + @AutoMap() + soilToPlaceArea?: number; + + @AutoMap() + soilToPlaceMaximumDepth?: number; + + @AutoMap() + soilToPlaceAverageDepth?: number; + + @AutoMap() + soilTypeRemoved?: string; + + @AutoMap() + soilToRemoveVolume?: number; + + @AutoMap() + soilToRemoveArea?: number; + + @AutoMap() + soilToRemoveMaximumDepth?: number; + + @AutoMap() + soilToRemoveAverageDepth?: number; + + @AutoMap() + noticeOfIntentDecisionComponentTypeCode: string; + + @AutoMap(() => NoticeOfIntentDecisionComponentTypeDto) + noticeOfIntentDecisionComponentType: NoticeOfIntentDecisionComponentTypeDto; +} + +export enum NOI_DECISION_COMPONENT_TYPE { + POFO = 'POFO', + ROSO = 'ROSO', + PFRS = 'PFRS', +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.entity.ts new file mode 100644 index 0000000000..46725fced2 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.entity.ts @@ -0,0 +1,190 @@ +import { AutoMap } from '@automapper/classes'; +import { Column, Entity, Index, ManyToMany, ManyToOne } from 'typeorm'; +import { Base } from '../../../common/entities/base.entity'; +import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; +import { NoticeOfIntentDecisionCondition } from '../notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; +import { NoticeOfIntentDecisionComponentType } from './notice-of-intent-decision-component-type.entity'; + +@Entity() +@Index( + ['noticeOfIntentDecisionComponentTypeCode', 'noticeOfIntentDecisionUuid'], + { + unique: true, + where: '"audit_deleted_date_at" is null', + }, +) +export class NoticeOfIntentDecisionComponent extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + comment: 'Area in hectares of ALR impacted by the decision component', + }) + alrArea?: number | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: 'Agricultural cap classification', + nullable: true, + }) + agCap?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: 'Agricultural capability classification system used', + nullable: true, + }) + agCapSource?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: 'Agricultural capability map sheet reference', + nullable: true, + }) + agCapMap?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: 'Consultant who determined the agricultural capability', + nullable: true, + }) + agCapConsultant?: string | null; + + @Column({ + type: 'timestamptz', + comment: 'Components` end date', + nullable: true, + }) + endDate?: Date | null; + + @Column({ + type: 'timestamptz', + comment: 'Components` expiry date', + nullable: true, + }) + expiryDate?: Date | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilFillTypeToPlace: string | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToPlaceVolume: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToPlaceArea: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToPlaceMaximumDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToPlaceAverageDepth: number | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + soilTypeRemoved: string | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToRemoveVolume: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToRemoveArea: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToRemoveMaximumDepth: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + soilToRemoveAverageDepth: number | null; + + @AutoMap() + @Column({ nullable: false }) + noticeOfIntentDecisionComponentTypeCode: string; + + @AutoMap() + @ManyToOne(() => NoticeOfIntentDecisionComponentType) + noticeOfIntentDecisionComponentType: NoticeOfIntentDecisionComponentType; + + @Column({ nullable: false }) + noticeOfIntentDecisionUuid: string; + + @AutoMap() + @ManyToOne(() => NoticeOfIntentDecision, { nullable: false }) + noticeOfIntentDecision: NoticeOfIntentDecision; + + @ManyToMany( + () => NoticeOfIntentDecisionCondition, + (condition) => condition.components, + ) + conditions: NoticeOfIntentDecisionCondition[]; +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts new file mode 100644 index 0000000000..7118c35dc8 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts @@ -0,0 +1,234 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateNoticeOfIntentDecisionComponentDto } from './notice-of-intent-decision-component.dto'; +import { NoticeOfIntentDecisionComponent } from './notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecisionComponentService } from './notice-of-intent-decision-component.service'; + +describe('ApplicationDecisionComponentService', () => { + let service: NoticeOfIntentDecisionComponentService; + let mockApplicationDecisionComponentRepository: DeepMocked< + Repository + >; + + beforeEach(async () => { + mockApplicationDecisionComponentRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentDecisionComponentService, + { + provide: getRepositoryToken(NoticeOfIntentDecisionComponent), + useValue: mockApplicationDecisionComponentRepository, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentDecisionComponentService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should call repo to get one or fails with correct parameters', async () => { + mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue( + new NoticeOfIntentDecisionComponent(), + ); + + const result = await service.getOneOrFail('fake'); + + expect( + mockApplicationDecisionComponentRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionComponentRepository.findOneOrFail, + ).toBeCalledWith({ + where: { uuid: 'fake' }, + }); + expect(result).toBeDefined(); + }); + + it('calls componentRepository.softRemove() method and soft removes an array of components', async () => { + const components = [ + new NoticeOfIntentDecisionComponent(), + new NoticeOfIntentDecisionComponent(), + ]; + + mockApplicationDecisionComponentRepository.softRemove.mockResolvedValue( + {} as NoticeOfIntentDecisionComponent, + ); + + await service.softRemove(components); + + expect( + mockApplicationDecisionComponentRepository.softRemove, + ).toHaveBeenCalledWith(components); + }); + + it('throws validation error if there are duplicate components', () => { + const firstComponent = { + uuid: 'fake', + noticeOfIntentDecisionComponentTypeCode: 'TURP', + agCap: '1', + agCapSource: '1', + alrArea: 1, + } as CreateNoticeOfIntentDecisionComponentDto; + + const secondComponent = { + uuid: 'fake-2', + noticeOfIntentDecisionComponentTypeCode: 'TURP', + agCap: '2', + agCapSource: '2', + alrArea: 2, + } as CreateNoticeOfIntentDecisionComponentDto; + + firstComponent.noticeOfIntentDecisionComponentTypeCode = 'fake'; + secondComponent.noticeOfIntentDecisionComponentTypeCode = 'fake'; + + const mockComponentsDto = [firstComponent, secondComponent]; + + expect(() => { + return service.validate(mockComponentsDto); + }).toThrowError(ServiceValidationException); + }); + + it('does not throw if there are no duplicate components', () => { + const firstComponent = { + uuid: 'fake', + noticeOfIntentDecisionComponentTypeCode: 'TURP', + agCap: '1', + agCapSource: '1', + alrArea: 1, + } as CreateNoticeOfIntentDecisionComponentDto; + + const secondComponent = { + uuid: 'fake-2', + noticeOfIntentDecisionComponentTypeCode: 'fake', + agCap: '2', + agCapSource: '2', + alrArea: 2, + } as CreateNoticeOfIntentDecisionComponentDto; + + const mockComponentsDto = [firstComponent, secondComponent]; + + expect(() => { + return service.validate(mockComponentsDto); + }).not.toThrowError(ServiceValidationException); + }); + + it('should create new components when given a DTO without a UUID', async () => { + mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue( + {} as NoticeOfIntentDecisionComponent, + ); + + const updateDtos = [ + new CreateNoticeOfIntentDecisionComponentDto(), + new CreateNoticeOfIntentDecisionComponentDto(), + ]; + + const result = await service.createOrUpdate(updateDtos, false); + + expect(result).toBeDefined(); + expect(result.length).toBe(2); + expect( + mockApplicationDecisionComponentRepository.findOneOrFail, + ).toBeCalledTimes(0); + }); + + it('should update existing components when given a DTO with a UUID', async () => { + mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue({ + uuid: 'fake', + noticeOfIntentDecisionComponentTypeCode: 'fake_code', + } as NoticeOfIntentDecisionComponent); + + const mockDto = new CreateNoticeOfIntentDecisionComponentDto(); + mockDto.uuid = 'fake'; + mockDto.alrArea = 1; + mockDto.agCap = '2'; + mockDto.agCapSource = '3'; + mockDto.agCapMap = '4'; + mockDto.agCapConsultant = '5'; + mockDto.noticeOfIntentDecisionComponentTypeCode = 'should_not_beUpdated'; + + const result = await service.createOrUpdate([mockDto], false); + + expect(result).toBeDefined(); + expect(result.length).toBe(1); + expect( + mockApplicationDecisionComponentRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionComponentRepository.findOneOrFail, + ).toBeCalledWith({ + where: { uuid: 'fake' }, + }); + expect(result[0].uuid).toEqual(mockDto.uuid); + expect(result[0].alrArea).toEqual(mockDto.alrArea); + expect(result[0].agCap).toEqual(mockDto.agCap); + expect(result[0].agCapSource).toEqual(mockDto.agCapSource); + expect(result[0].agCapMap).toEqual(mockDto.agCapMap); + expect(result[0].agCapConsultant).toEqual(mockDto.agCapConsultant); + expect(result[0].noticeOfIntentDecisionComponentTypeCode).toEqual( + 'fake_code', + ); + }); + + it('should persist entity if persist flag is true', async () => { + mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue( + {} as NoticeOfIntentDecisionComponent, + ); + mockApplicationDecisionComponentRepository.save.mockResolvedValue( + {} as NoticeOfIntentDecisionComponent, + ); + + const updateDtos = [new CreateNoticeOfIntentDecisionComponentDto()]; + + const result = await service.createOrUpdate(updateDtos, true); + + expect(result).toBeDefined(); + expect( + mockApplicationDecisionComponentRepository.findOneOrFail, + ).toBeCalledTimes(0); + expect(mockApplicationDecisionComponentRepository.save).toBeCalledTimes(1); + }); + + it('should not persist entity if persist flag is false', async () => { + mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue( + {} as NoticeOfIntentDecisionComponent, + ); + mockApplicationDecisionComponentRepository.save.mockResolvedValue( + {} as NoticeOfIntentDecisionComponent, + ); + + const updateDtos = [new CreateNoticeOfIntentDecisionComponentDto()]; + + const result = await service.createOrUpdate(updateDtos, false); + + expect(result).toBeDefined(); + expect( + mockApplicationDecisionComponentRepository.findOneOrFail, + ).toBeCalledTimes(0); + expect(mockApplicationDecisionComponentRepository.save).toBeCalledTimes(0); + }); + + it('should validation decision component fields and throw error if any', async () => { + const mockComponents = [ + { + noticeOfIntentDecisionComponentTypeCode: 'POFO', + }, + { noticeOfIntentDecisionComponentTypeCode: 'ROSO' }, + { noticeOfIntentDecisionComponentTypeCode: 'PFRS' }, + ] as CreateNoticeOfIntentDecisionComponentDto[]; + + const mockValidationWrapper = () => { + service.validate(mockComponents, false); + }; + + expect(mockValidationWrapper).toThrow(ServiceValidationException); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.ts new file mode 100644 index 0000000000..6128ec1abf --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.ts @@ -0,0 +1,228 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { + NOI_DECISION_COMPONENT_TYPE, + CreateNoticeOfIntentDecisionComponentDto, +} from './notice-of-intent-decision-component.dto'; +import { NoticeOfIntentDecisionComponent } from './notice-of-intent-decision-component.entity'; + +@Injectable() +export class NoticeOfIntentDecisionComponentService { + constructor( + @InjectRepository(NoticeOfIntentDecisionComponent) + private componentRepository: Repository, + ) {} + + async getOneOrFail(uuid: string) { + return this.componentRepository.findOneOrFail({ + where: { uuid }, + }); + } + + async createOrUpdate( + updateDtos: CreateNoticeOfIntentDecisionComponentDto[], + isPersist = true, + ) { + const updatedComponents: NoticeOfIntentDecisionComponent[] = []; + + for (const updateDto of updateDtos) { + let component: NoticeOfIntentDecisionComponent | null = null; + + if (updateDto.uuid) { + component = await this.getOneOrFail(updateDto.uuid); + } else { + component = new NoticeOfIntentDecisionComponent(); + component.noticeOfIntentDecisionComponentTypeCode = + updateDto.noticeOfIntentDecisionComponentTypeCode; + } + + component.alrArea = updateDto.alrArea; + component.agCap = updateDto.agCap; + component.agCapSource = updateDto.agCapSource; + component.agCapMap = updateDto.agCapMap; + component.agCapConsultant = updateDto.agCapConsultant; + + this.patchPofoFields(component, updateDto); + this.patchRosoFields(component, updateDto); + + updatedComponents.push(component); + } + + if (isPersist) { + return await this.componentRepository.save(updatedComponents); + } + + return updatedComponents; + } + + private patchPofoFields( + component: NoticeOfIntentDecisionComponent, + updateDto: CreateNoticeOfIntentDecisionComponentDto, + ) { + component.endDate = updateDto.endDate ? new Date(updateDto.endDate) : null; + component.soilFillTypeToPlace = updateDto.soilFillTypeToPlace ?? null; + component.soilToPlaceArea = updateDto.soilToPlaceArea ?? null; + component.soilToPlaceVolume = updateDto.soilToPlaceVolume ?? null; + component.soilToPlaceMaximumDepth = + updateDto.soilToPlaceMaximumDepth ?? null; + component.soilToPlaceAverageDepth = + updateDto.soilToPlaceAverageDepth ?? null; + } + + private patchRosoFields( + component: NoticeOfIntentDecisionComponent, + updateDto: CreateNoticeOfIntentDecisionComponentDto, + ) { + component.endDate = updateDto.endDate ? new Date(updateDto.endDate) : null; + component.soilTypeRemoved = updateDto.soilTypeRemoved ?? null; + component.soilToRemoveVolume = updateDto.soilToRemoveVolume ?? null; + component.soilToRemoveArea = updateDto.soilToRemoveArea ?? null; + component.soilToRemoveMaximumDepth = + updateDto.soilToRemoveMaximumDepth ?? null; + component.soilToRemoveAverageDepth = + updateDto.soilToRemoveAverageDepth ?? null; + } + + validate( + componentsDto: CreateNoticeOfIntentDecisionComponentDto[], + isDraftDecision = false, + ) { + if (!this.checkDuplicates(componentsDto)) { + throw new ServiceValidationException( + 'Only on component of each type is allowed', + ); + } + + if (!isDraftDecision) { + if (componentsDto.length < 1) { + throw new ServiceValidationException( + 'Decision components are required', + ); + } + + this.validateDecisionComponentFields(componentsDto); + } + } + + private checkDuplicates( + components: CreateNoticeOfIntentDecisionComponentDto[], + ) { + const typeCounts = {}; + + for (const { noticeOfIntentDecisionComponentTypeCode } of components) { + if (typeCounts[noticeOfIntentDecisionComponentTypeCode]) { + return false; + } + typeCounts[noticeOfIntentDecisionComponentTypeCode] = 1; + } + + return true; + } + + async getAllByNoticeOfIntentUUID(noticeOfIntentUuid: string) { + return await this.componentRepository.find({ + where: { + noticeOfIntentDecision: { + noticeOfIntentUuid, + }, + }, + relations: { + noticeOfIntentDecisionComponentType: true, + }, + }); + } + + validateDecisionComponentFields( + componentsDto: CreateNoticeOfIntentDecisionComponentDto[], + ) { + const errors: string[] = []; + + for (const component of componentsDto) { + if (!component.alrArea) { + errors.push('Alr Area is required'); + } + if (!component.agCap) { + errors.push('Agri Cap is required'); + } + if (!component.alrArea) { + errors.push('Agri Source is required'); + } + + if ( + component.noticeOfIntentDecisionComponentTypeCode === + NOI_DECISION_COMPONENT_TYPE.POFO + ) { + this.validatePofoDecisionComponentFields(component, errors); + } + + if ( + component.noticeOfIntentDecisionComponentTypeCode === + NOI_DECISION_COMPONENT_TYPE.ROSO + ) { + this.validateRosoDecisionComponentFields(component, errors); + } + + if ( + component.noticeOfIntentDecisionComponentTypeCode === + NOI_DECISION_COMPONENT_TYPE.PFRS + ) { + this.validatePofoDecisionComponentFields(component, errors); + this.validateRosoDecisionComponentFields(component, errors); + } + } + + if (errors.length > 0) { + throw new ServiceValidationException(errors.join(', ')); + } + } + + private validatePofoDecisionComponentFields( + component: CreateNoticeOfIntentDecisionComponentDto, + errors: string[], + ) { + if (!component.soilFillTypeToPlace) { + errors.push( + 'Type, origin and quality of fill approved to be placed is required', + ); + } + if (!component.soilToPlaceVolume) { + errors.push('Volume To Place is required'); + } + if (!component.soilToPlaceArea) { + errors.push('Area To Place is required'); + } + if (!component.soilToPlaceMaximumDepth) { + errors.push('Maximum Depth To Place is required'); + } + if (!component.soilToPlaceAverageDepth) { + errors.push('Average Depth To Place is required'); + } + } + + private validateRosoDecisionComponentFields( + component: CreateNoticeOfIntentDecisionComponentDto, + errors: string[], + ) { + if (!component.soilTypeRemoved) { + errors.push('Type of soil approved to be removed is required'); + } + if (!component.soilToRemoveVolume) { + errors.push('Volume To Remove is required'); + } + if (!component.soilToRemoveArea) { + errors.push('Area To Remove is required'); + } + if (!component.soilToRemoveMaximumDepth) { + errors.push('Maximum Depth To Remove is required'); + } + if (!component.soilToRemoveAverageDepth) { + errors.push('Average Depth To Remove is required'); + } + } + + async softRemove(components: NoticeOfIntentDecisionComponent[]) { + await this.componentRepository.softRemove(components); + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity.ts new file mode 100644 index 0000000000..595624db07 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity.ts @@ -0,0 +1,5 @@ +import { Entity } from 'typeorm'; +import { BaseCodeEntity } from '../../../common/entities/base.code.entity'; + +@Entity() +export class NoticeOfIntentDecisionConditionType extends BaseCodeEntity {} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts new file mode 100644 index 0000000000..b1756bf9bd --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts @@ -0,0 +1,109 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentDecisionProfile } from '../../../common/automapper/notice-of-intent-decision.automapper.profile'; +import { NoticeOfIntentDecisionConditionController } from './notice-of-intent-decision-condition.controller'; +import { UpdateNoticeOfIntentDecisionConditionDto } from './notice-of-intent-decision-condition.dto'; +import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition.entity'; +import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition.service'; + +describe('NoticeOfIntentDecisionConditionController', () => { + let controller: NoticeOfIntentDecisionConditionController; + let mockApplicationDecisionConditionService: DeepMocked; + + beforeEach(async () => { + mockApplicationDecisionConditionService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentDecisionConditionController], + providers: [ + NoticeOfIntentDecisionProfile, + { + provide: NoticeOfIntentDecisionConditionService, + useValue: mockApplicationDecisionConditionService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NoticeOfIntentDecisionConditionController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('update', () => { + it('should update the condition and return updated condition', async () => { + // Arrange + const uuid = 'example-uuid'; + const date = new Date(); + const updates: UpdateNoticeOfIntentDecisionConditionDto = { + approvalDependant: true, + securityAmount: 1000, + administrativeFee: 50, + description: 'example description', + completionDate: date.getTime(), + supersededDate: date.getTime(), + }; + + const condition = new NoticeOfIntentDecisionCondition({ + uuid, + approvalDependant: false, + securityAmount: 500, + administrativeFee: 25, + description: 'existing description', + completionDate: new Date(), + supersededDate: new Date(), + }); + + const updated = new NoticeOfIntentDecisionCondition({ + uuid, + approvalDependant: updates.approvalDependant, + securityAmount: updates.securityAmount, + administrativeFee: updates.administrativeFee, + description: updates.description, + completionDate: date, + supersededDate: date, + }); + + mockApplicationDecisionConditionService.getOneOrFail.mockResolvedValue( + condition, + ); + mockApplicationDecisionConditionService.update.mockResolvedValue(updated); + + const result = await controller.update(uuid, updates); + + expect( + mockApplicationDecisionConditionService.getOneOrFail, + ).toHaveBeenCalledWith(uuid); + expect( + mockApplicationDecisionConditionService.update, + ).toHaveBeenCalledWith(condition, { + ...updates, + completionDate: date, + supersededDate: date, + }); + expect(new Date(result.completionDate!)).toEqual(updated.completionDate); + expect(new Date(result.supersededDate!)).toEqual(updated.supersededDate); + expect(result.description).toEqual(updated.description); + expect(result.administrativeFee).toEqual(updated.administrativeFee); + expect(result.securityAmount).toEqual(updated.securityAmount); + expect(result.approvalDependant).toEqual(updated.approvalDependant); + }); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts new file mode 100644 index 0000000000..e2830c7b33 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.ts @@ -0,0 +1,48 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { + NoticeOfIntentDecisionConditionDto, + UpdateNoticeOfIntentDecisionConditionDto, +} from './notice-of-intent-decision-condition.dto'; +import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition.entity'; +import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition.service'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@Controller('notice-of-intent-decision-condition') +@UseGuards(RolesGuard) +export class NoticeOfIntentDecisionConditionController { + constructor( + private conditionService: NoticeOfIntentDecisionConditionService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Patch('/:uuid') + @UserRoles(...ANY_AUTH_ROLE) + async update( + @Param('uuid') uuid: string, + @Body() updates: UpdateNoticeOfIntentDecisionConditionDto, + ) { + const condition = await this.conditionService.getOneOrFail(uuid); + + const updatedCondition = await this.conditionService.update(condition, { + approvalDependant: updates.approvalDependant, + securityAmount: updates.securityAmount, + administrativeFee: updates.administrativeFee, + description: updates.description, + completionDate: formatIncomingDate(updates.completionDate), + supersededDate: formatIncomingDate(updates.supersededDate), + }); + return await this.mapper.mapAsync( + updatedCondition, + NoticeOfIntentDecisionCondition, + NoticeOfIntentDecisionConditionDto, + ); + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts new file mode 100644 index 0000000000..1cd1db72f5 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto.ts @@ -0,0 +1,103 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsArray, + IsBoolean, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { BaseCodeDto } from '../../../common/dtos/base.dto'; +import { NoticeOfIntentDecisionComponentDto } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.dto'; + +export class NoticeOfIntentDecisionConditionTypeDto extends BaseCodeDto {} +export class NoticeOfIntentDecisionConditionDto { + @AutoMap() + uuid: string; + + @AutoMap(() => Boolean) + approvalDependant: boolean | null; + + @AutoMap(() => Number) + securityAmount: number | null; + + @AutoMap(() => Number) + administrativeFee: number | null; + + @AutoMap(() => String) + description: string | null; + + @AutoMap(() => NoticeOfIntentDecisionConditionTypeDto) + type: NoticeOfIntentDecisionConditionTypeDto; + + @AutoMap(() => String) + componentUuid: string | null; + + @AutoMap() + completionDate?: number; + + @AutoMap() + supersededDate?: number; + + @AutoMap() + components?: NoticeOfIntentDecisionComponentDto[]; +} + +export class ComponentToConditionDto { + @IsOptional() + @IsUUID() + componentDecisionUuid?: string; + + @IsOptional() + @IsString() + componentToConditionType?: string; +} + +export class UpdateNoticeOfIntentDecisionConditionDto { + @IsOptional() + @IsString() + uuid?: string; + + @IsOptional() + @IsArray() + componentsToCondition?: ComponentToConditionDto[]; + + @IsOptional() + @IsBoolean() + approvalDependant?: boolean; + + @IsOptional() + @IsNumber() + securityAmount?: number; + + @IsOptional() + @IsNumber() + administrativeFee?: number; + + @IsOptional() + @IsString() + description?: string; + + @IsOptional() + @IsString() + type?: NoticeOfIntentDecisionConditionTypeDto; + + @IsOptional() + @IsNumber() + completionDate?: number; + + @IsOptional() + @IsNumber() + supersededDate?: number; +} + +export class UpdateNoticeOfIntentDecisionConditionServiceDto { + componentDecisionUuid?: string; + componentToConditionType?: string; + approvalDependant?: boolean; + securityAmount?: number; + administrativeFee?: number; + description?: string; + completionDate?: Date | null; + supersededDate?: Date | null; +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts new file mode 100644 index 0000000000..b40b90bf3e --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity.ts @@ -0,0 +1,85 @@ +import { AutoMap } from '@automapper/classes'; +import { Column, Entity, JoinTable, ManyToMany, ManyToOne } from 'typeorm'; +import { Base } from '../../../common/entities/base.entity'; +import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; +import { NoticeOfIntentDecisionComponent } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; +import { NoticeOfIntentDecisionConditionType } from './notice-of-intent-decision-condition-code.entity'; + +@Entity() +export class NoticeOfIntentDecisionCondition extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + approvalDependant: boolean | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + securityAmount: number | null; + + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + administrativeFee: number | null; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + description: string | null; + + @AutoMap(() => String) + @Column({ + type: 'timestamptz', + comment: 'Condition Completion date', + nullable: true, + }) + completionDate?: Date | null; + + @AutoMap() + @Column({ + type: 'timestamptz', + comment: 'Condition Superseded date', + nullable: true, + }) + supersededDate?: Date | null; + + @ManyToOne(() => NoticeOfIntentDecisionConditionType) + type: NoticeOfIntentDecisionConditionType; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + typeCode: string | null; + + @ManyToOne(() => NoticeOfIntentDecision, { nullable: false }) + decision: NoticeOfIntentDecision; + + @AutoMap(() => String) + @Column() + decisionUuid: string; + + @ManyToMany( + () => NoticeOfIntentDecisionComponent, + (component) => component.conditions, + { nullable: true }, + ) + @JoinTable({ + name: 'notice_of_intent_decision_condition_component', + }) + components: NoticeOfIntentDecisionComponent[] | null; +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts new file mode 100644 index 0000000000..1a9eb6b50a --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts @@ -0,0 +1,162 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NoticeOfIntentDecisionConditionType } from './notice-of-intent-decision-condition-code.entity'; +import { UpdateNoticeOfIntentDecisionConditionDto } from './notice-of-intent-decision-condition.dto'; +import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition.entity'; +import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition.service'; + +describe('ApplicationDecisionConditionService', () => { + let service: NoticeOfIntentDecisionConditionService; + let mockApplicationDecisionConditionRepository: DeepMocked< + Repository + >; + let mockAppDecCondTypeRepository: DeepMocked< + Repository + >; + + beforeEach(async () => { + mockApplicationDecisionConditionRepository = createMock(); + mockAppDecCondTypeRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentDecisionConditionService, + { + provide: getRepositoryToken(NoticeOfIntentDecisionCondition), + useValue: mockApplicationDecisionConditionRepository, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecisionConditionType), + useValue: mockAppDecCondTypeRepository, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentDecisionConditionService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should call repo to get one or fails with correct parameters', async () => { + mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + new NoticeOfIntentDecisionCondition(), + ); + + const result = await service.getOneOrFail('fake'); + + expect( + mockApplicationDecisionConditionRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionConditionRepository.findOneOrFail, + ).toBeCalledWith({ + where: { uuid: 'fake' }, + relations: { type: true }, + }); + expect(result).toBeDefined(); + }); + + it('calls remove method for deleted conditions', async () => { + const conditions = [ + new NoticeOfIntentDecisionCondition(), + new NoticeOfIntentDecisionCondition(), + ]; + + mockApplicationDecisionConditionRepository.remove.mockResolvedValue( + {} as NoticeOfIntentDecisionCondition, + ); + + await service.remove(conditions); + + expect(mockApplicationDecisionConditionRepository.remove).toBeCalledTimes( + 1, + ); + }); + + it('should create new components when given a DTO without a UUID', async () => { + mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + new NoticeOfIntentDecisionCondition(), + ); + + const updateDtos: UpdateNoticeOfIntentDecisionConditionDto[] = [{}, {}]; + + const result = await service.createOrUpdate(updateDtos, [], [], false); + + expect(result).toBeDefined(); + expect(result.length).toBe(2); + expect( + mockApplicationDecisionConditionRepository.findOneOrFail, + ).toBeCalledTimes(0); + }); + + it('should update existing components when given a DTO with a UUID', async () => { + mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + new NoticeOfIntentDecisionCondition({ + uuid: 'uuid', + }), + ); + + const mockDto: UpdateNoticeOfIntentDecisionConditionDto = { + uuid: 'uuid', + }; + + const result = await service.createOrUpdate([mockDto], [], [], false); + + expect(result).toBeDefined(); + expect(result.length).toBe(1); + expect( + mockApplicationDecisionConditionRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect( + mockApplicationDecisionConditionRepository.findOneOrFail, + ).toBeCalledWith({ + where: { uuid: 'uuid' }, + relations: { type: true }, + }); + expect(result[0].uuid).toEqual(mockDto.uuid); + }); + + it('should persist entity if persist flag is true', async () => { + mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + new NoticeOfIntentDecisionCondition(), + ); + mockApplicationDecisionConditionRepository.save.mockResolvedValue( + new NoticeOfIntentDecisionCondition(), + ); + + const updateDtos: UpdateNoticeOfIntentDecisionConditionDto[] = [{}]; + + const result = await service.createOrUpdate(updateDtos, [], [], true); + + expect(result).toBeDefined(); + expect( + mockApplicationDecisionConditionRepository.findOneOrFail, + ).toBeCalledTimes(0); + expect(mockApplicationDecisionConditionRepository.save).toBeCalledTimes(1); + }); + + it('should not persist entity if persist flag is false', async () => { + mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + new NoticeOfIntentDecisionCondition(), + ); + mockApplicationDecisionConditionRepository.save.mockResolvedValue( + new NoticeOfIntentDecisionCondition(), + ); + + const updateDtos: UpdateNoticeOfIntentDecisionConditionDto[] = [{}]; + + const result = await service.createOrUpdate(updateDtos, [], [], false); + + expect(result).toBeDefined(); + expect( + mockApplicationDecisionConditionRepository.findOneOrFail, + ).toBeCalledTimes(0); + expect(mockApplicationDecisionConditionRepository.save).toBeCalledTimes(0); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts new file mode 100644 index 0000000000..7f38ee9569 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ServiceValidationException } from '../../../../../../libs/common/src/exceptions/base.exception'; +import { NoticeOfIntentDecisionComponent } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecisionConditionType } from './notice-of-intent-decision-condition-code.entity'; +import { + UpdateNoticeOfIntentDecisionConditionDto, + UpdateNoticeOfIntentDecisionConditionServiceDto, +} from './notice-of-intent-decision-condition.dto'; +import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition.entity'; + +@Injectable() +export class NoticeOfIntentDecisionConditionService { + constructor( + @InjectRepository(NoticeOfIntentDecisionCondition) + private repository: Repository, + @InjectRepository(NoticeOfIntentDecisionConditionType) + private typeRepository: Repository, + ) {} + + async getOneOrFail(uuid: string) { + return this.repository.findOneOrFail({ + where: { uuid }, + relations: { + type: true, + }, + }); + } + + async createOrUpdate( + updateDtos: UpdateNoticeOfIntentDecisionConditionDto[], + allComponents: NoticeOfIntentDecisionComponent[], + newComponents: NoticeOfIntentDecisionComponent[], + isPersist = true, + ) { + const updatedConditions: NoticeOfIntentDecisionCondition[] = []; + + for (const updateDto of updateDtos) { + let condition: NoticeOfIntentDecisionCondition | null = null; + + if (updateDto.uuid) { + condition = await this.getOneOrFail(updateDto.uuid); + } else { + condition = new NoticeOfIntentDecisionCondition(); + } + if (updateDto.type?.code) { + condition.type = await this.typeRepository.findOneOrFail({ + where: { + code: updateDto.type.code, + }, + }); + } + + condition.administrativeFee = updateDto.administrativeFee ?? null; + condition.description = updateDto.description ?? null; + condition.securityAmount = updateDto.securityAmount ?? null; + condition.approvalDependant = updateDto.approvalDependant ?? null; + + if ( + updateDto.componentsToCondition !== undefined && + updateDto.componentsToCondition.length > 0 + ) { + const mappedComponents: NoticeOfIntentDecisionComponent[] = []; + for (const componentToCondition of updateDto.componentsToCondition) { + const matchingComponent = allComponents.find( + (component) => + componentToCondition.componentDecisionUuid === + component.noticeOfIntentDecisionUuid && + componentToCondition.componentToConditionType === + component.noticeOfIntentDecisionComponentTypeCode, + ); + + if (matchingComponent) { + mappedComponents.push(matchingComponent); + updatedConditions.push(condition); + continue; + } + + const matchingComponent2 = newComponents.find( + (component) => + componentToCondition.componentToConditionType === + component.noticeOfIntentDecisionComponentTypeCode, + ); + + if (matchingComponent2) { + mappedComponents.push(matchingComponent2); + updatedConditions.push(condition); + continue; + } + throw new ServiceValidationException( + 'Failed to find matching component', + ); + } + + condition.components = mappedComponents; + } else { + condition.components = null; + updatedConditions.push(condition); + } + } + + if (isPersist) { + return await this.repository.save(updatedConditions); + } + + return updatedConditions; + } + + async remove(conditions: NoticeOfIntentDecisionCondition[]) { + await this.repository.remove(conditions); + } + + async update( + existingCondition: NoticeOfIntentDecisionCondition, + updates: UpdateNoticeOfIntentDecisionConditionServiceDto, + ) { + await this.repository.update(existingCondition.uuid, updates); + return await this.getOneOrFail(existingCondition.uuid); + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller.spec.ts similarity index 80% rename from services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.controller.spec.ts rename to services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller.spec.ts index 9a48e1e66a..45398bf98a 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller.spec.ts @@ -3,24 +3,24 @@ import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; -import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; -import { NoticeOfIntentDecisionProfile } from '../../common/automapper/notice-of-intent-decision.automapper.profile'; -import { UserProfile } from '../../common/automapper/user.automapper.profile'; -import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; -import { NoticeOfIntentService } from '../notice-of-intent/notice-of-intent.service'; -import { NoticeOfIntentDecisionOutcome } from './notice-of-intent-decision-outcome.entity'; -import { NoticeOfIntentDecisionController } from './notice-of-intent-decision.controller'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentDecisionProfile } from '../../../common/automapper/notice-of-intent-decision.automapper.profile'; +import { UserProfile } from '../../../common/automapper/user.automapper.profile'; +import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity'; +import { NoticeOfIntentDecisionV1Controller } from './notice-of-intent-decision-v1.controller'; import { CreateNoticeOfIntentDecisionDto, UpdateNoticeOfIntentDecisionDto, -} from './notice-of-intent-decision.dto'; -import { NoticeOfIntentDecision } from './notice-of-intent-decision.entity'; -import { NoticeOfIntentDecisionService } from './notice-of-intent-decision.service'; -import { NoticeOfIntentModificationService } from './notice-of-intent-modification/notice-of-intent-modification.service'; +} from '../notice-of-intent-decision.dto'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; +import { NoticeOfIntentDecisionV1Service } from './notice-of-intent-decision-v1.service'; +import { NoticeOfIntentModificationService } from '../notice-of-intent-modification/notice-of-intent-modification.service'; describe('NoticeOfIntentDecisionController', () => { - let controller: NoticeOfIntentDecisionController; - let mockDecisionService: DeepMocked; + let controller: NoticeOfIntentDecisionV1Controller; + let mockDecisionService: DeepMocked; let mockNOIService: DeepMocked; let mockNOIModificationService: DeepMocked; @@ -46,12 +46,12 @@ describe('NoticeOfIntentDecisionController', () => { strategyInitializer: classes(), }), ], - controllers: [NoticeOfIntentDecisionController], + controllers: [NoticeOfIntentDecisionV1Controller], providers: [ NoticeOfIntentDecisionProfile, UserProfile, { - provide: NoticeOfIntentDecisionService, + provide: NoticeOfIntentDecisionV1Service, useValue: mockDecisionService, }, { @@ -70,8 +70,8 @@ describe('NoticeOfIntentDecisionController', () => { ], }).compile(); - controller = module.get( - NoticeOfIntentDecisionController, + controller = module.get( + NoticeOfIntentDecisionV1Controller, ); mockDecisionService.fetchCodes.mockResolvedValue({ @@ -120,7 +120,7 @@ describe('NoticeOfIntentDecisionController', () => { const decisionToCreate = { date: new Date(2022, 2, 2, 2, 2, 2, 2).valueOf(), - applicationFileNumber: mockNoi.fileNumber, + fileNumber: mockNoi.fileNumber, outcomeCode: 'outcome', } as CreateNoticeOfIntentDecisionDto; @@ -141,7 +141,7 @@ describe('NoticeOfIntentDecisionController', () => { it('should update the decision', async () => { mockDecisionService.update.mockResolvedValue(mockDecision); const updates = { - outcome: 'New Outcome', + outcomeCode: 'New Outcome', date: new Date(2022, 2, 2, 2, 2, 2, 2).valueOf(), } as UpdateNoticeOfIntentDecisionDto; @@ -149,7 +149,7 @@ describe('NoticeOfIntentDecisionController', () => { expect(mockDecisionService.update).toBeCalledTimes(1); expect(mockDecisionService.update).toBeCalledWith('fake-uuid', { - outcome: 'New Outcome', + outcomeCode: 'New Outcome', date: updates.date, }); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller.ts similarity index 83% rename from services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.controller.ts rename to services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller.ts index a6de65456f..9b6f3e826d 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller.ts @@ -14,27 +14,27 @@ import { } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; -import { ANY_AUTH_ROLE } from '../../common/authorization/roles'; -import { RolesGuard } from '../../common/authorization/roles-guard.service'; -import { UserRoles } from '../../common/authorization/roles.decorator'; -import { NoticeOfIntentService } from '../notice-of-intent/notice-of-intent.service'; -import { NoticeOfIntentDecisionOutcome } from './notice-of-intent-decision-outcome.entity'; -import { NoticeOfIntentDecision } from './notice-of-intent-decision.entity'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; import { CreateNoticeOfIntentDecisionDto, NoticeOfIntentDecisionDto, - NoticeOfIntentDecisionOutcomeDto, + NoticeOfIntentDecisionOutcomeCodeDto, UpdateNoticeOfIntentDecisionDto, -} from './notice-of-intent-decision.dto'; -import { NoticeOfIntentDecisionService } from './notice-of-intent-decision.service'; -import { NoticeOfIntentModificationService } from './notice-of-intent-modification/notice-of-intent-modification.service'; +} from '../notice-of-intent-decision.dto'; +import { NoticeOfIntentDecisionV1Service } from './notice-of-intent-decision-v1.service'; +import { NoticeOfIntentModificationService } from '../notice-of-intent-modification/notice-of-intent-modification.service'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('notice-of-intent-decision') @UseGuards(RolesGuard) -export class NoticeOfIntentDecisionController { +export class NoticeOfIntentDecisionV1Controller { constructor( - private noticeOfIntentDecisionService: NoticeOfIntentDecisionService, + private noticeOfIntentDecisionService: NoticeOfIntentDecisionV1Service, private noticeOfIntentService: NoticeOfIntentService, private noticeOfIntentModificationService: NoticeOfIntentModificationService, @InjectMapper() private mapper: Mapper, @@ -72,7 +72,7 @@ export class NoticeOfIntentDecisionController { @Body() createDto: CreateNoticeOfIntentDecisionDto, ): Promise { const noticeOfIntent = await this.noticeOfIntentService.getByFileNumber( - createDto.applicationFileNumber, + createDto.fileNumber, ); const modifiedNotice = createDto.modifiesUuid @@ -182,7 +182,7 @@ export class NoticeOfIntentDecisionController { outcomes: await this.mapper.mapArrayAsync( codes.outcomes, NoticeOfIntentDecisionOutcome, - NoticeOfIntentDecisionOutcomeDto, + NoticeOfIntentDecisionOutcomeCodeDto, ), }; } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.spec.ts similarity index 92% rename from services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.service.spec.ts rename to services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.spec.ts index 3277f923f9..d76567da3f 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.spec.ts @@ -8,20 +8,20 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { DocumentService } from '../../document/document.service'; -import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; -import { NoticeOfIntentService } from '../notice-of-intent/notice-of-intent.service'; -import { NoticeOfIntentDecisionDocument } from './notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; -import { NoticeOfIntentDecisionOutcome } from './notice-of-intent-decision-outcome.entity'; +import { DocumentService } from '../../../document/document.service'; +import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentDecisionDocument } from '../notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; +import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity'; import { CreateNoticeOfIntentDecisionDto, UpdateNoticeOfIntentDecisionDto, -} from './notice-of-intent-decision.dto'; -import { NoticeOfIntentDecision } from './notice-of-intent-decision.entity'; -import { NoticeOfIntentDecisionService } from './notice-of-intent-decision.service'; +} from '../notice-of-intent-decision.dto'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; +import { NoticeOfIntentDecisionV1Service } from './notice-of-intent-decision-v1.service'; describe('NoticeOfIntentDecisionService', () => { - let service: NoticeOfIntentDecisionService; + let service: NoticeOfIntentDecisionV1Service; let mockDecisionRepository: DeepMocked>; let mockDecisionDocumentRepository: DeepMocked< Repository @@ -51,7 +51,7 @@ describe('NoticeOfIntentDecisionService', () => { }), ], providers: [ - NoticeOfIntentDecisionService, + NoticeOfIntentDecisionV1Service, { provide: getRepositoryToken(NoticeOfIntentDecision), useValue: mockDecisionRepository, @@ -75,8 +75,8 @@ describe('NoticeOfIntentDecisionService', () => { ], }).compile(); - service = module.get( - NoticeOfIntentDecisionService, + service = module.get( + NoticeOfIntentDecisionV1Service, ); mockNOI = new NoticeOfIntent({ @@ -143,7 +143,7 @@ describe('NoticeOfIntentDecisionService', () => { const decisionDate = new Date(2022, 2, 2, 2, 2, 2, 2); const decisionToCreate = { date: decisionDate.getTime(), - applicationFileNumber: 'file-number', + fileNumber: 'file-number', outcomeCode: 'Outcome', } as CreateNoticeOfIntentDecisionDto; @@ -166,7 +166,7 @@ describe('NoticeOfIntentDecisionService', () => { resolutionNumber: 1, resolutionYear: 1, date: decisionDate.getTime(), - applicationFileNumber: 'file-number', + fileNumber: 'file-number', outcomeCode: 'Outcome', } as CreateNoticeOfIntentDecisionDto; @@ -188,7 +188,7 @@ describe('NoticeOfIntentDecisionService', () => { const decisionDate = new Date(2022, 2, 2, 2, 2, 2, 2); const decisionToCreate = { date: decisionDate.getTime(), - applicationFileNumber: 'file-number', + fileNumber: 'file-number', outcomeCode: 'Outcome', } as CreateNoticeOfIntentDecisionDto; diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.ts similarity index 90% rename from services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.service.ts rename to services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.ts index dfe58a1046..153a6d71ad 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.ts @@ -6,25 +6,27 @@ import { MultipartFile } from '@fastify/multipart'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, IsNull, Repository } from 'typeorm'; -import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM } from '../../document/document.dto'; -import { DocumentService } from '../../document/document.service'; -import { User } from '../../user/user.entity'; -import { formatIncomingDate } from '../../utils/incoming-date.formatter'; -import { filterUndefined } from '../../utils/undefined'; -import { ApplicationModification } from '../application-decision/application-modification/application-modification.entity'; -import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; -import { NoticeOfIntentService } from '../notice-of-intent/notice-of-intent.service'; -import { NoticeOfIntentDecisionDocument } from './notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; -import { NoticeOfIntentDecisionOutcome } from './notice-of-intent-decision-outcome.entity'; -import { NoticeOfIntentDecision } from './notice-of-intent-decision.entity'; +import { + DOCUMENT_SOURCE, + DOCUMENT_SYSTEM, +} from '../../../document/document.dto'; +import { DocumentService } from '../../../document/document.service'; +import { User } from '../../../user/user.entity'; +import { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { filterUndefined } from '../../../utils/undefined'; +import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentDecisionDocument } from '../notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; +import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; import { CreateNoticeOfIntentDecisionDto, UpdateNoticeOfIntentDecisionDto, -} from './notice-of-intent-decision.dto'; -import { NoticeOfIntentModification } from './notice-of-intent-modification/notice-of-intent-modification.entity'; +} from '../notice-of-intent-decision.dto'; +import { NoticeOfIntentModification } from '../notice-of-intent-modification/notice-of-intent-modification.entity'; @Injectable() -export class NoticeOfIntentDecisionService { +export class NoticeOfIntentDecisionV1Service { constructor( @InjectRepository(NoticeOfIntentDecision) private appDecisionRepository: Repository, diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts new file mode 100644 index 0000000000..336e3a546c --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts @@ -0,0 +1,250 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentDecisionProfile } from '../../../common/automapper/notice-of-intent-decision.automapper.profile'; +import { NoticeOfIntentProfile } from '../../../common/automapper/notice-of-intent.automapper.profile'; +import { UserProfile } from '../../../common/automapper/user.automapper.profile'; +import { EmailService } from '../../../providers/email/email.service'; +import { CodeService } from '../../code/code.service'; +import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity'; +import { + CreateNoticeOfIntentDecisionDto, + UpdateNoticeOfIntentDecisionDto, +} from '../notice-of-intent-decision.dto'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; +import { NoticeOfIntentModificationService } from '../notice-of-intent-modification/notice-of-intent-modification.service'; +import { NoticeOfIntentDecisionV2Controller } from './notice-of-intent-decision-v2.controller'; +import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2.service'; + +describe('NoticeOfIntentDecisionV2Controller', () => { + let controller: NoticeOfIntentDecisionV2Controller; + let mockDecisionService: DeepMocked; + let mockApplicationService: DeepMocked; + let mockCodeService: DeepMocked; + let mockModificationService: DeepMocked; + let mockEmailService: DeepMocked; + + let mockNoticeOfintent; + let mockDecision; + + beforeEach(async () => { + mockDecisionService = createMock(); + mockApplicationService = createMock(); + mockCodeService = createMock(); + mockModificationService = createMock(); + mockEmailService = createMock(); + + mockNoticeOfintent = new NoticeOfIntent(); + mockDecision = new NoticeOfIntentDecision({ + noticeOfIntent: mockNoticeOfintent, + }); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentDecisionV2Controller], + providers: [ + NoticeOfIntentProfile, + NoticeOfIntentDecisionProfile, + UserProfile, + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockDecisionService, + }, + { + provide: NoticeOfIntentService, + useValue: mockApplicationService, + }, + { + provide: CodeService, + useValue: mockCodeService, + }, + { + provide: NoticeOfIntentModificationService, + useValue: mockModificationService, + }, + { + provide: EmailService, + useValue: mockEmailService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NoticeOfIntentDecisionV2Controller, + ); + + mockDecisionService.fetchCodes.mockResolvedValue({ + outcomes: [ + { + code: 'decision-code', + label: 'decision-label', + } as NoticeOfIntentDecisionOutcome, + ], + decisionComponentTypes: [], + decisionConditionTypes: [], + }); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should get all for application', async () => { + mockDecisionService.getByAppFileNumber.mockResolvedValue([mockDecision]); + + const result = await controller.getByFileNumber('fake-number'); + + expect(mockDecisionService.getByAppFileNumber).toBeCalledTimes(1); + expect(result[0].uuid).toStrictEqual(mockDecision.uuid); + }); + + it('should get a specific decision', async () => { + mockDecisionService.get.mockResolvedValue(mockDecision); + const result = await controller.get('fake-uuid'); + + expect(mockDecisionService.get).toBeCalledTimes(1); + expect(result.uuid).toStrictEqual(mockDecision.uuid); + }); + + it('should call through for deletion', async () => { + mockDecisionService.delete.mockResolvedValue({} as any); + + await controller.delete('fake-uuid'); + + expect(mockDecisionService.delete).toBeCalledTimes(1); + expect(mockDecisionService.delete).toBeCalledWith('fake-uuid'); + }); + + it('should create the decision if application exists', async () => { + mockApplicationService.getByFileNumber.mockResolvedValue( + mockNoticeOfintent, + ); + mockDecisionService.create.mockResolvedValue(mockDecision); + + const decisionToCreate: CreateNoticeOfIntentDecisionDto = { + date: new Date(2022, 2, 2, 2, 2, 2, 2).valueOf(), + fileNumber: mockNoticeOfintent.fileNumber, + outcomeCode: 'outcome', + isDraft: true, + }; + + await controller.create(decisionToCreate); + + expect(mockDecisionService.create).toBeCalledTimes(1); + expect(mockDecisionService.create).toBeCalledWith( + { + fileNumber: mockNoticeOfintent.fileNumber, + outcomeCode: 'outcome', + date: decisionToCreate.date, + isDraft: true, + }, + mockNoticeOfintent, + undefined, + ); + }); + + it('should update the decision', async () => { + mockApplicationService.getFileNumber.mockResolvedValue('file-number'); + mockDecisionService.get.mockResolvedValue(new NoticeOfIntentDecision()); + mockDecisionService.getByAppFileNumber.mockResolvedValue([ + new NoticeOfIntentDecision(), + ]); + mockDecisionService.update.mockResolvedValue(mockDecision); + + const updates = { + outcome: 'New Outcome', + date: new Date(2022, 2, 2, 2, 2, 2, 2).valueOf(), + isDraft: true, + } as UpdateNoticeOfIntentDecisionDto; + + await controller.update('fake-uuid', updates); + + expect(mockDecisionService.update).toBeCalledTimes(1); + expect(mockDecisionService.update).toBeCalledWith( + 'fake-uuid', + { + outcome: 'New Outcome', + date: updates.date, + isDraft: true, + }, + undefined, + ); + }); + + it('should call through for attaching the document', async () => { + mockDecisionService.attachDocument.mockResolvedValue({} as any); + await controller.attachDocument('fake-uuid', { + isMultipart: () => true, + body: { + file: {}, + }, + user: { + entity: {}, + }, + }); + + expect(mockDecisionService.attachDocument).toBeCalledTimes(1); + }); + + it('should throw an exception if there is no file for file upload', async () => { + mockDecisionService.attachDocument.mockResolvedValue({} as any); + const promise = controller.attachDocument('fake-uuid', { + file: () => ({}), + isMultipart: () => false, + user: { + entity: {}, + }, + }); + + await expect(promise).rejects.toMatchObject( + new Error('Request is not multipart'), + ); + }); + + it('should call through for getting download url', async () => { + const fakeUrl = 'fake-url'; + mockDecisionService.getDownloadUrl.mockResolvedValue(fakeUrl); + const res = await controller.getDownloadUrl('fake-uuid', 'document-uuid'); + + expect(mockDecisionService.getDownloadUrl).toBeCalledTimes(1); + expect(res.url).toEqual(fakeUrl); + }); + + it('should call through for getting open url', async () => { + const fakeUrl = 'fake-url'; + mockDecisionService.getDownloadUrl.mockResolvedValue(fakeUrl); + const res = await controller.getOpenUrl('fake-uuid', 'document-uuid'); + + expect(mockDecisionService.getDownloadUrl).toBeCalledTimes(1); + expect(res.url).toEqual(fakeUrl); + }); + + it('should call through for document deletion', async () => { + mockDecisionService.deleteDocument.mockResolvedValue({} as any); + await controller.deleteDocument('fake-uuid', 'document-uuid'); + + expect(mockDecisionService.deleteDocument).toBeCalledTimes(1); + }); + + it('should call through for resolution number generation', async () => { + mockDecisionService.generateResolutionNumber.mockResolvedValue(1); + await controller.getNextAvailableResolutionNumber(2023); + + expect(mockDecisionService.generateResolutionNumber).toBeCalledTimes(1); + expect(mockDecisionService.generateResolutionNumber).toBeCalledWith(2023); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts new file mode 100644 index 0000000000..5e402a190c --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts @@ -0,0 +1,227 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { EmailService } from '../../../providers/email/email.service'; +import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentDecisionComponentType } from '../notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity'; +import { NoticeOfIntentDecisionComponentTypeDto } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.dto'; +import { NoticeOfIntentDecisionConditionType } from '../notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity'; +import { NoticeOfIntentDecisionConditionTypeDto } from '../notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto'; +import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity'; +import { + CreateNoticeOfIntentDecisionDto, + NoticeOfIntentDecisionDto, + NoticeOfIntentDecisionOutcomeCodeDto, + UpdateNoticeOfIntentDecisionDto, +} from '../notice-of-intent-decision.dto'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; +import { NoticeOfIntentModificationService } from '../notice-of-intent-modification/notice-of-intent-modification.service'; +import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2.service'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@Controller('notice-of-intent-decision/v2') +@UseGuards(RolesGuard) +export class NoticeOfIntentDecisionV2Controller { + constructor( + private noticeOfIntentDecisionV2Service: NoticeOfIntentDecisionV2Service, + private noticeOfIntentService: NoticeOfIntentService, + private emailService: EmailService, + private modificationService: NoticeOfIntentModificationService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('/notice-of-intent/:fileNumber') + @UserRoles(...ANY_AUTH_ROLE) + async getByFileNumber( + @Param('fileNumber') fileNumber, + ): Promise { + const decisions = + await this.noticeOfIntentDecisionV2Service.getByAppFileNumber(fileNumber); + + return await this.mapper.mapArrayAsync( + decisions, + NoticeOfIntentDecision, + NoticeOfIntentDecisionDto, + ); + } + + @Get('/codes') + @UserRoles(...ANY_AUTH_ROLE) + async getCodes() { + const codes = await this.noticeOfIntentDecisionV2Service.fetchCodes(); + return { + outcomes: await this.mapper.mapArrayAsync( + codes.outcomes, + NoticeOfIntentDecisionOutcome, + NoticeOfIntentDecisionOutcomeCodeDto, + ), + decisionComponentTypes: await this.mapper.mapArrayAsync( + codes.decisionComponentTypes, + NoticeOfIntentDecisionComponentType, + NoticeOfIntentDecisionComponentTypeDto, + ), + decisionConditionTypes: await this.mapper.mapArrayAsync( + codes.decisionConditionTypes, + NoticeOfIntentDecisionConditionType, + NoticeOfIntentDecisionConditionTypeDto, + ), + }; + } + + @Get('/:uuid') + @UserRoles(...ANY_AUTH_ROLE) + async get(@Param('uuid') uuid: string): Promise { + const decision = await this.noticeOfIntentDecisionV2Service.get(uuid); + + return this.mapper.mapAsync( + decision, + NoticeOfIntentDecision, + NoticeOfIntentDecisionDto, + ); + } + + @Post() + @UserRoles(...ANY_AUTH_ROLE) + async create( + @Body() createDto: CreateNoticeOfIntentDecisionDto, + ): Promise { + const noticeOfIntent = await this.noticeOfIntentService.getByFileNumber( + createDto.fileNumber, + ); + + const modification = createDto.modifiesUuid + ? await this.modificationService.getByUuid(createDto.modifiesUuid) + : undefined; + + const newDecision = await this.noticeOfIntentDecisionV2Service.create( + createDto, + noticeOfIntent, + modification, + ); + + return this.mapper.mapAsync( + newDecision, + NoticeOfIntentDecision, + NoticeOfIntentDecisionDto, + ); + } + + @Patch('/:uuid') + @UserRoles(...ANY_AUTH_ROLE) + async update( + @Param('uuid') uuid: string, + @Body() updateDto: UpdateNoticeOfIntentDecisionDto, + ): Promise { + let modifies; + if (updateDto.modifiesUuid) { + modifies = await this.modificationService.getByUuid( + updateDto.modifiesUuid, + ); + } else if (updateDto.modifiesUuid === null) { + modifies = null; + } + + const decision = await this.noticeOfIntentDecisionV2Service.get(uuid); + + const updatedDecision = await this.noticeOfIntentDecisionV2Service.update( + uuid, + updateDto, + modifies, + ); + + return this.mapper.mapAsync( + updatedDecision, + NoticeOfIntentDecision, + NoticeOfIntentDecisionDto, + ); + } + + @Delete('/:uuid') + @UserRoles(...ANY_AUTH_ROLE) + async delete(@Param('uuid') uuid: string) { + return await this.noticeOfIntentDecisionV2Service.delete(uuid); + } + + @Post('/:uuid/file') + @UserRoles(...ANY_AUTH_ROLE) + async attachDocument(@Param('uuid') decisionUuid: string, @Req() req) { + if (!req.isMultipart()) { + throw new BadRequestException('Request is not multipart'); + } + + const file = req.body.file; + await this.noticeOfIntentDecisionV2Service.attachDocument( + decisionUuid, + file, + req.user.entity, + ); + return { + uploaded: true, + }; + } + + @Get('/:uuid/file/:fileUuid/download') + @UserRoles(...ANY_AUTH_ROLE) + async getDownloadUrl( + @Param('uuid') decisionUuid: string, + @Param('fileUuid') documentUuid: string, + ) { + const downloadUrl = + await this.noticeOfIntentDecisionV2Service.getDownloadUrl(documentUuid); + return { + url: downloadUrl, + }; + } + + @Get('/:uuid/file/:fileUuid/open') + @UserRoles(...ANY_AUTH_ROLE) + async getOpenUrl( + @Param('uuid') decisionUuid: string, + @Param('fileUuid') documentUuid: string, + ) { + const downloadUrl = + await this.noticeOfIntentDecisionV2Service.getDownloadUrl( + documentUuid, + true, + ); + return { + url: downloadUrl, + }; + } + + @Delete('/:uuid/file/:fileUuid') + @UserRoles(...ANY_AUTH_ROLE) + async deleteDocument( + @Param('uuid') decisionUuid: string, + @Param('fileUuid') documentUuid: string, + ) { + await this.noticeOfIntentDecisionV2Service.deleteDocument(documentUuid); + return {}; + } + + @Get('next-resolution-number/:resolutionYear') + @UserRoles(...ANY_AUTH_ROLE) + async getNextAvailableResolutionNumber( + @Param('resolutionYear') resolutionYear: number, + ) { + return this.noticeOfIntentDecisionV2Service.generateResolutionNumber( + resolutionYear, + ); + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts new file mode 100644 index 0000000000..2e1109b7f5 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts @@ -0,0 +1,510 @@ +import { + ServiceNotFoundException, + ServiceValidationException, +} 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 { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { DocumentService } from '../../../document/document.service'; +import { NOI_SUBMISSION_STATUS } from '../../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NoticeOfIntentSubmissionStatusService } from '../../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service'; +import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentDecisionComponentType } from '../notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity'; +import { NoticeOfIntentDecisionComponentDto } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.dto'; +import { NoticeOfIntentDecisionComponentService } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.service'; +import { NoticeOfIntentDecisionConditionType } from '../notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity'; +import { NoticeOfIntentDecisionConditionService } from '../notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; +import { NoticeOfIntentDecisionDocument } from '../notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; +import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity'; +import { + CreateNoticeOfIntentDecisionDto, + UpdateNoticeOfIntentDecisionDto, +} from '../notice-of-intent-decision.dto'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; +import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2.service'; + +describe('NoticeOfIntentDecisionV2Service', () => { + let service: NoticeOfIntentDecisionV2Service; + let mockDecisionRepository: DeepMocked>; + let mockDecisionDocumentRepository: DeepMocked< + Repository + >; + let mockDecisionOutcomeRepository: DeepMocked< + Repository + >; + let mockNoticeOfIntentService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockNoticeOfIntentDecisionComponentTypeRepository: DeepMocked< + Repository + >; + let mockDecisionComponentService: DeepMocked; + let mockDecisionConditionService: DeepMocked; + let mockNoticeOfIntentSubmissionStatusService: DeepMocked; + + let mockNoticeOfIntent; + let mockDecision; + + beforeEach(async () => { + mockNoticeOfIntentService = createMock(); + mockDocumentService = createMock(); + mockDecisionRepository = createMock>(); + mockDecisionDocumentRepository = + createMock>(); + mockDecisionOutcomeRepository = + createMock>(); + mockNoticeOfIntentDecisionComponentTypeRepository = createMock(); + mockDecisionComponentService = createMock(); + mockDecisionConditionService = createMock(); + mockNoticeOfIntentSubmissionStatusService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + providers: [ + NoticeOfIntentDecisionV2Service, + { + provide: getRepositoryToken(NoticeOfIntentDecision), + useValue: mockDecisionRepository, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecisionDocument), + useValue: mockDecisionDocumentRepository, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecisionOutcome), + useValue: mockDecisionOutcomeRepository, + }, + { + provide: NoticeOfIntentService, + useValue: mockNoticeOfIntentService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecisionComponentType), + useValue: mockNoticeOfIntentDecisionComponentTypeRepository, + }, + { + provide: NoticeOfIntentDecisionComponentService, + useValue: mockDecisionComponentService, + }, + { + provide: NoticeOfIntentDecisionConditionService, + useValue: mockDecisionConditionService, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecisionConditionType), + useValue: mockNoticeOfIntentDecisionComponentTypeRepository, + }, + { + provide: NoticeOfIntentSubmissionStatusService, + useValue: mockNoticeOfIntentSubmissionStatusService, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentDecisionV2Service, + ); + + mockNoticeOfIntent = new NoticeOfIntent({ + uuid: '1111-1111-1111-1111', + }); + mockDecision = new NoticeOfIntentDecision({ + noticeOfIntent: mockNoticeOfIntent, + documents: [], + }); + + mockDecisionRepository.find.mockResolvedValue([mockDecision]); + mockDecisionRepository.findOne.mockResolvedValue(mockDecision); + mockDecisionRepository.save.mockResolvedValue(mockDecision); + + mockDecisionDocumentRepository.find.mockResolvedValue([]); + + mockNoticeOfIntentService.getByFileNumber.mockResolvedValue( + mockNoticeOfIntent, + ); + mockNoticeOfIntentService.update.mockResolvedValue({} as any); + mockNoticeOfIntentService.updateByUuid.mockResolvedValue({} as any); + + mockDecisionOutcomeRepository.find.mockResolvedValue([]); + mockDecisionOutcomeRepository.findOneOrFail.mockResolvedValue({} as any); + + mockNoticeOfIntentDecisionComponentTypeRepository.find.mockResolvedValue( + [], + ); + mockDecisionComponentService.createOrUpdate.mockResolvedValue([]); + mockDecisionConditionService.remove.mockResolvedValue({} as any); + mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber.mockResolvedValue( + {} as any, + ); + }); + + describe('NoticeOfIntentDecisionService Core Tests', () => { + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should get decisions by notice of intent', async () => { + const result = await service.getByAppFileNumber( + mockNoticeOfIntent.fileNumber, + ); + + expect(result).toStrictEqual([mockDecision]); + }); + + it('should return decisions by uuid', async () => { + const result = await service.get(mockDecision.uuid); + + expect(result).toStrictEqual(mockDecision); + }); + + it('should delete decision with uuid and update notice of intent and submission status', async () => { + mockDecisionRepository.softRemove.mockResolvedValue({} as any); + mockDecisionRepository.findOne.mockResolvedValue({ + ...mockDecision, + reconsiders: 'reconsider-uuid', + modifies: 'modified-uuid', + }); + mockDecisionRepository.find.mockResolvedValue([]); + mockDecisionComponentService.softRemove.mockResolvedValue(); + + await service.delete(mockDecision.uuid); + + expect(mockDecisionRepository.save.mock.calls[0][0].modifies).toBeNull(); + expect(mockDecisionRepository.softRemove).toBeCalledTimes(1); + expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledTimes(1); + expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledWith( + mockNoticeOfIntent.uuid, + { + decisionDate: null, + }, + ); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber, + ).toBeCalledTimes(1); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber, + ).toBeCalledWith( + mockNoticeOfIntent.fileNumber, + NOI_SUBMISSION_STATUS.ALC_DECISION, + null, + ); + }); + + it('should create a decision', async () => { + mockDecisionRepository.find.mockResolvedValue([]); + mockDecisionRepository.findOne.mockResolvedValue({ + documents: [] as NoticeOfIntentDecisionDocument[], + } as NoticeOfIntentDecision); + mockDecisionRepository.exist.mockResolvedValue(false); + + const decisionDate = new Date(2022, 2, 2, 2, 2, 2, 2); + const decisionToCreate = { + date: decisionDate.getTime(), + fileNumber: 'file-number', + outcomeCode: 'Outcome', + isDraft: true, + } as CreateNoticeOfIntentDecisionDto; + + await service.create(decisionToCreate, mockNoticeOfIntent, undefined); + + expect(mockDecisionRepository.save).toBeCalledTimes(1); + expect(mockNoticeOfIntentService.update).toHaveBeenCalledTimes(0); + }); + + it('should fail create a decision if the resolution number is already in use', async () => { + mockDecisionRepository.findOne.mockResolvedValue( + {} as NoticeOfIntentDecision, + ); + mockDecisionRepository.exist.mockResolvedValueOnce(false); + + const decisionDate = new Date(2022, 2, 2, 2, 2, 2, 2); + const decisionToCreate = { + resolutionNumber: 1, + resolutionYear: 1, + date: decisionDate.getTime(), + fileNumber: 'file-number', + outcomeCode: 'Outcome', + isDraft: true, + } as CreateNoticeOfIntentDecisionDto; + + await expect( + service.create(decisionToCreate, mockNoticeOfIntent, undefined), + ).rejects.toMatchObject( + new ServiceValidationException( + `Resolution number #${decisionToCreate.resolutionNumber}/${decisionToCreate.resolutionYear} is already in use`, + ), + ); + + expect(mockDecisionRepository.save).toBeCalledTimes(0); + expect(mockNoticeOfIntentService.update).toHaveBeenCalledTimes(0); + }); + + it('should fail create a draft decision if draft already exists', async () => { + mockDecisionRepository.findOne.mockResolvedValue({ + documents: [] as NoticeOfIntentDecisionDocument[], + } as NoticeOfIntentDecision); + mockDecisionRepository.exist.mockResolvedValueOnce(true); + + const decisionDate = new Date(2022, 2, 2, 2, 2, 2, 2); + const decisionToCreate = { + resolutionNumber: 1, + resolutionYear: 1, + date: decisionDate.getTime(), + fileNumber: 'file-number', + outcomeCode: 'Outcome', + isDraft: true, + } as CreateNoticeOfIntentDecisionDto; + + await expect( + service.create(decisionToCreate, mockNoticeOfIntent, undefined), + ).rejects.toMatchObject( + new ServiceValidationException( + 'Draft decision already exists for this notice of intent.', + ), + ); + + expect(mockDecisionRepository.save).toBeCalledTimes(0); + expect(mockNoticeOfIntentService.update).toHaveBeenCalledTimes(0); + }); + + it('should create a decision and NOT update the notice of intent if this was the second decision', async () => { + mockDecisionRepository.findOne.mockResolvedValue({ + documents: [] as NoticeOfIntentDecisionDocument[], + } as NoticeOfIntentDecision); + mockDecisionRepository.exist.mockResolvedValueOnce(false); + + const decisionDate = new Date(2022, 2, 2, 2, 2, 2, 2); + const decisionToCreate = { + date: decisionDate.getTime(), + fileNumber: 'file-number', + outcomeCode: 'Outcome', + } as CreateNoticeOfIntentDecisionDto; + + await service.create(decisionToCreate, mockNoticeOfIntent, undefined); + + expect(mockDecisionRepository.save).toBeCalledTimes(1); + expect(mockNoticeOfIntentService.update).not.toHaveBeenCalled(); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber, + ).not.toHaveBeenCalled(); + }); + + it('should update the decision and update the notice of intent and submission status if it was the only decision', async () => { + const decisionDate = new Date(2022, 3, 3, 3, 3, 3, 3); + const decisionUpdate: UpdateNoticeOfIntentDecisionDto = { + date: decisionDate.getTime(), + outcomeCode: 'New Outcome', + isDraft: false, + decisionComponents: [ + { + uuid: 'fake', + alrArea: 1, + agCap: '1', + agCapSource: '1', + noticeOfIntentDecisionComponentTypeCode: 'fake', + }, + ] as NoticeOfIntentDecisionComponentDto[], + }; + + const createdDecision = new NoticeOfIntentDecision({ + date: decisionDate, + documents: [], + }); + + mockDecisionRepository.find.mockResolvedValue([createdDecision]); + + await service.update(mockDecision.uuid, decisionUpdate, undefined); + + expect(mockDecisionRepository.findOne).toBeCalledTimes(2); + expect(mockDecisionRepository.save).toHaveBeenCalledTimes(1); + expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledTimes(1); + expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledWith( + mockNoticeOfIntent.uuid, + { + decisionDate, + }, + ); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber, + ).toBeCalledTimes(1); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber, + ).toBeCalledWith( + mockNoticeOfIntent.fileNumber, + NOI_SUBMISSION_STATUS.ALC_DECISION, + decisionDate, + ); + }); + + it('should update decision and update the notice of intent date to null if it is draft decision', async () => { + const decisionDate = new Date(2022, 3, 3, 3, 3, 3, 3); + const decisionUpdate: UpdateNoticeOfIntentDecisionDto = { + date: decisionDate.getTime(), + outcomeCode: 'New Outcome', + isDraft: true, + }; + + await service.update(mockDecision.uuid, decisionUpdate, undefined); + + expect(mockDecisionRepository.findOne).toBeCalledTimes(2); + expect(mockDecisionRepository.save).toBeCalledTimes(1); + expect(mockNoticeOfIntentService.updateByUuid).toHaveBeenCalledTimes(1); + expect(mockNoticeOfIntentService.updateByUuid).toBeCalledWith( + '1111-1111-1111-1111', + { + decisionDate: null, + }, + ); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber, + ).toBeCalledTimes(1); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber, + ).toBeCalledWith( + mockNoticeOfIntent.fileNumber, + NOI_SUBMISSION_STATUS.ALC_DECISION, + null, + ); + }); + + it('should not update the notice of intent dates when updating a draft decision', async () => { + const secondDecision = new NoticeOfIntentDecision({ + noticeOfIntent: mockNoticeOfIntent, + documents: [], + }); + secondDecision.isDraft = true; + secondDecision.uuid = 'second-uuid'; + mockDecisionRepository.find.mockResolvedValue([ + secondDecision, + mockDecision, + ]); + mockDecisionRepository.findOne.mockResolvedValue(secondDecision); + + const decisionUpdate: UpdateNoticeOfIntentDecisionDto = { + outcomeCode: 'New Outcome', + isDraft: true, + }; + + await service.update(mockDecision.uuid, decisionUpdate, undefined); + + expect(mockDecisionRepository.findOne).toBeCalledTimes(2); + expect(mockDecisionRepository.save).toBeCalledTimes(1); + expect(mockNoticeOfIntentService.update).not.toHaveBeenCalled(); + expect( + mockNoticeOfIntentSubmissionStatusService.setStatusDateByFileNumber, + ).not.toHaveBeenCalled(); + }); + + it('should fail on update if the decision is not found', async () => { + const nonExistantUuid = 'bad-uuid'; + mockDecisionRepository.findOne.mockResolvedValue(null); + const decisionUpdate: UpdateNoticeOfIntentDecisionDto = { + date: new Date(2022, 2, 2, 2, 2, 2, 2).getTime(), + outcomeCode: 'New Outcome', + isDraft: true, + }; + const promise = service.update( + nonExistantUuid, + decisionUpdate, + undefined, + ); + + await expect(promise).rejects.toMatchObject( + new ServiceNotFoundException( + `Decision with UUID ${nonExistantUuid} not found`, + ), + ); + expect(mockDecisionRepository.save).toBeCalledTimes(0); + }); + + it('should call through for get code', async () => { + await service.fetchCodes(); + expect(mockDecisionOutcomeRepository.find).toHaveBeenCalledTimes(1); + }); + + it('should call through for get for applicant', async () => { + await service.getForPortal(''); + expect(mockDecisionRepository.find).toHaveBeenCalledTimes(1); + }); + }); + + describe('NoticeOfIntentDecisionService File Tests', () => { + let mockDocument; + beforeEach(() => { + mockDecisionDocumentRepository.findOne.mockResolvedValue(mockDocument); + mockDecisionDocumentRepository.save.mockResolvedValue(mockDocument); + + mockDocument = { + uuid: 'fake-uuid', + decisionUuid: 'decision-uuid', + } as NoticeOfIntentDecisionDocument; + }); + + it('should call the repository for attaching a file', async () => { + mockDocumentService.create.mockResolvedValue({} as any); + + await service.attachDocument('uuid', {} as any, {} as any); + expect(mockDecisionDocumentRepository.save).toHaveBeenCalledTimes(1); + expect(mockDocumentService.create).toHaveBeenCalledTimes(1); + }); + + it('should throw an exception when attaching a document to a non-existent decision', async () => { + mockDecisionRepository.findOne.mockResolvedValue(null); + await expect( + service.attachDocument('uuid', {} as any, {} as any), + ).rejects.toMatchObject( + new ServiceNotFoundException(`Decision with UUID uuid not found`), + ); + expect(mockDocumentService.create).not.toHaveBeenCalled(); + }); + + it('should call the repository to delete documents', async () => { + mockDecisionDocumentRepository.softRemove.mockResolvedValue({} as any); + + await service.deleteDocument('fake-uuid'); + expect(mockDecisionDocumentRepository.softRemove).toHaveBeenCalledTimes( + 1, + ); + }); + + it('should throw an exception when document not found for deletion', async () => { + mockDecisionDocumentRepository.findOne.mockResolvedValue(null); + await expect(service.deleteDocument('fake-uuid')).rejects.toMatchObject( + new ServiceNotFoundException( + `Failed to find document with uuid fake-uuid`, + ), + ); + expect(mockDocumentService.softRemove).not.toHaveBeenCalled(); + }); + + it('should call through to document service for download', async () => { + const downloadUrl = 'download-url'; + mockDocumentService.getDownloadUrl.mockResolvedValue(downloadUrl); + + const res = await service.getDownloadUrl('fake-uuid'); + + expect(mockDocumentService.getDownloadUrl).toHaveBeenCalledTimes(1); + expect(res).toEqual(downloadUrl); + }); + + it('should throw an exception when document not found for download', async () => { + mockDecisionDocumentRepository.findOne.mockResolvedValue(null); + await expect(service.getDownloadUrl('fake-uuid')).rejects.toMatchObject( + new ServiceNotFoundException( + `Failed to find document with uuid fake-uuid`, + ), + ); + }); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts new file mode 100644 index 0000000000..e8e979b964 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts @@ -0,0 +1,616 @@ +import { + ServiceNotFoundException, + ServiceValidationException, +} from '@app/common/exceptions/base.exception'; +import { MultipartFile } from '@fastify/multipart'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, IsNull, Repository } from 'typeorm'; +import { + DOCUMENT_SOURCE, + DOCUMENT_SYSTEM, +} from '../../../document/document.dto'; +import { DocumentService } from '../../../document/document.service'; +import { User } from '../../../user/user.entity'; +import { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { NOI_SUBMISSION_STATUS } from '../../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NoticeOfIntentSubmissionStatusService } from '../../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.service'; +import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentDecisionComponentType } from '../notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity'; +import { NoticeOfIntentDecisionComponent } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecisionComponentService } from '../notice-of-intent-decision-component/notice-of-intent-decision-component.service'; +import { NoticeOfIntentDecisionConditionType } from '../notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity'; +import { NoticeOfIntentDecisionConditionService } from '../notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; +import { NoticeOfIntentDecisionDocument } from '../notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; +import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity'; +import { + CreateNoticeOfIntentDecisionDto, + UpdateNoticeOfIntentDecisionDto, +} from '../notice-of-intent-decision.dto'; +import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; +import { NoticeOfIntentModification } from '../notice-of-intent-modification/notice-of-intent-modification.entity'; + +@Injectable() +export class NoticeOfIntentDecisionV2Service { + constructor( + @InjectRepository(NoticeOfIntentDecision) + private noticeOfIntentDecisionRepository: Repository, + @InjectRepository(NoticeOfIntentDecisionDocument) + private decisionDocumentRepository: Repository, + @InjectRepository(NoticeOfIntentDecisionOutcome) + private decisionOutcomeRepository: Repository, + @InjectRepository(NoticeOfIntentDecisionComponentType) + private decisionComponentTypeRepository: Repository, + @InjectRepository(NoticeOfIntentDecisionConditionType) + private decisionConditionTypeRepository: Repository, + private noticeOfIntentService: NoticeOfIntentService, + private documentService: DocumentService, + private decisionComponentService: NoticeOfIntentDecisionComponentService, + private decisionConditionService: NoticeOfIntentDecisionConditionService, + private noticeOfIntentSubmissionStatusService: NoticeOfIntentSubmissionStatusService, + ) {} + + async getForPortal(fileNumber: string) { + const noticeOfIntent = await this.noticeOfIntentService.getByFileNumber( + fileNumber, + ); + + return await this.noticeOfIntentDecisionRepository.find({ + where: { + noticeOfIntentUuid: noticeOfIntent.uuid, + isDraft: false, + }, + relations: { + outcome: true, + documents: { + document: true, + }, + modifies: { + modifiesDecisions: true, + }, + }, + order: { + auditCreatedAt: 'DESC', + }, + }); + } + + async getByAppFileNumber(number: string) { + const noticeOfIntent = await this.noticeOfIntentService.getByFileNumber( + number, + ); + + const decisions = await this.noticeOfIntentDecisionRepository.find({ + where: { + noticeOfIntentUuid: noticeOfIntent.uuid, + }, + order: { + createdAt: 'DESC', + }, + relations: { + outcome: true, + modifies: { + modifiesDecisions: true, + }, + components: { + noticeOfIntentDecisionComponentType: true, + }, + conditions: { + type: true, + }, + }, + }); + + // do not place modifiedBy into query above, it will kill performance + const decisionsWithModifiedBy = + await this.noticeOfIntentDecisionRepository.find({ + where: { + noticeOfIntentUuid: noticeOfIntent.uuid, + modifiedBy: { + resultingDecision: { + isDraft: false, + }, + }, + }, + relations: { + modifiedBy: { + resultingDecision: true, + reviewOutcome: true, + }, + }, + }); + + for (const decision of decisions) { + decision.modifiedBy = + decisionsWithModifiedBy.find((r) => r.uuid === decision.uuid) + ?.modifiedBy || []; + } + + //Query Documents separately as when added to the above joins caused performance issues + for (const decision of decisions) { + decision.documents = await this.decisionDocumentRepository.find({ + where: { + decisionUuid: decision.uuid, + document: { + auditDeletedDateAt: IsNull(), + }, + }, + relations: { + document: true, + }, + }); + } + return decisions; + } + + async get(uuid) { + const decision = await this.noticeOfIntentDecisionRepository.findOne({ + where: { + uuid, + }, + relations: { + outcome: true, + documents: { + document: true, + }, + modifies: { + modifiesDecisions: true, + }, + components: { + noticeOfIntentDecisionComponentType: true, + }, + conditions: { + type: true, + components: true, + }, + }, + }); + + if (!decision) { + throw new ServiceNotFoundException( + `Failed to load decision with uuid ${uuid}`, + ); + } + + decision.documents = decision.documents.filter( + (document) => !!document.document, + ); + + return decision; + } + + async update( + uuid: string, + updateDto: UpdateNoticeOfIntentDecisionDto, + modifies: NoticeOfIntentModification | undefined | null, + ) { + const existingDecision: Partial = + await this.getOrFail(uuid); + + // resolution number is int64 in postgres, which means it is a string in JS + if ( + updateDto.resolutionNumber && + updateDto.resolutionYear && + (existingDecision.resolutionNumber !== updateDto.resolutionNumber || + existingDecision.resolutionYear !== updateDto.resolutionYear) + ) { + await this.validateResolutionNumber( + updateDto.resolutionNumber, + updateDto.resolutionYear, + ); + } + + const isChangingDraftStatus = + existingDecision.isDraft !== updateDto.isDraft; + + existingDecision.auditDate = formatIncomingDate(updateDto.auditDate); + existingDecision.modifies = modifies; + existingDecision.resolutionNumber = updateDto.resolutionNumber; + existingDecision.resolutionYear = updateDto.resolutionYear; + existingDecision.isSubjectToConditions = updateDto.isSubjectToConditions; + existingDecision.decisionDescription = updateDto.decisionDescription; + existingDecision.isStatsRequired = updateDto.isStatsRequired; + existingDecision.isDraft = updateDto.isDraft; + existingDecision.rescindedDate = formatIncomingDate( + updateDto.rescindedDate, + ); + existingDecision.rescindedComment = updateDto.rescindedComment; + existingDecision.wasReleased = + existingDecision.wasReleased || !updateDto.isDraft; + + if (updateDto.outcomeCode) { + existingDecision.outcome = await this.getOutcomeByCode( + updateDto.outcomeCode, + ); + } + + let dateHasChanged = false; + if ( + updateDto.date !== undefined && + existingDecision.date !== formatIncomingDate(updateDto.date) + ) { + dateHasChanged = true; + existingDecision.date = formatIncomingDate(updateDto.date); + } + + await this.updateComponents(updateDto, existingDecision); + + //Must be called after update components + await this.updateConditions(updateDto, existingDecision); + + const updatedDecision = await this.noticeOfIntentDecisionRepository.save( + existingDecision, + ); + + if (dateHasChanged || isChangingDraftStatus) { + await this.updateDecisionDates(updatedDecision); + } + + return this.get(existingDecision.uuid); + } + + async create( + createDto: CreateNoticeOfIntentDecisionDto, + noticeOfIntent: NoticeOfIntent, + modifies: NoticeOfIntentModification | undefined | null, + ) { + const isDraftExists = await this.noticeOfIntentDecisionRepository.exist({ + where: { + noticeOfIntent: { fileNumber: createDto.fileNumber }, + isDraft: true, + }, + }); + + if (isDraftExists) { + throw new ServiceValidationException( + 'Draft decision already exists for this notice of intent.', + ); + } + + let decisionComponents: NoticeOfIntentDecisionComponent[] = []; + if (createDto.decisionComponents) { + this.decisionComponentService.validate( + createDto.decisionComponents, + createDto.isDraft, + ); + decisionComponents = await this.decisionComponentService.createOrUpdate( + createDto.decisionComponents, + false, + ); + } + + const decision = new NoticeOfIntentDecision({ + outcome: await this.getOutcomeByCode(createDto.outcomeCode), + date: new Date(createDto.date), + resolutionNumber: createDto.resolutionNumber, + resolutionYear: createDto.resolutionYear, + auditDate: createDto.auditDate + ? new Date(createDto.auditDate) + : undefined, + isDraft: true, + isSubjectToConditions: createDto.isSubjectToConditions, + decisionDescription: createDto.decisionDescription, + isStatsRequired: createDto.isStatsRequired, + rescindedDate: createDto.rescindedDate + ? new Date(createDto.rescindedDate) + : null, + rescindedComment: createDto.rescindedComment, + noticeOfIntent, + modifies, + components: decisionComponents, + }); + + await this.validateResolutionNumber( + createDto.resolutionNumber, + createDto.resolutionYear, + ); + + const savedDecision = await this.noticeOfIntentDecisionRepository.save( + decision, + { + transaction: true, + }, + ); + + return this.get(savedDecision.uuid); + } + + private async validateResolutionNumber(number, year) { + // we do not need to validate decision without number + if (!number) { + return; + } + + // we do not need to include deleted items since there may be multiple deleted draft decision wih the same or different numbers + const existingDecision = + await this.noticeOfIntentDecisionRepository.findOne({ + where: { + resolutionNumber: number, + resolutionYear: year ?? IsNull(), + }, + }); + + if (existingDecision) { + throw new ServiceValidationException( + `Resolution number #${number}/${year} is already in use`, + ); + } + } + + async delete(uuid) { + const noticeOfIntentDecision = + await this.noticeOfIntentDecisionRepository.findOne({ + where: { uuid }, + relations: { + outcome: true, + documents: { + document: true, + }, + noticeOfIntent: true, + components: true, + }, + }); + + if (!noticeOfIntentDecision) { + throw new ServiceNotFoundException( + `Failed to find decision with uuid ${uuid}`, + ); + } + + for (const document of noticeOfIntentDecision.documents) { + await this.documentService.softRemove(document.document); + } + + await this.decisionComponentService.softRemove( + noticeOfIntentDecision.components, + ); + + //Clear potential links + noticeOfIntentDecision.modifies = null; + await this.noticeOfIntentDecisionRepository.save(noticeOfIntentDecision); + + await this.noticeOfIntentDecisionRepository.softRemove([ + noticeOfIntentDecision, + ]); + await this.updateDecisionDates(noticeOfIntentDecision); + } + + async attachDocument(decisionUuid: string, file: MultipartFile, user: User) { + const decision = await this.getOrFail(decisionUuid); + const document = await this.documentService.create( + `decision/${decision.uuid}`, + file.filename, + file, + user, + DOCUMENT_SOURCE.ALC, + DOCUMENT_SYSTEM.ALCS, + ); + const appDocument = new NoticeOfIntentDecisionDocument({ + decision, + document, + }); + + return this.decisionDocumentRepository.save(appDocument); + } + + async deleteDocument(decisionDocumentUuid: string) { + const decisionDocument = await this.getDecisionDocumentOrFail( + decisionDocumentUuid, + ); + + await this.decisionDocumentRepository.softRemove(decisionDocument); + return decisionDocument; + } + + async getDownloadUrl(decisionDocumentUuid: string, openInline = false) { + const decisionDocument = await this.getDecisionDocumentOrFail( + decisionDocumentUuid, + ); + + return this.documentService.getDownloadUrl( + decisionDocument.document, + openInline, + ); + } + + getOutcomeByCode(code: string) { + return this.decisionOutcomeRepository.findOneOrFail({ + where: { + code, + }, + }); + } + + async fetchCodes() { + const values = await Promise.all([ + this.decisionOutcomeRepository.find(), + this.decisionComponentTypeRepository.find(), + this.decisionConditionTypeRepository.find(), + ]); + + return { + outcomes: values[0], + decisionComponentTypes: values[1], + decisionConditionTypes: values[2], + }; + } + + getMany(modifiesDecisionUuids: string[]) { + return this.noticeOfIntentDecisionRepository.find({ + where: { + uuid: In(modifiesDecisionUuids), + }, + }); + } + + async generateResolutionNumber(resolutionYear: number) { + const result = await this.noticeOfIntentDecisionRepository.query( + `SELECT * FROM alcs.generate_next_resolution_number(${resolutionYear})`, + ); + + return result[0].generate_next_resolution_number; + } + + private async updateComponents( + updateDto: UpdateNoticeOfIntentDecisionDto, + existingDecision: Partial, + ) { + if (updateDto.decisionComponents) { + if ( + existingDecision.outcomeCode && + ['APPA', 'APPR'].includes(existingDecision.outcomeCode) + ) { + this.decisionComponentService.validate( + updateDto.decisionComponents, + updateDto.isDraft, + ); + } + + if (existingDecision.components) { + const componentsToRemove = existingDecision.components.filter( + (component) => + !updateDto.decisionComponents?.some( + (componentDto) => componentDto.uuid === component.uuid, + ), + ); + + await this.decisionComponentService.softRemove(componentsToRemove); + } + + existingDecision.components = + await this.decisionComponentService.createOrUpdate( + updateDto.decisionComponents, + false, + ); + } else if ( + updateDto.decisionComponents === null && + existingDecision.components + ) { + await this.decisionComponentService.softRemove( + existingDecision.components, + ); + } + } + + private async updateConditions( + updateDto: UpdateNoticeOfIntentDecisionDto, + existingDecision: Partial, + ) { + if (updateDto.conditions) { + if (existingDecision.noticeOfIntentUuid && existingDecision.conditions) { + const conditionsToRemove = existingDecision.conditions.filter( + (condition) => + !updateDto.conditions?.some( + (conditionDto) => conditionDto.uuid === condition.uuid, + ), + ); + + await this.decisionConditionService.remove(conditionsToRemove); + } + + const existingComponents = + await this.decisionComponentService.getAllByNoticeOfIntentUUID( + existingDecision.noticeOfIntentUuid!, + ); + + existingDecision.conditions = + await this.decisionConditionService.createOrUpdate( + updateDto.conditions, + existingComponents, + existingDecision.components ?? [], + false, + ); + } else if (updateDto.conditions === null && existingDecision.conditions) { + await this.decisionConditionService.remove(existingDecision.conditions); + } + } + + private async getOrFail(uuid: string) { + const existingDecision = + await this.noticeOfIntentDecisionRepository.findOne({ + where: { + uuid, + }, + relations: { + noticeOfIntent: true, + components: true, + conditions: true, + }, + }); + + if (!existingDecision) { + throw new ServiceNotFoundException( + `Decision with UUID ${uuid} not found`, + ); + } + return existingDecision; + } + + private async updateDecisionDates( + noticeOfIntentDecision: NoticeOfIntentDecision, + ) { + const existingDecisions = await this.getByAppFileNumber( + noticeOfIntentDecision.noticeOfIntent.fileNumber, + ); + const releasedDecisions = existingDecisions.filter( + (decision) => !decision.isDraft, + ); + if (releasedDecisions.length === 0) { + await this.noticeOfIntentService.updateByUuid( + noticeOfIntentDecision.noticeOfIntent.uuid, + { + decisionDate: null, + }, + ); + + await this.noticeOfIntentSubmissionStatusService.setStatusDateByFileNumber( + noticeOfIntentDecision.noticeOfIntent.fileNumber, + NOI_SUBMISSION_STATUS.ALC_DECISION, + null, + ); + } else { + const decisionDate = existingDecisions[existingDecisions.length - 1].date; + await this.noticeOfIntentService.updateByUuid( + noticeOfIntentDecision.noticeOfIntent.uuid, + { + decisionDate, + }, + ); + + await this.setDecisionReleasedStatus( + decisionDate, + noticeOfIntentDecision, + ); + } + } + + private async setDecisionReleasedStatus( + decisionDate: Date | null, + noticeOfIntentDecision: NoticeOfIntentDecision, + ) { + await this.noticeOfIntentSubmissionStatusService.setStatusDateByFileNumber( + noticeOfIntentDecision.noticeOfIntent.fileNumber, + NOI_SUBMISSION_STATUS.ALC_DECISION, + decisionDate, + ); + } + + private async getDecisionDocumentOrFail(decisionDocumentUuid: string) { + const decisionDocument = await this.decisionDocumentRepository.findOne({ + where: { + uuid: decisionDocumentUuid, + }, + relations: { + document: true, + }, + }); + + if (!decisionDocument) { + throw new ServiceNotFoundException( + `Failed to find document with uuid ${decisionDocumentUuid}`, + ); + } + return decisionDocument; + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts index 5bafa50d90..23db0752ad 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts @@ -1,8 +1,23 @@ import { AutoMap } from '@automapper/classes'; -import { IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; +import { + IsArray, + IsBoolean, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; import { BaseCodeDto } from '../../common/dtos/base.dto'; +import { + NoticeOfIntentDecisionComponentDto, + UpdateNoticeOfIntentDecisionComponentDto, +} from './notice-of-intent-decision-component/notice-of-intent-decision-component.dto'; +import { + NoticeOfIntentDecisionConditionDto, + UpdateNoticeOfIntentDecisionConditionDto, +} from './notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto'; -export class NoticeOfIntentDecisionOutcomeDto extends BaseCodeDto {} +export class NoticeOfIntentDecisionOutcomeCodeDto extends BaseCodeDto {} export class UpdateNoticeOfIntentDecisionDto { @IsNumber() @@ -32,11 +47,46 @@ export class UpdateNoticeOfIntentDecisionDto { @IsString() @IsOptional() decisionMakerName?: string | null; + + @IsUUID() + @IsOptional() + modifiesUuid?: string | null; + + @IsBoolean() + @IsOptional() + isSubjectToConditions?: boolean | null; + + @IsString() + @IsOptional() + decisionDescription?: string | null; + + @IsBoolean() + @IsOptional() + isStatsRequired?: boolean | null; + + @IsNumber() + @IsOptional() + rescindedDate?: number | null; + + @IsString() + @IsOptional() + rescindedComment?: string | null; + + @IsBoolean() + @IsOptional() + isDraft?: boolean; + + @IsOptional() + decisionComponents?: UpdateNoticeOfIntentDecisionComponentDto[]; + + @IsOptional() + @IsArray() + conditions?: UpdateNoticeOfIntentDecisionConditionDto[]; } export class CreateNoticeOfIntentDecisionDto extends UpdateNoticeOfIntentDecisionDto { @IsString() - applicationFileNumber; + fileNumber: string; @IsNumber() date: number; @@ -45,14 +95,19 @@ export class CreateNoticeOfIntentDecisionDto extends UpdateNoticeOfIntentDecisio outcomeCode: string; @IsNumber() - resolutionNumber: number; + @IsOptional() + resolutionNumber?: number; @IsNumber() - resolutionYear: number; + @IsOptional() + resolutionYear?: number; @IsUUID() @IsOptional() modifiesUuid?: string; + + @IsOptional() + override isDraft = true; } export class LinkedResolutionDto { @@ -65,7 +120,7 @@ export class NoticeOfIntentDecisionDto { uuid: string; @AutoMap() - applicationFileNumber: string; + fileNumber: string; @AutoMap() date: number; @@ -79,6 +134,24 @@ export class NoticeOfIntentDecisionDto { @AutoMap() resolutionYear: number; + @AutoMap(() => Boolean) + isSubjectToConditions: boolean | null; + + @AutoMap(() => Boolean) + isDraft: boolean; + + @AutoMap(() => String) + decisionDescription?: string | null; + + @AutoMap(() => Boolean) + isStatsRequired?: boolean | null; + + @AutoMap(() => Number) + rescindedDate?: number | null; + + @AutoMap(() => String) + rescindedComment?: string | null; + @AutoMap(() => [NoticeOfIntentDecisionDocumentDto]) documents: NoticeOfIntentDecisionDocumentDto[]; @@ -88,11 +161,16 @@ export class NoticeOfIntentDecisionDto { @AutoMap(() => String) decisionMakerName?: string | null; - @AutoMap(() => NoticeOfIntentDecisionOutcomeDto) - outcome?: NoticeOfIntentDecisionOutcomeDto | null; + @AutoMap(() => NoticeOfIntentDecisionOutcomeCodeDto) + outcome?: NoticeOfIntentDecisionOutcomeCodeDto | null; modifies?: LinkedResolutionDto; modifiedBy?: LinkedResolutionDto[]; + + components?: NoticeOfIntentDecisionComponentDto[]; + + @AutoMap(() => [NoticeOfIntentDecisionConditionDto]) + conditions?: NoticeOfIntentDecisionConditionDto[]; } export class NoticeOfIntentDecisionDocumentDto { diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts index d3327ffefa..c3beae8601 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts @@ -12,6 +12,8 @@ import { } from 'typeorm'; import { Base } from '../../common/entities/base.entity'; import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentDecisionComponent } from './notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity'; import { NoticeOfIntentDecisionDocument } from './notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; import { NoticeOfIntentDecisionOutcome } from './notice-of-intent-decision-outcome.entity'; import { NoticeOfIntentModification } from './notice-of-intent-modification/notice-of-intent-modification.entity'; @@ -30,8 +32,8 @@ export class NoticeOfIntentDecision extends Base { } @AutoMap() - @Column({ type: 'timestamptz' }) - date: Date; + @Column({ type: 'timestamptz', nullable: true }) + date: Date | null; @AutoMap() @Column({ type: 'timestamptz', nullable: true }) @@ -56,13 +58,64 @@ export class NoticeOfIntentDecision extends Base { resolutionYear: number; @AutoMap() - @Column() - decisionMaker: string; + @Column({ type: 'varchar', nullable: true }) + decisionMaker?: string; @AutoMap() @Column({ type: 'text', nullable: true }) decisionMakerName?: string; + @AutoMap() + @Column({ type: 'boolean', default: false }) + wasReleased: boolean; + + @AutoMap() + @Column({ + comment: 'Indicates whether the decision is currently draft or not', + default: false, + }) + isDraft: boolean; + + @AutoMap(() => Boolean) + @Column({ + comment: 'Indicates whether the decision is subject to conditions', + type: 'boolean', + nullable: true, + }) + isSubjectToConditions?: boolean | null; + + @AutoMap(() => String) + @Column({ + comment: 'Staff input field for a description of the decision', + nullable: true, + type: 'text', + }) + decisionDescription?: string | null; + + @AutoMap(() => Boolean) + @Column({ + comment: 'Indicates whether the stats are required for the decision', + nullable: true, + type: 'boolean', + }) + isStatsRequired?: boolean | null; + + @AutoMap(() => Date) + @Column({ + type: 'timestamptz', + nullable: true, + comment: 'Date when decision was rescinded', + }) + rescindedDate?: Date | null; + + @AutoMap(() => String) + @Column({ + comment: 'Comment provided by the staff when the decision was rescinded', + nullable: true, + type: 'text', + }) + rescindedComment?: string | null; + @CreateDateColumn({ type: 'timestamptz', nullable: false, @@ -101,4 +154,20 @@ export class NoticeOfIntentDecision extends Base { ) @JoinColumn() modifies?: NoticeOfIntentModification | null; + + @AutoMap(() => [NoticeOfIntentDecisionComponent]) + @OneToMany( + () => NoticeOfIntentDecisionComponent, + (component) => component.noticeOfIntentDecision, + { cascade: ['insert', 'update'] }, + ) + components: NoticeOfIntentDecisionComponent[]; + + @AutoMap(() => [NoticeOfIntentDecisionCondition]) + @OneToMany( + () => NoticeOfIntentDecisionCondition, + (component) => component.decision, + { cascade: ['insert', 'update'] }, + ) + conditions: NoticeOfIntentDecisionCondition[]; } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts index a88e7515c1..518dacc971 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -4,12 +4,21 @@ import { NoticeOfIntentDecisionProfile } from '../../common/automapper/notice-of import { DocumentModule } from '../../document/document.module'; import { BoardModule } from '../board/board.module'; import { CardModule } from '../card/card.module'; +import { NoticeOfIntentSubmissionStatusModule } from '../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentModule } from '../notice-of-intent/notice-of-intent.module'; +import { NoticeOfIntentDecisionComponentType } from './notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity'; +import { NoticeOfIntentDecisionComponent } from './notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecisionComponentService } from './notice-of-intent-decision-component/notice-of-intent-decision-component.service'; +import { NoticeOfIntentDecisionConditionType } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity'; +import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity'; +import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; import { NoticeOfIntentDecisionDocument } from './notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; import { NoticeOfIntentDecisionOutcome } from './notice-of-intent-decision-outcome.entity'; +import { NoticeOfIntentDecisionV2Controller } from './notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller'; +import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2/notice-of-intent-decision-v2.service'; import { NoticeOfIntentDecision } from './notice-of-intent-decision.entity'; -import { NoticeOfIntentDecisionController } from './notice-of-intent-decision.controller'; -import { NoticeOfIntentDecisionService } from './notice-of-intent-decision.service'; +import { NoticeOfIntentDecisionV1Controller } from './notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller'; +import { NoticeOfIntentDecisionV1Service } from './notice-of-intent-decision-v1/notice-of-intent-decision-v1.service'; import { NoticeOfIntentModificationOutcomeType } from './notice-of-intent-modification/notice-of-intent-modification-outcome-type/notice-of-intent-modification-outcome-type.entity'; import { NoticeOfIntentModificationController } from './notice-of-intent-modification/notice-of-intent-modification.controller'; import { NoticeOfIntentModification } from './notice-of-intent-modification/notice-of-intent-modification.entity'; @@ -23,21 +32,30 @@ import { NoticeOfIntentModificationService } from './notice-of-intent-modificati NoticeOfIntentDecisionDocument, NoticeOfIntentModification, NoticeOfIntentModificationOutcomeType, + NoticeOfIntentDecisionComponent, + NoticeOfIntentDecisionComponentType, + NoticeOfIntentDecisionCondition, + NoticeOfIntentDecisionConditionType, ]), forwardRef(() => BoardModule), CardModule, DocumentModule, NoticeOfIntentModule, + NoticeOfIntentSubmissionStatusModule, ], providers: [ - NoticeOfIntentDecisionService, + NoticeOfIntentDecisionV1Service, + NoticeOfIntentDecisionV2Service, + NoticeOfIntentDecisionComponentService, + NoticeOfIntentDecisionConditionService, NoticeOfIntentDecisionProfile, NoticeOfIntentModificationService, ], controllers: [ - NoticeOfIntentDecisionController, + NoticeOfIntentDecisionV1Controller, + NoticeOfIntentDecisionV2Controller, NoticeOfIntentModificationController, ], - exports: [NoticeOfIntentModificationService, NoticeOfIntentDecisionService], + exports: [NoticeOfIntentModificationService, NoticeOfIntentDecisionV1Service], }) export class NoticeOfIntentDecisionModule {} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts index 385ca4c487..54cffee43d 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts @@ -8,10 +8,10 @@ import { IsString, } from 'class-validator'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; +import { NoticeOfIntentTypeDto } from '../../code/application-code/notice-of-intent-type/notice-of-intent-type.dto'; import { LocalGovernmentDto } from '../../local-government/local-government.dto'; import { CardDto } from '../../card/card.dto'; import { ApplicationRegionDto } from '../../code/application-code/application-region/application-region.dto'; -import { ApplicationTypeDto } from '../../code/application-code/application-type/application-type.dto'; import { NoticeOfIntentDecisionDto } from '../notice-of-intent-decision.dto'; export class NoticeOfIntentModificationOutcomeCodeDto extends BaseCodeDto {} @@ -70,7 +70,7 @@ export class NoticeOfIntentModificationUpdateDto { export class NoticeOfIntentForModificationDto { fileNumber: string; - type: ApplicationTypeDto; + type: NoticeOfIntentTypeDto; statusCode: string; applicant: string; region: ApplicationRegionDto; diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.service.spec.ts index 80ce3c69a0..382f69839b 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.service.spec.ts @@ -13,7 +13,7 @@ import { CardService } from '../../card/card.service'; import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; -import { NoticeOfIntentDecisionService } from '../notice-of-intent-decision.service'; +import { NoticeOfIntentDecisionV1Service } from '../notice-of-intent-decision-v1/notice-of-intent-decision-v1.service'; import { NoticeOfIntentModificationCreateDto, NoticeOfIntentModificationUpdateDto, @@ -26,7 +26,7 @@ describe('NoticeOfIntentModificationService', () => { let service: NoticeOfIntentModificationService; let noticeOfIntentServiceMock: DeepMocked; let cardServiceMock: DeepMocked; - let decisionServiceMock: DeepMocked; + let decisionServiceMock: DeepMocked; let mockModification; let mockModificationCreateDto: NoticeOfIntentModificationCreateDto; @@ -83,7 +83,7 @@ describe('NoticeOfIntentModificationService', () => { useValue: cardServiceMock, }, { - provide: NoticeOfIntentDecisionService, + provide: NoticeOfIntentDecisionV1Service, useValue: decisionServiceMock, }, { diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.service.ts index ff8c210b4f..3267288a59 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.service.ts @@ -14,7 +14,7 @@ import { Board } from '../../board/board.entity'; import { CARD_TYPE } from '../../card/card-type/card-type.entity'; import { CardService } from '../../card/card.service'; import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; -import { NoticeOfIntentDecisionService } from '../notice-of-intent-decision.service'; +import { NoticeOfIntentDecisionV1Service } from '../notice-of-intent-decision-v1/notice-of-intent-decision-v1.service'; import { NoticeOfIntentModificationCreateDto, NoticeOfIntentModificationDto, @@ -29,7 +29,7 @@ export class NoticeOfIntentModificationService { private modificationRepository: Repository, @InjectMapper() private mapper: Mapper, private noticeOfIntentService: NoticeOfIntentService, - private noticeOfIntentDecisionService: NoticeOfIntentDecisionService, + private noticeOfIntentDecisionService: NoticeOfIntentDecisionV1Service, private cardService: CardService, ) {} diff --git a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts index c777a7f6d6..984afa4a64 100644 --- a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts @@ -62,16 +62,6 @@ export class ApplicationDecisionProfile extends AutomapperProfile { ), ), ), - forMember( - (ad) => ad.documents, - mapFrom((a) => - this.mapper.mapArray( - a.documents || [], - ApplicationDecisionDocument, - DecisionDocumentDto, - ), - ), - ), forMember( (a) => a.reconsiders, mapFrom((dec) => @@ -167,6 +157,7 @@ export class ApplicationDecisionProfile extends AutomapperProfile { ApplicationDecisionOutcomeCode, ApplicationDecisionOutcomeCodeDto, ); + createMap(mapper, NaruSubtype, NaruSubtypeDto); createMap( mapper, diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts index 13e4c79d8f..883aa72dc6 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts @@ -1,16 +1,28 @@ import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; -import { LocalGovernmentDto } from '../../alcs/local-government/local-government.dto'; import { CardDto } from '../../alcs/card/card.dto'; import { Card } from '../../alcs/card/card.entity'; +import { LocalGovernmentDto } from '../../alcs/local-government/local-government.dto'; import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { NoticeOfIntentDecisionComponentType } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity'; +import { + NoticeOfIntentDecisionComponentDto, + NoticeOfIntentDecisionComponentTypeDto, +} from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.dto'; +import { NoticeOfIntentDecisionComponent } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecisionConditionType } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity'; +import { + NoticeOfIntentDecisionConditionDto, + NoticeOfIntentDecisionConditionTypeDto, +} from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.dto'; +import { NoticeOfIntentDecisionCondition } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity'; import { NoticeOfIntentDecisionDocument } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; import { NoticeOfIntentDecisionOutcome } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-outcome.entity'; import { NoticeOfIntentDecisionDocumentDto, NoticeOfIntentDecisionDto, - NoticeOfIntentDecisionOutcomeDto, + NoticeOfIntentDecisionOutcomeCodeDto, } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision.dto'; import { NoticeOfIntentDecision } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision.entity'; import { NoticeOfIntentModificationOutcomeType } from '../../alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification-outcome-type/notice-of-intent-modification-outcome-type.entity'; @@ -46,12 +58,16 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { ), forMember( (ad) => ad.date, - mapFrom((a) => a.date.getTime()), + mapFrom((a) => a.date?.getTime()), ), forMember( (ad) => ad.auditDate, mapFrom((a) => a.auditDate?.getTime()), ), + forMember( + (ad) => ad.rescindedDate, + mapFrom((a) => a.rescindedDate?.getTime()), + ), forMember( (a) => a.modifies, mapFrom((dec) => @@ -81,12 +97,78 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { })), ), ), + forMember( + (a) => a.components, + mapFrom((ad) => { + if (ad.components) { + return this.mapper.mapArray( + ad.components, + NoticeOfIntentDecisionComponent, + NoticeOfIntentDecisionComponentDto, + ); + } else { + return []; + } + }), + ), + ); + + createMap( + mapper, + NoticeOfIntentDecisionComponent, + NoticeOfIntentDecisionComponentDto, + forMember( + (ad) => ad.endDate, + mapFrom((a) => a.endDate?.getTime()), + ), + forMember( + (ad) => ad.expiryDate, + mapFrom((a) => a.expiryDate?.getTime()), + ), + ); + + createMap( + mapper, + NoticeOfIntentDecisionComponentType, + NoticeOfIntentDecisionComponentTypeDto, + ); + + createMap( + mapper, + NoticeOfIntentDecisionCondition, + NoticeOfIntentDecisionConditionDto, + forMember( + (ad) => ad.completionDate, + mapFrom((a) => a.completionDate?.getTime()), + ), + forMember( + (ad) => ad.supersededDate, + mapFrom((a) => a.supersededDate?.getTime()), + ), + forMember( + (ad) => ad.components, + mapFrom((a) => + a.components && a.components.length > 0 + ? this.mapper.mapArray( + a.components, + NoticeOfIntentDecisionComponent, + NoticeOfIntentDecisionComponentDto, + ) + : [], + ), + ), + ); + + createMap( + mapper, + NoticeOfIntentDecisionConditionType, + NoticeOfIntentDecisionConditionTypeDto, ); createMap( mapper, NoticeOfIntentDecisionOutcome, - NoticeOfIntentDecisionOutcomeDto, + NoticeOfIntentDecisionOutcomeCodeDto, ); createMap( diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692812627565-noi_decisions_v2.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692812627565-noi_decisions_v2.ts new file mode 100644 index 0000000000..960568f5bb --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692812627565-noi_decisions_v2.ts @@ -0,0 +1,185 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class noiDecisionsV21692812627565 implements MigrationInterface { + name = 'noiDecisionsV21692812627565'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_decision_condition_type" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "label" character varying NOT NULL, "code" text NOT NULL, "description" text NOT NULL, CONSTRAINT "UQ_d42e55b4aef0fdc3c676b32a2a3" UNIQUE ("description"), CONSTRAINT "PK_30a33ede5fb646124dd846719cd" PRIMARY KEY ("code"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_decision_condition" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "approval_dependant" boolean, "security_amount" numeric(12,2), "administrative_fee" numeric(12,2), "description" text, "completion_date" TIMESTAMP WITH TIME ZONE, "superseded_date" TIMESTAMP WITH TIME ZONE, "type_code" text, "decision_uuid" uuid NOT NULL, CONSTRAINT "PK_51e53b1c3920b8957bfe368c46a" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notice_of_intent_decision_condition"."completion_date" IS 'Condition Completion date'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_condition"."superseded_date" IS 'Condition Superseded date'`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_decision_component_type" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "label" character varying NOT NULL, "code" text NOT NULL, "description" text NOT NULL, CONSTRAINT "UQ_6c4783f61a9a7fa784deac579ff" UNIQUE ("description"), CONSTRAINT "PK_67c2c14d96bec32f1d9c8e2e9b0" PRIMARY KEY ("code"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_decision_component" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "alr_area" numeric(12,2), "ag_cap" text, "ag_cap_source" text, "ag_cap_map" text, "ag_cap_consultant" text, "end_date" TIMESTAMP WITH TIME ZONE, "expiry_date" TIMESTAMP WITH TIME ZONE, "soil_fill_type_to_place" text, "soil_to_place_volume" numeric(12,2), "soil_to_place_area" numeric(12,2), "soil_to_place_maximum_depth" numeric(12,2), "soil_to_place_average_depth" numeric(12,2), "soil_type_removed" text, "soil_to_remove_volume" numeric(12,2), "soil_to_remove_area" numeric(12,2), "soil_to_remove_maximum_depth" numeric(12,2), "soil_to_remove_average_depth" numeric(12,2), "notice_of_intent_decision_component_type_code" text NOT NULL, "notice_of_intent_decision_uuid" uuid NOT NULL, CONSTRAINT "PK_cd1ed330456b906001d6b6288f6" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."alr_area" IS 'Area in hectares of ALR impacted by the decision component'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."ag_cap" IS 'Agricultural cap classification'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."ag_cap_source" IS 'Agricultural capability classification system used'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."ag_cap_map" IS 'Agricultural capability map sheet reference'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."ag_cap_consultant" IS 'Consultant who determined the agricultural capability'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."end_date" IS 'Components\` end date'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."expiry_date" IS 'Components\` expiry date'`, + ); + await queryRunner.query( + `CREATE UNIQUE INDEX "IDX_4014f28095a987bb6bc515aa2b" ON "alcs"."notice_of_intent_decision_component" ("notice_of_intent_decision_component_type_code", "notice_of_intent_decision_uuid") WHERE "audit_deleted_date_at" is null`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notice_of_intent_decision_condition_component" ("notice_of_intent_decision_condition_uuid" uuid NOT NULL, "notice_of_intent_decision_component_uuid" uuid NOT NULL, CONSTRAINT "PK_e91078d3e07b0f292333ff9d5d6" PRIMARY KEY ("notice_of_intent_decision_condition_uuid", "notice_of_intent_decision_component_uuid"))`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_4f1ecd62bb990e102c6af22a2e" ON "alcs"."notice_of_intent_decision_condition_component" ("notice_of_intent_decision_condition_uuid") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_a8090f60d15374a96a0d15bb13" ON "alcs"."notice_of_intent_decision_condition_component" ("notice_of_intent_decision_component_uuid") `, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ADD "was_released" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ADD "is_draft" boolean NOT NULL DEFAULT false`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."is_draft" IS 'Indicates whether the decision is currently draft or not'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ADD "is_subject_to_conditions" boolean`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."is_subject_to_conditions" IS 'Indicates whether the decision is subject to conditions'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ADD "decision_description" text`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."decision_description" IS 'Staff input field for a description of the decision'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ADD "is_stats_required" boolean`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."is_stats_required" IS 'Indicates whether the stats are required for the decision'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ADD "rescinded_date" TIMESTAMP WITH TIME ZONE`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."rescinded_date" IS 'Date when decision was rescinded'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ADD "rescinded_comment" text`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."rescinded_comment" IS 'Comment provided by the staff when the decision was rescinded'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ALTER COLUMN "date" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition" ADD CONSTRAINT "FK_30a33ede5fb646124dd846719cd" FOREIGN KEY ("type_code") REFERENCES "alcs"."notice_of_intent_decision_condition_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition" ADD CONSTRAINT "FK_88a63053a271ef84bc673e2bf9b" FOREIGN KEY ("decision_uuid") REFERENCES "alcs"."notice_of_intent_decision"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_component" ADD CONSTRAINT "FK_50ae784448f16c849fc9e9355c2" FOREIGN KEY ("notice_of_intent_decision_component_type_code") REFERENCES "alcs"."notice_of_intent_decision_component_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_component" ADD CONSTRAINT "FK_ac731e8132c6eb334dc44481c33" FOREIGN KEY ("notice_of_intent_decision_uuid") REFERENCES "alcs"."notice_of_intent_decision"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition_component" ADD CONSTRAINT "FK_4f1ecd62bb990e102c6af22a2e1" FOREIGN KEY ("notice_of_intent_decision_condition_uuid") REFERENCES "alcs"."notice_of_intent_decision_condition"("uuid") ON DELETE CASCADE ON UPDATE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition_component" ADD CONSTRAINT "FK_a8090f60d15374a96a0d15bb13f" FOREIGN KEY ("notice_of_intent_decision_component_uuid") REFERENCES "alcs"."notice_of_intent_decision_component"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ALTER COLUMN "decision_maker" DROP NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ALTER COLUMN "decision_maker" SET NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition_component" DROP CONSTRAINT "FK_a8090f60d15374a96a0d15bb13f"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition_component" DROP CONSTRAINT "FK_4f1ecd62bb990e102c6af22a2e1"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_component" DROP CONSTRAINT "FK_ac731e8132c6eb334dc44481c33"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_component" DROP CONSTRAINT "FK_50ae784448f16c849fc9e9355c2"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition" DROP CONSTRAINT "FK_88a63053a271ef84bc673e2bf9b"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision_condition" DROP CONSTRAINT "FK_30a33ede5fb646124dd846719cd"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ALTER COLUMN "date" SET NOT NULL`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."rescinded_comment" IS 'Comment provided by the staff when the decision was rescinded'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "rescinded_comment"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."rescinded_date" IS 'Date when decision was rescinded'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "rescinded_date"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."is_stats_required" IS 'Indicates whether the stats are required for the decision'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "is_stats_required"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."decision_description" IS 'Staff input field for a description of the decision'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "decision_description"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."is_subject_to_conditions" IS 'Indicates whether the decision is subject to conditions'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "is_subject_to_conditions"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notice_of_intent_decision"."is_draft" IS 'Indicates whether the decision is currently draft or not'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "is_draft"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" DROP COLUMN "was_released"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_a8090f60d15374a96a0d15bb13"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_4f1ecd62bb990e102c6af22a2e"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."notice_of_intent_decision_condition_component"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_4014f28095a987bb6bc515aa2b"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."notice_of_intent_decision_component"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."notice_of_intent_decision_component_type"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."notice_of_intent_decision_condition"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."notice_of_intent_decision_condition_type"`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692812692333-seed_noi_dec_v2_tables.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692812692333-seed_noi_dec_v2_tables.ts new file mode 100644 index 0000000000..1e1d1671a8 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692812692333-seed_noi_dec_v2_tables.ts @@ -0,0 +1,48 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class seedNoiDecV2Tables1692812692333 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO "alcs"."notice_of_intent_decision_component_type" ("audit_deleted_date_at", "audit_created_at", "audit_updated_at", "audit_created_by", "audit_updated_by", "label", "code", "description") VALUES + (NULL, NOW(), NULL, 'seed-migration', 'seed-migration', 'Removal of Soil', 'ROSO', 'Removal of Soil'), + (NULL, NOW(), NULL, 'seed-migration', 'seed-migration', 'Placement of Fill', 'POFO', 'Placement of Fill'), + (NULL, NOW(), NULL, 'seed-migration', 'seed-migration', 'Placement of Fill/Removal of Soil', 'PFRS', 'Placement of Fill/Removal of Soil'); + `); + + await queryRunner.query(` + INSERT INTO "alcs"."notice_of_intent_decision_condition_type" ("audit_deleted_date_at", "audit_created_at", "audit_updated_at", "audit_created_by", "audit_updated_by", "label", "code", "description") VALUES + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Adoption and/or compliance with By-Laws, OCP, etc.', 'ACBO', 'Responsibility to comply with applicable Acts, regulations, bylaws of the local government, and decisions and orders of any person or body having jurisdiction over the land under an enactment'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Access', 'ACCE', 'Creating a new parcel requires legal access, associated with subdivision'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Administrative Fees', 'AFEE', 'Fees will be charged by the ALC to administrate condition compliance'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Bond', 'BOND', 'Financial security to ensure compliance with conditions of approval'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Benefit to Agriculture', 'BTOA', 'Provide some action that is a benefit to agriculture'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Buffering', 'BUFF', 'Landscape buffering along the edge of an approval to mitigate impacts to/from adjacent properties'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Consolidation', 'CONS', 'Resurveying multiple properties into fewer properties and consolidating by legal title'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Covenant', 'COVE', 'Registered on title to ensure ongoing compliance to the conditions of approval'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Fencing', 'FENC', 'Fencing along the edge of an approval to mitigate impacts to/from adjacent properties'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Final Report', 'FRPT', 'Closing report prepared by a qualified professional outlining condition compliance'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Homesite Severance', 'HOME', 'Compliance with criteria outlined in Policy L-11'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Inclusion', 'INCL', 'Subject to the inclusion of alternate agriculturally-capable land into the ALR to offset an approval'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Lease', 'LEAS', 'Require a lease agreement to ensure the property is used for agricultural purposes'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Monitored by qualified registered professional', 'MBRP', 'Oversight of the approved use by a qualified professional'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'No Expansion', 'NOEX', 'No expansion beyond the approved footprint or site plan'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'No Homesite Severance', 'NOHS', 'Does not meet criteria outlined in Policy L-11'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Non-Transferrable', 'NONT', 'For the sole benefit of the applicant'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Other', 'OTHR', 'Other condition not outlined by standard condition types'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Rehabilitation/Reclamation', 'RERC', 'Plan to reclaim or rehabilitate the property to an appropriate agricultural standard'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Right of First Refusal', 'ROFR', 'Gives a party the first opportunity to make an offer in a particular transaction'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Require Survey Plan', 'RSPL', 'Legal survey plan completed by a BC Land Surveyor provided within a defined time limit'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'SCA Report', 'SCAR', 'Soil Conservation Act (repealed) required reporting'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Substantial Compliance with Submitted Plan', 'SCSP', 'Approved use is consistent with the submitted plan'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Status Report', 'SRPT', 'Report prepared by a qualified professional provided on a recurring basis to outline progress'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Siting or Site Development Plan', 'SSDP', 'Site plan detailing the approved or proposed use on the landscape'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Standard Reclamation Conditions', 'STRC', 'Standard reclamation conditions outlined in ALC policies and bulletins'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Time Limit', 'TIME', 'Specified deadline to complete a specific condition or use'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Vegetative Screening', 'VEGS', 'The use of vegetation to mitigate impacts to/from adjacent properties'); + `); + } + + public async down(): Promise { + //No can has + } +} From 78d7dfd98810624987c0d796c15831b67fe0126c Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Wed, 23 Aug 2023 15:50:36 -0700 Subject: [PATCH 293/954] Bugfix/alcs 1050 (#905) * update search query for NOI in portal * switch from api level filtering to queryBuilder, adjust filter criteria on search app submission for LG --- .../application-submission.service.spec.ts | 46 +++++++- .../application-submission.service.ts | 103 +++++++++++------- .../notice-of-intent-submission.service.ts | 5 +- 3 files changed, 110 insertions(+), 44 deletions(-) diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts index d4257f3d28..5871d6e127 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.spec.ts @@ -1,4 +1,7 @@ -import { BaseServiceException } from '@app/common/exceptions/base.exception'; +import { + BaseServiceException, + ServiceNotFoundException, +} from '@app/common/exceptions/base.exception'; import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; @@ -204,7 +207,9 @@ describe('ApplicationSubmissionService', () => { statusTypeCode: SUBMISSION_STATUS.SUBMITTED_TO_LG, submissionUuid: 'fake', }), - createdBy: new User(), + createdBy: new User({ + bceidBusinessGuid: 'cats', + }), }); mockRepository.findOne.mockResolvedValue(application); @@ -221,6 +226,43 @@ describe('ApplicationSubmissionService', () => { expect(res).toBe(application); }); + it('should fail on getForGovernmentByFileId if application was not submitted previously', async () => { + const submission = new ApplicationSubmission({ + uuid: 'fake-uuid', + fileNumber: 'fake-number', + submissionStatuses: [ + new ApplicationSubmissionToSubmissionStatus({ + statusTypeCode: SUBMISSION_STATUS.SUBMITTED_TO_LG, + effectiveDate: null, + }), + ], + status: new ApplicationSubmissionToSubmissionStatus({ + statusTypeCode: SUBMISSION_STATUS.CANCELLED, + submissionUuid: 'fake', + }), + createdBy: new User(), + }); + mockRepository.findOne.mockResolvedValue(submission); + + const promise = service.getForGovernmentByFileId( + submission.fileNumber, + new LocalGovernment({ + uuid: '', + name: '', + isFirstNation: false, + bceidBusinessGuid: 'cats', + }), + ); + + await expect(promise).rejects.toMatchObject( + new ServiceNotFoundException( + `Failed to load application with File ID ${submission.fileNumber}`, + ), + ); + + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + }); + it('should load the canceled status and save the application for cancel', async () => { const application = new ApplicationSubmission({ uuid: 'fake', diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index f611d60466..0b9f8c0df4 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -11,6 +11,7 @@ import { ApplicationDocumentService } from '../../alcs/application/application-d import { ApplicationSubmissionStatusService } from '../../alcs/application/application-submission-status/application-submission-status.service'; import { ApplicationSubmissionStatusType } from '../../alcs/application/application-submission-status/submission-status-type.entity'; import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; +import { ApplicationSubmissionToSubmissionStatus } from '../../alcs/application/application-submission-status/submission-status.entity'; import { Application } from '../../alcs/application/application.entity'; import { ApplicationService } from '../../alcs/application/application.service'; import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; @@ -317,36 +318,41 @@ export class ApplicationSubmissionService { throw new Error("Cannot load by governments that don't have guids"); } - const submissions = await this.applicationSubmissionRepository.find({ - where: [ - //Owns + const submissions = await this.applicationSubmissionRepository + .createQueryBuilder('aps') + .leftJoinAndSelect( + (sq) => + sq + .select('apsst.submission_uuid') + .from(ApplicationSubmissionToSubmissionStatus, 'apsst') + .where('apsst.status_type_code IN (:...statuses)', { + statuses: ['SUBG', 'SUBM'], + }) + .andWhere('apsst.effective_date IS NOT NULL') + .groupBy('apsst.submission_uuid'), + 'fapsst', + 'aps.uuid = fapsst.submission_uuid', + ) + .leftJoinAndSelect( + User, + 'createdBy', + 'createdBy.uuid = aps.created_by_uuid', + ) + .leftJoin(LocalGovernment, 'lg', 'lg.uuid = aps.local_government_uuid') + .where('aps.isDraft=False') + .andWhere( + '(createdBy.bceid_business_guid = :bceidGuid or (fapsst is not null and lg.uuid=:lgUuid))', { - createdBy: { - bceidBusinessGuid: localGovernment.bceidBusinessGuid, - }, - isDraft: false, - }, - //Local Government - { - localGovernmentUuid: localGovernment.uuid, - isDraft: false, + bceidGuid: localGovernment.bceidBusinessGuid, + lgUuid: localGovernment.uuid, }, - ], - order: { - auditUpdatedAt: 'DESC', - }, - relations: { - createdBy: true, - }, - }); + ) + .leftJoinAndSelect('aps.submissionStatuses', 'submissionStatuses') + .leftJoinAndSelect('submissionStatuses.statusType', 'statusType') + .orderBy('aps.auditUpdatedAt', 'DESC') + .getMany(); - return submissions.filter( - (s) => - s.createdBy?.bceidBusinessGuid === localGovernment.bceidBusinessGuid || - LG_VISIBLE_STATUSES.includes( - s.status?.statusTypeCode as SUBMISSION_STATUS, - ), - ); + return submissions; } async getForGovernmentByUuid(uuid: string, localGovernment: LocalGovernment) { @@ -385,12 +391,10 @@ export class ApplicationSubmissionService { if ( !existingApplication || - (existingApplication && - existingApplication.createdBy.bceidBusinessGuid !== - localGovernment.bceidBusinessGuid && - !LG_VISIBLE_STATUSES.includes( - existingApplication.status.statusTypeCode as SUBMISSION_STATUS, - )) + !this.isSubmissionVisibleToLocalGovernment( + existingApplication, + localGovernment, + ) ) { throw new ServiceNotFoundException( `Failed to load application with uuid ${uuid}`, @@ -400,6 +404,28 @@ export class ApplicationSubmissionService { return existingApplication; } + private isSubmissionVisibleToLocalGovernment( + existingApplication: ApplicationSubmission, + localGovernment: LocalGovernment, + ) { + return ( + (existingApplication.createdBy && + existingApplication.createdBy.bceidBusinessGuid === + localGovernment.bceidBusinessGuid) || + (LG_VISIBLE_STATUSES.includes( + existingApplication.status.statusTypeCode as SUBMISSION_STATUS, + ) && + existingApplication.submissionStatuses.some( + (status) => + [ + SUBMISSION_STATUS.SUBMITTED_TO_ALC, + SUBMISSION_STATUS.SUBMITTED_TO_LG, + ].includes(status.statusTypeCode as SUBMISSION_STATUS) && + status.effectiveDate !== null, + )) + ); + } + async getForGovernmentByFileId( fileNumber: string, localGovernment: LocalGovernment, @@ -436,14 +462,13 @@ export class ApplicationSubmissionService { }, relations: { ...this.DEFAULT_RELATIONS, createdBy: true }, }); + if ( !existingApplication || - (existingApplication && - existingApplication.createdBy.bceidBusinessGuid !== - localGovernment.bceidBusinessGuid && - !LG_VISIBLE_STATUSES.includes( - existingApplication.status.statusTypeCode as SUBMISSION_STATUS, - )) + !this.isSubmissionVisibleToLocalGovernment( + existingApplication, + localGovernment, + ) ) { throw new ServiceNotFoundException( `Failed to load application with File ID ${fileNumber}`, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index 729fcbe99e..7ebe9a7502 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -240,9 +240,8 @@ export class NoticeOfIntentSubmissionService { ...searchOptions, localGovernmentUuid: matchingLocalGovernment.uuid, isDraft: false, - submissionStatuses: { - effectiveDate: Not(IsNull()), - statusTypeCode: Not(NOI_SUBMISSION_STATUS.IN_PROGRESS), + noticeOfIntent: { + dateSubmittedToAlc: Not(IsNull()), }, }); } From ed40d3a8ee469403c7a238840b7e2bea39e5d09f Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 23 Aug 2023 18:20:07 -0700 Subject: [PATCH 294/954] using dict to create NESW descriptions --- .../submissions/app_submissions.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 03db7279d8..c885b9e166 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -65,6 +65,43 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): """ cursor.execute(adj_rows_query) adj_rows = cursor.fetchall() + test_dict = {} + for row in adj_rows: + application_id = row['alr_application_id'] + if application_id in test_dict: + if row['cardinal_direction'] == 'EAST': + test_dict[application_id]['east_description'] = row['description'] + test_dict[application_id]['east_type_code'] = row['nonfarm_use_type_code'] + # test_dict[application_id].append(row) + if row['cardinal_direction'] == 'WEST': + test_dict[application_id]['west_description'] = row['description'] + test_dict[application_id]['west_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'NORTH': + test_dict[application_id]['north_description'] = row['description'] + test_dict[application_id]['north_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'SOUTH': + test_dict[application_id]['south_description'] = row['description'] + test_dict[application_id]['south_type_code'] = row['nonfarm_use_type_code'] + else: + test_dict[application_id] = {} + test_dict[application_id]['alr_application_id'] = row['alr_application_id'] + # test_dict[application_id] = [row] + if row['cardinal_direction'] == 'EAST': + test_dict[application_id]['east_description'] = row['description'] + test_dict[application_id]['east_type_code'] = row['nonfarm_use_type_code'] + # test_dict[application_id].append(row) + if row['cardinal_direction'] == 'WEST': + test_dict[application_id]['west_description'] = row['description'] + test_dict[application_id]['west_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'NORTH': + test_dict[application_id]['north_description'] = row['description'] + test_dict[application_id]['north_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'SOUTH': + test_dict[application_id]['south_description'] = row['description'] + test_dict[application_id]['south_type_code'] = row['nonfarm_use_type_code'] + + # for row in {adj_rows['alr_application_id']: adj_rows for adj_rows in adj_rows} + print(test_dict) submissions_to_be_inserted_count = len(rows) From 31f6589a7d4c412c511c010079254236d3539b16 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 23 Aug 2023 18:44:30 -0700 Subject: [PATCH 295/954] EOD commit --- .../submissions/app_submissions.py | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index c885b9e166..63f5e44ef1 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -101,11 +101,15 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): test_dict[application_id]['south_type_code'] = row['nonfarm_use_type_code'] # for row in {adj_rows['alr_application_id']: adj_rows for adj_rows in adj_rows} - print(test_dict) + # print(test_dict) + + # print("this is rows") + # print(rows) + # print("end rows") submissions_to_be_inserted_count = len(rows) - insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows) + insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows, test_dict) successful_inserts_count = ( successful_inserts_count + submissions_to_be_inserted_count @@ -129,7 +133,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): print("Total failed inserts:", failed_inserts) log_end(etl_name) -def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows): +def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows, test_dict): """ Function to insert submission records in batches. @@ -146,7 +150,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows): nfu_data_list, other_data_list, inc_exc_data_list, - ) = prepare_app_sub_data(rows, adj_rows) + ) = prepare_app_sub_data(rows, adj_rows, test_dict) if len(nfu_data_list) > 0: execute_batch( @@ -174,7 +178,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows): conn.commit() -def prepare_app_sub_data(app_sub_raw_data_list, raw_dir_data_list): +def prepare_app_sub_data(app_sub_raw_data_list, raw_dir_data_list, test_dict): """ This function prepares different lists of data based on the 'alr_change_code' field of each data dict in 'app_sub_raw_data_list'. @@ -194,9 +198,12 @@ def prepare_app_sub_data(app_sub_raw_data_list, raw_dir_data_list): for row in app_sub_raw_data_list: data = dict(row) data = add_direction_field(data) - for adj_row in raw_dir_data_list: - dir_data = dict(adj_row) - data = map_direction_field(data, dir_data) + if data["alr_application_id"] in test_dict: + print(test_dict[data["alr_application_id"]]["alr_application_id"]) + #leaving off here to insert values from new fcn + # for adj_row in raw_dir_data_list: + # dir_data = dict(adj_row) + # data = map_direction_field(data, dir_data) # currently rather slow # ToDo optimize, potentially give index for dir_data resume point From 9e6770464dbace2cc581c7eede7f098246e7d9eb Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 24 Aug 2023 09:42:14 -0700 Subject: [PATCH 296/954] Code Review Feedback * Rename some Apps -> NOIs * Add table comments for new tables --- ...cision-document-upload-dialog.component.ts | 4 +- .../condition/condition.component.ts | 4 -- .../decision-v1/decision-v1.component.spec.ts | 2 +- .../decision-conditions.component.spec.ts | 1 - ...cision-document-upload-dialog.component.ts | 4 +- .../decision-input-v2.component.ts | 5 +- .../notice-of-intent.component.ts | 4 +- .../application-decision-v2.dto.ts | 4 +- .../notice-of-intent.service.ts | 2 +- ...-intent-decision-component.service.spec.ts | 62 +++++++---------- ...tent-decision-condition.controller.spec.ts | 33 +++++----- ...-intent-decision-condition.service.spec.ts | 66 +++++++------------ ...e-of-intent-decision-v2.controller.spec.ts | 14 ++-- .../notice-of-intent-decision.module.ts | 1 + .../1692812627565-noi_decisions_v2.ts | 15 +++++ 15 files changed, 98 insertions(+), 123 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts index fdde9c666d..aeb4b66027 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts @@ -1,7 +1,7 @@ import { Component, Inject, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { DecisionDocumentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; +import { ApplicationDecisionDocumentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { DOCUMENT_SOURCE } from '../../../../../../shared/document/document.dto'; @@ -39,7 +39,7 @@ export class DecisionDocumentUploadDialogComponent implements OnInit { constructor( @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; decisionUuid: string; existingDocument?: DecisionDocumentDto }, + public data: { fileId: string; decisionUuid: string; existingDocument?: ApplicationDecisionDocumentDto }, protected dialog: MatDialogRef, private decisionService: ApplicationDecisionV2Service ) {} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts index 99927ef81c..975703b4f6 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts @@ -1,6 +1,5 @@ import { AfterViewInit, Component, Input, OnInit } from '@angular/core'; import moment from 'moment'; -import { UpdateApplicationDecisionConditionDto } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { NoticeOfIntentDecisionConditionService } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; import { UpdateNoticeOfIntentDecisionConditionDto } from '../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; import { @@ -34,7 +33,6 @@ export class ConditionComponent implements OnInit, AfterViewInit { isReadMoreClicked = false; isReadMoreVisible = false; conditionStatus: string = ''; - isRequireSurveyPlan = false; constructor(private conditionService: NoticeOfIntentDecisionConditionService) {} @@ -45,8 +43,6 @@ export class ConditionComponent implements OnInit, AfterViewInit { ...this.condition, componentLabelsStr: this.condition.conditionComponentsLabels?.flatMap((e) => e.label).join(';\n'), }; - - this.isRequireSurveyPlan = this.condition.type?.code === 'RSPL'; } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.spec.ts index 900a5dcadc..0ea55942b9 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.spec.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.spec.ts @@ -12,7 +12,7 @@ import { ConfirmationDialogService } from '../../../../shared/confirmation-dialo import { DecisionV1Component } from './decision-v1.component'; -describe('DecisionComponent', () => { +describe('DecisionV1Component', () => { let component: DecisionV1Component; let fixture: ComponentFixture; let mockNOIDetailService: DeepMocked; diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts index a0015d4867..0c274c5231 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-conditions/decision-conditions.component.spec.ts @@ -3,7 +3,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatMenuModule } from '@angular/material/menu'; import { createMock } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; -import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { NoticeOfIntentDecisionV2Service } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; import { NoticeOfIntentDecisionDto, diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts index ecb07ad94d..4682493a1b 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts @@ -1,8 +1,8 @@ import { Component, Inject, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { DecisionDocumentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { NoticeOfIntentDecisionV2Service } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentDecisionDocumentDto } from '../../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; import { DOCUMENT_SOURCE } from '../../../../../../shared/document/document.dto'; @Component({ @@ -39,7 +39,7 @@ export class DecisionDocumentUploadDialogComponent implements OnInit { constructor( @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; decisionUuid: string; existingDocument?: DecisionDocumentDto }, + public data: { fileId: string; decisionUuid: string; existingDocument?: NoticeOfIntentDecisionDocumentDto }, protected dialog: MatDialogRef, private decisionService: NoticeOfIntentDecisionV2Service ) {} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts index 87425ac561..192e631a31 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -204,8 +204,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { modifications: NoticeOfIntentModificationDto[], existingDecision?: NoticeOfIntentDecisionDto ) { - //TOOD: Clean this up - const mappedModifications = modifications + this.postDecisions = modifications .filter( (modification) => (existingDecision && existingDecision.modifies?.uuid === modification.uuid) || @@ -217,8 +216,6 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { .join(', ')}`, uuid: modification.uuid, })); - - this.postDecisions = [...mappedModifications]; } private patchFormWithExistingData(existingDecision: NoticeOfIntentDecisionDto) { diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts index f344756e9a..3b5486bc9b 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.ts @@ -93,12 +93,10 @@ export class NoticeOfIntentComponent implements OnInit, OnDestroy { ) {} ngOnInit(): void { - this.fileNumber = '100135'; - this.load(); - this.route.params.pipe(takeUntil(this.destroy)).subscribe(async (routeParams) => { const { fileNumber } = routeParams; this.fileNumber = fileNumber; + this.load(); }); this.noticeOfIntentDetailService.$noticeOfIntent.pipe(takeUntil(this.destroy)).subscribe((noticeOfIntent) => { diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts index 903e0e5bf0..db99cf2efe 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.dto.ts @@ -59,7 +59,7 @@ export interface ApplicationDecisionDto { chairReviewOutcome: ChairReviewOutcomeCodeDto | null; linkedResolutionOutcome: LinkedResolutionOutcomeTypeDto | null; applicationFileNumber: string; - documents: DecisionDocumentDto[]; + documents: ApplicationDecisionDocumentDto[]; isTimeExtension?: boolean | null; isOther?: boolean | null; isDraft: boolean; @@ -88,7 +88,7 @@ export interface ApplicationDecisionWithLinkedResolutionDto extends ApplicationD index: number; } -export interface DecisionDocumentDto { +export interface ApplicationDecisionDocumentDto { uuid: string; fileName: string; mimeType: string; diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.service.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.service.ts index 74db344959..823cf9f663 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.service.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent.service.ts @@ -56,7 +56,7 @@ export class NoticeOfIntentService { async fetchByFileNumber(fileNumber: string) { try { - return firstValueFrom(this.http.get(`${this.url}/${fileNumber}`)); + return await firstValueFrom(this.http.get(`${this.url}/${fileNumber}`)); } catch (e) { console.error(e); this.toastService.showErrorToast('Failed to fetch Notice of Intent'); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts index 7118c35dc8..f24061a5f0 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.service.spec.ts @@ -7,21 +7,21 @@ import { CreateNoticeOfIntentDecisionComponentDto } from './notice-of-intent-dec import { NoticeOfIntentDecisionComponent } from './notice-of-intent-decision-component.entity'; import { NoticeOfIntentDecisionComponentService } from './notice-of-intent-decision-component.service'; -describe('ApplicationDecisionComponentService', () => { +describe('NoticeOfIntentDecisionComponentService', () => { let service: NoticeOfIntentDecisionComponentService; - let mockApplicationDecisionComponentRepository: DeepMocked< + let mockDecisionComponentRepository: DeepMocked< Repository >; beforeEach(async () => { - mockApplicationDecisionComponentRepository = createMock(); + mockDecisionComponentRepository = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ NoticeOfIntentDecisionComponentService, { provide: getRepositoryToken(NoticeOfIntentDecisionComponent), - useValue: mockApplicationDecisionComponentRepository, + useValue: mockDecisionComponentRepository, }, ], }).compile(); @@ -36,18 +36,14 @@ describe('ApplicationDecisionComponentService', () => { }); it('should call repo to get one or fails with correct parameters', async () => { - mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue( + mockDecisionComponentRepository.findOneOrFail.mockResolvedValue( new NoticeOfIntentDecisionComponent(), ); const result = await service.getOneOrFail('fake'); - expect( - mockApplicationDecisionComponentRepository.findOneOrFail, - ).toBeCalledTimes(1); - expect( - mockApplicationDecisionComponentRepository.findOneOrFail, - ).toBeCalledWith({ + expect(mockDecisionComponentRepository.findOneOrFail).toBeCalledTimes(1); + expect(mockDecisionComponentRepository.findOneOrFail).toBeCalledWith({ where: { uuid: 'fake' }, }); expect(result).toBeDefined(); @@ -59,15 +55,15 @@ describe('ApplicationDecisionComponentService', () => { new NoticeOfIntentDecisionComponent(), ]; - mockApplicationDecisionComponentRepository.softRemove.mockResolvedValue( + mockDecisionComponentRepository.softRemove.mockResolvedValue( {} as NoticeOfIntentDecisionComponent, ); await service.softRemove(components); - expect( - mockApplicationDecisionComponentRepository.softRemove, - ).toHaveBeenCalledWith(components); + expect(mockDecisionComponentRepository.softRemove).toHaveBeenCalledWith( + components, + ); }); it('throws validation error if there are duplicate components', () => { @@ -122,7 +118,7 @@ describe('ApplicationDecisionComponentService', () => { }); it('should create new components when given a DTO without a UUID', async () => { - mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue( + mockDecisionComponentRepository.findOneOrFail.mockResolvedValue( {} as NoticeOfIntentDecisionComponent, ); @@ -135,13 +131,11 @@ describe('ApplicationDecisionComponentService', () => { expect(result).toBeDefined(); expect(result.length).toBe(2); - expect( - mockApplicationDecisionComponentRepository.findOneOrFail, - ).toBeCalledTimes(0); + expect(mockDecisionComponentRepository.findOneOrFail).toBeCalledTimes(0); }); it('should update existing components when given a DTO with a UUID', async () => { - mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue({ + mockDecisionComponentRepository.findOneOrFail.mockResolvedValue({ uuid: 'fake', noticeOfIntentDecisionComponentTypeCode: 'fake_code', } as NoticeOfIntentDecisionComponent); @@ -159,12 +153,8 @@ describe('ApplicationDecisionComponentService', () => { expect(result).toBeDefined(); expect(result.length).toBe(1); - expect( - mockApplicationDecisionComponentRepository.findOneOrFail, - ).toBeCalledTimes(1); - expect( - mockApplicationDecisionComponentRepository.findOneOrFail, - ).toBeCalledWith({ + expect(mockDecisionComponentRepository.findOneOrFail).toBeCalledTimes(1); + expect(mockDecisionComponentRepository.findOneOrFail).toBeCalledWith({ where: { uuid: 'fake' }, }); expect(result[0].uuid).toEqual(mockDto.uuid); @@ -179,10 +169,10 @@ describe('ApplicationDecisionComponentService', () => { }); it('should persist entity if persist flag is true', async () => { - mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue( + mockDecisionComponentRepository.findOneOrFail.mockResolvedValue( {} as NoticeOfIntentDecisionComponent, ); - mockApplicationDecisionComponentRepository.save.mockResolvedValue( + mockDecisionComponentRepository.save.mockResolvedValue( {} as NoticeOfIntentDecisionComponent, ); @@ -191,17 +181,15 @@ describe('ApplicationDecisionComponentService', () => { const result = await service.createOrUpdate(updateDtos, true); expect(result).toBeDefined(); - expect( - mockApplicationDecisionComponentRepository.findOneOrFail, - ).toBeCalledTimes(0); - expect(mockApplicationDecisionComponentRepository.save).toBeCalledTimes(1); + expect(mockDecisionComponentRepository.findOneOrFail).toBeCalledTimes(0); + expect(mockDecisionComponentRepository.save).toBeCalledTimes(1); }); it('should not persist entity if persist flag is false', async () => { - mockApplicationDecisionComponentRepository.findOneOrFail.mockResolvedValue( + mockDecisionComponentRepository.findOneOrFail.mockResolvedValue( {} as NoticeOfIntentDecisionComponent, ); - mockApplicationDecisionComponentRepository.save.mockResolvedValue( + mockDecisionComponentRepository.save.mockResolvedValue( {} as NoticeOfIntentDecisionComponent, ); @@ -210,10 +198,8 @@ describe('ApplicationDecisionComponentService', () => { const result = await service.createOrUpdate(updateDtos, false); expect(result).toBeDefined(); - expect( - mockApplicationDecisionComponentRepository.findOneOrFail, - ).toBeCalledTimes(0); - expect(mockApplicationDecisionComponentRepository.save).toBeCalledTimes(0); + expect(mockDecisionComponentRepository.findOneOrFail).toBeCalledTimes(0); + expect(mockDecisionComponentRepository.save).toBeCalledTimes(0); }); it('should validation decision component fields and throw error if any', async () => { diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts index b1756bf9bd..338b655f1f 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller.spec.ts @@ -12,10 +12,10 @@ import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decis describe('NoticeOfIntentDecisionConditionController', () => { let controller: NoticeOfIntentDecisionConditionController; - let mockApplicationDecisionConditionService: DeepMocked; + let mockNOIDecisionConditionService: DeepMocked; beforeEach(async () => { - mockApplicationDecisionConditionService = createMock(); + mockNOIDecisionConditionService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -28,7 +28,7 @@ describe('NoticeOfIntentDecisionConditionController', () => { NoticeOfIntentDecisionProfile, { provide: NoticeOfIntentDecisionConditionService, - useValue: mockApplicationDecisionConditionService, + useValue: mockNOIDecisionConditionService, }, { provide: ClsService, @@ -81,23 +81,22 @@ describe('NoticeOfIntentDecisionConditionController', () => { supersededDate: date, }); - mockApplicationDecisionConditionService.getOneOrFail.mockResolvedValue( - condition, - ); - mockApplicationDecisionConditionService.update.mockResolvedValue(updated); + mockNOIDecisionConditionService.getOneOrFail.mockResolvedValue(condition); + mockNOIDecisionConditionService.update.mockResolvedValue(updated); const result = await controller.update(uuid, updates); - expect( - mockApplicationDecisionConditionService.getOneOrFail, - ).toHaveBeenCalledWith(uuid); - expect( - mockApplicationDecisionConditionService.update, - ).toHaveBeenCalledWith(condition, { - ...updates, - completionDate: date, - supersededDate: date, - }); + expect(mockNOIDecisionConditionService.getOneOrFail).toHaveBeenCalledWith( + uuid, + ); + expect(mockNOIDecisionConditionService.update).toHaveBeenCalledWith( + condition, + { + ...updates, + completionDate: date, + supersededDate: date, + }, + ); expect(new Date(result.completionDate!)).toEqual(updated.completionDate); expect(new Date(result.supersededDate!)).toEqual(updated.supersededDate); expect(result.description).toEqual(updated.description); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts index 1a9eb6b50a..f4e00a7089 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-condition/notice-of-intent-decision-condition.service.spec.ts @@ -7,29 +7,29 @@ import { UpdateNoticeOfIntentDecisionConditionDto } from './notice-of-intent-dec import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition.entity'; import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition.service'; -describe('ApplicationDecisionConditionService', () => { +describe('NoticeOfIntentDecisionConditionService', () => { let service: NoticeOfIntentDecisionConditionService; - let mockApplicationDecisionConditionRepository: DeepMocked< + let mockNOIDecisionConditionRepository: DeepMocked< Repository >; - let mockAppDecCondTypeRepository: DeepMocked< + let mockNOIDecisionConditionTypeRepository: DeepMocked< Repository >; beforeEach(async () => { - mockApplicationDecisionConditionRepository = createMock(); - mockAppDecCondTypeRepository = createMock(); + mockNOIDecisionConditionRepository = createMock(); + mockNOIDecisionConditionTypeRepository = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ NoticeOfIntentDecisionConditionService, { provide: getRepositoryToken(NoticeOfIntentDecisionCondition), - useValue: mockApplicationDecisionConditionRepository, + useValue: mockNOIDecisionConditionRepository, }, { provide: getRepositoryToken(NoticeOfIntentDecisionConditionType), - useValue: mockAppDecCondTypeRepository, + useValue: mockNOIDecisionConditionTypeRepository, }, ], }).compile(); @@ -44,18 +44,14 @@ describe('ApplicationDecisionConditionService', () => { }); it('should call repo to get one or fails with correct parameters', async () => { - mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + mockNOIDecisionConditionRepository.findOneOrFail.mockResolvedValue( new NoticeOfIntentDecisionCondition(), ); const result = await service.getOneOrFail('fake'); - expect( - mockApplicationDecisionConditionRepository.findOneOrFail, - ).toBeCalledTimes(1); - expect( - mockApplicationDecisionConditionRepository.findOneOrFail, - ).toBeCalledWith({ + expect(mockNOIDecisionConditionRepository.findOneOrFail).toBeCalledTimes(1); + expect(mockNOIDecisionConditionRepository.findOneOrFail).toBeCalledWith({ where: { uuid: 'fake' }, relations: { type: true }, }); @@ -68,19 +64,17 @@ describe('ApplicationDecisionConditionService', () => { new NoticeOfIntentDecisionCondition(), ]; - mockApplicationDecisionConditionRepository.remove.mockResolvedValue( + mockNOIDecisionConditionRepository.remove.mockResolvedValue( {} as NoticeOfIntentDecisionCondition, ); await service.remove(conditions); - expect(mockApplicationDecisionConditionRepository.remove).toBeCalledTimes( - 1, - ); + expect(mockNOIDecisionConditionRepository.remove).toBeCalledTimes(1); }); it('should create new components when given a DTO without a UUID', async () => { - mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + mockNOIDecisionConditionRepository.findOneOrFail.mockResolvedValue( new NoticeOfIntentDecisionCondition(), ); @@ -90,13 +84,11 @@ describe('ApplicationDecisionConditionService', () => { expect(result).toBeDefined(); expect(result.length).toBe(2); - expect( - mockApplicationDecisionConditionRepository.findOneOrFail, - ).toBeCalledTimes(0); + expect(mockNOIDecisionConditionRepository.findOneOrFail).toBeCalledTimes(0); }); it('should update existing components when given a DTO with a UUID', async () => { - mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + mockNOIDecisionConditionRepository.findOneOrFail.mockResolvedValue( new NoticeOfIntentDecisionCondition({ uuid: 'uuid', }), @@ -110,12 +102,8 @@ describe('ApplicationDecisionConditionService', () => { expect(result).toBeDefined(); expect(result.length).toBe(1); - expect( - mockApplicationDecisionConditionRepository.findOneOrFail, - ).toBeCalledTimes(1); - expect( - mockApplicationDecisionConditionRepository.findOneOrFail, - ).toBeCalledWith({ + expect(mockNOIDecisionConditionRepository.findOneOrFail).toBeCalledTimes(1); + expect(mockNOIDecisionConditionRepository.findOneOrFail).toBeCalledWith({ where: { uuid: 'uuid' }, relations: { type: true }, }); @@ -123,10 +111,10 @@ describe('ApplicationDecisionConditionService', () => { }); it('should persist entity if persist flag is true', async () => { - mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + mockNOIDecisionConditionRepository.findOneOrFail.mockResolvedValue( new NoticeOfIntentDecisionCondition(), ); - mockApplicationDecisionConditionRepository.save.mockResolvedValue( + mockNOIDecisionConditionRepository.save.mockResolvedValue( new NoticeOfIntentDecisionCondition(), ); @@ -135,17 +123,15 @@ describe('ApplicationDecisionConditionService', () => { const result = await service.createOrUpdate(updateDtos, [], [], true); expect(result).toBeDefined(); - expect( - mockApplicationDecisionConditionRepository.findOneOrFail, - ).toBeCalledTimes(0); - expect(mockApplicationDecisionConditionRepository.save).toBeCalledTimes(1); + expect(mockNOIDecisionConditionRepository.findOneOrFail).toBeCalledTimes(0); + expect(mockNOIDecisionConditionRepository.save).toBeCalledTimes(1); }); it('should not persist entity if persist flag is false', async () => { - mockApplicationDecisionConditionRepository.findOneOrFail.mockResolvedValue( + mockNOIDecisionConditionRepository.findOneOrFail.mockResolvedValue( new NoticeOfIntentDecisionCondition(), ); - mockApplicationDecisionConditionRepository.save.mockResolvedValue( + mockNOIDecisionConditionRepository.save.mockResolvedValue( new NoticeOfIntentDecisionCondition(), ); @@ -154,9 +140,7 @@ describe('ApplicationDecisionConditionService', () => { const result = await service.createOrUpdate(updateDtos, [], [], false); expect(result).toBeDefined(); - expect( - mockApplicationDecisionConditionRepository.findOneOrFail, - ).toBeCalledTimes(0); - expect(mockApplicationDecisionConditionRepository.save).toBeCalledTimes(0); + expect(mockNOIDecisionConditionRepository.findOneOrFail).toBeCalledTimes(0); + expect(mockNOIDecisionConditionRepository.save).toBeCalledTimes(0); }); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts index 336e3a546c..82af45ac3f 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts @@ -24,7 +24,7 @@ import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2. describe('NoticeOfIntentDecisionV2Controller', () => { let controller: NoticeOfIntentDecisionV2Controller; let mockDecisionService: DeepMocked; - let mockApplicationService: DeepMocked; + let mockNoticeOfIntentService: DeepMocked; let mockCodeService: DeepMocked; let mockModificationService: DeepMocked; let mockEmailService: DeepMocked; @@ -34,7 +34,7 @@ describe('NoticeOfIntentDecisionV2Controller', () => { beforeEach(async () => { mockDecisionService = createMock(); - mockApplicationService = createMock(); + mockNoticeOfIntentService = createMock(); mockCodeService = createMock(); mockModificationService = createMock(); mockEmailService = createMock(); @@ -61,7 +61,7 @@ describe('NoticeOfIntentDecisionV2Controller', () => { }, { provide: NoticeOfIntentService, - useValue: mockApplicationService, + useValue: mockNoticeOfIntentService, }, { provide: CodeService, @@ -103,7 +103,7 @@ describe('NoticeOfIntentDecisionV2Controller', () => { expect(controller).toBeDefined(); }); - it('should get all for application', async () => { + it('should get all for notice of intent', async () => { mockDecisionService.getByAppFileNumber.mockResolvedValue([mockDecision]); const result = await controller.getByFileNumber('fake-number'); @@ -129,8 +129,8 @@ describe('NoticeOfIntentDecisionV2Controller', () => { expect(mockDecisionService.delete).toBeCalledWith('fake-uuid'); }); - it('should create the decision if application exists', async () => { - mockApplicationService.getByFileNumber.mockResolvedValue( + it('should create the decision if noi exists', async () => { + mockNoticeOfIntentService.getByFileNumber.mockResolvedValue( mockNoticeOfintent, ); mockDecisionService.create.mockResolvedValue(mockDecision); @@ -158,7 +158,7 @@ describe('NoticeOfIntentDecisionV2Controller', () => { }); it('should update the decision', async () => { - mockApplicationService.getFileNumber.mockResolvedValue('file-number'); + mockNoticeOfIntentService.getFileNumber.mockResolvedValue('file-number'); mockDecisionService.get.mockResolvedValue(new NoticeOfIntentDecision()); mockDecisionService.getByAppFileNumber.mockResolvedValue([ new NoticeOfIntentDecision(), diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts index 518dacc971..e3c9f9c127 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -44,6 +44,7 @@ import { NoticeOfIntentModificationService } from './notice-of-intent-modificati NoticeOfIntentSubmissionStatusModule, ], providers: [ + //These are in the same module, so be careful to import the correct one NoticeOfIntentDecisionV1Service, NoticeOfIntentDecisionV2Service, NoticeOfIntentDecisionComponentService, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692812627565-noi_decisions_v2.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692812627565-noi_decisions_v2.ts index 960568f5bb..f299e3f309 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1692812627565-noi_decisions_v2.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692812627565-noi_decisions_v2.ts @@ -7,21 +7,36 @@ export class noiDecisionsV21692812627565 implements MigrationInterface { await queryRunner.query( `CREATE TABLE "alcs"."notice_of_intent_decision_condition_type" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "label" character varying NOT NULL, "code" text NOT NULL, "description" text NOT NULL, CONSTRAINT "UQ_d42e55b4aef0fdc3c676b32a2a3" UNIQUE ("description"), CONSTRAINT "PK_30a33ede5fb646124dd846719cd" PRIMARY KEY ("code"))`, ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notice_of_intent_decision_condition_type" IS 'Decision Condition Types Code Table for Notice of Intents'`, + ); await queryRunner.query( `CREATE TABLE "alcs"."notice_of_intent_decision_condition" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "approval_dependant" boolean, "security_amount" numeric(12,2), "administrative_fee" numeric(12,2), "description" text, "completion_date" TIMESTAMP WITH TIME ZONE, "superseded_date" TIMESTAMP WITH TIME ZONE, "type_code" text, "decision_uuid" uuid NOT NULL, CONSTRAINT "PK_51e53b1c3920b8957bfe368c46a" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notice_of_intent_decision_condition"."completion_date" IS 'Condition Completion date'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_condition"."superseded_date" IS 'Condition Superseded date'`, ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notice_of_intent_decision_condition" IS 'Decision Conditions for Notice of Intents'`, + ); await queryRunner.query( `CREATE TABLE "alcs"."notice_of_intent_decision_component_type" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "label" character varying NOT NULL, "code" text NOT NULL, "description" text NOT NULL, CONSTRAINT "UQ_6c4783f61a9a7fa784deac579ff" UNIQUE ("description"), CONSTRAINT "PK_67c2c14d96bec32f1d9c8e2e9b0" PRIMARY KEY ("code"))`, ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notice_of_intent_decision_component_type" IS 'Decision Component Types Code Table for Notice of Intents'`, + ); await queryRunner.query( `CREATE TABLE "alcs"."notice_of_intent_decision_component" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "alr_area" numeric(12,2), "ag_cap" text, "ag_cap_source" text, "ag_cap_map" text, "ag_cap_consultant" text, "end_date" TIMESTAMP WITH TIME ZONE, "expiry_date" TIMESTAMP WITH TIME ZONE, "soil_fill_type_to_place" text, "soil_to_place_volume" numeric(12,2), "soil_to_place_area" numeric(12,2), "soil_to_place_maximum_depth" numeric(12,2), "soil_to_place_average_depth" numeric(12,2), "soil_type_removed" text, "soil_to_remove_volume" numeric(12,2), "soil_to_remove_area" numeric(12,2), "soil_to_remove_maximum_depth" numeric(12,2), "soil_to_remove_average_depth" numeric(12,2), "notice_of_intent_decision_component_type_code" text NOT NULL, "notice_of_intent_decision_uuid" uuid NOT NULL, CONSTRAINT "PK_cd1ed330456b906001d6b6288f6" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."alr_area" IS 'Area in hectares of ALR impacted by the decision component'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."ag_cap" IS 'Agricultural cap classification'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."ag_cap_source" IS 'Agricultural capability classification system used'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."ag_cap_map" IS 'Agricultural capability map sheet reference'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."ag_cap_consultant" IS 'Consultant who determined the agricultural capability'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."end_date" IS 'Components\` end date'; COMMENT ON COLUMN "alcs"."notice_of_intent_decision_component"."expiry_date" IS 'Components\` expiry date'`, ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notice_of_intent_decision_component" IS 'Decision Components for Notice of Intents'`, + ); await queryRunner.query( `CREATE UNIQUE INDEX "IDX_4014f28095a987bb6bc515aa2b" ON "alcs"."notice_of_intent_decision_component" ("notice_of_intent_decision_component_type_code", "notice_of_intent_decision_uuid") WHERE "audit_deleted_date_at" is null`, ); await queryRunner.query( `CREATE TABLE "alcs"."notice_of_intent_decision_condition_component" ("notice_of_intent_decision_condition_uuid" uuid NOT NULL, "notice_of_intent_decision_component_uuid" uuid NOT NULL, CONSTRAINT "PK_e91078d3e07b0f292333ff9d5d6" PRIMARY KEY ("notice_of_intent_decision_condition_uuid", "notice_of_intent_decision_component_uuid"))`, ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notice_of_intent_decision_condition_component" IS 'Tracks Conditions links Components'`, + ); await queryRunner.query( `CREATE INDEX "IDX_4f1ecd62bb990e102c6af22a2e" ON "alcs"."notice_of_intent_decision_condition_component" ("notice_of_intent_decision_condition_uuid") `, ); From df0e93dff5bae9666b76866187ccbecbe6bc96d9 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 24 Aug 2023 09:52:47 -0700 Subject: [PATCH 297/954] Add enum for outcomes with components --- .../decision-input/decision-input-v2.component.ts | 5 +++-- .../decision/decision-v2/decision-v2.component.html | 4 ++-- .../decision/decision-v2/decision-v2.component.ts | 3 +++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts index 192e631a31..cb5027a952 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -21,6 +21,7 @@ import { NoticeOfIntentModificationService } from '../../../../../services/notic import { ToastService } from '../../../../../services/toast/toast.service'; import { formatDateForApi } from '../../../../../shared/utils/api-date-formatter'; import { parseBooleanToString, parseStringToBoolean } from '../../../../../shared/utils/boolean-helper'; +import { OUTCOMES_WITH_COMPONENTS } from '../decision-v2.component'; import { ReleaseDialogComponent } from '../release-dialog/release-dialog.component'; import { DecisionComponentsComponent } from './decision-components/decision-components.component'; import { DecisionConditionsComponent } from './decision-conditions/decision-conditions.component'; @@ -250,7 +251,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { this.requireComponents = ['APPR', 'APPA'].includes(existingDecision.outcome.code); - if (['APPR', 'APPA', 'RESC'].includes(existingDecision.outcome.code)) { + if (OUTCOMES_WITH_COMPONENTS.includes(existingDecision.outcome.code)) { this.showComponents = true; } else { this.showComponents = false; @@ -434,7 +435,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { } onChangeDecisionOutcome(selectedOutcome: NoticeOfIntentDecisionOutcomeCodeDto) { - if (['APPR', 'APPA', 'RESC'].includes(selectedOutcome.code)) { + if (OUTCOMES_WITH_COMPONENTS.includes(selectedOutcome.code)) { if (this.form.controls.isSubjectToConditions.disabled) { this.showComponents = true; this.form.controls.isSubjectToConditions.enable(); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html index 7dbb975abb..481d7d9ba9 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html @@ -129,7 +129,7 @@

Documents

- +

Components

{{ component.noticeOfIntentDecisionComponentType?.label }}
- +

Conditions

diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts index f275aae97c..190c48fbd0 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.ts @@ -26,6 +26,8 @@ type LoadingDecision = NoticeOfIntentDecisionDto & { loading: boolean; }; +export const OUTCOMES_WITH_COMPONENTS = ['APPR', 'APPA', 'RESC']; + @Component({ selector: 'app-noi-decision-v2', templateUrl: './decision-v2.component.html', @@ -42,6 +44,7 @@ export class DecisionV2Component implements OnInit, OnDestroy { decisions: LoadingDecision[] = []; outcomes: NoticeOfIntentDecisionOutcomeCodeDto[] = []; isPaused = true; + OUTCOMES_WITH_COMPONENTS = OUTCOMES_WITH_COMPONENTS; modificationLabel = MODIFICATION_TYPE_LABEL; noticeOfIntent: NoticeOfIntentDto | undefined; From 06499130732e9a77ebda33a8c31a23c5dcce2f4c Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 24 Aug 2023 10:31:27 -0700 Subject: [PATCH 298/954] Decision Fixes * Update styling for POFO/PFRS/ROSO Input Components to match new designs * Update document table to have upload button on the right like other sections --- .../decision-documents.component.html | 27 +- .../pfrs-input/pfrs-input.component.html | 2 +- .../pfrs-input/pfrs-input.component.scss | 15 ++ .../pofo-input/pofo-input.component.html | 124 ++++----- .../roso-input/roso-input.component.html | 124 ++++----- .../decision-input-v2.component.html | 1 - .../decision-input-v2.component.scss | 25 +- .../decision-documents.component.html | 27 +- .../pfrs-input/pfrs-input.component.html | 250 +++++++++--------- .../pfrs-input/pfrs-input.component.scss | 15 ++ .../pofo-input/pofo-input.component.html | 124 ++++----- .../roso-input/roso-input.component.html | 125 ++++----- .../decision-input-v2.component.html | 1 - .../decision-input-v2.component.scss | 24 +- 14 files changed, 397 insertions(+), 487 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html index cb3638941a..351641d7a0 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html @@ -1,15 +1,20 @@
- - +
+

Documents

+
+ + +
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html index 5469971cd0..0235ef5514 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html @@ -50,7 +50,7 @@
-
+
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss index e69de29bb2..d9a9d9af0b 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss @@ -0,0 +1,15 @@ +.soil-table { + display: grid; + grid-template-columns: calc(20% - 10px) 40% 40%; + grid-row-gap: 28px; + padding: 10px; + column-gap: 10px; + + .full-width { + width: 100%; + } + + label { + font-weight: 700; + } +} diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html index 7f4a43207b..5f0138eda5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html @@ -38,77 +38,57 @@ > -
-
-
-
- -
-
- -
-
- - - m3 - -
-
- -
Note: 0.01 ha is 100m2
-
-
- - - ha - -
-
- -
-
- - - m - -
-
- -
-
- - - m - -
-
+
+ + + m3 + + + + + ha + + + + + m + + + + + m +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html index f175fb8b59..12da03f807 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html @@ -38,77 +38,57 @@ > -
-
-
-
- -
-
- -
-
- - - m3 - -
-
- -
Note: 0.01 ha is 100m2
-
-
- - - ha - -
-
- -
-
- - - m - -
-
- -
-
- - - m - -
-
+
+ + + m3 + + + + + ha + + + + + m + + + + + m +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html index 972f1f2ee3..e6be6ba00f 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.html @@ -211,7 +211,6 @@

Resolution

-

Documents

- - +
+

Documents

+
+ + +
+
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html index 5469971cd0..fa2213b901 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.html @@ -49,132 +49,130 @@ > -
-
-
-
- -
-
- -
-
- -
-
- - - m3 - -
-
- - - m3 - -
-
- -
Note: 0.01 ha is 100m2
-
-
- - - ha - -
-
- - - ha - -
-
- -
-
- - - m - -
-
- - - m - -
-
- -
-
- - - m - -
-
- - - m - -
+
+
+
+ +
+
+ +
+
+ +
+
+ + + m3 + +
+
+ + + m3 + +
+
+ +
Note: 0.01 ha is 100m2
+
+
+ + + ha + +
+
+ + + ha + +
+
+ +
+
+ + + m + +
+
+ + + m + +
+
+ +
+
+ + + m + +
+
+ + + m +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss index e69de29bb2..d9a9d9af0b 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pfrs-input/pfrs-input.component.scss @@ -0,0 +1,15 @@ +.soil-table { + display: grid; + grid-template-columns: calc(20% - 10px) 40% 40%; + grid-row-gap: 28px; + padding: 10px; + column-gap: 10px; + + .full-width { + width: 100%; + } + + label { + font-weight: 700; + } +} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html index 7f4a43207b..5f0138eda5 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/pofo-input/pofo-input.component.html @@ -38,77 +38,57 @@ > -
-
-
-
- -
-
- -
-
- - - m3 - -
-
- -
Note: 0.01 ha is 100m2
-
-
- - - ha - -
-
- -
-
- - - m - -
-
- -
-
- - - m - -
-
+
+ + + m3 + + + + + ha + + + + + m + + + + + m +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html index f175fb8b59..db43104f4f 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/roso-input/roso-input.component.html @@ -31,84 +31,63 @@ -
-
-
-
- -
-
- -
-
- - - m3 - -
-
- -
Note: 0.01 ha is 100m2
-
-
- - - ha - -
-
- -
-
- - - m - -
-
- -
-
- - - m - -
-
+
+ + + m3 + + + + + ha + + + + + m + + + + + m +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html index 17a889722c..06250cfd89 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html @@ -166,7 +166,6 @@

Resolution

-

Documents

Date: Thu, 24 Aug 2023 10:37:19 -0700 Subject: [PATCH 299/954] Send NOI Submission and Cancelled Emails (#908) * Create ParentType enum and update email template to set type label * Add SUBM noi template and generalize type label * Send email after noi portal submission - Resolve circular module import errors with forwardRef - Add new noi submission service method to getStatus for email template - Add noi method to email service and refactor * Send gov separate email for noi submission * Add noi cancelled template * Send noi cancelled status email in portal * Add noi decision released template * Rename email service application methods * Refactor and fix unit tests * Add new email unit test and wip resolving submission test * Clean up and pass all unit tests * Update enum from MR feedback --- ...cation-decision-meeting.controller.spec.ts | 13 +- ...application-decision-meeting.controller.ts | 6 +- .../application-decision-v1.module.ts | 2 +- .../application-decision-v2.module.ts | 2 +- ...application-decision-v2.controller.spec.ts | 17 +-- .../application-decision-v2.controller.ts | 10 +- .../application.controller.spec.ts | 13 +- .../application/application.controller.ts | 10 +- .../apps/alcs/src/alcs/board/board.module.ts | 2 +- .../card/card-subtask/card-subtask.dto.ts | 16 ++- .../alcs/src/alcs/home/home.controller.ts | 15 +- .../notice-of-intent-decision.module.ts | 2 +- ...ation-submission-review.controller.spec.ts | 46 +++--- ...pplication-submission-review.controller.ts | 20 ++- .../application-submission.controller.spec.ts | 53 ++++--- .../application-submission.controller.ts | 24 ++-- ...ce-of-intent-submission.controller.spec.ts | 87 +++++++++++- .../notice-of-intent-submission.controller.ts | 51 +++++++ .../notice-of-intent-submission.module.ts | 4 +- ...otice-of-intent-submission.service.spec.ts | 9 ++ .../notice-of-intent-submission.service.ts | 11 ++ .../alcs/src/providers/email/email.module.ts | 4 + .../src/providers/email/email.service.spec.ts | 67 ++++++++- .../alcs/src/providers/email/email.service.ts | 133 ++++++++++++++++-- .../application.template.ts} | 12 +- services/templates/emails/cancelled/index.ts | 2 + .../cancelled/notice-of-intent.template.ts | 44 ++++++ .../application.template.ts} | 8 +- .../emails/decision-released/index.ts | 2 + .../notice-of-intent.template.ts | 54 +++++++ .../emails/partials/header.template.ts | 2 +- .../emails/refused-to-forward.template.ts | 2 +- .../emails/returned-as-incomplete.template.ts | 2 +- ...nt.template.ts => application.template.ts} | 2 +- .../emails/submitted-to-alc/index.ts | 4 +- .../noi-applicant.template.ts | 105 ++++++++++++++ .../noi-government.template.ts | 44 ++++++ .../tur-applicant.template.ts | 2 +- .../tur-government.template.ts | 2 +- .../submitted-to-lfng/applicant.template.ts | 2 +- .../submitted-to-lfng/government.template.ts | 2 +- .../emails/under-review-by-lfng.template.ts | 2 +- .../templates/emails/wrong-lfng.template.ts | 2 +- 43 files changed, 767 insertions(+), 145 deletions(-) rename services/templates/emails/{cancelled.template.ts => cancelled/application.template.ts} (67%) create mode 100644 services/templates/emails/cancelled/index.ts create mode 100644 services/templates/emails/cancelled/notice-of-intent.template.ts rename services/templates/emails/{decision-released.template.ts => decision-released/application.template.ts} (88%) create mode 100644 services/templates/emails/decision-released/index.ts create mode 100644 services/templates/emails/decision-released/notice-of-intent.template.ts rename services/templates/emails/submitted-to-alc/{applicant.template.ts => application.template.ts} (99%) create mode 100644 services/templates/emails/submitted-to-alc/noi-applicant.template.ts create mode 100644 services/templates/emails/submitted-to-alc/noi-government.template.ts diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.controller.spec.ts index 20d890d6bb..3f02420d41 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.controller.spec.ts @@ -151,12 +151,12 @@ describe('ApplicationDecisionMeetingController', () => { mockMeetingService.getByAppFileNumber.mockResolvedValue([ new ApplicationDecisionMeeting(), ]); - mockEmailService.getSubmissionStatusEmailData.mockResolvedValue({ + mockEmailService.getApplicationEmailData.mockResolvedValue({ applicationSubmission: mockSubmission, primaryContact: mockOwner, submissionGovernment: mockGovernment, }); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const meetingToUpdate = { date: new Date(2022, 2, 2, 2, 2, 2, 2).valueOf(), @@ -165,12 +165,13 @@ describe('ApplicationDecisionMeetingController', () => { await controller.create(meetingToUpdate); - expect(mockEmailService.sendStatusEmail).toBeCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toBeCalledWith({ + expect(mockEmailService.sendApplicationStatusEmail).toBeCalledTimes(1); + expect(mockEmailService.sendApplicationStatusEmail).toBeCalledWith({ generateStatusHtml: generateREVAHtml, status: SUBMISSION_STATUS.IN_REVIEW_BY_ALC, applicationSubmission: mockSubmission, government: mockGovernment, + parentType: 'application', primaryContact: mockOwner, ccGovernment: true, }); @@ -182,7 +183,7 @@ describe('ApplicationDecisionMeetingController', () => { new ApplicationDecisionMeeting(), new ApplicationDecisionMeeting(), ]); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const meetingToUpdate = { date: new Date(2022, 2, 2, 2, 2, 2, 2).valueOf(), @@ -191,7 +192,7 @@ describe('ApplicationDecisionMeetingController', () => { await controller.create(meetingToUpdate); - expect(mockEmailService.sendStatusEmail).toBeCalledTimes(0); + expect(mockEmailService.sendApplicationStatusEmail).toBeCalledTimes(0); }); it('should update meeting', async () => { diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.controller.ts index 6c1a81b697..9cebc2dfe6 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.controller.ts @@ -33,6 +33,7 @@ import { formatIncomingDate } from '../../../../utils/incoming-date.formatter'; import { EmailService } from '../../../../providers/email/email.service'; import { generateREVAHtml } from '../../../../../../../templates/emails/under-review-by-alc.template'; import { SUBMISSION_STATUS } from '../../../application/application-submission-status/submission-status.dto'; +import { PARENT_TYPE } from '../../../card/card-subtask/card-subtask.dto'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('application-decision-meeting') @@ -117,16 +118,17 @@ export class ApplicationDecisionMeetingController { // Send status email for first review discussion if (meetings.length === 1) { const { applicationSubmission, primaryContact, submissionGovernment } = - await this.emailService.getSubmissionStatusEmailData( + await this.emailService.getApplicationEmailData( meeting.applicationFileNumber, ); if (primaryContact) { - await this.emailService.sendStatusEmail({ + await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateREVAHtml, status: SUBMISSION_STATUS.IN_REVIEW_BY_ALC, applicationSubmission, government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, ccGovernment: true, }); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-v1.module.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-v1.module.ts index b4156af08f..19088783fa 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-v1.module.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-v1.module.ts @@ -52,7 +52,7 @@ import { ApplicationDecisionV1Service } from './application-decision/application ApplicationSubmissionStatusType, ]), forwardRef(() => BoardModule), - ApplicationModule, + forwardRef(() => ApplicationModule), CardModule, DocumentModule, ApplicationSubmissionStatusModule, diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts index 80b657f0f4..3fccff1009 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision-v2.module.ts @@ -73,7 +73,7 @@ import { LinkedResolutionOutcomeType } from './application-decision/linked-resol ApplicationDecisionConditionComponentPlanNumber, ]), forwardRef(() => BoardModule), - ApplicationModule, + forwardRef(() => ApplicationModule), CardModule, DocumentModule, ApplicationDecisionV2Module, diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts index 321b5708b4..be4f628954 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts @@ -26,7 +26,7 @@ import { EmailService } from '../../../../providers/email/email.service'; import { ApplicationSubmission } from '../../../../portal/application-submission/application-submission.entity'; import { LocalGovernment } from '../../../local-government/local-government.entity'; import { ApplicationOwner } from '../../../../portal/application-submission/application-owner/application-owner.entity'; -import { generateALCDHtml } from '../../../../../../../templates/emails/decision-released.template'; +import { generateALCDApplicationHtml } from '../../../../../../../templates/emails/decision-released'; import { SUBMISSION_STATUS } from '../../../application/application-submission-status/submission-status.dto'; import { ApplicationDecision } from '../../application-decision.entity'; @@ -283,12 +283,12 @@ describe('ApplicationDecisionV2Controller', () => { new ApplicationDecision({ wasReleased: false }), ); mockDecisionService.update.mockResolvedValue(mockDecision); - mockEmailService.getSubmissionStatusEmailData.mockResolvedValue({ + mockEmailService.getApplicationEmailData.mockResolvedValue({ applicationSubmission: mockApplicationSubmission, primaryContact: mockOwner, submissionGovernment: mockGovernment, }); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const updates = { outcome: 'New Outcome', @@ -310,12 +310,13 @@ describe('ApplicationDecisionV2Controller', () => { undefined, ); - expect(mockEmailService.sendStatusEmail).toBeCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toBeCalledWith({ - generateStatusHtml: generateALCDHtml, + expect(mockEmailService.sendApplicationStatusEmail).toBeCalledTimes(1); + expect(mockEmailService.sendApplicationStatusEmail).toBeCalledWith({ + generateStatusHtml: generateALCDApplicationHtml, status: SUBMISSION_STATUS.ALC_DECISION, applicationSubmission: mockApplicationSubmission, government: mockGovernment, + parentType: 'application', primaryContact: mockOwner, ccGovernment: true, decisionReleaseMaskedDate: new Date().toLocaleDateString('en-CA', { @@ -332,7 +333,7 @@ describe('ApplicationDecisionV2Controller', () => { new ApplicationDecision({ wasReleased: true }), ); mockDecisionService.update.mockResolvedValue(mockDecision); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const updates = { outcome: 'New Outcome', @@ -353,6 +354,6 @@ describe('ApplicationDecisionV2Controller', () => { undefined, undefined, ); - expect(mockEmailService.sendStatusEmail).toBeCalledTimes(0); + expect(mockEmailService.sendApplicationStatusEmail).toBeCalledTimes(0); }); }); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts index db5695690c..b8aca8ebaa 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts @@ -43,7 +43,8 @@ import { ApplicationDecisionComponentTypeDto } from './component/application-dec import { LinkedResolutionOutcomeType } from './linked-resolution-outcome-type.entity'; import { EmailService } from '../../../../providers/email/email.service'; import { SUBMISSION_STATUS } from '../../../application/application-submission-status/submission-status.dto'; -import { generateALCDHtml } from '../../../../../../../templates/emails/decision-released.template'; +import { generateALCDApplicationHtml } from '../../../../../../../templates/emails/decision-released'; +import { PARENT_TYPE } from '../../../card/card-subtask/card-subtask.dto'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('application-decision') @@ -293,7 +294,7 @@ export class ApplicationDecisionV2Controller { ); const { applicationSubmission, primaryContact, submissionGovernment } = - await this.emailService.getSubmissionStatusEmailData(fileNumber); + await this.emailService.getApplicationEmailData(fileNumber); const date = decision.date ? new Date(decision.date) : new Date(); @@ -305,11 +306,12 @@ export class ApplicationDecisionV2Controller { }; if (primaryContact) { - await this.emailService.sendStatusEmail({ - generateStatusHtml: generateALCDHtml, + await this.emailService.sendApplicationStatusEmail({ + generateStatusHtml: generateALCDApplicationHtml, status: SUBMISSION_STATUS.ALC_DECISION, applicationSubmission, government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, ccGovernment: true, decisionReleaseMaskedDate: date.toLocaleDateString('en-CA', options), diff --git a/services/apps/alcs/src/alcs/application/application.controller.spec.ts b/services/apps/alcs/src/alcs/application/application.controller.spec.ts index 091b5fcfea..f091fd4ec8 100644 --- a/services/apps/alcs/src/alcs/application/application.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application/application.controller.spec.ts @@ -25,7 +25,7 @@ import { EmailService } from '../../providers/email/email.service'; import { ApplicationSubmission } from '../../portal/application-submission/application-submission.entity'; import { ApplicationOwner } from '../../portal/application-submission/application-owner/application-owner.entity'; import { LocalGovernment } from '../local-government/local-government.entity'; -import { generateCANCHtml } from '../../../../../templates/emails/cancelled.template'; +import { generateCANCApplicationHtml } from '../../../../../templates/emails/cancelled'; import { SUBMISSION_STATUS } from './application-submission-status/submission-status.dto'; describe('ApplicationController', () => { @@ -391,23 +391,24 @@ describe('ApplicationController', () => { localGovernmentUuid, }); - emailService.getSubmissionStatusEmailData.mockResolvedValue({ + emailService.getApplicationEmailData.mockResolvedValue({ applicationSubmission: mockApplicationSubmission, primaryContact: mockOwner, submissionGovernment: mockGovernment, }); - emailService.sendStatusEmail.mockResolvedValue(); + emailService.sendApplicationStatusEmail.mockResolvedValue(); applicationService.cancel.mockResolvedValue(); await controller.cancel(mockApplicationEntity.uuid); expect(applicationService.cancel).toBeCalledTimes(1); - expect(emailService.sendStatusEmail).toBeCalledTimes(1); - expect(emailService.sendStatusEmail).toBeCalledWith({ - generateStatusHtml: generateCANCHtml, + expect(emailService.sendApplicationStatusEmail).toBeCalledTimes(1); + expect(emailService.sendApplicationStatusEmail).toBeCalledWith({ + generateStatusHtml: generateCANCApplicationHtml, status: SUBMISSION_STATUS.CANCELLED, applicationSubmission: mockApplicationSubmission, government: mockGovernment, + parentType: 'application', primaryContact: mockOwner, ccGovernment: true, }); diff --git a/services/apps/alcs/src/alcs/application/application.controller.ts b/services/apps/alcs/src/alcs/application/application.controller.ts index 02fb5ad4f4..c3a11ee20e 100644 --- a/services/apps/alcs/src/alcs/application/application.controller.ts +++ b/services/apps/alcs/src/alcs/application/application.controller.ts @@ -30,8 +30,9 @@ import { } from './application.dto'; import { ApplicationService } from './application.service'; import { EmailService } from '../../providers/email/email.service'; -import { generateCANCHtml } from '../../../../../templates/emails/cancelled.template'; +import { generateCANCApplicationHtml } from '../../../../../templates/emails/cancelled'; import { SUBMISSION_STATUS } from '../application/application-submission-status/submission-status.dto'; +import { PARENT_TYPE } from '../card/card-subtask/card-subtask.dto'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('application') @@ -127,14 +128,15 @@ export class ApplicationController { @UserRoles(...ROLES_ALLOWED_APPLICATIONS) async cancel(@Param('fileNumber') fileNumber): Promise { const { applicationSubmission, primaryContact, submissionGovernment } = - await this.emailService.getSubmissionStatusEmailData(fileNumber); + await this.emailService.getApplicationEmailData(fileNumber); if (primaryContact) { - await this.emailService.sendStatusEmail({ - generateStatusHtml: generateCANCHtml, + await this.emailService.sendApplicationStatusEmail({ + generateStatusHtml: generateCANCApplicationHtml, status: SUBMISSION_STATUS.CANCELLED, applicationSubmission, government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, ccGovernment: !!submissionGovernment, }); diff --git a/services/apps/alcs/src/alcs/board/board.module.ts b/services/apps/alcs/src/alcs/board/board.module.ts index 3856999d95..7f5362b6f2 100644 --- a/services/apps/alcs/src/alcs/board/board.module.ts +++ b/services/apps/alcs/src/alcs/board/board.module.ts @@ -16,7 +16,7 @@ import { BoardService } from './board.service'; @Module({ imports: [ TypeOrmModule.forFeature([Board, BoardStatus]), - ApplicationModule, + forwardRef(() => ApplicationModule), CardModule, forwardRef(() => ApplicationDecisionModule), PlanningReviewModule, diff --git a/services/apps/alcs/src/alcs/card/card-subtask/card-subtask.dto.ts b/services/apps/alcs/src/alcs/card/card-subtask/card-subtask.dto.ts index 6ddf1c218b..a8464d5739 100644 --- a/services/apps/alcs/src/alcs/card/card-subtask/card-subtask.dto.ts +++ b/services/apps/alcs/src/alcs/card/card-subtask/card-subtask.dto.ts @@ -4,6 +4,14 @@ import { ApplicationTypeDto } from '../../code/application-code/application-type import { AssigneeDto } from '../../../user/user.dto'; import { CardDto } from '../card.dto'; +export enum PARENT_TYPE { + APPLICATION = 'application', + RECONSIDERATION = 'reconsideration', + COVENANT = 'covenant', + MODIFICATION = 'modification', + PLANNING_REVIEW = 'planning-review', + NOTICE_OF_INTENT = 'notice-of-intent', +} export class UpdateCardSubtaskDto { @AutoMap() @IsUUID() @@ -50,13 +58,7 @@ export class HomepageSubtaskDTO extends CardSubtaskDto { card: CardDto; title: string; appType?: ApplicationTypeDto; - parentType: - | 'application' - | 'reconsideration' - | 'covenant' - | 'modification' - | 'planning-review' - | 'notice-of-intent'; + parentType: PARENT_TYPE; activeDays?: number; paused: boolean; } diff --git a/services/apps/alcs/src/alcs/home/home.controller.ts b/services/apps/alcs/src/alcs/home/home.controller.ts index aaffa1b0b4..d694c28427 100644 --- a/services/apps/alcs/src/alcs/home/home.controller.ts +++ b/services/apps/alcs/src/alcs/home/home.controller.ts @@ -23,6 +23,7 @@ import { CARD_STATUS } from '../card/card-status/card-status.entity'; import { CARD_SUBTASK_TYPE, HomepageSubtaskDTO, + PARENT_TYPE, } from '../card/card-subtask/card-subtask.dto'; import { CardDto } from '../card/card.dto'; import { Card } from '../card/card.entity'; @@ -219,7 +220,7 @@ export class HomeController { paused: false, title: `${recon.application.fileNumber} (${recon.application.applicant})`, appType: recon.application.type, - parentType: 'reconsideration', + parentType: PARENT_TYPE.RECONSIDERATION, }); } } @@ -251,7 +252,7 @@ export class HomeController { paused: appPausedMap.get(application.uuid) || false, title: `${application.fileNumber} (${application.applicant})`, appType: application.type, - parentType: 'application', + parentType: PARENT_TYPE.APPLICATION, }); } } @@ -271,7 +272,7 @@ export class HomeController { completedAt: subtask.completedAt?.getTime(), paused: false, title: `${planningReview.fileNumber} (${planningReview.type})`, - parentType: 'planning-review', + parentType: PARENT_TYPE.PLANNING_REVIEW, }); } } @@ -291,7 +292,7 @@ export class HomeController { completedAt: subtask.completedAt?.getTime(), paused: false, title: `${covenant.fileNumber} (${covenant.applicant})`, - parentType: 'covenant', + parentType: PARENT_TYPE.COVENANT, }); } } @@ -312,7 +313,7 @@ export class HomeController { completedAt: subtask.completedAt?.getTime(), paused: false, title: `${noticeOfIntent.fileNumber} (${noticeOfIntent.applicant})`, - parentType: 'notice-of-intent', + parentType: PARENT_TYPE.NOTICE_OF_INTENT, }); } } @@ -337,7 +338,7 @@ export class HomeController { paused: false, title: `${modification.application.fileNumber} (${modification.application.applicant})`, appType: modification.application.type, - parentType: 'modification', + parentType: PARENT_TYPE.MODIFICATION, }); } } @@ -362,7 +363,7 @@ export class HomeController { completedAt: subtask.completedAt?.getTime(), paused: false, title: `${modification.noticeOfIntent.fileNumber} (${modification.noticeOfIntent.applicant})`, - parentType: 'modification', + parentType: PARENT_TYPE.MODIFICATION, }); } } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts index e3c9f9c127..e1cb3f7c86 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -40,7 +40,7 @@ import { NoticeOfIntentModificationService } from './notice-of-intent-modificati forwardRef(() => BoardModule), CardModule, DocumentModule, - NoticeOfIntentModule, + forwardRef(() => NoticeOfIntentModule), NoticeOfIntentSubmissionStatusModule, ], providers: [ diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts index 8e3bfd8998..3feedf7fec 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.spec.ts @@ -33,7 +33,7 @@ import { ApplicationSubmissionReviewController } from './application-submission- import { ApplicationSubmissionReviewDto } from './application-submission-review.dto'; import { ApplicationSubmissionReview } from './application-submission-review.entity'; import { ApplicationSubmissionReviewService } from './application-submission-review.service'; -import { generateSUBMApplicantHtml } from '../../../../../templates/emails/submitted-to-alc'; +import { generateSUBMApplicationHtml } from '../../../../../templates/emails/submitted-to-alc'; import { generateRFFGHtml } from '../../../../../templates/emails/refused-to-forward.template'; import { generateINCMHtml } from '../../../../../templates/emails/returned-as-incomplete.template'; import { generateWRNGHtml } from '../../../../../templates/emails/wrong-lfng.template'; @@ -217,7 +217,7 @@ describe('ApplicationSubmissionReviewController', () => { it('should send an email through the service when creating with a valid primary contact', async () => { mockLGService.getByGuid.mockResolvedValue(mockLG); mockAppReviewService.startReview.mockResolvedValue(applicationReview); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const user = new User({ bceidBusinessGuid: 'id', @@ -251,7 +251,9 @@ describe('ApplicationSubmissionReviewController', () => { }, }); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledTimes(1); + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledTimes( + 1, + ); expect(mockLGService.getByGuid).toHaveBeenCalledTimes(2); expect(mockAppReviewService.startReview).toHaveBeenCalledTimes(1); expect( @@ -340,7 +342,7 @@ describe('ApplicationSubmissionReviewController', () => { ); mockAppReviewService.getByFileNumber.mockResolvedValue(applicationReview); mockAppDocService.list.mockResolvedValue([]); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); mockAppReviewService.verifyComplete.mockReturnValue({ ...applicationReview, @@ -384,12 +386,15 @@ describe('ApplicationSubmissionReviewController', () => { expect(mockAppSubmissionService.updateStatus.mock.calls[0][1]).toEqual( SUBMISSION_STATUS.SUBMITTED_TO_ALC, ); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledWith({ - generateStatusHtml: generateSUBMApplicantHtml, + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledTimes( + 1, + ); + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledWith({ + generateStatusHtml: generateSUBMApplicationHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_ALC, applicationSubmission: mockSubmission, government: mockLG, + parentType: 'application', primaryContact: mockOwner, ccGovernment: true, }); @@ -432,7 +437,7 @@ describe('ApplicationSubmissionReviewController', () => { errors: [], }); mockAppSubmissionService.updateStatus.mockResolvedValue({} as any); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); mockAppDocService.list.mockResolvedValue([]); await controller.finish(fileNumber, { @@ -454,12 +459,15 @@ describe('ApplicationSubmissionReviewController', () => { expect(mockAppSubmissionService.updateStatus.mock.calls[0][1]).toEqual( SUBMISSION_STATUS.REFUSED_TO_FORWARD_LG, ); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledWith({ + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledTimes( + 1, + ); + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledWith({ generateStatusHtml: generateRFFGHtml, status: SUBMISSION_STATUS.REFUSED_TO_FORWARD_LG, applicationSubmission: mockSubmission, government: mockLG, + parentType: 'application', primaryContact: mockOwner, ccGovernment: true, }); @@ -490,7 +498,7 @@ describe('ApplicationSubmissionReviewController', () => { mockAppSubmissionService.update.mockResolvedValue( new ApplicationSubmission(), ); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const documents = [ new ApplicationDocument({ @@ -551,12 +559,15 @@ describe('ApplicationSubmissionReviewController', () => { expect( mockApplicationSubmissionStatusService.setStatusDate, ).toHaveBeenCalledTimes(2); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledWith({ + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledTimes( + 1, + ); + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledWith({ generateStatusHtml: generateINCMHtml, status: SUBMISSION_STATUS.INCOMPLETE, applicationSubmission: mockSubmission, government: mockLG, + parentType: 'application', primaryContact: mockOwner, ccGovernment: true, }); @@ -587,7 +598,7 @@ describe('ApplicationSubmissionReviewController', () => { mockAppSubmissionService.update.mockResolvedValue( new ApplicationSubmission(), ); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const documents = [ new ApplicationDocument({ @@ -624,12 +635,15 @@ describe('ApplicationSubmissionReviewController', () => { }, ); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledWith({ + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledTimes( + 1, + ); + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledWith({ generateStatusHtml: generateWRNGHtml, status: SUBMISSION_STATUS.WRONG_GOV, applicationSubmission: mockSubmission, government: mockLG, + parentType: 'application', primaryContact: mockOwner, }); }); diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index 41674d6f06..777229b76c 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -34,7 +34,8 @@ import { import { ApplicationSubmissionReviewService } from './application-submission-review.service'; import { generateINCMHtml } from '../../../../../templates/emails/returned-as-incomplete.template'; import { generateRFFGHtml } from '../../../../../templates/emails/refused-to-forward.template'; -import { generateSUBMApplicantHtml } from '../../../../../templates/emails/submitted-to-alc'; +import { generateSUBMApplicationHtml } from '../../../../../templates/emails/submitted-to-alc'; +import { PARENT_TYPE } from '../../alcs/card/card-subtask/card-subtask.dto'; @Controller('application-review') @UseGuards(PortalAuthGuard) @@ -188,11 +189,12 @@ export class ApplicationSubmissionReviewController { ); if (primaryContact) { - await this.emailService.sendStatusEmail({ + await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateREVGHtml, status: SUBMISSION_STATUS.IN_REVIEW_BY_LG, applicationSubmission, government: userLocalGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, }); } @@ -287,11 +289,12 @@ export class ApplicationSubmissionReviewController { ); if (primaryContact) { - await this.emailService.sendStatusEmail({ - generateStatusHtml: generateSUBMApplicantHtml, + await this.emailService.sendApplicationStatusEmail({ + generateStatusHtml: generateSUBMApplicationHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_ALC, applicationSubmission: application, government: userLocalGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, ccGovernment: true, }); @@ -303,11 +306,12 @@ export class ApplicationSubmissionReviewController { ); if (primaryContact) { - await this.emailService.sendStatusEmail({ + await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateRFFGHtml, status: SUBMISSION_STATUS.REFUSED_TO_FORWARD_LG, applicationSubmission: application, government: userLocalGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, ccGovernment: true, }); @@ -388,21 +392,23 @@ export class ApplicationSubmissionReviewController { if (primaryContact) { if (returnDto.reasonForReturn === 'wrongGovernment') { - await this.emailService.sendStatusEmail({ + await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateWRNGHtml, status: SUBMISSION_STATUS.WRONG_GOV, applicationSubmission, government: userLocalGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, }); } if (returnDto.reasonForReturn === 'incomplete') { - await this.emailService.sendStatusEmail({ + await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateINCMHtml, status: SUBMISSION_STATUS.INCOMPLETE, applicationSubmission, government: userLocalGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, ccGovernment: true, }); diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts index c199026416..67cff64d3a 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.spec.ts @@ -28,7 +28,7 @@ import { ApplicationSubmission } from './application-submission.entity'; import { ApplicationSubmissionService } from './application-submission.service'; import { EmailService } from '../../providers/email/email.service'; import { ApplicationOwner } from './application-owner/application-owner.entity'; -import { generateCANCHtml } from '../../../../../templates/emails/cancelled.template'; +import { generateCANCApplicationHtml } from '../../../../../templates/emails/cancelled'; import { generateSUBGTurApplicantHtml, generateSUBGTurGovernmentHtml, @@ -182,13 +182,13 @@ describe('ApplicationSubmissionController', () => { mockAppSubmissionService.cancel.mockResolvedValue(); const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); - mockEmailService.getSubmissionStatusEmailData.mockResolvedValue({ + mockEmailService.getApplicationEmailData.mockResolvedValue({ applicationSubmission: mockApplication, primaryContact: mockOwner, submissionGovernment: mockGovernment, }); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const application = await controller.cancel('file-id', { user: { @@ -205,12 +205,15 @@ describe('ApplicationSubmissionController', () => { 'file-id', new User(), ); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledWith({ - generateStatusHtml: generateCANCHtml, + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledTimes( + 1, + ); + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledWith({ + generateStatusHtml: generateCANCApplicationHtml, status: SUBMISSION_STATUS.CANCELLED, applicationSubmission: mockApplication, government: mockGovernment, + parentType: 'application', primaryContact: mockOwner, ccGovernment: true, }); @@ -394,13 +397,13 @@ describe('ApplicationSubmissionController', () => { }); const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); - mockEmailService.getSubmissionStatusEmailData.mockResolvedValue({ + mockEmailService.getApplicationEmailData.mockResolvedValue({ applicationSubmission: mockApplicationSubmission, primaryContact: mockOwner, submissionGovernment: mockGovernment, }); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); await controller.submitAsApplicant(mockFileId, { user: { @@ -413,19 +416,23 @@ describe('ApplicationSubmissionController', () => { ); expect(mockAppSubmissionService.submitToAlcs).toHaveBeenCalledTimes(1); expect(mockAppSubmissionService.updateStatus).toHaveBeenCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledTimes(2); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledWith({ + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledTimes( + 2, + ); + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledWith({ generateStatusHtml: generateSUBGTurApplicantHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_ALC, applicationSubmission: mockApplicationSubmission, government: mockGovernment, + parentType: 'application', primaryContact: mockOwner, }); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledWith({ + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledWith({ generateStatusHtml: generateSUBGTurGovernmentHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_ALC, applicationSubmission: mockApplicationSubmission, government: mockGovernment, + parentType: 'application', }); }); @@ -449,10 +456,10 @@ describe('ApplicationSubmissionController', () => { new ApplicationSubmission() as ValidatedApplicationSubmission, errors: [], }); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); - mockEmailService.getSubmissionStatusEmailData.mockResolvedValue({ + mockEmailService.getApplicationEmailData.mockResolvedValue({ applicationSubmission: mockApplicationSubmission, primaryContact: mockOwner, submissionGovernment: mockGovernment, @@ -468,19 +475,23 @@ describe('ApplicationSubmissionController', () => { 1, ); expect(mockAppSubmissionService.submitToLg).toHaveBeenCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledTimes(2); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledWith({ + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledTimes( + 2, + ); + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledWith({ generateStatusHtml: generateSUBGApplicantHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_LG, applicationSubmission: mockApplicationSubmission, government: mockGovernment, + parentType: 'application', primaryContact: mockOwner, }); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledWith({ + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledWith({ generateStatusHtml: generateSUBGGovernmentHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_LG, applicationSubmission: mockApplicationSubmission, government: mockGovernment, + parentType: 'application', }); }); @@ -511,10 +522,10 @@ describe('ApplicationSubmissionController', () => { new ApplicationSubmission() as ValidatedApplicationSubmission, errors: [], }); - mockEmailService.sendStatusEmail.mockResolvedValue(); + mockEmailService.sendApplicationStatusEmail.mockResolvedValue(); const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); - mockEmailService.getSubmissionStatusEmailData.mockResolvedValue({ + mockEmailService.getApplicationEmailData.mockResolvedValue({ applicationSubmission: mockApplicationSubmission, primaryContact: mockOwner, submissionGovernment: mockGovernment, @@ -530,7 +541,9 @@ describe('ApplicationSubmissionController', () => { 1, ); expect(mockAppSubmissionService.submitToLg).toHaveBeenCalledTimes(1); - expect(mockEmailService.sendStatusEmail).toHaveBeenCalledTimes(0); + expect(mockEmailService.sendApplicationStatusEmail).toHaveBeenCalledTimes( + 0, + ); }); it('should throw an exception if application fails validation', async () => { @@ -542,7 +555,7 @@ describe('ApplicationSubmissionController', () => { mockAppSubmissionService.verifyAccessByUuid.mockResolvedValue( mockApplicationSubmission, ); - mockEmailService.getSubmissionStatusEmailData.mockResolvedValue({ + mockEmailService.getApplicationEmailData.mockResolvedValue({ applicationSubmission: mockApplicationSubmission, primaryContact: new ApplicationOwner(), submissionGovernment: new LocalGovernment(), diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts index 449e80bf1f..d495f331e1 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts @@ -31,7 +31,8 @@ import { generateSUBGTurApplicantHtml, generateSUBGTurGovernmentHtml, } from '../../../../../templates/emails/submitted-to-alc'; -import { generateCANCHtml } from '../../../../../templates/emails/cancelled.template'; +import { generateCANCApplicationHtml } from '../../../../../templates/emails/cancelled'; +import { PARENT_TYPE } from '../../alcs/card/card-subtask/card-subtask.dto'; @Controller('application-submission') @UseGuards(PortalAuthGuard) @@ -200,17 +201,18 @@ export class ApplicationSubmissionController { } const { primaryContact, submissionGovernment } = - await this.emailService.getSubmissionStatusEmailData( + await this.emailService.getApplicationEmailData( application.fileNumber, application, ); if (primaryContact) { - await this.emailService.sendStatusEmail({ - generateStatusHtml: generateCANCHtml, + await this.emailService.sendApplicationStatusEmail({ + generateStatusHtml: generateCANCApplicationHtml, status: SUBMISSION_STATUS.CANCELLED, applicationSubmission: application, government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, ccGovernment: !!submissionGovernment, }); @@ -237,7 +239,7 @@ export class ApplicationSubmissionController { ); const { primaryContact, submissionGovernment } = - await this.emailService.getSubmissionStatusEmailData( + await this.emailService.getApplicationEmailData( applicationSubmission.fileNumber, applicationSubmission, ); @@ -251,21 +253,23 @@ export class ApplicationSubmissionController { ); if (primaryContact) { - await this.emailService.sendStatusEmail({ + await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateSUBGTurApplicantHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_ALC, applicationSubmission, government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, }); } if (submissionGovernment) { - await this.emailService.sendStatusEmail({ + await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateSUBGTurGovernmentHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_ALC, applicationSubmission, government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, }); } @@ -289,21 +293,23 @@ export class ApplicationSubmissionController { // Send status emails for first time submissions if (!wasSubmittedToLfng) { if (primaryContact) { - await this.emailService.sendStatusEmail({ + await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateSUBGApplicantHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_LG, applicationSubmission, government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, primaryContact, }); } if (submissionGovernment) { - await this.emailService.sendStatusEmail({ + await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateSUBGGovernmentHtml, status: SUBMISSION_STATUS.SUBMITTED_TO_LG, applicationSubmission, government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, }); } } diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts index 07535d9139..56d726b91a 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.spec.ts @@ -25,6 +25,13 @@ import { } from './notice-of-intent-submission.dto'; import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; +import { EmailService } from '../../providers/email/email.service'; +import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; +import { generateCANCNoticeOfIntentHtml } from '../../../../../templates/emails/cancelled'; +import { + generateSUBMNoiApplicantHtml, + generateSUBMNoiGovernmentHtml, +} from '../../../../../templates/emails/submitted-to-alc'; describe('NoticeOfIntentSubmissionController', () => { let controller: NoticeOfIntentSubmissionController; @@ -32,7 +39,9 @@ describe('NoticeOfIntentSubmissionController', () => { let mockDocumentService: DeepMocked; let mockLgService: DeepMocked; let mockNoiValidatorService: DeepMocked; + let mockEmailService: DeepMocked; + const primaryContactOwnerUuid = 'primary-contact'; const localGovernmentUuid = 'local-government'; const applicant = 'fake-applicant'; const bceidBusinessGuid = 'business-guid'; @@ -42,6 +51,7 @@ describe('NoticeOfIntentSubmissionController', () => { mockDocumentService = createMock(); mockLgService = createMock(); mockNoiValidatorService = createMock(); + mockEmailService = createMock(); const module: TestingModule = await Test.createTestingModule({ controllers: [NoticeOfIntentSubmissionController], @@ -63,6 +73,10 @@ describe('NoticeOfIntentSubmissionController', () => { provide: NoticeOfIntentSubmissionValidatorService, useValue: mockNoiValidatorService, }, + { + provide: EmailService, + useValue: mockEmailService, + }, { provide: ClsService, useValue: {}, @@ -123,11 +137,19 @@ describe('NoticeOfIntentSubmissionController', () => { expect(mockNoiSubmissionService.getAllByUser).toHaveBeenCalledTimes(1); }); - it('should call out to service when cancelling an notice of intent', async () => { + it('should call out to service when cancelling a notice of intent', async () => { + const mockOwner = new NoticeOfIntentOwner({ + uuid: primaryContactOwnerUuid, + }); + const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); + const mockApplication = new NoticeOfIntentSubmission({ status: new NoticeOfIntentSubmissionToSubmissionStatus({ statusTypeCode: NOI_SUBMISSION_STATUS.IN_PROGRESS, }), + owners: [mockOwner], + primaryContactOwnerUuid, + localGovernmentUuid, }); mockNoiSubmissionService.mapToDTOs.mockResolvedValue([ @@ -137,6 +159,11 @@ describe('NoticeOfIntentSubmissionController', () => { mockNoiSubmissionService.cancel.mockResolvedValue( new NoticeOfIntentSubmissionToSubmissionStatus(), ); + mockEmailService.getNoticeOfIntentEmailData.mockResolvedValue({ + primaryContact: mockOwner, + submissionGovernment: mockGovernment, + }); + mockEmailService.sendNoticeOfIntentStatusEmail.mockResolvedValue(); const noticeOfIntentSubmission = await controller.cancel('file-id', { user: { @@ -151,6 +178,20 @@ describe('NoticeOfIntentSubmissionController', () => { 'file-id', new User(), ); + expect( + mockEmailService.sendNoticeOfIntentStatusEmail, + ).toHaveBeenCalledTimes(1); + expect(mockEmailService.sendNoticeOfIntentStatusEmail).toHaveBeenCalledWith( + { + generateStatusHtml: generateCANCNoticeOfIntentHtml, + status: NOI_SUBMISSION_STATUS.CANCELLED, + noticeOfIntentSubmission: mockApplication, + government: mockGovernment, + parentType: 'application', + primaryContact: mockOwner, + ccGovernment: true, + }, + ); }); it('should throw an exception when trying to cancel a notice of intent that is not in progress', async () => { @@ -313,20 +354,34 @@ describe('NoticeOfIntentSubmissionController', () => { it('should call out to service on submitAlcs', async () => { const mockFileId = 'file-id'; + const mockOwner = new NoticeOfIntentOwner({ + uuid: primaryContactOwnerUuid, + }); + const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); + const mockSubmission = new NoticeOfIntentSubmission({ + fileNumber: mockFileId, + owners: [mockOwner], + primaryContactOwnerUuid, + localGovernmentUuid, + }); + mockNoiSubmissionService.submitToAlcs.mockResolvedValue( new NoticeOfIntent(), ); - mockNoiSubmissionService.getByUuid.mockResolvedValue( - new NoticeOfIntentSubmission(), - ); + mockNoiSubmissionService.getByUuid.mockResolvedValue(mockSubmission); mockNoiValidatorService.validateSubmission.mockResolvedValue({ noticeOfIntentSubmission: - new NoticeOfIntentSubmission() as ValidatedNoticeOfIntentSubmission, + mockSubmission as ValidatedNoticeOfIntentSubmission, errors: [], }); mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( {} as NoticeOfIntentSubmissionDetailedDto, ); + mockEmailService.getNoticeOfIntentEmailData.mockResolvedValue({ + primaryContact: mockOwner, + submissionGovernment: mockGovernment, + }); + mockEmailService.sendNoticeOfIntentStatusEmail.mockResolvedValue(); await controller.submitAsApplicant(mockFileId, { user: { @@ -338,5 +393,27 @@ describe('NoticeOfIntentSubmissionController', () => { expect(mockNoiSubmissionService.submitToAlcs).toHaveBeenCalledTimes(1); expect(mockNoiValidatorService.validateSubmission).toHaveBeenCalledTimes(1); expect(mockNoiSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(1); + expect( + mockEmailService.sendNoticeOfIntentStatusEmail, + ).toHaveBeenCalledTimes(2); + expect(mockEmailService.sendNoticeOfIntentStatusEmail).toHaveBeenCalledWith( + { + generateStatusHtml: generateSUBMNoiApplicantHtml, + status: NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, + noticeOfIntentSubmission: mockSubmission, + government: mockGovernment, + parentType: 'notice-of-intent', + primaryContact: mockOwner, + }, + ); + expect(mockEmailService.sendNoticeOfIntentStatusEmail).toHaveBeenCalledWith( + { + generateStatusHtml: generateSUBMNoiGovernmentHtml, + status: NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, + noticeOfIntentSubmission: mockSubmission, + government: mockGovernment, + parentType: 'notice-of-intent', + }, + ); }); }); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts index 17cb2a859c..84d3b110d0 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.controller.ts @@ -20,6 +20,13 @@ import { NoticeOfIntentSubmissionUpdateDto, } from './notice-of-intent-submission.dto'; import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; +import { EmailService } from '../../providers/email/email.service'; +import { + generateSUBMNoiApplicantHtml, + generateSUBMNoiGovernmentHtml, +} from '../../../../../templates/emails/submitted-to-alc'; +import { PARENT_TYPE } from '../../alcs/card/card-subtask/card-subtask.dto'; +import { generateCANCNoticeOfIntentHtml } from '../../../../../templates/emails/cancelled'; @Controller('notice-of-intent-submission') @UseGuards(PortalAuthGuard) @@ -29,6 +36,7 @@ export class NoticeOfIntentSubmissionController { constructor( private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, private noticeOfIntentValidatorService: NoticeOfIntentSubmissionValidatorService, + private emailService: EmailService, ) {} @Get() @@ -136,6 +144,23 @@ export class NoticeOfIntentSubmissionController { ); } + const { primaryContact, submissionGovernment } = + await this.emailService.getNoticeOfIntentEmailData( + noticeOfIntentSubmission, + ); + + if (primaryContact) { + await this.emailService.sendNoticeOfIntentStatusEmail({ + generateStatusHtml: generateCANCNoticeOfIntentHtml, + status: NOI_SUBMISSION_STATUS.CANCELLED, + noticeOfIntentSubmission, + government: submissionGovernment, + parentType: PARENT_TYPE.APPLICATION, + primaryContact, + ccGovernment: !!submissionGovernment, + }); + } + await this.noticeOfIntentSubmissionService.cancel(noticeOfIntentSubmission); return { @@ -164,6 +189,32 @@ export class NoticeOfIntentSubmissionController { validatedApplicationSubmission, ); + const { primaryContact, submissionGovernment } = + await this.emailService.getNoticeOfIntentEmailData( + noticeOfIntentSubmission, + ); + + if (primaryContact) { + await this.emailService.sendNoticeOfIntentStatusEmail({ + generateStatusHtml: generateSUBMNoiApplicantHtml, + status: NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, + noticeOfIntentSubmission, + government: submissionGovernment, + parentType: PARENT_TYPE.NOTICE_OF_INTENT, + primaryContact, + }); + } + + if (submissionGovernment) { + await this.emailService.sendNoticeOfIntentStatusEmail({ + generateStatusHtml: generateSUBMNoiGovernmentHtml, + status: NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, + noticeOfIntentSubmission, + government: submissionGovernment, + parentType: PARENT_TYPE.NOTICE_OF_INTENT, + }); + } + const finalSubmission = await this.noticeOfIntentSubmissionService.getByUuid( uuid, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts index 42e4e28789..257dce1c7a 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts @@ -22,6 +22,7 @@ import { NoticeOfIntentSubmissionValidatorService } from './notice-of-intent-sub import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; +import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; @Module({ imports: [ @@ -31,10 +32,11 @@ import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.s NoticeOfIntentParcelOwnershipType, OwnerType, NoticeOfIntentOwner, + NoticeOfIntentSubmissionStatusType, ]), NoticeOfIntentSubmissionStatusModule, forwardRef(() => NoticeOfIntentModule), - AuthorizationModule, + forwardRef(() => AuthorizationModule), forwardRef(() => DocumentModule), forwardRef(() => BoardModule), LocalGovernmentModule, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts index 400f3aee40..962e1482b6 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -21,10 +21,14 @@ import { PORTAL_TO_ALCS_STRUCTURE_TYPES_MAPPING, } from './notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; +import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; describe('NoticeOfIntentSubmissionService', () => { let service: NoticeOfIntentSubmissionService; let mockRepository: DeepMocked>; + let mockStatusRepository: DeepMocked< + Repository + >; let mockNoiService: DeepMocked; let mockLGService: DeepMocked; let mockNoiDocService: DeepMocked; @@ -34,6 +38,7 @@ describe('NoticeOfIntentSubmissionService', () => { beforeEach(async () => { mockRepository = createMock(); + mockStatusRepository = createMock(); mockNoiService = createMock(); mockLGService = createMock(); mockNoiDocService = createMock(); @@ -53,6 +58,10 @@ describe('NoticeOfIntentSubmissionService', () => { provide: getRepositoryToken(NoticeOfIntentSubmission), useValue: mockRepository, }, + { + provide: getRepositoryToken(NoticeOfIntentSubmissionStatusType), + useValue: mockStatusRepository, + }, { provide: NoticeOfIntentService, useValue: mockNoiService, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index 7ebe9a7502..10d6854aef 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -31,6 +31,7 @@ import { NoticeOfIntentSubmission, PORTAL_TO_ALCS_STRUCTURE_TYPES_MAPPING, } from './notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; @Injectable() export class NoticeOfIntentSubmissionService { @@ -50,6 +51,8 @@ export class NoticeOfIntentSubmissionService { constructor( @InjectRepository(NoticeOfIntentSubmission) private noticeOfIntentSubmissionRepository: Repository, + @InjectRepository(NoticeOfIntentSubmissionStatusType) + private noticeOfIntentStatusRepository: Repository, private noticeOfIntentService: NoticeOfIntentService, private localGovernmentService: LocalGovernmentService, private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, @@ -412,6 +415,14 @@ export class NoticeOfIntentSubmissionService { ); } + async getStatus(code: NOI_SUBMISSION_STATUS) { + return await this.noticeOfIntentStatusRepository.findOneOrFail({ + where: { + code, + }, + }); + } + async cancel(noticeOfIntentSubmission: NoticeOfIntentSubmission) { return await this.noticeOfIntentSubmissionStatusService.setStatusDate( noticeOfIntentSubmission.uuid, diff --git a/services/apps/alcs/src/providers/email/email.module.ts b/services/apps/alcs/src/providers/email/email.module.ts index 535c4b276c..00ed8d94be 100644 --- a/services/apps/alcs/src/providers/email/email.module.ts +++ b/services/apps/alcs/src/providers/email/email.module.ts @@ -5,6 +5,8 @@ import { EmailStatus } from './email-status.entity'; import { EmailService } from './email.service'; import { ApplicationSubmissionModule } from '../../portal/application-submission/application-submission.module'; import { ApplicationModule } from '../../alcs/application/application.module'; +import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; +import { NoticeOfIntentSubmissionModule } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.module'; @Module({ imports: [ @@ -12,6 +14,8 @@ import { ApplicationModule } from '../../alcs/application/application.module'; TypeOrmModule.forFeature([EmailStatus]), forwardRef(() => ApplicationModule), forwardRef(() => ApplicationSubmissionModule), + forwardRef(() => NoticeOfIntentModule), + forwardRef(() => NoticeOfIntentSubmissionModule), ], providers: [EmailService], exports: [EmailService], diff --git a/services/apps/alcs/src/providers/email/email.service.spec.ts b/services/apps/alcs/src/providers/email/email.service.spec.ts index 6f3d2ac5c4..fca6ee04c6 100644 --- a/services/apps/alcs/src/providers/email/email.service.spec.ts +++ b/services/apps/alcs/src/providers/email/email.service.spec.ts @@ -17,6 +17,13 @@ import { LocalGovernment } from '../../alcs/local-government/local-government.en import { ApplicationOwner } from '../../portal/application-submission/application-owner/application-owner.entity'; import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; import { ApplicationSubmissionStatusType } from '../../alcs/application/application-submission-status/submission-status-type.entity'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; +import { NoticeOfIntentSubmissionService } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; +import { PARENT_TYPE } from '../../alcs/card/card-subtask/card-subtask.dto'; describe('EmailService', () => { let service: EmailService; @@ -25,6 +32,8 @@ describe('EmailService', () => { let mockLocalGovernmentService: DeepMocked; let mockApplicationSubmissionService: DeepMocked; let mockApplicationService: DeepMocked; + let mockNoticeOfIntentSubmissionService: DeepMocked; + let mockNoticeOfIntentService: DeepMocked; beforeEach(async () => { mockHttpService = createMock(); @@ -33,6 +42,9 @@ describe('EmailService', () => { mockApplicationSubmissionService = createMock(); mockApplicationService = createMock(); + mockNoticeOfIntentSubmissionService = + createMock(); + mockNoticeOfIntentService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ConfigModule], @@ -54,6 +66,14 @@ describe('EmailService', () => { provide: ApplicationService, useValue: mockApplicationService, }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoticeOfIntentSubmissionService, + }, + { + provide: NoticeOfIntentService, + useValue: mockNoticeOfIntentService, + }, { provide: getRepositoryToken(EmailStatus), useValue: mockRepo, @@ -175,13 +195,13 @@ describe('EmailService', () => { ); }); - it('should call through services and return submission data', async () => { - const mockSubmission = new ApplicationSubmission({}); + it('should call through services and return application data', async () => { + const mockSubmission = new ApplicationSubmission(); mockApplicationSubmissionService.getOrFailByFileNumber.mockResolvedValue( mockSubmission, ); - const res = await service.getSubmissionStatusEmailData('file-number'); + const res = await service.getApplicationEmailData('file-number'); expect( mockApplicationSubmissionService.getOrFailByFileNumber, @@ -196,11 +216,23 @@ describe('EmailService', () => { }); }); - it('should call through services to set email template', async () => { + it('should call through services and return notice of intent data', async () => { + const res = await service.getNoticeOfIntentEmailData( + new NoticeOfIntentSubmission(), + ); + + expect(res).toStrictEqual({ + primaryContact: undefined, + submissionGovernment: null, + }); + }); + + it('should call through services to set application email template', async () => { const mockData = { generateStatusHtml: () => ({} as MJMLParseResults), status: SUBMISSION_STATUS.IN_REVIEW_BY_LG, applicationSubmission: new ApplicationSubmission({ typeCode: 'TURP' }), + parentType: 'application' as PARENT_TYPE, government: new LocalGovernment({ emails: [] }), primaryContact: new ApplicationOwner(), }; @@ -211,7 +243,7 @@ describe('EmailService', () => { mockApplicationService.fetchApplicationTypes.mockResolvedValue([]); mockApplicationService.getUuid.mockResolvedValue('fake-uuid'); - await service.sendStatusEmail(mockData); + await service.sendApplicationStatusEmail(mockData); expect(mockApplicationSubmissionService.getStatus).toBeCalledTimes(1); expect(mockApplicationSubmissionService.getStatus).toBeCalledWith( @@ -219,4 +251,29 @@ describe('EmailService', () => { ); expect(mockApplicationService.fetchApplicationTypes).toBeCalledTimes(1); }); + + it('should call through services to set notice of intent email template', async () => { + const mockData = { + generateStatusHtml: () => ({} as MJMLParseResults), + status: NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, + noticeOfIntentSubmission: new NoticeOfIntentSubmission(), + parentType: 'notice-of-intent' as PARENT_TYPE, + government: new LocalGovernment({ emails: [] }), + primaryContact: new NoticeOfIntentOwner(), + }; + + mockNoticeOfIntentSubmissionService.getStatus.mockResolvedValue( + new NoticeOfIntentSubmissionStatusType(), + ); + mockNoticeOfIntentService.listTypes.mockResolvedValue([]); + mockNoticeOfIntentService.getUuid.mockResolvedValue('fake-uuid'); + + await service.sendNoticeOfIntentStatusEmail(mockData); + + expect(mockNoticeOfIntentSubmissionService.getStatus).toBeCalledTimes(1); + expect(mockNoticeOfIntentSubmissionService.getStatus).toBeCalledWith( + mockData.status, + ); + expect(mockNoticeOfIntentService.listTypes).toBeCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/providers/email/email.service.ts b/services/apps/alcs/src/providers/email/email.service.ts index ac8f9f16f4..74e62eace8 100644 --- a/services/apps/alcs/src/providers/email/email.service.ts +++ b/services/apps/alcs/src/providers/email/email.service.ts @@ -14,24 +14,40 @@ import { ApplicationOwner } from '../../portal/application-submission/applicatio import { ApplicationSubmissionService } from '../../portal/application-submission/application-submission.service'; import { ApplicationService } from '../../alcs/application/application.service'; import { FALLBACK_APPLICANT_NAME } from '../../utils/owner.constants'; +import { PARENT_TYPE } from '../../alcs/card/card-subtask/card-subtask.dto'; +import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentSubmissionService } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; +import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; export interface StatusUpdateEmail { fileNumber: string; applicantName: string; status: string; - applicationType: string; + childType: string; governmentName: string; + parentType: PARENT_TYPE; } -type StatusEmailData = { +type BaseStatusEmailData = { generateStatusHtml: MJMLParseResults; - status: SUBMISSION_STATUS; - applicationSubmission: ApplicationSubmission; government: LocalGovernment | null; - primaryContact?: ApplicationOwner; + parentType: PARENT_TYPE; ccGovernment?: boolean; decisionReleaseMaskedDate?: string; }; +type ApplicationEmailData = BaseStatusEmailData & { + applicationSubmission: ApplicationSubmission; + status: SUBMISSION_STATUS; + primaryContact?: ApplicationOwner; +}; + +type NoticeOfIntentEmailData = BaseStatusEmailData & { + noticeOfIntentSubmission: NoticeOfIntentSubmission; + status: NOI_SUBMISSION_STATUS; + primaryContact?: NoticeOfIntentOwner; +}; export const appFees = [ { type: 'Exclusion', fee: 750 }, @@ -42,6 +58,20 @@ export const appFees = [ { type: 'Inclusion', fee: 0 }, ]; +export const noiFees = [ + { type: 'Removal of Soil', fee: 150 }, + { type: 'Placement of Fill', fee: 150 }, + { type: 'Placement of Fill/Removal of Soil', fee: 150 }, +]; + +const parentTypeLabel: Record< + PARENT_TYPE.APPLICATION | PARENT_TYPE.NOTICE_OF_INTENT, + string +> = { + application: 'Application', + 'notice-of-intent': 'NOI', +}; + @Injectable() export class EmailService { private logger: Logger = new Logger(EmailService.name); @@ -54,6 +84,8 @@ export class EmailService { private localGovernmentService: LocalGovernmentService, private applicationSubmissionService: ApplicationSubmissionService, private applicationService: ApplicationService, + private noticeOfIntentService: NoticeOfIntentService, + private noticeOfInterntSubmissionService: NoticeOfIntentSubmissionService, ) {} private token = ''; @@ -182,7 +214,9 @@ export class EmailService { } } - async getSubmissionGovernmentOrFail(submission: ApplicationSubmission) { + async getSubmissionGovernmentOrFail( + submission: ApplicationSubmission | NoticeOfIntentSubmission, + ) { const submissionGovernment = await this.getSubmissionGovernment(submission); if (!submissionGovernment) { throw new NotFoundException('Submission local government not found'); @@ -190,7 +224,9 @@ export class EmailService { return submissionGovernment; } - private async getSubmissionGovernment(submission: ApplicationSubmission) { + private async getSubmissionGovernment( + submission: ApplicationSubmission | NoticeOfIntentSubmission, + ) { if (submission.localGovernmentUuid) { const localGovernment = await this.localGovernmentService.getByUuid( submission.localGovernmentUuid, @@ -202,12 +238,12 @@ export class EmailService { return undefined; } - async getSubmissionStatusEmailData( + async getApplicationEmailData( fileNumber: string, - submissionData?: ApplicationSubmission, + submission?: ApplicationSubmission, ) { const applicationSubmission = - submissionData || + submission || (await this.applicationSubmissionService.getOrFailByFileNumber( fileNumber, )); @@ -223,7 +259,19 @@ export class EmailService { return { applicationSubmission, primaryContact, submissionGovernment }; } - async sendStatusEmail(data: StatusEmailData) { + async getNoticeOfIntentEmailData(submission: NoticeOfIntentSubmission) { + const primaryContact = submission.owners?.find( + (owner) => owner.uuid === submission.primaryContactOwnerUuid, + ); + + const submissionGovernment = submission.localGovernmentUuid + ? await this.getSubmissionGovernmentOrFail(submission) + : null; + + return { primaryContact, submissionGovernment }; + } + + private async setApplicationEmailTemplate(data: ApplicationEmailData) { const status = await this.applicationSubmissionService.getStatus( data.status, ); @@ -241,25 +289,82 @@ export class EmailService { const emailTemplate = data.generateStatusHtml({ fileNumber, applicantName, - applicationType: + childType: matchingType?.portalLabel ?? matchingType?.label ?? FALLBACK_APPLICANT_NAME, governmentName: data.government?.name, status: status.label, + parentTypeLabel: parentTypeLabel[data.parentType], decisionReleaseMaskedDate: data?.decisionReleaseMaskedDate, }); const parentId = await this.applicationService.getUuid(fileNumber); - const email = { + return { body: emailTemplate.html, subject: `Agricultural Land Commission Application ID: ${fileNumber} (${applicantName})`, - parentType: 'application', + parentType: data.parentType, + parentId, + triggerStatus: status.code, + }; + } + + private async setNoticeOfIntentEmailTemplate(data: NoticeOfIntentEmailData) { + const status = await this.noticeOfInterntSubmissionService.getStatus( + data.status, + ); + + const types = await this.noticeOfIntentService.listTypes(); + + const matchingType = types.find( + (type) => type.code === data.noticeOfIntentSubmission.typeCode, + ); + + const fileNumber = data.noticeOfIntentSubmission.fileNumber; + + const applicantName = + data.noticeOfIntentSubmission.applicant || FALLBACK_APPLICANT_NAME; + + const emailTemplate = data.generateStatusHtml({ + fileNumber, + applicantName, + childType: + matchingType?.portalLabel ?? + matchingType?.label ?? + FALLBACK_APPLICANT_NAME, + governmentName: data.government?.name, + status: status.label, + parentTypeLabel: parentTypeLabel[data.parentType], + }); + + const parentId = await this.noticeOfIntentService.getUuid(fileNumber); + + return { + body: emailTemplate.html, + subject: `Agricultural Land Commission NOI ID: ${fileNumber} (${applicantName})`, + parentType: data.parentType, parentId, triggerStatus: status.code, }; + } + async sendApplicationStatusEmail(data: ApplicationEmailData) { + const email = await this.setApplicationEmailTemplate(data); + + this.sendStatusEmail(data, email); + } + + async sendNoticeOfIntentStatusEmail(data: NoticeOfIntentEmailData) { + const email = await this.setNoticeOfIntentEmailTemplate(data); + + this.sendStatusEmail(data, email); + } + + private sendStatusEmail( + data: ApplicationEmailData | NoticeOfIntentEmailData, + email, + ) { if (data.primaryContact && data.primaryContact.email) { this.sendEmail({ ...email, diff --git a/services/templates/emails/cancelled.template.ts b/services/templates/emails/cancelled/application.template.ts similarity index 67% rename from services/templates/emails/cancelled.template.ts rename to services/templates/emails/cancelled/application.template.ts index 4d0a6e3dd2..60df235aaf 100644 --- a/services/templates/emails/cancelled.template.ts +++ b/services/templates/emails/cancelled/application.template.ts @@ -1,7 +1,7 @@ import { MJMLParseResults } from 'mjml-core'; -import { EmailTemplateService } from '../../libs/common/src/email-template-service/email-template.service'; -import { header, footer, notificationOnly, portalButton } from './partials'; -import { StatusUpdateEmail } from '../../apps/alcs/src/providers/email/email.service'; +import { EmailTemplateService } from '../../../libs/common/src/email-template-service/email-template.service'; +import { header, footer, notificationOnly, portalButton } from '../partials'; +import { StatusUpdateEmail } from '../../../apps/alcs/src/providers/email/email.service'; const template = ` @@ -21,7 +21,7 @@ const template = ` - This email is to advise that the above noted {{ applicationType }} application has been cancelled and will not be considered further. + This email is to advise that the above noted {{ childType }} application has been cancelled and will not be considered further. If you are an agent acting on behalf of the applicant(s)/landowner(s), it is your responsibility to advise your client(s) of this, and any future, correspondence. @@ -37,6 +37,8 @@ const template = ` `; -export const generateCANCHtml = (data: StatusUpdateEmail): MJMLParseResults => { +export const generateCANCApplicationHtml = ( + data: StatusUpdateEmail, +): MJMLParseResults => { return new EmailTemplateService().generateEmailBase(template, data); }; diff --git a/services/templates/emails/cancelled/index.ts b/services/templates/emails/cancelled/index.ts new file mode 100644 index 0000000000..cf7fa6130d --- /dev/null +++ b/services/templates/emails/cancelled/index.ts @@ -0,0 +1,2 @@ +export * from './application.template'; +export * from './notice-of-intent.template'; diff --git a/services/templates/emails/cancelled/notice-of-intent.template.ts b/services/templates/emails/cancelled/notice-of-intent.template.ts new file mode 100644 index 0000000000..dbc1a771f8 --- /dev/null +++ b/services/templates/emails/cancelled/notice-of-intent.template.ts @@ -0,0 +1,44 @@ +import { MJMLParseResults } from 'mjml-core'; +import { EmailTemplateService } from '../../../libs/common/src/email-template-service/email-template.service'; +import { header, footer, notificationOnly, portalButton } from '../partials'; +import { StatusUpdateEmail } from '../../../apps/alcs/src/providers/email/email.service'; + +const template = ` + + + .line-height div { + line-height: 24px !important; + } + + .align-left { + float: left !important; + } + + + + ${header} + + + + + This email is to advise that the above noted {{ childType }} Notice of Intent has been cancelled and will not be considered further. + + + If you are an agent acting on behalf of the applicant(s)/landowner(s), it is your responsibility to advise your client(s) of this, and any future, correspondence. + + ${notificationOnly} + + + + ${portalButton} + + ${footer} + + +`; + +export const generateCANCNoticeOfIntentHtml = ( + data: StatusUpdateEmail, +): MJMLParseResults => { + return new EmailTemplateService().generateEmailBase(template, data); +}; diff --git a/services/templates/emails/decision-released.template.ts b/services/templates/emails/decision-released/application.template.ts similarity index 88% rename from services/templates/emails/decision-released.template.ts rename to services/templates/emails/decision-released/application.template.ts index c49fd06a7b..4ef596f6bf 100644 --- a/services/templates/emails/decision-released.template.ts +++ b/services/templates/emails/decision-released/application.template.ts @@ -1,7 +1,7 @@ import { MJMLParseResults } from 'mjml-core'; -import { EmailTemplateService } from '../../libs/common/src/email-template-service/email-template.service'; -import { header, footer, notificationOnly, portalButton } from './partials'; -import { StatusUpdateEmail } from '../../apps/alcs/src/providers/email/email.service'; +import { EmailTemplateService } from '../../../libs/common/src/email-template-service/email-template.service'; +import { header, footer, notificationOnly, portalButton } from '../partials'; +import { StatusUpdateEmail } from '../../../apps/alcs/src/providers/email/email.service'; type DecisionReleasedStatusEmail = StatusUpdateEmail & { decisionReleaseMaskedDate: number; @@ -47,7 +47,7 @@ const template = ` `; -export const generateALCDHtml = ( +export const generateALCDApplicationHtml = ( data: DecisionReleasedStatusEmail, ): MJMLParseResults => { return new EmailTemplateService().generateEmailBase(template, data); diff --git a/services/templates/emails/decision-released/index.ts b/services/templates/emails/decision-released/index.ts new file mode 100644 index 0000000000..cf7fa6130d --- /dev/null +++ b/services/templates/emails/decision-released/index.ts @@ -0,0 +1,2 @@ +export * from './application.template'; +export * from './notice-of-intent.template'; diff --git a/services/templates/emails/decision-released/notice-of-intent.template.ts b/services/templates/emails/decision-released/notice-of-intent.template.ts new file mode 100644 index 0000000000..ade548924c --- /dev/null +++ b/services/templates/emails/decision-released/notice-of-intent.template.ts @@ -0,0 +1,54 @@ +import { MJMLParseResults } from 'mjml-core'; +import { EmailTemplateService } from '../../../libs/common/src/email-template-service/email-template.service'; +import { header, footer, notificationOnly, portalButton } from '../partials'; +import { StatusUpdateEmail } from '../../../apps/alcs/src/providers/email/email.service'; + +type DecisionReleasedStatusEmail = StatusUpdateEmail & { + decisionReleaseMaskedDate: number; +}; + +const template = ` + + + .line-height div { + line-height: 24px !important; + } + + .align-left { + float: left !important; + } + + + + ${header} + + + + + The decision for the above noted Notice of Intent (NOI) has been released on the the ALC Portal. + + + The decision document can be found by clicking 'View' from the NOI Inbox table in the ALC Portal, and then navigating to the 'ALC Review and Decision' tab. The decision will be available to the public on {{ decisionReleaseMaskedDate }}. + + + Further correspondence with respect to this NOI should be directed to ALC.Soil@gov.bc.ca. + + + If you are an agent acting on behalf of the applicant(s)/landowner(s), it is your responsibility to advise your client(s) of this, and any future, correspondence. + + ${notificationOnly} + + + + ${portalButton} + + ${footer} + + +`; + +export const generateALCDNoticeOfIntentHtml = ( + data: DecisionReleasedStatusEmail, +): MJMLParseResults => { + return new EmailTemplateService().generateEmailBase(template, data); +}; diff --git a/services/templates/emails/partials/header.template.ts b/services/templates/emails/partials/header.template.ts index 7993fe3bfd..e49082c8a3 100644 --- a/services/templates/emails/partials/header.template.ts +++ b/services/templates/emails/partials/header.template.ts @@ -3,7 +3,7 @@ import * as config from 'config'; export const header = ` - Application ID #{{fileNumber}} + {{ parentTypeLabel }} ID #{{fileNumber}} Owner Name: {{applicantName}} Status: {{status}} diff --git a/services/templates/emails/refused-to-forward.template.ts b/services/templates/emails/refused-to-forward.template.ts index 15007c6f1b..3cdba2e2cf 100644 --- a/services/templates/emails/refused-to-forward.template.ts +++ b/services/templates/emails/refused-to-forward.template.ts @@ -21,7 +21,7 @@ const template = ` - This email is to advise that the above noted {{ applicationType }} application has been reviewed by the {{ governmentName }} which has determined not to forward your application to the Agricultural Land Commission for further review. + This email is to advise that the above noted {{ childType }} application has been reviewed by the {{ governmentName }} which has determined not to forward your application to the Agricultural Land Commission for further review. If you have any questions about this outcome, please contact {{ governmentName }}. diff --git a/services/templates/emails/returned-as-incomplete.template.ts b/services/templates/emails/returned-as-incomplete.template.ts index e6d7173180..e97ce576b0 100644 --- a/services/templates/emails/returned-as-incomplete.template.ts +++ b/services/templates/emails/returned-as-incomplete.template.ts @@ -21,7 +21,7 @@ const template = ` - This email is to advise that the above noted {{ applicationType }} application is considered to be incomplete by the {{ governmentName }} and consequently it has been returned to the applicant. + This email is to advise that the above noted {{ childType }} application is considered to be incomplete by the {{ governmentName }} and consequently it has been returned to the applicant. Please login to the ALC Portal to view the comments left by the {{ governmentName }}. Please review, edit and re-submit the application. diff --git a/services/templates/emails/submitted-to-alc/applicant.template.ts b/services/templates/emails/submitted-to-alc/application.template.ts similarity index 99% rename from services/templates/emails/submitted-to-alc/applicant.template.ts rename to services/templates/emails/submitted-to-alc/application.template.ts index e73947d051..4c4e32e8e3 100644 --- a/services/templates/emails/submitted-to-alc/applicant.template.ts +++ b/services/templates/emails/submitted-to-alc/application.template.ts @@ -104,7 +104,7 @@ const template = ` `; -export const generateSUBMApplicantHtml = ( +export const generateSUBMApplicationHtml = ( data: StatusUpdateEmail, ): MJMLParseResults => { return new EmailTemplateService().generateEmailBase(template, data); diff --git a/services/templates/emails/submitted-to-alc/index.ts b/services/templates/emails/submitted-to-alc/index.ts index 218deecbd4..0232ab77f0 100644 --- a/services/templates/emails/submitted-to-alc/index.ts +++ b/services/templates/emails/submitted-to-alc/index.ts @@ -1,3 +1,5 @@ -export * from './applicant.template'; +export * from './application.template'; +export * from './noi-applicant.template'; +export * from './noi-government.template'; export * from './tur-applicant.template'; export * from './tur-government.template'; diff --git a/services/templates/emails/submitted-to-alc/noi-applicant.template.ts b/services/templates/emails/submitted-to-alc/noi-applicant.template.ts new file mode 100644 index 0000000000..e21577fab1 --- /dev/null +++ b/services/templates/emails/submitted-to-alc/noi-applicant.template.ts @@ -0,0 +1,105 @@ +import { MJMLParseResults } from 'mjml-core'; +import { EmailTemplateService } from '../../../libs/common/src/email-template-service/email-template.service'; +import { header, footer, notificationOnly, portalButton } from '../partials'; +import { + StatusUpdateEmail, + noiFees, +} from '../../../apps/alcs/src/providers/email/email.service'; + +const template = ` + + + .line-height div { + line-height: 24px !important; + } + + .align-left { + float: left !important; + } + + + + ${header} + + + + + This email is to acknowledge that the Agricultural Land Commission (ALC) is in receipt of the above noted {{ childType }} Notice of Intent (NOI). Please refer to the ALC NOI ID in all future correspondence with this office. A copy of this NOI has been forwarded to the {{ governmentName }} for information purposes. + + + NOTICE OF INTENT FEES - Payable to the Minister of Finance c/o the ALC + + +
+ + + + ${noiFees + .map((a) => { + return ` + + + + + `; + }) + .join('')} + + + This fee can be paid: +
    +
  1. Cheque: Made payable to the Minister of Finance c/o the ALC
  2. +
  3. Credit card: Over the phone or in-person
  4. +
+
+ + Please include your assigned NOI ID with your payment. + + + Mailing address: +
+ Agricultural Land Commission +
+ 201-4940 Canada Way +
+ Burnaby, BC, Canada +
+ V5G 4K6 +
+
+ + Paying via telephone: +
+ Tel: 604-660-7000 +
+
+ + If you are making a long-distance call to a provincial government agency, you can place your call through Enquiry BC free of charge: +
+ In Victoria call: 250-387-6121 +
+ Elsewhere in BC call: 1-800-663-7867 +
+
+ + If you are an agent acting on behalf of the applicant(s) / landowner(s), it is your responsibility to advise them of this, and any future, correspondence. + + + Please log into the ALC Portal for further updates on the NOI as it progresses. + + ${notificationOnly} + + + + ${portalButton} + + ${footer} + + +`; + +export const generateSUBMNoiApplicantHtml = ( + data: StatusUpdateEmail, +): MJMLParseResults => { + return new EmailTemplateService().generateEmailBase(template, data); +}; diff --git a/services/templates/emails/submitted-to-alc/noi-government.template.ts b/services/templates/emails/submitted-to-alc/noi-government.template.ts new file mode 100644 index 0000000000..84ed87c25d --- /dev/null +++ b/services/templates/emails/submitted-to-alc/noi-government.template.ts @@ -0,0 +1,44 @@ +import { MJMLParseResults } from 'mjml-core'; +import { EmailTemplateService } from '../../../libs/common/src/email-template-service/email-template.service'; +import { header, footer, notificationOnly, portalButton } from '../partials'; +import { StatusUpdateEmail } from '../../../apps/alcs/src/providers/email/email.service'; + +const template = ` + + + .line-height div { + line-height: 24px !important; + } + + .align-left { + float: left !important; + } + + + + ${header} + + + + + Agricultural Land Commission {{ childType }} NOI ID: {{ fileNumber }} ({{ applicantName }}) has been successfully submitted to the Agricultural Land Commission. A read-only copy of the Notice of Intent (NOI) has been submitted to the {{ governmentName }} for informational purposes. Should the {{ governmentName}} wish to comment on the NOI, please submit comments directly to the ALC. + + + Please log into the ALC Portal to view the NOI. + + ${notificationOnly} + + + + ${portalButton} + + ${footer} + + +`; + +export const generateSUBMNoiGovernmentHtml = ( + data: StatusUpdateEmail, +): MJMLParseResults => { + return new EmailTemplateService().generateEmailBase(template, data); +}; diff --git a/services/templates/emails/submitted-to-alc/tur-applicant.template.ts b/services/templates/emails/submitted-to-alc/tur-applicant.template.ts index 91840da803..1eb1ca18f5 100644 --- a/services/templates/emails/submitted-to-alc/tur-applicant.template.ts +++ b/services/templates/emails/submitted-to-alc/tur-applicant.template.ts @@ -21,7 +21,7 @@ const template = ` - This email is to acknowledge that the Agricultural Land Commission (ALC) is in receipt of the above noted {{ applicationType }} application. Please refer to the ALC Application ID in all future correspondence with this office. A copy of this application has been forwarded to the {{ governmentName }} for information purposes. + This email is to acknowledge that the Agricultural Land Commission (ALC) is in receipt of the above noted {{ childType }} application. Please refer to the ALC Application ID in all future correspondence with this office. A copy of this application has been forwarded to the {{ governmentName }} for information purposes. APPLICATION FEES - Payable to the Minister of Finance c/o the ALC diff --git a/services/templates/emails/submitted-to-alc/tur-government.template.ts b/services/templates/emails/submitted-to-alc/tur-government.template.ts index 13c599073c..c17f3e2125 100644 --- a/services/templates/emails/submitted-to-alc/tur-government.template.ts +++ b/services/templates/emails/submitted-to-alc/tur-government.template.ts @@ -21,7 +21,7 @@ const template = ` - Agricultural Land Commission {{ applicationType }} Application ID: {{ fileNumber }} ({{ applicantName }}) has been successfully submitted to the Agricultural Land Commission. A read-only copy of the application has been submitted to the {{ governmentName }} for informational purposes. Should the {{ governmentName }} wish to comment on the application, please submit comments directly to the ALC. + Agricultural Land Commission {{ childType }} Application ID: {{ fileNumber }} ({{ applicantName }}) has been successfully submitted to the Agricultural Land Commission. A read-only copy of the application has been submitted to the {{ governmentName }} for informational purposes. Should the {{ governmentName }} wish to comment on the application, please submit comments directly to the ALC. Please log into the ALC Portal to view the application. diff --git a/services/templates/emails/submitted-to-lfng/applicant.template.ts b/services/templates/emails/submitted-to-lfng/applicant.template.ts index 320c139d61..5bce0d49c7 100644 --- a/services/templates/emails/submitted-to-lfng/applicant.template.ts +++ b/services/templates/emails/submitted-to-lfng/applicant.template.ts @@ -24,7 +24,7 @@ const template = ` - This email is to acknowledge that you have submitted the above noted {{ applicationType }} application to the {{ governmentName }}. + This email is to acknowledge that you have submitted the above noted {{ childType }} application to the {{ governmentName }}. diff --git a/services/templates/emails/submitted-to-lfng/government.template.ts b/services/templates/emails/submitted-to-lfng/government.template.ts index 14cdd37905..53c9e73d44 100644 --- a/services/templates/emails/submitted-to-lfng/government.template.ts +++ b/services/templates/emails/submitted-to-lfng/government.template.ts @@ -30,7 +30,7 @@ const template = ` - Agricultural Land Commission {{ applicationType }} Application ID: {{ fileNumber }} ({{ applicantName }}) has been successfully submitted to the {{ governmentName }}. The Applicant has been instructed to contact the {{ governmentName }} for payment instructions regarding the applicable application fee. + Agricultural Land Commission {{ childType }} Application ID: {{ fileNumber }} ({{ applicantName }}) has been successfully submitted to the {{ governmentName }}. The Applicant has been instructed to contact the {{ governmentName }} for payment instructions regarding the applicable application fee. diff --git a/services/templates/emails/under-review-by-lfng.template.ts b/services/templates/emails/under-review-by-lfng.template.ts index 50bc360415..bb37198427 100644 --- a/services/templates/emails/under-review-by-lfng.template.ts +++ b/services/templates/emails/under-review-by-lfng.template.ts @@ -21,7 +21,7 @@ const template = ` - This email is to advise that the above noted {{ applicationType }} application has been received by the {{ governmentName }} for review. + This email is to advise that the above noted {{ childType }} application has been received by the {{ governmentName }} for review. If you have not already done so, please contact the {{ governmentName }} to determine the preferred form of payment as the application may not be processed until payment is received. diff --git a/services/templates/emails/wrong-lfng.template.ts b/services/templates/emails/wrong-lfng.template.ts index a0acfa46b3..7e6f856dd0 100644 --- a/services/templates/emails/wrong-lfng.template.ts +++ b/services/templates/emails/wrong-lfng.template.ts @@ -21,7 +21,7 @@ const template = ` - This email is to advise that the above noted {{ applicationType }} application does not fall within the jurisdiction of the {{ governmentName }} and consequently it has been returned to the applicant. + This email is to advise that the above noted {{ childType }} application does not fall within the jurisdiction of the {{ governmentName }} and consequently it has been returned to the applicant. Please login to the ALC Portal to select the correct Local or First Nation Government jurisdiction and re-submit the application. From aed186cbbd040c1e12cd53537d1bd3716b25dc79 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 24 Aug 2023 11:05:34 -0700 Subject: [PATCH 300/954] dicts working, code working, committing before refactor --- bin/migrate-oats-data/submap/direction_mapping.py | 5 ++++- bin/migrate-oats-data/submissions/app_submissions.py | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/submap/direction_mapping.py b/bin/migrate-oats-data/submap/direction_mapping.py index a661e11f66..38534ced5d 100644 --- a/bin/migrate-oats-data/submap/direction_mapping.py +++ b/bin/migrate-oats-data/submap/direction_mapping.py @@ -26,4 +26,7 @@ def map_direction_field(data, dir_data): data['south_land_use_type'] = dir_data['nonfarm_use_type_code'] else: return data - return data \ No newline at end of file + return data + +# def map_dict_field(data, NESW) +# if data[] \ No newline at end of file diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 63f5e44ef1..e6fe6cb8c4 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -200,6 +200,15 @@ def prepare_app_sub_data(app_sub_raw_data_list, raw_dir_data_list, test_dict): data = add_direction_field(data) if data["alr_application_id"] in test_dict: print(test_dict[data["alr_application_id"]]["alr_application_id"]) + data['east_land_use_type_description'] = test_dict[data["alr_application_id"]]['east_description'] + data['east_land_use_type'] = test_dict[data["alr_application_id"]]['east_type_code'] + data['west_land_use_type_description'] = test_dict[data["alr_application_id"]]['west_description'] + data['west_land_use_type'] = test_dict[data["alr_application_id"]]['west_type_code'] + data['north_land_use_type_description'] = test_dict[data["alr_application_id"]]['north_description'] + data['north_land_use_type'] = test_dict[data["alr_application_id"]]['north_type_code'] + data['south_land_use_type_description'] = test_dict[data["alr_application_id"]]['south_description'] + data['south_land_use_type'] = test_dict[data["alr_application_id"]]['south_type_code'] + print(data) #leaving off here to insert values from new fcn # for adj_row in raw_dir_data_list: # dir_data = dict(adj_row) From c91bcf698749ad1922b4ac37d164f740862483e9 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 24 Aug 2023 11:52:25 -0700 Subject: [PATCH 301/954] Cleaned up, fixed key error if NESW not all present. Ready for MR --- .../submap/direction_mapping.py | 78 +++++++++++++---- .../submissions/app_submissions.py | 83 +++---------------- 2 files changed, 71 insertions(+), 90 deletions(-) diff --git a/bin/migrate-oats-data/submap/direction_mapping.py b/bin/migrate-oats-data/submap/direction_mapping.py index 38534ced5d..621178242f 100644 --- a/bin/migrate-oats-data/submap/direction_mapping.py +++ b/bin/migrate-oats-data/submap/direction_mapping.py @@ -1,5 +1,6 @@ def add_direction_field(data): + # populates columns to be inserted data['east_land_use_type_description'] = None data['east_land_use_type'] = None data['west_land_use_type_description'] = None @@ -10,23 +11,64 @@ def add_direction_field(data): data['south_land_use_type'] = None return data -def map_direction_field(data, dir_data): - if data['alr_application_id'] == dir_data['alr_application_id']: - if dir_data['cardinal_direction'] == 'EAST': - data['east_land_use_type_description'] = dir_data['description'] - data['east_land_use_type'] = dir_data['nonfarm_use_type_code'] - if dir_data['cardinal_direction'] == 'WEST': - data['west_land_use_type_description'] = dir_data['description'] - data['west_land_use_type'] = dir_data['nonfarm_use_type_code'] - if dir_data['cardinal_direction'] == 'NORTH': - data['north_land_use_type_description'] = dir_data['description'] - data['north_land_use_type'] = dir_data['nonfarm_use_type_code'] - if dir_data['cardinal_direction'] == 'SOUTH': - data['south_land_use_type_description'] = dir_data['description'] - data['south_land_use_type'] = dir_data['nonfarm_use_type_code'] - else: - return data +def get_NESW_rows(rows, cursor): + # fetches adjacent land use data, specifically direction, description and type code + application_ids = [dict(item)["alr_application_id"] for item in rows] + application_ids_string = ', '.join(str(item) for item in application_ids) + adj_rows_query = f"""SELECT * from + oats.oats_adjacent_land_uses oalu + WHERE oalu.alr_application_id in ({application_ids_string}) + """ + cursor.execute(adj_rows_query) + adj_rows = cursor.fetchall() + return adj_rows + +def map_direction_values(data, direction_data): + # adds direction field values into data row + data['east_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('east_description', 'No entry found') + data['east_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('east_type_code', None) + data['west_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('west_description', 'No entry found') + data['west_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('west_type_code', None) + data['north_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('north_description', 'No entry found') + data['north_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('north_type_code', None) + data['south_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('south_description', 'No entry found') + data['south_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('south_type_code', None) return data -# def map_dict_field(data, NESW) -# if data[] \ No newline at end of file +def create_dir_dict(adj_rows): + # creates dictionary of adjacent land use data with all directions attributed to one application id + dir_dict = {} + for row in adj_rows: + application_id = row['alr_application_id'] + + if application_id in dir_dict: + if row['cardinal_direction'] == 'EAST': + dir_dict[application_id]['east_description'] = row['description'] + dir_dict[application_id]['east_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'WEST': + dir_dict[application_id]['west_description'] = row['description'] + dir_dict[application_id]['west_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'NORTH': + dir_dict[application_id]['north_description'] = row['description'] + dir_dict[application_id]['north_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'SOUTH': + dir_dict[application_id]['south_description'] = row['description'] + dir_dict[application_id]['south_type_code'] = row['nonfarm_use_type_code'] + else: + dir_dict[application_id] = {} + dir_dict[application_id]['alr_application_id'] = row['alr_application_id'] + + if row['cardinal_direction'] == 'EAST': + dir_dict[application_id]['east_description'] = row['description'] + dir_dict[application_id]['east_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'WEST': + dir_dict[application_id]['west_description'] = row['description'] + dir_dict[application_id]['west_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'NORTH': + dir_dict[application_id]['north_description'] = row['description'] + dir_dict[application_id]['north_type_code'] = row['nonfarm_use_type_code'] + if row['cardinal_direction'] == 'SOUTH': + dir_dict[application_id]['south_description'] = row['description'] + dir_dict[application_id]['south_type_code'] = row['nonfarm_use_type_code'] + + return dir_dict \ No newline at end of file diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index e6fe6cb8c4..62e5c869fe 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -5,7 +5,9 @@ ) from submap import ( add_direction_field, - map_direction_field, + map_direction_values, + create_dir_dict, + get_NESW_rows, ) from db import inject_conn_pool from constants import BATCH_UPLOAD_SIZE @@ -57,59 +59,12 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): if not rows: break try: - application_ids = [dict(item)["alr_application_id"] for item in rows] - application_ids_string = ', '.join(str(item) for item in application_ids) - adj_rows_query = f"""SELECT * from - oats.oats_adjacent_land_uses oalu - WHERE oalu.alr_application_id in ({application_ids_string}) - """ - cursor.execute(adj_rows_query) - adj_rows = cursor.fetchall() - test_dict = {} - for row in adj_rows: - application_id = row['alr_application_id'] - if application_id in test_dict: - if row['cardinal_direction'] == 'EAST': - test_dict[application_id]['east_description'] = row['description'] - test_dict[application_id]['east_type_code'] = row['nonfarm_use_type_code'] - # test_dict[application_id].append(row) - if row['cardinal_direction'] == 'WEST': - test_dict[application_id]['west_description'] = row['description'] - test_dict[application_id]['west_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'NORTH': - test_dict[application_id]['north_description'] = row['description'] - test_dict[application_id]['north_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'SOUTH': - test_dict[application_id]['south_description'] = row['description'] - test_dict[application_id]['south_type_code'] = row['nonfarm_use_type_code'] - else: - test_dict[application_id] = {} - test_dict[application_id]['alr_application_id'] = row['alr_application_id'] - # test_dict[application_id] = [row] - if row['cardinal_direction'] == 'EAST': - test_dict[application_id]['east_description'] = row['description'] - test_dict[application_id]['east_type_code'] = row['nonfarm_use_type_code'] - # test_dict[application_id].append(row) - if row['cardinal_direction'] == 'WEST': - test_dict[application_id]['west_description'] = row['description'] - test_dict[application_id]['west_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'NORTH': - test_dict[application_id]['north_description'] = row['description'] - test_dict[application_id]['north_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'SOUTH': - test_dict[application_id]['south_description'] = row['description'] - test_dict[application_id]['south_type_code'] = row['nonfarm_use_type_code'] - - # for row in {adj_rows['alr_application_id']: adj_rows for adj_rows in adj_rows} - # print(test_dict) - - # print("this is rows") - # print(rows) - # print("end rows") + adj_rows = get_NESW_rows(rows, cursor) + direction_data = create_dir_dict(adj_rows) submissions_to_be_inserted_count = len(rows) - insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows, test_dict) + insert_app_sub_records(conn, batch_size, cursor, rows, direction_data) successful_inserts_count = ( successful_inserts_count + submissions_to_be_inserted_count @@ -133,7 +88,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): print("Total failed inserts:", failed_inserts) log_end(etl_name) -def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows, test_dict): +def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data): """ Function to insert submission records in batches. @@ -150,7 +105,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows, test_dict): nfu_data_list, other_data_list, inc_exc_data_list, - ) = prepare_app_sub_data(rows, adj_rows, test_dict) + ) = prepare_app_sub_data(rows, direction_data) if len(nfu_data_list) > 0: execute_batch( @@ -178,7 +133,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, adj_rows, test_dict): conn.commit() -def prepare_app_sub_data(app_sub_raw_data_list, raw_dir_data_list, test_dict): +def prepare_app_sub_data(app_sub_raw_data_list, direction_data): """ This function prepares different lists of data based on the 'alr_change_code' field of each data dict in 'app_sub_raw_data_list'. @@ -198,24 +153,8 @@ def prepare_app_sub_data(app_sub_raw_data_list, raw_dir_data_list, test_dict): for row in app_sub_raw_data_list: data = dict(row) data = add_direction_field(data) - if data["alr_application_id"] in test_dict: - print(test_dict[data["alr_application_id"]]["alr_application_id"]) - data['east_land_use_type_description'] = test_dict[data["alr_application_id"]]['east_description'] - data['east_land_use_type'] = test_dict[data["alr_application_id"]]['east_type_code'] - data['west_land_use_type_description'] = test_dict[data["alr_application_id"]]['west_description'] - data['west_land_use_type'] = test_dict[data["alr_application_id"]]['west_type_code'] - data['north_land_use_type_description'] = test_dict[data["alr_application_id"]]['north_description'] - data['north_land_use_type'] = test_dict[data["alr_application_id"]]['north_type_code'] - data['south_land_use_type_description'] = test_dict[data["alr_application_id"]]['south_description'] - data['south_land_use_type'] = test_dict[data["alr_application_id"]]['south_type_code'] - print(data) - #leaving off here to insert values from new fcn - # for adj_row in raw_dir_data_list: - # dir_data = dict(adj_row) - # data = map_direction_field(data, dir_data) - # currently rather slow - # ToDo optimize, potentially give index for dir_data resume point - + if data["alr_application_id"] in direction_data: + data = map_direction_values(data, direction_data) if data["alr_change_code"] == ALRChangeCode.NFU.value: nfu_data_list.append(data) elif data["alr_change_code"] == ALRChangeCode.EXC.value or data["alr_change_code"] == ALRChangeCode.INC.value: From 1006063127d69297ebbcbba17403d3ebfbd89dc2 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 24 Aug 2023 13:46:10 -0700 Subject: [PATCH 302/954] changed 'No entry found' to 'No data found' --- bin/migrate-oats-data/submap/direction_mapping.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/migrate-oats-data/submap/direction_mapping.py b/bin/migrate-oats-data/submap/direction_mapping.py index 621178242f..ff8afa18e1 100644 --- a/bin/migrate-oats-data/submap/direction_mapping.py +++ b/bin/migrate-oats-data/submap/direction_mapping.py @@ -25,13 +25,13 @@ def get_NESW_rows(rows, cursor): def map_direction_values(data, direction_data): # adds direction field values into data row - data['east_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('east_description', 'No entry found') + data['east_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('east_description', 'No data found') data['east_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('east_type_code', None) - data['west_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('west_description', 'No entry found') + data['west_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('west_description', 'No data found') data['west_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('west_type_code', None) - data['north_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('north_description', 'No entry found') + data['north_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('north_description', 'No data found') data['north_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('north_type_code', None) - data['south_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('south_description', 'No entry found') + data['south_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('south_description', 'No data found') data['south_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('south_type_code', None) return data From 47236f008327f2037288a912ee82d88b8d564a85 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 24 Aug 2023 15:13:07 -0700 Subject: [PATCH 303/954] NOI Decisions V2 Pt2 * Update UI to match NOI * Update number generation for decisions to include NOI and Application Decisions * Rename DecisionDocumentDto -> ApplicationDecisionDocumentDto --- .../pofo/pofo.component.html | 75 +++++------------- .../roso/roso.component.html | 72 +++++------------ .../decision-documents.component.ts | 13 +-- .../decision-input-v2.component.ts | 12 +-- ...reate-noi-modification-dialog.component.ts | 10 ++- .../decision-dialog.component.ts | 2 +- .../decision-v1/decision-v1.component.html | 2 +- .../decision-v1/decision-v1.component.ts | 4 +- .../pofo/pofo.component.html | 79 +++++-------------- .../roso/roso.component.html | 72 +++++------------ .../decision-documents.component.ts | 16 ++-- .../decision-components.component.ts | 4 +- .../decision-input-v2.component.html | 23 +++--- .../decision-input-v2.component.scss | 2 +- .../decision-input-v2.component.ts | 46 ++++++----- .../decision-v2/decision-v2.component.html | 34 +++++--- .../overview/overview.component.ts | 2 +- .../application-decision.dto.ts | 4 +- .../decision/notice-of-intent-decision.dto.ts | 12 +-- .../notice-of-intent-decision-v1.service.ts | 4 +- ...e-of-intent-decision-v2.controller.spec.ts | 6 +- ...notice-of-intent-decision-v2.controller.ts | 4 +- ...tice-of-intent-decision-v2.service.spec.ts | 2 +- .../notice-of-intent-decision-v2.service.ts | 12 ++- .../notice-of-intent-decision.dto.ts | 34 ++------ .../notice-of-intent-decision.entity.ts | 18 ++--- .../notice-of-intent-decision.module.ts | 8 +- ...e-of-intent-decision.automapper.profile.ts | 12 +++ ...0-update_dec_number_generation_for_nois.ts | 54 +++++++++++++ 29 files changed, 282 insertions(+), 356 deletions(-) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692902429290-update_dec_number_generation_for_nois.ts diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html index f6960c3d2b..3288a2c74e 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/pofo/pofo.component.html @@ -11,60 +11,23 @@ *ngIf="component.soilFillTypeToPlace === null || component.soilFillTypeToPlace === undefined" > -
-
Application TypeALC Portion of Fee
${a.type}$${a.fee}
- - - - - - - - - - - - - - - - - - - - - - -
Fill to be Placed
Volume - - {{ component.soilToPlaceVolume }} m3 - - -
- Area -
Note: 0.01 ha is 100m2
-
- - {{ component.soilToPlaceArea }} ha - - -
Maximum Depth - - {{ component.soilToPlaceMaximumDepth }} m - - -
Average Depth - {{ component.soilToPlaceAverageDepth }} m - -
+
+
Volume
+ {{ component.soilToPlaceVolume }} m3 + +
+
+
Area
+ {{ component.soilToPlaceArea }} m2 + +
+
+
Maximum Depth
+ {{ component.soilToPlaceMaximumDepth }} m + +
+
+
Average Depth
+ {{ component.soilToPlaceAverageDepth }} m +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html index 52a9c85e0e..160bf4b370 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-component/roso/roso.component.html @@ -9,57 +9,23 @@
{{ component.soilTypeRemoved }}
-
- - - - - - - - - - - - - - - - - - - - - - - -
Soil to be Removed
Volume - {{ component.soilToRemoveVolume }} m3 - -
Area - - {{ component.soilToRemoveArea }} ha - - -
Maximum Depth - {{ component.soilToRemoveMaximumDepth }} m - -
Average Depth - {{ component.soilToRemoveAverageDepth }} m - -
+
+
Volume
+ {{ component.soilToRemoveVolume }} m3 + +
+
+
Area
+ {{ component.soilToRemoveArea }} m2 + +
+
+
Maximum Depth
+ {{ component.soilToRemoveMaximumDepth }} m + +
+
+
Average Depth
+ {{ component.soilToRemoveAverageDepth }} m +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts index 2010e9202f..eac574e122 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts @@ -3,7 +3,7 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { Subject, takeUntil } from 'rxjs'; -import { DecisionDocumentDto } from '../../../../../services/application/decision/application-decision-v1/application-decision.dto'; +import { ApplicationDecisionDocumentDto } from '../../../../../services/application/decision/application-decision-v1/application-decision.dto'; import { ApplicationDecisionDto } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; import { ApplicationDecisionV2Service } from '../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; import { ToastService } from '../../../../../services/toast/toast.service'; @@ -25,11 +25,12 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { @Output() beforeDocumentUpload = new EventEmitter(); displayedColumns: string[] = ['type', 'fileName', 'source', 'visibilityFlags', 'uploadedAt', 'actions']; - documents: DecisionDocumentDto[] = []; + documents: ApplicationDecisionDocumentDto[] = []; private fileId = ''; @ViewChild(MatSort) sort!: MatSort; - dataSource: MatTableDataSource = new MatTableDataSource(); + dataSource: MatTableDataSource = + new MatTableDataSource(); constructor( private decisionService: ApplicationDecisionV2Service, @@ -67,11 +68,11 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { this.openFileDialog(); } - onEditFile(element: DecisionDocumentDto) { + onEditFile(element: ApplicationDecisionDocumentDto) { this.openFileDialog(element); } - private openFileDialog(existingDocument?: DecisionDocumentDto) { + private openFileDialog(existingDocument?: ApplicationDecisionDocumentDto) { if (this.decision) { this.dialog .open(DecisionDocumentUploadDialogComponent, { @@ -93,7 +94,7 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { } } - onDeleteFile(element: DecisionDocumentDto) { + onDeleteFile(element: ApplicationDecisionDocumentDto) { this.confirmationDialogService .openDialog({ body: 'Are you sure you want to delete the selected file?', diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index 7173b2f6aa..672049a355 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -122,6 +122,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { if (this.fileNumber) { this.loadData(); + this.setupSubscribers(); } } @@ -169,11 +170,9 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { this.decisionMakers = this.codes.decisionMakers; this.ceoCriterionItems = this.codes.ceoCriterion; this.linkedResolutionOutcomes = this.codes.linkedResolutionOutcomeTypes; - - await this.prepareDataForEdit(); } - private async prepareDataForEdit() { + private setupSubscribers() { this.decisionService.$decision .pipe(takeUntil(this.$destroy)) .pipe( @@ -219,15 +218,18 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { this.minDate = new Date(minDate); } - if (!this.isFirstDecision) { + if (this.isFirstDecision) { + this.form.controls.postDecision.disable(); + } else { this.form.controls.postDecision.addValidators([Validators.required]); - this.form.controls.decisionMaker.disable(); + this.form.controls.postDecision.enable(); this.onSelectPostDecision({ type: this.existingDecision.modifies ? PostDecisionType.Modification : PostDecisionType.Reconsideration, }); } } else { this.isFirstDecision = true; + this.form.controls.postDecision.disable(); } } else { this.resolutionYearControl.enable(); diff --git a/alcs-frontend/src/app/features/board/dialogs/noi-modification/create/create-noi-modification-dialog.component.ts b/alcs-frontend/src/app/features/board/dialogs/noi-modification/create/create-noi-modification-dialog.component.ts index 6df8001b33..3d433ace85 100644 --- a/alcs-frontend/src/app/features/board/dialogs/noi-modification/create/create-noi-modification-dialog.component.ts +++ b/alcs-frontend/src/app/features/board/dialogs/noi-modification/create/create-noi-modification-dialog.component.ts @@ -167,10 +167,12 @@ export class CreateNoiModificationDialogComponent implements OnInit, OnDestroy { async loadDecisions(fileNumber: string) { const decisions = await this.decisionService.fetchByFileNumber(fileNumber); if (decisions.length > 0) { - this.decisions = decisions.map((decision) => ({ - uuid: decision.uuid, - resolution: `#${decision.resolutionNumber}/${decision.resolutionYear}`, - })); + this.decisions = decisions + .filter((dec) => !dec.isDraft) + .map((decision) => ({ + uuid: decision.uuid, + resolution: `#${decision.resolutionNumber}/${decision.resolutionYear}`, + })); this.modifiesDecisions.enable(); } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-dialog/decision-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-dialog/decision-dialog.component.ts index 21aaa1d4fa..a3aa9ceb03 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-dialog/decision-dialog.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-dialog/decision-dialog.component.ts @@ -141,7 +141,7 @@ export class DecisionDialogComponent implements OnInit { outcome: existingDecision.outcome.code, decisionMaker: existingDecision.decisionMaker, decisionMakerName: existingDecision.decisionMakerName, - date: new Date(existingDecision.date), + date: new Date(existingDecision.date!), resolutionYear: existingDecision.resolutionYear, resolutionNumber: existingDecision.resolutionNumber.toString(10), auditDate: existingDecision.auditDate ? new Date(existingDecision.auditDate) : undefined, diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.html index 6992915501..4609dbca54 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.html @@ -48,7 +48,7 @@

Decision

Decision Date
- {{ decision.date | momentFormat }} + {{ decision.date! | momentFormat }}
Decision Outcome
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.ts index 22c087f4c1..309c2b87c5 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v1/decision-v1.component.ts @@ -72,7 +72,7 @@ export class DecisionV1Component implements OnInit, OnDestroy { onCreate() { let minDate = new Date(0); if (this.decisions.length > 0) { - minDate = new Date(this.decisions[this.decisions.length - 1].date); + minDate = new Date(this.decisions[this.decisions.length - 1].date!); } this.dialog @@ -101,7 +101,7 @@ export class DecisionV1Component implements OnInit, OnDestroy { const decisionIndex = this.decisions.indexOf(decision); let minDate = new Date(0); if (decisionIndex !== this.decisions.length - 1) { - minDate = new Date(this.decisions[this.decisions.length - 1].date); + minDate = new Date(this.decisions[this.decisions.length - 1].date!); } this.dialog .open(DecisionDialogComponent, { diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.html index f6960c3d2b..5e2e786e4e 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/pofo/pofo.component.html @@ -7,64 +7,25 @@
Type, origin and quality of fill proposed to be placed
{{ component.soilFillTypeToPlace }}
- +
-
- - - - - - - - - - - - - - - - - - - - - - - -
Fill to be Placed
Volume - - {{ component.soilToPlaceVolume }} m3 - - -
- Area -
Note: 0.01 ha is 100m2
-
- - {{ component.soilToPlaceArea }} ha - - -
Maximum Depth - - {{ component.soilToPlaceMaximumDepth }} m - - -
Average Depth - {{ component.soilToPlaceAverageDepth }} m - -
+
+
Volume
+ {{ component.soilToPlaceVolume }} m3 + +
+
+
Area
+ {{ component.soilToPlaceArea }} m2 + +
+
+
Maximum Depth
+ {{ component.soilToPlaceMaximumDepth }} m + +
+
+
Average Depth
+ {{ component.soilToPlaceAverageDepth }} m +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.html index 52a9c85e0e..160bf4b370 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-component/roso/roso.component.html @@ -9,57 +9,23 @@
{{ component.soilTypeRemoved }}
-
- - - - - - - - - - - - - - - - - - - - - - - -
Soil to be Removed
Volume - {{ component.soilToRemoveVolume }} m3 - -
Area - - {{ component.soilToRemoveArea }} ha - - -
Maximum Depth - {{ component.soilToRemoveMaximumDepth }} m - -
Average Depth - {{ component.soilToRemoveAverageDepth }} m - -
+
+
Volume
+ {{ component.soilToRemoveVolume }} m3 + +
+
+
Area
+ {{ component.soilToRemoveArea }} m2 + +
+
+
Maximum Depth
+ {{ component.soilToRemoveMaximumDepth }} m + +
+
+
Average Depth
+ {{ component.soilToRemoveAverageDepth }} m +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts index 70aa70d30d..6dd0e805af 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts @@ -3,9 +3,11 @@ import { MatDialog } from '@angular/material/dialog'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { Subject, takeUntil } from 'rxjs'; -import { DecisionDocumentDto } from '../../../../../services/application/decision/application-decision-v1/application-decision.dto'; import { NoticeOfIntentDecisionV2Service } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; -import { NoticeOfIntentDecisionDto } from '../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; +import { + NoticeOfIntentDecisionDocumentDto, + NoticeOfIntentDecisionDto, +} from '../../../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; import { ToastService } from '../../../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DecisionDocumentUploadDialogComponent } from '../decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component'; @@ -25,11 +27,11 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { @Output() beforeDocumentUpload = new EventEmitter(); displayedColumns: string[] = ['type', 'fileName', 'source', 'visibilityFlags', 'uploadedAt', 'actions']; - documents: DecisionDocumentDto[] = []; + documents: NoticeOfIntentDecisionDocumentDto[] = []; private fileId = ''; @ViewChild(MatSort) sort!: MatSort; - dataSource: MatTableDataSource = new MatTableDataSource(); + dataSource = new MatTableDataSource(); constructor( private decisionService: NoticeOfIntentDecisionV2Service, @@ -67,11 +69,11 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { this.openFileDialog(); } - onEditFile(element: DecisionDocumentDto) { + onEditFile(element: NoticeOfIntentDecisionDocumentDto) { this.openFileDialog(element); } - private openFileDialog(existingDocument?: DecisionDocumentDto) { + private openFileDialog(existingDocument?: NoticeOfIntentDecisionDocumentDto) { if (this.decision) { this.dialog .open(DecisionDocumentUploadDialogComponent, { @@ -93,7 +95,7 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { } } - onDeleteFile(element: DecisionDocumentDto) { + onDeleteFile(element: NoticeOfIntentDecisionDocumentDto) { this.confirmationDialogService .openDialog({ body: 'Are you sure you want to delete the selected file?', diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index 57efe1cd61..ef7853468e 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -67,9 +67,7 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView .subscribe(async (noticeOfIntent) => { if (noticeOfIntent) { this.noticeOfIntent = noticeOfIntent; - - //TODO:What? - //this.noticeOfIntent.submittedApplication = await this.submissionService.fetchSubmission(noticeOfIntent.fileNumber); + this.noticeOfIntentSubmission = await this.submissionService.fetchSubmission(noticeOfIntent.fileNumber); } }); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html index 06250cfd89..8178384585 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html @@ -65,13 +65,25 @@

Resolution

+
+ Decision Maker* + + CEO Delegate + CEO + +
+ + + Decision Maker Name + + + Resolution > -
- - Decision Maker - - -
- (null, [Validators.required]), date: new FormControl(undefined, [Validators.required]), decisionMaker: new FormControl(null, [Validators.required]), + decisionMakerName: new FormControl(null), postDecision: new FormControl(null), resolutionNumber: this.resolutionNumberControl, resolutionYear: this.resolutionYearControl, @@ -94,11 +95,12 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { ngOnInit(): void { this.resolutionYearControl.disable(); - this.setYear(); + this.populateResolutionYears(); this.extractAndPopulateQueryParams(); if (this.fileNumber) { + this.setupSubscribers(); this.loadData(); } } @@ -118,7 +120,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { } } - private setYear() { + private populateResolutionYears() { const year = moment('1974'); const currentYear = moment().year(); while (year.year() <= currentYear) { @@ -143,15 +145,14 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { await this.decisionService.loadDecisions(this.fileNumber); this.codes = await this.decisionService.fetchCodes(); + this.modificationService.fetchByFileNumber(this.fileNumber); this.outcomes = this.codes.outcomes; - - await this.prepareDataForEdit(); } - private async prepareDataForEdit() { + private setupSubscribers() { this.decisionService.$decision - .pipe(takeUntil(this.$destroy)) .pipe(combineLatestWith(this.modificationService.$modifications, this.decisionService.$decisions)) + .pipe(takeUntil(this.$destroy)) .subscribe(([decision, modifications, decisions]) => { if (decision) { this.existingDecision = decision; @@ -188,12 +189,15 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { this.minDate = new Date(minDate); } - if (!this.isFirstDecision) { + if (this.isFirstDecision) { + this.form.controls.postDecision.disable(); + } else { + this.form.controls.postDecision.enable(); this.form.controls.postDecision.addValidators([Validators.required]); - this.form.controls.decisionMaker.disable(); } } else { this.isFirstDecision = true; + this.form.controls.postDecision.disable(); } } else { this.resolutionYearControl.enable(); @@ -205,24 +209,24 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { modifications: NoticeOfIntentModificationDto[], existingDecision?: NoticeOfIntentDecisionDto ) { - this.postDecisions = modifications - .filter( - (modification) => - (existingDecision && existingDecision.modifies?.uuid === modification.uuid) || - (modification.reviewOutcome.code === 'PRC' && !modification.resultingDecision) - ) - .map((modification, index) => ({ - label: `Modification Request #${modifications.length - index} - ${modification.modifiesDecisions - .map((dec) => `#${dec.resolutionNumber}/${dec.resolutionYear}`) - .join(', ')}`, - uuid: modification.uuid, - })); + const proceededModifications = modifications.filter( + (modification) => + (existingDecision && existingDecision.modifies?.uuid === modification.uuid) || + (modification.reviewOutcome.code === 'PRC' && !modification.resultingDecision) + ); + this.postDecisions = proceededModifications.map((modification, index) => ({ + label: `Modification Request #${modifications.length - index} - ${modification.modifiesDecisions + .map((dec) => `#${dec.resolutionNumber}/${dec.resolutionYear}`) + .join(', ')}`, + uuid: modification.uuid, + })); } private patchFormWithExistingData(existingDecision: NoticeOfIntentDecisionDto) { this.form.patchValue({ outcome: existingDecision.outcome.code, decisionMaker: existingDecision.decisionMaker, + decisionMakerName: existingDecision.decisionMakerName, date: existingDecision.date ? new Date(existingDecision.date) : undefined, resolutionYear: existingDecision.resolutionYear, resolutionNumber: existingDecision.resolutionNumber?.toString(10) || undefined, @@ -299,6 +303,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { date, outcome, decisionMaker, + decisionMakerName, resolutionNumber, resolutionYear, auditDate, @@ -314,6 +319,7 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { date: formatDateForApi(date!), resolutionNumber: parseInt(resolutionNumber!), resolutionYear: resolutionYear!, + decisionMakerName, auditDate: auditDate ? formatDateForApi(auditDate) : auditDate, outcomeCode: outcome!, fileNumber: this.fileNumber, diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html index 481d7d9ba9..a8243709e2 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-v2.component.html @@ -23,15 +23,15 @@
Modified By:  - {{ decision.modifiedBy?.join(', ') }} - N/A + {{ decision.modifiedByResolutions?.join(', ') }} + N/A
-

Decision #{{ i }}

+

Decision #{{ decisions.length - i }}

calendar_month @@ -79,7 +79,7 @@

Resolution

Decision Date
- {{ decision.date | momentFormat }} + {{ decision.date | momentFormat }}
@@ -93,6 +93,14 @@

Resolution

{{ decision.outcome.label }} {{ decision.isSubjectToConditions ? '- Subject to Conditions' : '' }}
+
+
Decision Maker Name
+ {{ decision.decisionMakerName }} + +
+
Decision Description
{{ decision.decisionDescription }} @@ -124,9 +132,9 @@

Resolution

Documents

- - - +
+ +
@@ -149,8 +157,8 @@
{{ component.noticeOfIntentDecisionComponentType?.label }}
[component]="component" [fillRow]="true" (saveAlrArea)="onSaveAlrArea(decision.uuid, component.uuid, $event)" - > + > + {{ component.noticeOfIntentDecisionComponentType?.label }} [component]="component" [fillRow]="true" (saveAlrArea)="onSaveAlrArea(decision.uuid, component.uuid, $event)" - > + > + {{ component.noticeOfIntentDecisionComponentType?.label }} [component]="component" [fillRow]="true" (saveAlrArea)="onSaveAlrArea(decision.uuid, component.uuid, $event)" - > + > +
diff --git a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.ts b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.ts index 4eebf2cea9..facf0fe847 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.ts @@ -152,7 +152,7 @@ export class OverviewComponent implements OnInit, OnDestroy { htmlText: `Decision #${decisions.length - index} Made${ decisions.length - 1 === index ? ` - Active Days: ${noticeOfIntent.activeDays}` : '' }`, - startDate: new Date(decision.date + SORTING_ORDER.DECISION_MADE), + startDate: new Date(decision.date! + SORTING_ORDER.DECISION_MADE), isFulfilled: true, }); } diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v1/application-decision.dto.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v1/application-decision.dto.ts index 3f2184de92..b8c93d0b06 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v1/application-decision.dto.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v1/application-decision.dto.ts @@ -47,7 +47,7 @@ export interface ApplicationDecisionDto { chairReviewRequired: boolean; chairReviewOutcome?: ChairReviewOutcomeCodeDto | null; applicationFileNumber: string; - documents: DecisionDocumentDto[]; + documents: ApplicationDecisionDocumentDto[]; isTimeExtension?: boolean | null; isOther?: boolean | null; modifies?: LinkedResolutionDto; @@ -62,7 +62,7 @@ export interface LinkedResolutionDto { linkedResolutions: string[]; } -export interface DecisionDocumentDto { +export interface ApplicationDecisionDocumentDto { uuid: string; fileName: string; mimeType: string; diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.dto.ts index e508c61b64..791d4d37cf 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.dto.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/decision/notice-of-intent-decision.dto.ts @@ -21,14 +21,9 @@ export interface UpdateNoticeOfIntentDecisionDto { export interface CreateNoticeOfIntentDecisionDto extends UpdateNoticeOfIntentDecisionDto { date: number; - outcomeCode?: string; resolutionNumber?: number; resolutionYear: number; fileNumber: string; - decisionMaker?: string; - decisionMakerName?: string; - auditDate?: number | null; - modifiesUuid?: string; } export interface LinkedResolutionDto { @@ -38,14 +33,14 @@ export interface LinkedResolutionDto { export interface NoticeOfIntentDecisionDto { uuid: string; - date: number; + date: number | null; createdAt: Date; outcome: NoticeOfIntentDecisionOutcomeCodeDto; resolutionNumber: number; resolutionYear: number; auditDate?: number | null; - decisionMaker: string; - decisionMakerName?: string; + decisionMaker: string | null; + decisionMakerName: string | null; isSubjectToConditions: boolean | null; isDraft: boolean; wasReleased: boolean; @@ -57,6 +52,7 @@ export interface NoticeOfIntentDecisionDto { documents: NoticeOfIntentDecisionDocumentDto[]; modifies?: LinkedResolutionDto; modifiedBy?: LinkedResolutionDto[]; + modifiedByResolutions?: string[]; components: NoticeOfIntentDecisionComponentDto[]; conditions: NoticeOfIntentDecisionConditionDto[]; } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.ts index 153a6d71ad..21a36ca7b7 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v1/notice-of-intent-decision-v1.service.ts @@ -211,8 +211,8 @@ export class NoticeOfIntentDecisionV1Service { modifies: NoticeOfIntentModification | undefined | null, ) { const decision = new NoticeOfIntentDecision({ - outcome: await this.getOutcomeByCode(createDto.outcomeCode), - date: new Date(createDto.date), + outcome: await this.getOutcomeByCode(createDto.outcomeCode ?? 'APPR'), + date: createDto.date ? new Date(createDto.date) : null, resolutionNumber: createDto.resolutionNumber, resolutionYear: createDto.resolutionYear, decisionMaker: filterUndefined(createDto.decisionMaker, undefined), diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts index 82af45ac3f..90d6fe17e7 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts @@ -104,11 +104,11 @@ describe('NoticeOfIntentDecisionV2Controller', () => { }); it('should get all for notice of intent', async () => { - mockDecisionService.getByAppFileNumber.mockResolvedValue([mockDecision]); + mockDecisionService.getByFileNumber.mockResolvedValue([mockDecision]); const result = await controller.getByFileNumber('fake-number'); - expect(mockDecisionService.getByAppFileNumber).toBeCalledTimes(1); + expect(mockDecisionService.getByFileNumber).toBeCalledTimes(1); expect(result[0].uuid).toStrictEqual(mockDecision.uuid); }); @@ -160,7 +160,7 @@ describe('NoticeOfIntentDecisionV2Controller', () => { it('should update the decision', async () => { mockNoticeOfIntentService.getFileNumber.mockResolvedValue('file-number'); mockDecisionService.get.mockResolvedValue(new NoticeOfIntentDecision()); - mockDecisionService.getByAppFileNumber.mockResolvedValue([ + mockDecisionService.getByFileNumber.mockResolvedValue([ new NoticeOfIntentDecision(), ]); mockDecisionService.update.mockResolvedValue(mockDecision); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts index 5e402a190c..3c9f9ad092 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts @@ -52,7 +52,7 @@ export class NoticeOfIntentDecisionV2Controller { @Param('fileNumber') fileNumber, ): Promise { const decisions = - await this.noticeOfIntentDecisionV2Service.getByAppFileNumber(fileNumber); + await this.noticeOfIntentDecisionV2Service.getByFileNumber(fileNumber); return await this.mapper.mapArrayAsync( decisions, @@ -137,8 +137,6 @@ export class NoticeOfIntentDecisionV2Controller { modifies = null; } - const decision = await this.noticeOfIntentDecisionV2Service.get(uuid); - const updatedDecision = await this.noticeOfIntentDecisionV2Service.update( uuid, updateDto, diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts index 2e1109b7f5..e0d224b6ab 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts @@ -155,7 +155,7 @@ describe('NoticeOfIntentDecisionV2Service', () => { }); it('should get decisions by notice of intent', async () => { - const result = await service.getByAppFileNumber( + const result = await service.getByFileNumber( mockNoticeOfIntent.fileNumber, ); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts index e8e979b964..151e734c6a 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts @@ -76,7 +76,7 @@ export class NoticeOfIntentDecisionV2Service { }); } - async getByAppFileNumber(number: string) { + async getByFileNumber(number: string) { const noticeOfIntent = await this.noticeOfIntentService.getByFileNumber( number, ); @@ -206,6 +206,8 @@ export class NoticeOfIntentDecisionV2Service { existingDecision.auditDate = formatIncomingDate(updateDto.auditDate); existingDecision.modifies = modifies; + existingDecision.decisionMaker = updateDto.decisionMaker; + existingDecision.decisionMakerName = updateDto.decisionMakerName; existingDecision.resolutionNumber = updateDto.resolutionNumber; existingDecision.resolutionYear = updateDto.resolutionYear; existingDecision.isSubjectToConditions = updateDto.isSubjectToConditions; @@ -281,10 +283,12 @@ export class NoticeOfIntentDecisionV2Service { } const decision = new NoticeOfIntentDecision({ - outcome: await this.getOutcomeByCode(createDto.outcomeCode), - date: new Date(createDto.date), + outcome: await this.getOutcomeByCode(createDto.outcomeCode ?? 'APPR'), + date: createDto.date ? new Date(createDto.date) : null, resolutionNumber: createDto.resolutionNumber, resolutionYear: createDto.resolutionYear, + decisionMaker: createDto.decisionMaker, + decisionMakerName: createDto.decisionMakerName, auditDate: createDto.auditDate ? new Date(createDto.auditDate) : undefined, @@ -550,7 +554,7 @@ export class NoticeOfIntentDecisionV2Service { private async updateDecisionDates( noticeOfIntentDecision: NoticeOfIntentDecision, ) { - const existingDecisions = await this.getByAppFileNumber( + const existingDecisions = await this.getByFileNumber( noticeOfIntentDecision.noticeOfIntent.fileNumber, ); const releasedDecisions = existingDecisions.filter( diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts index 23db0752ad..cd1bab37ad 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.dto.ts @@ -88,24 +88,6 @@ export class CreateNoticeOfIntentDecisionDto extends UpdateNoticeOfIntentDecisio @IsString() fileNumber: string; - @IsNumber() - date: number; - - @IsString() - outcomeCode: string; - - @IsNumber() - @IsOptional() - resolutionNumber?: number; - - @IsNumber() - @IsOptional() - resolutionYear?: number; - - @IsUUID() - @IsOptional() - modifiesUuid?: string; - @IsOptional() override isDraft = true; } @@ -122,11 +104,9 @@ export class NoticeOfIntentDecisionDto { @AutoMap() fileNumber: string; - @AutoMap() date: number; - - @AutoMap() auditDate: number; + createdAt: number; @AutoMap() resolutionNumber: string; @@ -134,6 +114,12 @@ export class NoticeOfIntentDecisionDto { @AutoMap() resolutionYear: number; + @AutoMap(() => String) + decisionMaker: string | null; + + @AutoMap(() => String) + decisionMakerName: string | null; + @AutoMap(() => Boolean) isSubjectToConditions: boolean | null; @@ -155,12 +141,6 @@ export class NoticeOfIntentDecisionDto { @AutoMap(() => [NoticeOfIntentDecisionDocumentDto]) documents: NoticeOfIntentDecisionDocumentDto[]; - @AutoMap(() => String) - decisionMaker?: string | null; - - @AutoMap(() => String) - decisionMakerName?: string | null; - @AutoMap(() => NoticeOfIntentDecisionOutcomeCodeDto) outcome?: NoticeOfIntentDecisionOutcomeCodeDto | null; diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts index c3beae8601..ccc808eabb 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.entity.ts @@ -40,30 +40,28 @@ export class NoticeOfIntentDecision extends Base { auditDate: Date | null; @AutoMap(() => NoticeOfIntentDecisionOutcome) - @ManyToOne(() => NoticeOfIntentDecisionOutcome, { - nullable: false, - }) + @ManyToOne(() => NoticeOfIntentDecisionOutcome, { nullable: false }) outcome: NoticeOfIntentDecisionOutcome; - @AutoMap() + @AutoMap(() => String) @Column() outcomeCode: string; - @AutoMap() + @AutoMap(() => Number) @Column({ type: 'int4', nullable: true }) - resolutionNumber: number; + resolutionNumber: number | null; @AutoMap() @Column({ type: 'smallint' }) resolutionYear: number; - @AutoMap() + @AutoMap(() => String) @Column({ type: 'varchar', nullable: true }) - decisionMaker?: string; + decisionMaker?: string | null; - @AutoMap() + @AutoMap(() => String) @Column({ type: 'text', nullable: true }) - decisionMakerName?: string; + decisionMakerName?: string | null; @AutoMap() @Column({ type: 'boolean', default: false }) diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts index e1cb3f7c86..b9894bcaf0 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -7,18 +7,20 @@ import { CardModule } from '../card/card.module'; import { NoticeOfIntentSubmissionStatusModule } from '../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentModule } from '../notice-of-intent/notice-of-intent.module'; import { NoticeOfIntentDecisionComponentType } from './notice-of-intent-decision-component/notice-of-intent-decision-component-type.entity'; +import { NoticeOfIntentDecisionComponentController } from './notice-of-intent-decision-component/notice-of-intent-decision-component.controller'; import { NoticeOfIntentDecisionComponent } from './notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; import { NoticeOfIntentDecisionComponentService } from './notice-of-intent-decision-component/notice-of-intent-decision-component.service'; import { NoticeOfIntentDecisionConditionType } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition-code.entity'; +import { NoticeOfIntentDecisionConditionController } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition.controller'; import { NoticeOfIntentDecisionCondition } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition.entity'; import { NoticeOfIntentDecisionConditionService } from './notice-of-intent-decision-condition/notice-of-intent-decision-condition.service'; import { NoticeOfIntentDecisionDocument } from './notice-of-intent-decision-document/notice-of-intent-decision-document.entity'; import { NoticeOfIntentDecisionOutcome } from './notice-of-intent-decision-outcome.entity'; +import { NoticeOfIntentDecisionV1Controller } from './notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller'; +import { NoticeOfIntentDecisionV1Service } from './notice-of-intent-decision-v1/notice-of-intent-decision-v1.service'; import { NoticeOfIntentDecisionV2Controller } from './notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller'; import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2/notice-of-intent-decision-v2.service'; import { NoticeOfIntentDecision } from './notice-of-intent-decision.entity'; -import { NoticeOfIntentDecisionV1Controller } from './notice-of-intent-decision-v1/notice-of-intent-decision-v1.controller'; -import { NoticeOfIntentDecisionV1Service } from './notice-of-intent-decision-v1/notice-of-intent-decision-v1.service'; import { NoticeOfIntentModificationOutcomeType } from './notice-of-intent-modification/notice-of-intent-modification-outcome-type/notice-of-intent-modification-outcome-type.entity'; import { NoticeOfIntentModificationController } from './notice-of-intent-modification/notice-of-intent-modification.controller'; import { NoticeOfIntentModification } from './notice-of-intent-modification/notice-of-intent-modification.entity'; @@ -56,6 +58,8 @@ import { NoticeOfIntentModificationService } from './notice-of-intent-modificati NoticeOfIntentDecisionV1Controller, NoticeOfIntentDecisionV2Controller, NoticeOfIntentModificationController, + NoticeOfIntentDecisionComponentController, + NoticeOfIntentDecisionConditionController, ], exports: [NoticeOfIntentModificationService, NoticeOfIntentDecisionV1Service], }) diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts index 883aa72dc6..43eb63d3ec 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts @@ -32,6 +32,8 @@ import { NoticeOfIntentModificationOutcomeCodeDto, } from '../../alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto'; import { NoticeOfIntentModification } from '../../alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.entity'; +import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; +import { NoticeOfIntentStatusDto } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; @Injectable() @@ -60,6 +62,10 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { (ad) => ad.date, mapFrom((a) => a.date?.getTime()), ), + forMember( + (ad) => ad.createdAt, + mapFrom((a) => a.createdAt?.getTime()), + ), forMember( (ad) => ad.auditDate, mapFrom((a) => a.auditDate?.getTime()), @@ -113,6 +119,12 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { ), ); + createMap( + mapper, + NoticeOfIntentSubmissionStatusType, + NoticeOfIntentStatusDto, + ); + createMap( mapper, NoticeOfIntentDecisionComponent, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692902429290-update_dec_number_generation_for_nois.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692902429290-update_dec_number_generation_for_nois.ts new file mode 100644 index 0000000000..0953947b97 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692902429290-update_dec_number_generation_for_nois.ts @@ -0,0 +1,54 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class updateDecNumberGenerationForNois1692902429290 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE OR REPLACE FUNCTION alcs.generate_next_resolution_number(p_resolution_year integer) + RETURNS integer + LANGUAGE plpgsql + AS $function$ + + declare next_resolution_number integer; + BEGIN + select + row_num into next_resolution_number + from + ( + select + coalesce(resolution_number, 0) as resolution_number, + row_number() over ( + order by resolution_number) row_num + from + ( + SELECT + resolution_number, resolution_year, audit_deleted_date_at + FROM + alcs.application_decision + UNION + SELECT + resolution_number, resolution_year, audit_deleted_date_at + FROM + alcs.notice_of_intent_decision) rows + where + resolution_year = p_resolution_year + and audit_deleted_date_at is null + ) z + where + row_num != resolution_number + order by + row_num offset 0 row fetch next 1 row only; + + return coalesce(next_resolution_number, 1); + END; + $function$ + ; + `, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + //Nope + } +} From eccc56d7a6b99c2ffc9252eb536081d0c491d463 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 24 Aug 2023 15:54:04 -0700 Subject: [PATCH 304/954] Add update file functionality for NOI/Application Decisisions * Add ability to update file name for existing documents * Add service/controller/test functions --- .../decision-document-upload-dialog.component.ts | 8 +++++++- .../decision-document-upload-dialog.component.ts | 8 +++++++- .../application-decision-v2.service.spec.ts | 13 +++++++++++++ .../application-decision-v2.service.ts | 13 +++++++++++++ .../notice-of-intent-decision-v2.service.spec.ts | 13 +++++++++++++ .../notice-of-intent-decision-v2.service.ts | 13 +++++++++++++ .../application-decision-v2.controller.spec.ts | 9 +++++++++ .../application-decision-v2.controller.ts | 13 +++++++++++++ .../application-decision-v2.service.spec.ts | 7 +++++++ .../application-decision-v2.service.ts | 8 ++++++++ ...tice-of-intent-decision-v2.controller.spec.ts | 9 +++++++++ .../notice-of-intent-decision-v2.controller.ts | 16 ++++++++++++++++ .../notice-of-intent-decision-v2.service.spec.ts | 7 +++++++ .../notice-of-intent-decision-v2.service.ts | 8 ++++++++ 14 files changed, 143 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts index aeb4b66027..127d31347b 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts @@ -58,13 +58,19 @@ export class DecisionDocumentUploadDialogComponent implements OnInit { async onSubmit() { const file = this.pendingFile; if (file) { - const renamedFile = new File([file], this.name.getRawValue() ?? file.name); + const renamedFile = new File([file], this.name.value ?? file.name); this.isSaving = true; if (this.data.existingDocument) { await this.decisionService.deleteFile(this.data.decisionUuid, this.data.existingDocument.uuid); } await this.decisionService.uploadFile(this.data.decisionUuid, renamedFile); + this.dialog.close(true); + this.isSaving = false; + } else if (this.data.existingDocument) { + this.isSaving = true; + await this.decisionService.updateFile(this.data.decisionUuid, this.data.existingDocument.uuid, this.name.value!); + this.dialog.close(true); this.isSaving = false; } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts index 4682493a1b..8a104ea557 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts @@ -58,13 +58,19 @@ export class DecisionDocumentUploadDialogComponent implements OnInit { async onSubmit() { const file = this.pendingFile; if (file) { - const renamedFile = new File([file], this.name.getRawValue() ?? file.name); + const renamedFile = new File([file], this.name.value ?? file.name); this.isSaving = true; if (this.data.existingDocument) { await this.decisionService.deleteFile(this.data.decisionUuid, this.data.existingDocument.uuid); } await this.decisionService.uploadFile(this.data.decisionUuid, renamedFile); + this.dialog.close(true); + this.isSaving = false; + } else if (this.data.existingDocument) { + this.isSaving = true; + await this.decisionService.updateFile(this.data.decisionUuid, this.data.existingDocument.uuid, this.name.value!); + this.dialog.close(true); this.isSaving = false; } diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.spec.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.spec.ts index 1bee660d82..9af77d3153 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.spec.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.spec.ts @@ -172,6 +172,19 @@ describe('ApplicationDecisionV2Service', () => { expect(toastService.showErrorToast).toHaveBeenCalledTimes(1); }); + it('should make an http patch call for update file', async () => { + httpClient.patch.mockReturnValue( + of([ + { + fileNumber: '1', + }, + ]) + ); + await service.updateFile('', '', ''); + + expect(httpClient.patch).toHaveBeenCalledTimes(1); + }); + it('should show a toast warning when uploading a file thats too large', async () => { const file = createMock(); Object.defineProperty(file, 'size', { value: environment.maxFileSize + 1 }); diff --git a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts index 5fc5d2b385..51133aa130 100644 --- a/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts +++ b/alcs-frontend/src/app/services/application/decision/application-decision-v2/application-decision-v2.service.ts @@ -117,6 +117,19 @@ export class ApplicationDecisionV2Service { } } + async updateFile(decisionUuid: string, documentUuid: string, fileName: string) { + try { + await firstValueFrom( + this.http.patch(`${this.url}/${decisionUuid}/file/${documentUuid}`, { + fileName, + }) + ); + this.toastService.showSuccessToast('File updated'); + } catch (err) { + this.toastService.showErrorToast('Failed to update file'); + } + } + async deleteFile(decisionUuid: string, documentUuid: string) { const url = `${this.url}/${decisionUuid}/file/${documentUuid}`; return await firstValueFrom(this.http.delete<{ url: string }>(url)); diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.spec.ts index bdc6ee227c..de9ed5c6ef 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.spec.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.spec.ts @@ -182,6 +182,19 @@ describe('NoticeOfIntentDecisionV2Service', () => { expect(httpClient.post).toHaveBeenCalledTimes(0); }); + it('should make an http patch call for update file', async () => { + httpClient.patch.mockReturnValue( + of([ + { + fileNumber: '1', + }, + ]) + ); + await service.updateFile('', '', ''); + + expect(httpClient.patch).toHaveBeenCalledTimes(1); + }); + it('should make an http delete when deleting a file', async () => { httpClient.delete.mockReturnValue( of({ diff --git a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.ts b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.ts index 86b38bb0a6..89075f04ef 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service.ts @@ -102,6 +102,19 @@ export class NoticeOfIntentDecisionV2Service { return res; } + async updateFile(decisionUuid: string, documentUuid: string, fileName: string) { + try { + await firstValueFrom( + this.http.patch(`${this.url}/${decisionUuid}/file/${documentUuid}`, { + fileName, + }) + ); + this.toastService.showSuccessToast('File updated'); + } catch (err) { + this.toastService.showErrorToast('Failed to update file'); + } + } + async downloadFile(decisionUuid: string, documentUuid: string, fileName: string, isInline = true) { const url = `${this.url}/${decisionUuid}/file/${documentUuid}`; const finalUrl = isInline ? `${url}/open` : `${url}/download`; diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts index be4f628954..47fbe6ff88 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts @@ -241,6 +241,15 @@ describe('ApplicationDecisionV2Controller', () => { expect(res.url).toEqual(fakeUrl); }); + it('should call through for updating the file', async () => { + mockDecisionService.updateDocument.mockResolvedValue({} as any); + await controller.updateDocument('fake-uuid', 'document-uuid', { + fileName: '', + }); + + expect(mockDecisionService.updateDocument).toBeCalledTimes(1); + }); + it('should call through for getting open url', async () => { const fakeUrl = 'fake-url'; mockDecisionService.getDownloadUrl.mockResolvedValue(fakeUrl); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts index b8aca8ebaa..8dfc2386b4 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts @@ -241,6 +241,19 @@ export class ApplicationDecisionV2Controller { }; } + @Patch('/:uuid/file/:documentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async updateDocument( + @Param('uuid') decisionUuid: string, + @Param('documentUuid') documentUuid: string, + @Body() body: { fileName: string }, + ) { + await this.appDecisionService.updateDocument(documentUuid, body.fileName); + return { + uploaded: true, + }; + } + @Get('/:uuid/file/:fileUuid/download') @UserRoles(...ANY_AUTH_ROLE) async getDownloadUrl( diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts index bfc1507ad7..9f17506abc 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.spec.ts @@ -610,6 +610,13 @@ describe('ApplicationDecisionV2Service', () => { expect(mockDocumentService.softRemove).not.toHaveBeenCalled(); }); + it('should call through to document service for update', async () => { + mockDocumentService.update.mockResolvedValue({} as any); + + await service.updateDocument('document-uuid', 'file-name'); + expect(mockDocumentService.update).toHaveBeenCalledTimes(1); + }); + it('should call through to document service for download', async () => { const downloadUrl = 'download-url'; mockDocumentService.getDownloadUrl.mockResolvedValue(downloadUrl); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts index d555f09e30..f5cdfadbc1 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service.ts @@ -735,4 +735,12 @@ export class ApplicationDecisionV2Service { } return decisionDocument; } + + async updateDocument(documentUuid: string, fileName: string) { + const document = await this.getDecisionDocumentOrFail(documentUuid); + await this.documentService.update(document.document, { + fileName, + source: DOCUMENT_SOURCE.ALC, + }); + } } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts index 90d6fe17e7..2f0906fa24 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts @@ -224,6 +224,15 @@ describe('NoticeOfIntentDecisionV2Controller', () => { expect(res.url).toEqual(fakeUrl); }); + it('should call through for updating the file', async () => { + mockDecisionService.updateDocument.mockResolvedValue({} as any); + await controller.updateDocument('fake-uuid', 'document-uuid', { + fileName: '', + }); + + expect(mockDecisionService.updateDocument).toBeCalledTimes(1); + }); + it('should call through for getting open url', async () => { const fakeUrl = 'fake-url'; mockDecisionService.getDownloadUrl.mockResolvedValue(fakeUrl); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts index 3c9f9ad092..f243e2211f 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts @@ -174,6 +174,22 @@ export class NoticeOfIntentDecisionV2Controller { }; } + @Patch('/:uuid/file/:documentUuid') + @UserRoles(...ANY_AUTH_ROLE) + async updateDocument( + @Param('uuid') decisionUuid: string, + @Param('documentUuid') documentUuid: string, + @Body() body: { fileName: string }, + ) { + await this.noticeOfIntentDecisionV2Service.updateDocument( + documentUuid, + body.fileName, + ); + return { + uploaded: true, + }; + } + @Get('/:uuid/file/:fileUuid/download') @UserRoles(...ANY_AUTH_ROLE) async getDownloadUrl( diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts index e0d224b6ab..078c77f30d 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.spec.ts @@ -488,6 +488,13 @@ describe('NoticeOfIntentDecisionV2Service', () => { expect(mockDocumentService.softRemove).not.toHaveBeenCalled(); }); + it('should call through to document service for update', async () => { + mockDocumentService.update.mockResolvedValue({} as any); + + await service.updateDocument('document-uuid', 'file-name'); + expect(mockDocumentService.update).toHaveBeenCalledTimes(1); + }); + it('should call through to document service for download', async () => { const downloadUrl = 'download-url'; mockDocumentService.getDownloadUrl.mockResolvedValue(downloadUrl); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts index 151e734c6a..98c7b0b662 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts @@ -617,4 +617,12 @@ export class NoticeOfIntentDecisionV2Service { } return decisionDocument; } + + async updateDocument(documentUuid: string, fileName: string) { + const document = await this.getDecisionDocumentOrFail(documentUuid); + await this.documentService.update(document.document, { + fileName, + source: DOCUMENT_SOURCE.ALC, + }); + } } From 8150d2932de900d808029013d759a68be9c7f94d Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Thu, 24 Aug 2023 16:22:28 -0700 Subject: [PATCH 305/954] Pass correct argument to fix error in noi cancel (#911) --- .../view-notice-of-intent-submission.component.html | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.html index 9c4f716361..318bfe8d9f 100644 --- a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.component.html @@ -67,12 +67,7 @@

Applicant Submission

-
From e8a6fa2d9afa51fdc0722a7c520b215941fc1f61 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 24 Aug 2023 16:39:11 -0700 Subject: [PATCH 306/954] Change updated flags to re-calculate when we save the submission --- .../alcs-edit-submission.component.ts | 38 ++++++++++++------- .../additional-information.component.html | 5 ++- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts index ec9de0dc23..553bf9cf93 100644 --- a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts @@ -39,7 +39,7 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView $noiSubmission = new BehaviorSubject(undefined); $noiDocuments = new BehaviorSubject([]); noiSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; - originalSubmissionUuid = ''; + originalSubmission: NoticeOfIntentSubmissionDetailedDto | undefined; steps = EditNoiSteps; expandedParcelUuid?: string; @@ -72,6 +72,13 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView ngOnInit(): void { this.expandedParcelUuid = undefined; + + this.$noiSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + if (submission) { + this.noiSubmission = submission; + this.calculateUpdatedFields(); + } + }); } ngAfterViewInit(): void { @@ -111,18 +118,8 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView private async loadOriginalSubmission(fileId: string) { const originalSubmission = await this.noticeOfIntentSubmissionService.getByFileId(fileId); if (originalSubmission) { - this.originalSubmissionUuid = originalSubmission?.uuid; - - const diffResult = getDiff(originalSubmission, this.noiSubmission); - const changedFields = new Set(); - for (const diff of diffResult) { - const fullPath = diff.path.join('.'); - if (!fullPath.toLowerCase().includes('uuid')) { - changedFields.add(diff.path.join('.')); - changedFields.add(diff.path[0].toString()); - } - } - this.updatedFields = [...changedFields.keys()]; + this.originalSubmission = originalSubmission; + this.calculateUpdatedFields(); } } @@ -130,7 +127,7 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView if (!this.noiSubmission) { this.overlayService.showSpinner(); this.noiSubmission = await this.noticeOfIntentSubmissionDraftService.getByFileId(fileId); - this.loadOriginalSubmission(fileId); + await this.loadOriginalSubmission(fileId); const documents = await this.noticeOfIntentDocumentService.getByFileId(fileId); if (documents) { this.$noiDocuments.next(documents); @@ -227,4 +224,17 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView } }); } + + private calculateUpdatedFields() { + const diffResult = getDiff(this.originalSubmission, this.noiSubmission); + const changedFields = new Set(); + for (const diff of diffResult) { + const fullPath = diff.path.join('.'); + if (!fullPath.toLowerCase().includes('uuid')) { + changedFields.add(diff.path.join('.')); + changedFields.add(diff.path[0].toString()); + } + } + this.updatedFields = [...changedFields.keys()]; + } } diff --git a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/additional-information/additional-information.component.html b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/additional-information/additional-information.component.html index 4c4ae5349f..431948076b 100644 --- a/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/additional-information/additional-information.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/notice-of-intent-details/additional-information/additional-information.component.html @@ -14,7 +14,10 @@
-
The total floor area (m2) of the proposed structure(s)
+
+ The total floor area (m2) of the proposed structure(s) + +
#
Type
From d1aa4efe4dfa41bcd8b6ac6599de0b363ee037e2 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 25 Aug 2023 11:14:49 -0700 Subject: [PATCH 307/954] Show NOI Decisions in Portal * Add modules/controllers/ui * Fix small typos * Rename existing DTO to include Application --- .../decisions/decisions.component.ts | 4 +- .../alc-review/alc-review.component.html | 1 + .../decisions/decisions.component.html | 26 ++++++ .../decisions/decisions.component.scss | 20 +++++ .../decisions/decisions.component.spec.ts | 33 +++++++ .../decisions/decisions.component.ts | 39 ++++++++ .../submission-documents.component.html | 2 +- ...view-notice-of-intent-submission.module.ts | 9 +- .../application-decision.dto.ts | 2 +- .../application-decision.service.ts | 4 +- .../notice-of-intent-decision.dto.ts | 19 ++++ .../notice-of-intent-decision.service.spec.ts | 36 ++++++++ .../notice-of-intent-decision.service.ts | 37 ++++++++ .../notice-of-intent-decision.module.ts | 6 +- ...lication-decision-v2.automapper.profile.ts | 4 +- ...e-of-intent-decision.automapper.profile.ts | 25 ++++++ .../application-decision.controller.ts | 6 +- .../application-decision.dto.ts | 2 +- ...tice-of-intent-decision.controller.spec.ts | 89 +++++++++++++++++++ .../notice-of-intent-decision.controller.ts | 49 ++++++++++ .../notice-of-intent-decision.dto.ts | 32 +++++++ .../notice-of-intent-decision.module.ts | 12 +++ .../apps/alcs/src/portal/portal.module.ts | 3 + ...0-update_dec_number_generation_for_nois.ts | 3 + 24 files changed, 449 insertions(+), 14 deletions(-) create mode 100644 portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.html create mode 100644 portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.scss create mode 100644 portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.spec.ts create mode 100644 portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.dto.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.service.spec.ts create mode 100644 portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.service.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.controller.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.dto.ts create mode 100644 services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.module.ts diff --git a/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.ts b/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.ts index f0c910a4b0..1280cc0118 100644 --- a/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/decisions/decisions.component.ts @@ -1,5 +1,5 @@ import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; -import { PortalDecisionDto } from '../../../../../services/application-decision/application-decision.dto'; +import { ApplicationPortalDecisionDto } from '../../../../../services/application-decision/application-decision.dto'; import { ApplicationDecisionService } from '../../../../../services/application-decision/application-decision.service'; @Component({ @@ -9,7 +9,7 @@ import { ApplicationDecisionService } from '../../../../../services/application- }) export class DecisionsComponent implements OnInit, OnChanges { @Input() fileNumber = ''; - decisions: PortalDecisionDto[] = []; + decisions: ApplicationPortalDecisionDto[] = []; constructor(private decisionService: ApplicationDecisionService) {} diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/alc-review.component.html b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/alc-review.component.html index 2191680ae3..a080e0c620 100644 --- a/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/alc-review.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/alc-review.component.html @@ -21,6 +21,7 @@

ALC Review and Decision

noiSubmission.status.code === NOI_SUBMISSION_STATUS.ALC_DECISION " > +
diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.html b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.html new file mode 100644 index 0000000000..ee4d949dba --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.html @@ -0,0 +1,26 @@ +
+

Decision #{{ decisions.length - index }}

+
+
+
Decision Date
+ {{ decision.date | momentFormat }} +
+ +
+
Resolution Number
+ #{{ decision.resolutionNumber }}/{{ decision.resolutionYear }} +
+ +
+
Decision Outcome
+ {{ decision.outcome.label }} {{ decision.isSubjectToConditions ? '- Subject to Conditions' : '' }} +
+ +
+
Decision Document
+ +
+
+
diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.scss b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.scss new file mode 100644 index 0000000000..c92c6fb6a8 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.scss @@ -0,0 +1,20 @@ +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; + +.decision-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: 100%; + word-wrap: break-word; + hyphens: auto; + + @media screen and (min-width: $tabletBreakpoint) { + padding: rem(16); + margin: rem(24) 0 rem(40) 0; + grid-template-columns: 50% 50%; + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.spec.ts b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.spec.ts new file mode 100644 index 0000000000..3bde8b71ad --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock } from '@golevelup/ts-jest'; +import { NoticeOfIntentDecisionService } from '../../../../../services/notice-of-intent-decision/notice-of-intent-decision.service'; + +import { DecisionsComponent } from './decisions.component'; + +describe('DecisionsComponent', () => { + let component: DecisionsComponent; + let fixture: ComponentFixture; + let mockDecisionService: NoticeOfIntentDecisionService; + + beforeEach(async () => { + mockDecisionService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [DecisionsComponent], + providers: [ + { + provide: NoticeOfIntentDecisionService, + useValue: mockDecisionService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(DecisionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.ts b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.ts new file mode 100644 index 0000000000..02d647de95 --- /dev/null +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/decisions/decisions.component.ts @@ -0,0 +1,39 @@ +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { NoticeOfIntentPortalDecisionDto } from '../../../../../services/notice-of-intent-decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentDecisionService } from '../../../../../services/notice-of-intent-decision/notice-of-intent-decision.service'; + +@Component({ + selector: 'app-decisions[fileNumber]', + templateUrl: './decisions.component.html', + styleUrls: ['./decisions.component.scss'], +}) +export class DecisionsComponent implements OnInit, OnChanges { + @Input() fileNumber = ''; + decisions: NoticeOfIntentPortalDecisionDto[] = []; + + constructor(private decisionService: NoticeOfIntentDecisionService) {} + + ngOnInit(): void { + this.loadDecisions(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.loadDecisions(); + } + + async openFile(uuid: string) { + const res = await this.decisionService.openFile(uuid); + if (res) { + window.open(res.url, '_blank'); + } + } + + private async loadDecisions() { + if (this.fileNumber) { + const decisions = await this.decisionService.getByFileId(this.fileNumber); + if (decisions) { + this.decisions = decisions; + } + } + } +} diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/submission-documents/submission-documents.component.html b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/submission-documents/submission-documents.component.html index 437bbd407e..822474bef5 100644 --- a/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/submission-documents/submission-documents.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/alc-review/submission-documents/submission-documents.component.html @@ -1,4 +1,4 @@ -

Application Documents

+

Notice of Intent Documents

diff --git a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.module.ts b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.module.ts index 5de0b47a0b..5a7d8a2138 100644 --- a/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/view-submission/view-notice-of-intent-submission.module.ts @@ -1,10 +1,12 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; +import { MatSortModule } from '@angular/material/sort'; import { RouterModule, Routes } from '@angular/router'; import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; import { SharedModule } from '../../../shared/shared.module'; import { NoticeOfIntentDetailsModule } from '../notice-of-intent-details/notice-of-intent-details.module'; import { AlcReviewComponent } from './alc-review/alc-review.component'; +import { DecisionsComponent } from './alc-review/decisions/decisions.component'; import { SubmissionDocumentsComponent } from './alc-review/submission-documents/submission-documents.component'; import { ViewNoticeOfIntentSubmissionComponent } from './view-notice-of-intent-submission.component'; @@ -24,6 +26,11 @@ const routes: Routes = [ NgxMaskDirective, NgxMaskPipe, ], - declarations: [ViewNoticeOfIntentSubmissionComponent, AlcReviewComponent, SubmissionDocumentsComponent], + declarations: [ + ViewNoticeOfIntentSubmissionComponent, + AlcReviewComponent, + SubmissionDocumentsComponent, + DecisionsComponent, + ], }) export class ViewNoticeOfIntentSubmissionModule {} diff --git a/portal-frontend/src/app/services/application-decision/application-decision.dto.ts b/portal-frontend/src/app/services/application-decision/application-decision.dto.ts index c22d63a639..a444a87146 100644 --- a/portal-frontend/src/app/services/application-decision/application-decision.dto.ts +++ b/portal-frontend/src/app/services/application-decision/application-decision.dto.ts @@ -6,7 +6,7 @@ export interface LinkedResolutionDto { linkedResolutions: string[]; } -export interface PortalDecisionDto { +export interface ApplicationPortalDecisionDto { uuid: string; date: number; outcome: BaseCodeDto; diff --git a/portal-frontend/src/app/services/application-decision/application-decision.service.ts b/portal-frontend/src/app/services/application-decision/application-decision.service.ts index 0678052441..0f6df323ec 100644 --- a/portal-frontend/src/app/services/application-decision/application-decision.service.ts +++ b/portal-frontend/src/app/services/application-decision/application-decision.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ToastService } from '../toast/toast.service'; -import { PortalDecisionDto } from './application-decision.dto'; +import { ApplicationPortalDecisionDto } from './application-decision.dto'; @Injectable({ providedIn: 'root', @@ -16,7 +16,7 @@ export class ApplicationDecisionService { async getByFileId(fileNumber: string) { try { return await firstValueFrom( - this.httpClient.get(`${this.serviceUrl}/application/${fileNumber}`) + this.httpClient.get(`${this.serviceUrl}/application/${fileNumber}`) ); } catch (e) { console.error(e); diff --git a/portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.dto.ts b/portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.dto.ts new file mode 100644 index 0000000000..4684fbaa36 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.dto.ts @@ -0,0 +1,19 @@ +import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { ApplicationDocumentDto } from '../application-document/application-document.dto'; + +export interface LinkedResolutionDto { + uuid: string; + linkedResolutions: string[]; +} + +export interface NoticeOfIntentPortalDecisionDto { + uuid: string; + date: number; + outcome: BaseCodeDto; + resolutionNumber: number; + resolutionYear: number; + documents: ApplicationDocumentDto[]; + isSubjectToConditions: boolean; + reconsiders?: LinkedResolutionDto; + modifies?: LinkedResolutionDto; +} diff --git a/portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.service.spec.ts new file mode 100644 index 0000000000..07166dd8d9 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.service.spec.ts @@ -0,0 +1,36 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ToastService } from '../toast/toast.service'; +import { NoticeOfIntentDecisionService } from './notice-of-intent-decision.service'; + +describe('NoticeOfIntentDecisionService', () => { + let service: NoticeOfIntentDecisionService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentDecisionService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.service.ts b/portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.service.ts new file mode 100644 index 0000000000..4268cc31d3 --- /dev/null +++ b/portal-frontend/src/app/services/notice-of-intent-decision/notice-of-intent-decision.service.ts @@ -0,0 +1,37 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { ToastService } from '../toast/toast.service'; +import { NoticeOfIntentPortalDecisionDto } from './notice-of-intent-decision.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentDecisionService { + private serviceUrl = `${environment.apiUrl}/notice-of-intent-decision`; + + constructor(private httpClient: HttpClient, private toastService: ToastService) {} + + async getByFileId(fileNumber: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/notice-of-intent/${fileNumber}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load decisions, please try again'); + } + return undefined; + } + + async openFile(documentUuid: string) { + try { + return await firstValueFrom(this.httpClient.get<{ url: string }>(`${this.serviceUrl}/${documentUuid}/open`)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to open the document, please try again'); + } + return undefined; + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts index b9894bcaf0..bed848ef83 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -61,6 +61,10 @@ import { NoticeOfIntentModificationService } from './notice-of-intent-modificati NoticeOfIntentDecisionComponentController, NoticeOfIntentDecisionConditionController, ], - exports: [NoticeOfIntentModificationService, NoticeOfIntentDecisionV1Service], + exports: [ + NoticeOfIntentModificationService, + NoticeOfIntentDecisionV1Service, + NoticeOfIntentDecisionV2Service, + ], }) export class NoticeOfIntentDecisionModule {} diff --git a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts index 984afa4a64..6a2f91c2b8 100644 --- a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts @@ -33,7 +33,7 @@ import { import { ApplicationDecisionComponent } from '../../alcs/application-decision/application-decision-v2/application-decision/component/application-decision-component.entity'; import { LinkedResolutionOutcomeType } from '../../alcs/application-decision/application-decision-v2/application-decision/linked-resolution-outcome-type.entity'; import { ApplicationDecision } from '../../alcs/application-decision/application-decision.entity'; -import { PortalDecisionDto } from '../../portal/application-decision/application-decision.dto'; +import { ApplicationPortalDecisionDto } from '../../portal/application-decision/application-decision.dto'; import { NaruSubtypeDto } from '../../portal/application-submission/application-submission.dto'; import { NaruSubtype } from '../../portal/application-submission/naru-subtype/naru-subtype.entity'; import { ApplicationDecisionConditionToComponentLotDto } from '../../alcs/application-decision/application-condition-to-component-lot/application-condition-to-component-lot.controller.dto'; @@ -264,7 +264,7 @@ export class ApplicationDecisionProfile extends AutomapperProfile { createMap( mapper, ApplicationDecision, - PortalDecisionDto, + ApplicationPortalDecisionDto, forMember( (a) => a.reconsiders, mapFrom((dec) => diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts index 43eb63d3ec..4ffa68ff3a 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-decision.automapper.profile.ts @@ -35,6 +35,7 @@ import { NoticeOfIntentModification } from '../../alcs/notice-of-intent-decision import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; import { NoticeOfIntentStatusDto } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; +import { NoticeOfIntentPortalDecisionDto } from '../../portal/notice-of-intent-decision/notice-of-intent-decision.dto'; @Injectable() export class NoticeOfIntentDecisionProfile extends AutomapperProfile { @@ -282,6 +283,30 @@ export class NoticeOfIntentDecisionProfile extends AutomapperProfile { mapFrom((rd) => this.mapper.map(rd.card, Card, CardDto)), ), ); + + createMap( + mapper, + NoticeOfIntentDecision, + NoticeOfIntentPortalDecisionDto, + forMember( + (a) => a.date, + mapFrom((rd) => rd.date?.getTime()), + ), + forMember( + (a) => a.modifies, + mapFrom((dec) => + dec.modifies + ? { + uuid: dec.modifies.uuid, + linkedResolutions: dec.modifies.modifiesDecisions.map( + (decision) => + `#${decision.resolutionNumber}/${decision.resolutionYear}`, + ), + } + : undefined, + ), + ), + ); }; } } diff --git a/services/apps/alcs/src/portal/application-decision/application-decision.controller.ts b/services/apps/alcs/src/portal/application-decision/application-decision.controller.ts index 831405e1cc..545d694111 100644 --- a/services/apps/alcs/src/portal/application-decision/application-decision.controller.ts +++ b/services/apps/alcs/src/portal/application-decision/application-decision.controller.ts @@ -7,7 +7,7 @@ import { ApplicationDecision } from '../../alcs/application-decision/application import { ApplicationDecisionV2Service } from '../../alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.service'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; import { ApplicationSubmissionService } from '../application-submission/application-submission.service'; -import { PortalDecisionDto } from './application-decision.dto'; +import { ApplicationPortalDecisionDto } from './application-decision.dto'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @UseGuards(PortalAuthGuard) @@ -23,7 +23,7 @@ export class ApplicationDecisionController { async listDecisions( @Param('fileNumber') fileNumber: string, @Req() req, - ): Promise { + ): Promise { await this.applicationSubmissionService.verifyAccessByFileId( fileNumber, req.user.entity, @@ -34,7 +34,7 @@ export class ApplicationDecisionController { return this.mapper.mapArray( decisions, ApplicationDecision, - PortalDecisionDto, + ApplicationPortalDecisionDto, ); } diff --git a/services/apps/alcs/src/portal/application-decision/application-decision.dto.ts b/services/apps/alcs/src/portal/application-decision/application-decision.dto.ts index ff2c7580d2..755da4b7ca 100644 --- a/services/apps/alcs/src/portal/application-decision/application-decision.dto.ts +++ b/services/apps/alcs/src/portal/application-decision/application-decision.dto.ts @@ -5,7 +5,7 @@ import { } from '../../alcs/application-decision/application-decision-v1/application-decision/application-decision.dto'; import { ApplicationDecisionOutcomeCodeDto } from '../../alcs/application-decision/application-decision-v2/application-decision/application-decision.dto'; -export class PortalDecisionDto { +export class ApplicationPortalDecisionDto { @AutoMap() uuid: string; diff --git a/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.controller.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.controller.spec.ts new file mode 100644 index 0000000000..ca88b19658 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.controller.spec.ts @@ -0,0 +1,89 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; +import { NoticeOfIntentDecisionV2Service } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentDecision } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision.entity'; +import { NoticeOfIntentDecisionProfile } from '../../common/automapper/notice-of-intent-decision.automapper.profile'; +import { User } from '../../user/user.entity'; +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 { NoticeOfIntentDecisionController } from './notice-of-intent-decision.controller'; + +describe('NoticeOfIntentDecisionController', () => { + let controller: NoticeOfIntentDecisionController; + let mockNOISubmissionService: DeepMocked; + let mockDecisionService: DeepMocked; + + beforeEach(async () => { + mockNOISubmissionService = createMock(); + mockDecisionService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NoticeOfIntentDecisionController], + providers: [ + NoticeOfIntentDecisionProfile, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNOISubmissionService, + }, + { + provide: NoticeOfIntentDecisionV2Service, + useValue: mockDecisionService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + controller = module.get( + NoticeOfIntentDecisionController, + ); + + mockNOISubmissionService.getByFileNumber.mockResolvedValue( + new NoticeOfIntentSubmission(), + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should verify access and call through for loading decisions', async () => { + mockDecisionService.getForPortal.mockResolvedValue([ + new NoticeOfIntentDecision(), + ]); + + const res = await controller.listDecisions('', { + user: { + entity: new User(), + }, + }); + + expect(res.length).toEqual(1); + expect(mockNOISubmissionService.getByFileNumber).toHaveBeenCalledTimes(1); + expect(mockDecisionService.getForPortal).toHaveBeenCalledTimes(1); + }); + + it('should call through for loading files', async () => { + mockDecisionService.getDownloadUrl.mockResolvedValue('Mock Url'); + + const res = await controller.openFile('', { + user: { + entity: new User(), + }, + }); + + expect(res.url).toBeTruthy(); + expect(mockDecisionService.getDownloadUrl).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.controller.ts b/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.controller.ts new file mode 100644 index 0000000000..3ea5857a99 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.controller.ts @@ -0,0 +1,49 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Controller, Get, Param, Req, UseGuards } from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { NoticeOfIntentDecisionV2Service } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service'; +import { NoticeOfIntentDecision } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision.entity'; +import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; +import { NoticeOfIntentSubmissionService } from '../notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentPortalDecisionDto } from './notice-of-intent-decision.dto'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(PortalAuthGuard) +@Controller('notice-of-intent-decision') +export class NoticeOfIntentDecisionController { + constructor( + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, + private decisionService: NoticeOfIntentDecisionV2Service, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('/notice-of-intent/:fileNumber') + async listDecisions( + @Param('fileNumber') fileNumber: string, + @Req() req, + ): Promise { + await this.noticeOfIntentSubmissionService.getByFileNumber( + fileNumber, + req.user.entity, + ); + + const decisions = await this.decisionService.getForPortal(fileNumber); + + return this.mapper.mapArray( + decisions, + NoticeOfIntentDecision, + NoticeOfIntentPortalDecisionDto, + ); + } + + @Get('/:uuid/open') + async openFile(@Param('uuid') fileUuid: string, @Req() req) { + const url = await this.decisionService.getDownloadUrl(fileUuid); + + //TODO: How do we know which documents applicant can access? + + return { url }; + } +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.dto.ts new file mode 100644 index 0000000000..0a44cc1d24 --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.dto.ts @@ -0,0 +1,32 @@ +import { AutoMap } from '@automapper/classes'; +import { LinkedResolutionDto } from '../../alcs/application-decision/application-decision-v1/application-decision/application-decision.dto'; +import { + NoticeOfIntentDecisionDocumentDto, + NoticeOfIntentDecisionOutcomeCodeDto, +} from '../../alcs/notice-of-intent-decision/notice-of-intent-decision.dto'; +import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; + +export class NoticeOfIntentPortalDecisionDto { + @AutoMap() + uuid: string; + + @AutoMap() + date: number; + + @AutoMap(() => NoticeOfIntentDecisionOutcomeCodeDto) + outcome: NoticeOfIntentDecisionOutcomeCodeDto; + + @AutoMap(() => String) + resolutionNumber: number; + + @AutoMap(() => String) + resolutionYear: number; + + @AutoMap(() => [NoticeOfIntentDecisionDocumentDto]) + documents: NoticeOfIntentDocumentDto[]; + + @AutoMap(() => Boolean) + isSubjectToConditions: boolean; + + modifies?: LinkedResolutionDto; +} diff --git a/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.module.ts new file mode 100644 index 0000000000..7912000bee --- /dev/null +++ b/services/apps/alcs/src/portal/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { NoticeOfIntentDecisionModule } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision.module'; +import { NoticeOfIntentSubmissionModule } from '../notice-of-intent-submission/notice-of-intent-submission.module'; +import { NoticeOfIntentDecisionController } from './notice-of-intent-decision.controller'; + +@Module({ + imports: [NoticeOfIntentDecisionModule, NoticeOfIntentSubmissionModule], + controllers: [NoticeOfIntentDecisionController], + providers: [], + exports: [], +}) +export class PortalNoticeOfIntentDecisionModule {} diff --git a/services/apps/alcs/src/portal/portal.module.ts b/services/apps/alcs/src/portal/portal.module.ts index 880e4ebf74..1819e29a5b 100644 --- a/services/apps/alcs/src/portal/portal.module.ts +++ b/services/apps/alcs/src/portal/portal.module.ts @@ -12,6 +12,7 @@ import { ApplicationSubmissionReviewModule } from './application-submission-revi import { ApplicationSubmissionModule } from './application-submission/application-submission.module'; import { CodeController } from './code/code.controller'; import { PortalDocumentModule } from './document/document.module'; +import { PortalNoticeOfIntentDecisionModule } from './notice-of-intent-decision/notice-of-intent-decision.module'; import { PortalNoticeOfIntentDocumentModule } from './notice-of-intent-document/notice-of-intent-document.module'; import { NoticeOfIntentSubmissionDraftModule } from './notice-of-intent-submission-draft/notice-of-intent-submission-draft.module'; import { NoticeOfIntentSubmissionModule } from './notice-of-intent-submission/notice-of-intent-submission.module'; @@ -36,6 +37,7 @@ import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; NoticeOfIntentSubmissionModule, PortalNoticeOfIntentDocumentModule, NoticeOfIntentSubmissionDraftModule, + PortalNoticeOfIntentDecisionModule, RouterModule.register([ { path: 'portal', module: ApplicationSubmissionModule }, { path: 'portal', module: NoticeOfIntentSubmissionModule }, @@ -48,6 +50,7 @@ import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; { path: 'portal', module: PortalApplicationDecisionModule }, { path: 'portal', module: PortalNoticeOfIntentDocumentModule }, { path: 'portal', module: NoticeOfIntentSubmissionDraftModule }, + { path: 'portal', module: PortalNoticeOfIntentDecisionModule }, ]), ], controllers: [CodeController], diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692902429290-update_dec_number_generation_for_nois.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692902429290-update_dec_number_generation_for_nois.ts index 0953947b97..31254bc77e 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1692902429290-update_dec_number_generation_for_nois.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692902429290-update_dec_number_generation_for_nois.ts @@ -4,6 +4,9 @@ export class updateDecNumberGenerationForNois1692902429290 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { + // This function gets the next available decision number for the given year, these numbers are shared among Applications and NOIs + // When a decision is deleted, that number returns to the pool. + // This function unions both NOI and Application tables then uses row_number by resolution_number to find the next one await queryRunner.query( `CREATE OR REPLACE FUNCTION alcs.generate_next_resolution_number(p_resolution_year integer) RETURNS integer From f61c087997022415bd25d5a7cc8ca02875524055 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 25 Aug 2023 13:02:57 -0700 Subject: [PATCH 308/954] MR feedback --- .../submap/direction_mapping.py | 74 ----------------- .../submissions/app_submissions.py | 15 ++-- .../{ => submissions}/submap/__init__.py | 0 .../submissions/submap/direction_mapping.py | 80 +++++++++++++++++++ 4 files changed, 89 insertions(+), 80 deletions(-) delete mode 100644 bin/migrate-oats-data/submap/direction_mapping.py rename bin/migrate-oats-data/{ => submissions}/submap/__init__.py (100%) create mode 100644 bin/migrate-oats-data/submissions/submap/direction_mapping.py diff --git a/bin/migrate-oats-data/submap/direction_mapping.py b/bin/migrate-oats-data/submap/direction_mapping.py deleted file mode 100644 index ff8afa18e1..0000000000 --- a/bin/migrate-oats-data/submap/direction_mapping.py +++ /dev/null @@ -1,74 +0,0 @@ - -def add_direction_field(data): - # populates columns to be inserted - data['east_land_use_type_description'] = None - data['east_land_use_type'] = None - data['west_land_use_type_description'] = None - data['west_land_use_type'] = None - data['north_land_use_type_description'] = None - data['north_land_use_type'] = None - data['south_land_use_type_description'] = None - data['south_land_use_type'] = None - return data - -def get_NESW_rows(rows, cursor): - # fetches adjacent land use data, specifically direction, description and type code - application_ids = [dict(item)["alr_application_id"] for item in rows] - application_ids_string = ', '.join(str(item) for item in application_ids) - adj_rows_query = f"""SELECT * from - oats.oats_adjacent_land_uses oalu - WHERE oalu.alr_application_id in ({application_ids_string}) - """ - cursor.execute(adj_rows_query) - adj_rows = cursor.fetchall() - return adj_rows - -def map_direction_values(data, direction_data): - # adds direction field values into data row - data['east_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('east_description', 'No data found') - data['east_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('east_type_code', None) - data['west_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('west_description', 'No data found') - data['west_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('west_type_code', None) - data['north_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('north_description', 'No data found') - data['north_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('north_type_code', None) - data['south_land_use_type_description'] = direction_data.get(data["alr_application_id"], {}).get('south_description', 'No data found') - data['south_land_use_type'] = direction_data.get(data["alr_application_id"], {}).get('south_type_code', None) - return data - -def create_dir_dict(adj_rows): - # creates dictionary of adjacent land use data with all directions attributed to one application id - dir_dict = {} - for row in adj_rows: - application_id = row['alr_application_id'] - - if application_id in dir_dict: - if row['cardinal_direction'] == 'EAST': - dir_dict[application_id]['east_description'] = row['description'] - dir_dict[application_id]['east_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'WEST': - dir_dict[application_id]['west_description'] = row['description'] - dir_dict[application_id]['west_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'NORTH': - dir_dict[application_id]['north_description'] = row['description'] - dir_dict[application_id]['north_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'SOUTH': - dir_dict[application_id]['south_description'] = row['description'] - dir_dict[application_id]['south_type_code'] = row['nonfarm_use_type_code'] - else: - dir_dict[application_id] = {} - dir_dict[application_id]['alr_application_id'] = row['alr_application_id'] - - if row['cardinal_direction'] == 'EAST': - dir_dict[application_id]['east_description'] = row['description'] - dir_dict[application_id]['east_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'WEST': - dir_dict[application_id]['west_description'] = row['description'] - dir_dict[application_id]['west_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'NORTH': - dir_dict[application_id]['north_description'] = row['description'] - dir_dict[application_id]['north_type_code'] = row['nonfarm_use_type_code'] - if row['cardinal_direction'] == 'SOUTH': - dir_dict[application_id]['south_description'] = row['description'] - dir_dict[application_id]['south_type_code'] = row['nonfarm_use_type_code'] - - return dir_dict \ No newline at end of file diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 62e5c869fe..8407f456c5 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -3,11 +3,11 @@ log_end, log_start, ) -from submap import ( +from .submap import ( add_direction_field, map_direction_values, - create_dir_dict, - get_NESW_rows, + create_direction_dict, + get_directions_rows, ) from db import inject_conn_pool from constants import BATCH_UPLOAD_SIZE @@ -59,8 +59,8 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): if not rows: break try: - adj_rows = get_NESW_rows(rows, cursor) - direction_data = create_dir_dict(adj_rows) + adj_rows = get_directions_rows(rows, cursor) + direction_data = create_direction_dict(adj_rows) submissions_to_be_inserted_count = len(rows) @@ -97,6 +97,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data): batch_size (int): Number of rows to execute at one time. cursor (obj): Cursor object to execute queries. rows (list): Rows of data to insert in the database. + direction_data (dict): Dictionary of adjacent parcel data Returns: None: Commits the changes to the database. @@ -138,11 +139,13 @@ def prepare_app_sub_data(app_sub_raw_data_list, direction_data): This function prepares different lists of data based on the 'alr_change_code' field of each data dict in 'app_sub_raw_data_list'. :param app_sub_raw_data_list: A list of raw data dictionaries. - :return: Five lists, each containing dictionaries from 'app_sub_raw_data_list' grouped based on the 'alr_change_code' field + :param direction_data: A dictionary of adjacent parcel data. + :return: Five lists, each containing dictionaries from 'app_sub_raw_data_list' and 'direction_data' grouped based on the 'alr_change_code' field Detailed Workflow: - Initializes empty lists - Iterates over 'app_sub_raw_data_list' + - Maps adjacent parcel data based on alr_application_id - Maps the basic fields of the data dictionary based on the alr_change_code - Returns the mapped lists """ diff --git a/bin/migrate-oats-data/submap/__init__.py b/bin/migrate-oats-data/submissions/submap/__init__.py similarity index 100% rename from bin/migrate-oats-data/submap/__init__.py rename to bin/migrate-oats-data/submissions/submap/__init__.py diff --git a/bin/migrate-oats-data/submissions/submap/direction_mapping.py b/bin/migrate-oats-data/submissions/submap/direction_mapping.py new file mode 100644 index 0000000000..ac086e8507 --- /dev/null +++ b/bin/migrate-oats-data/submissions/submap/direction_mapping.py @@ -0,0 +1,80 @@ + +def add_direction_field(data): + # populates columns to be inserted + data['east_land_use_type_description'] = None + data['east_land_use_type'] = None + data['west_land_use_type_description'] = None + data['west_land_use_type'] = None + data['north_land_use_type_description'] = None + data['north_land_use_type'] = None + data['south_land_use_type_description'] = None + data['south_land_use_type'] = None + return data + +def get_directions_rows(rows, cursor): + # fetches adjacent land use data, specifically direction, description and type code + application_ids = [dict(item)["alr_application_id"] for item in rows] + application_ids_string = ', '.join(str(item) for item in application_ids) + adj_rows_query = f"""SELECT * from + oats.oats_adjacent_land_uses oalu + WHERE oalu.alr_application_id in ({application_ids_string}) + """ + cursor.execute(adj_rows_query) + adj_rows = cursor.fetchall() + return adj_rows + +def map_direction_values(data, direction_data): + # adds direction field values into data row + no_data = 'No data found' + app_id = "alr_application_id" + data['east_land_use_type_description'] = direction_data.get(data[app_id], {}).get('east_description', no_data) + data['east_land_use_type'] = direction_data.get(data[app_id], {}).get('east_type_code', None) + data['west_land_use_type_description'] = direction_data.get(data[app_id], {}).get('west_description', no_data) + data['west_land_use_type'] = direction_data.get(data[app_id], {}).get('west_type_code', None) + data['north_land_use_type_description'] = direction_data.get(data[app_id], {}).get('north_description', no_data) + data['north_land_use_type'] = direction_data.get(data[app_id], {}).get('north_type_code', None) + data['south_land_use_type_description'] = direction_data.get(data[app_id], {}).get('south_description', no_data) + data['south_land_use_type'] = direction_data.get(data[app_id], {}).get('south_type_code', None) + return data + +def create_direction_dict(adj_rows): + # creates dictionary of adjacent land use data with all directions attributed to one application id + compass = 'cardinal_direction' + description = 'description' + type_code = 'nonfarm_use_type_code' + alr_id = 'alr_application_id' + direction_dict = {} + for row in adj_rows: + application_id = row[alr_id] + + if application_id in direction_dict: + if row[compass] == 'EAST': + direction_dict[application_id]['east_description'] = row[description] + direction_dict[application_id]['east_type_code'] = row[type_code] + if row[compass] == 'WEST': + direction_dict[application_id]['west_description'] = row[description] + direction_dict[application_id]['west_type_code'] = row[type_code] + if row[compass] == 'NORTH': + direction_dict[application_id]['north_description'] = row[description] + direction_dict[application_id]['north_type_code'] = row[type_code] + if row[compass] == 'SOUTH': + direction_dict[application_id]['south_description'] = row[description] + direction_dict[application_id]['south_type_code'] = row[type_code] + else: + direction_dict[application_id] = {} + direction_dict[application_id][alr_id] = row[alr_id] + + if row[compass] == 'EAST': + direction_dict[application_id]['east_description'] = row[description] + direction_dict[application_id]['east_type_code'] = row[type_code] + if row[compass] == 'WEST': + direction_dict[application_id]['west_description'] = row[description] + direction_dict[application_id]['west_type_code'] = row[type_code] + if row[compass] == 'NORTH': + direction_dict[application_id]['north_description'] = row[description] + direction_dict[application_id]['north_type_code'] = row[type_code] + if row[compass] == 'SOUTH': + direction_dict[application_id]['south_description'] = row[description] + direction_dict[application_id]['south_type_code'] = row[type_code] + + return direction_dict \ No newline at end of file From d5f679166e154cf414210637f85e69a76138786e Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 25 Aug 2023 13:24:49 -0700 Subject: [PATCH 309/954] Decision Bug Fixes * Rename Ag fields to full names * Change padding on scroller to line buttons up with edges * Fix ordering of INCL/EXCL components --- .../decision-component.component.html | 12 ++++++------ .../incl-excl-input.component.html | 16 ++++++++-------- .../decision-input-v2.component.scss | 2 +- .../decision-component.component.html | 8 ++++---- .../decision-input-v2.component.scss | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html index ac4b7060b3..80bc71f308 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html @@ -49,7 +49,7 @@
{{ data.applicationDecisionComponentType?.label }}
bindLabel="label" bindValue="value" required - placeholder="Ag Cap*" + placeholder="Agricultural Capability*" [clearable]="false" > @@ -63,19 +63,19 @@
{{ data.applicationDecisionComponentType?.label }}
bindLabel="label" bindValue="value" required - placeholder="Ag Cap Source*" + placeholder="Agricultural Capability Source*" [clearable]="false" > - Ag Cap Map - + Agricultural Capability Mapsheet Reference + - Ag Cap Consultant - + Agricultural Capability Consultant + diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.html index 2bdded468d..1022aa19e4 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-component/incl-excl-input/incl-excl-input.component.html @@ -13,14 +13,6 @@ /> -
- Applicant Type* - - Land Owner - L/FNG Initiated - -
- Expiry Date + +
+ Applicant Type* + + Land Owner + L/FNG Initiated + +
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss index 463b5a53e0..9e994e5cd3 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.scss @@ -7,7 +7,7 @@ left: 212px; right: 0; background-color: #fff; - padding: 16px; + padding: 16px 94px 16px 48px; z-index: 2; border-top: 1px solid colors.$grey-light; diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html index f8601a7b5a..3f8d3b0196 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-component/decision-component.component.html @@ -25,7 +25,7 @@
{{ data.noticeOfIntentDecisionComponentType?.label }}
bindLabel="label" bindValue="value" required - placeholder="Ag Cap*" + placeholder="Agricultural Capability*" [clearable]="false" > @@ -39,18 +39,18 @@
{{ data.noticeOfIntentDecisionComponentType?.label }}
bindLabel="label" bindValue="value" required - placeholder="Ag Cap Source*" + placeholder="Agricultural Capability Source*" [clearable]="false" > - Ag Cap Map + Agricultural Capability Mapsheet Reference - Ag Cap Consultant + Agricultural Capability Consultant diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.scss index 65072d2bb9..000036bef8 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.scss +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.scss @@ -7,7 +7,7 @@ left: 240px; right: 0; background-color: #fff; - padding: 16px; + padding: 16px 94px 16px 48px; z-index: 2; border-top: 1px solid colors.$grey-light; From 5d441d4724fab2d45cf25e7847e3317f528deba8 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 25 Aug 2023 17:27:26 -0700 Subject: [PATCH 310/954] initial commit, query & dict appear functional --- .../submissions/app_submissions.py | 8 ++++ .../submissions/sql/app_submission.sql | 3 +- .../submissions/submap/__init__.py | 3 +- .../submissions/submap/subdiv_plot.py | 46 +++++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 bin/migrate-oats-data/submissions/submap/subdiv_plot.py diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 8407f456c5..43bb52bfcd 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -8,12 +8,15 @@ map_direction_values, create_direction_dict, get_directions_rows, + get_subdiv_rows, + create_subdiv_dict, ) from db import inject_conn_pool from constants import BATCH_UPLOAD_SIZE from psycopg2.extras import execute_batch, RealDictCursor import traceback from enum import Enum +import json etl_name = "alcs_app_sub" @@ -59,8 +62,13 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): if not rows: break try: + # print(rows) adj_rows = get_directions_rows(rows, cursor) direction_data = create_direction_dict(adj_rows) + subdiv_rows = get_subdiv_rows(rows, cursor) + print(subdiv_rows) + subdiv_data = create_subdiv_dict(subdiv_rows) + print(subdiv_data) submissions_to_be_inserted_count = len(rows) diff --git a/bin/migrate-oats-data/submissions/sql/app_submission.sql b/bin/migrate-oats-data/submissions/sql/app_submission.sql index 6d3605e5c9..d4c0b36d64 100644 --- a/bin/migrate-oats-data/submissions/sql/app_submission.sql +++ b/bin/migrate-oats-data/submissions/sql/app_submission.sql @@ -19,7 +19,8 @@ SELECT oc.alr_change_code, acg.alr_application_id, aa.applicant, - aa.alr_area + aa.alr_area, + oc.alr_appl_component_id FROM appl_components_grouped acg LEFT JOIN alcs.application aa ON aa.file_number = acg.alr_application_id::TEXT diff --git a/bin/migrate-oats-data/submissions/submap/__init__.py b/bin/migrate-oats-data/submissions/submap/__init__.py index 775acecbf2..39cbcf6a23 100644 --- a/bin/migrate-oats-data/submissions/submap/__init__.py +++ b/bin/migrate-oats-data/submissions/submap/__init__.py @@ -1 +1,2 @@ -from .direction_mapping import * \ No newline at end of file +from .direction_mapping import * +from .subdiv_plot import * \ No newline at end of file diff --git a/bin/migrate-oats-data/submissions/submap/subdiv_plot.py b/bin/migrate-oats-data/submissions/submap/subdiv_plot.py new file mode 100644 index 0000000000..a2a7bf4dab --- /dev/null +++ b/bin/migrate-oats-data/submissions/submap/subdiv_plot.py @@ -0,0 +1,46 @@ +def get_subdiv_rows(rows, cursor): + # fetches subdivision_data, + component_ids = [dict(item)["alr_appl_component_id"] for item in rows] + component_ids_string = ', '.join(str(item) for item in component_ids) + print(component_ids_string) + subdiv_rows_query = f""" + SELECT + spi.alr_appl_component_id, spi.parcel_area, road_dedication_area, spi.subdiv_design_parcel_id + FROM + oats.oats_subdiv_parcel_intents spi + LEFT JOIN oats.oats_subdivision_designs sd ON spi.alr_appl_component_id = sd.alr_appl_component_id + LEFT JOIN oats.oats_subdiv_design_parcels sdp ON spi.subdiv_design_parcel_id = sdp.subdiv_design_parcel_id + WHERE spi.alr_appl_component_id in ({component_ids_string}) + """ + # print(subdiv_rows_query) + cursor.execute(subdiv_rows_query) + subdiv_rows = cursor.fetchall() + return subdiv_rows + +def create_subdiv_dict(subdiv_rows): + # creates dictionary of adjacent land use data with all directions attributed to one application id + alr_id = 'alr_appl_component_id' + parcel_design_id = 'subdiv_design_parcel_id' + area = 'parcel_area' + lot = 'Lot' + type = 'type' + size = 'size' + + subdiv_dict = {} + for row in subdiv_rows: + app_component_id = row[alr_id] + + if app_component_id in subdiv_dict: + if row[parcel_design_id] not in subdiv_dict: + subdiv_dict[app_component_id][size] = row[area] + subdiv_dict[app_component_id][type] = lot + + else: + subdiv_dict[app_component_id] = {} + subdiv_dict[app_component_id][size] = row[area] + subdiv_dict[app_component_id][type] = lot + subdiv_dict[app_component_id][size] = row['road_dedication_area'] + subdiv_dict[app_component_id][type] = 'Road_Dedication' + + return subdiv_dict + \ No newline at end of file From 2471d86277507d6373c1b22efa42cd480b7d3b68 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 28 Aug 2023 09:57:44 -0700 Subject: [PATCH 311/954] Add Unit Tests * Add unit tests to classes a bit low on code coverage --- .../board-management.controller.spec.ts | 106 ++++++++++++++++++ .../alcs/src/alcs/board/board.service.spec.ts | 50 +++++++++ .../alcs/src/alcs/card/card.service.spec.ts | 78 +++++++++++++ .../notice-of-intent.controller.spec.ts | 30 +++++ .../notice-of-intent.controller.ts | 2 +- .../notice-of-intent.service.spec.ts | 75 ++++++++++++- .../alcs/notice-of-intent/notice-of-intent.ts | 18 --- 7 files changed, 339 insertions(+), 20 deletions(-) delete mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.ts diff --git a/services/apps/alcs/src/alcs/admin/board-management/board-management.controller.spec.ts b/services/apps/alcs/src/alcs/admin/board-management/board-management.controller.spec.ts index 645912a24f..1861b70268 100644 --- a/services/apps/alcs/src/alcs/admin/board-management/board-management.controller.spec.ts +++ b/services/apps/alcs/src/alcs/admin/board-management/board-management.controller.spec.ts @@ -1,9 +1,11 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; import { BoardService } from '../../board/board.service'; +import { Card } from '../../card/card.entity'; import { CardService } from '../../card/card.service'; import { BoardManagementController } from './board-management.controller'; @@ -44,4 +46,108 @@ describe('BoardManagementController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + it('should call through for get card type', async () => { + mockCardService.getCardTypes.mockResolvedValue([]); + + await controller.fetchCardTypes(); + + expect(mockCardService.getCardTypes).toHaveBeenCalledTimes(1); + }); + + it('should call through for update', async () => { + mockBoardService.update.mockResolvedValue(); + + await controller.update('code', { + allowedCardTypes: [], + code: '', + createCardTypes: [], + showOnSchedule: false, + statuses: [], + title: '', + }); + + expect(mockBoardService.update).toHaveBeenCalledTimes(1); + }); + + it('should call through for create', async () => { + mockBoardService.create.mockResolvedValue(); + + await controller.create({ + allowedCardTypes: [], + code: '', + createCardTypes: [], + showOnSchedule: false, + statuses: [], + title: '', + }); + + expect(mockBoardService.create).toHaveBeenCalledTimes(1); + }); + + it('should call through for delete when board has no cards', async () => { + mockCardService.getByBoard.mockResolvedValue([]); + mockBoardService.delete.mockResolvedValue(); + + await controller.delete('code'); + + expect(mockCardService.getByBoard).toHaveBeenCalledTimes(1); + expect(mockBoardService.delete).toHaveBeenCalledTimes(1); + }); + + it('should throw an exception for delete when board has cards', async () => { + mockCardService.getByBoard.mockResolvedValue([new Card()]); + + const promise = controller.delete('code'); + await expect(promise).rejects.toMatchObject( + new ServiceValidationException('Cannot delete boards with cards'), + ); + + expect(mockCardService.getByBoard).toHaveBeenCalledTimes(1); + expect(mockBoardService.delete).toHaveBeenCalledTimes(0); + }); + + it('should return card counts', async () => { + const mockCards = [ + { statusCode: 'status1' }, + { statusCode: 'status2' }, + { statusCode: 'status1' }, + ]; + mockCardService.getByBoard.mockResolvedValue(mockCards as Card[]); + + const result = await controller.fetchCardCount('code'); + + expect(result).toEqual({ status1: 2, status2: 1 }); + }); + + describe('canDelete', () => { + it('should allow board deletion if code is not "vett" and no cards on the board', async () => { + mockCardService.getByBoard.mockResolvedValue([]); + + const result = await controller.canDelete('code'); + + expect(result).toEqual({ canDelete: true }); + }); + + it('should disallow board deletion if code is "vett"', async () => { + const result = await controller.canDelete('vett'); + + expect(result).toEqual({ + canDelete: false, + reason: + 'Board is critical to application functionality and can never be deleted', + }); + }); + + it('should disallow board deletion if cards are present on the board', async () => { + mockCardService.getByBoard.mockResolvedValue([new Card()]); + + const result = await controller.canDelete('code'); + + expect(result).toEqual({ + canDelete: false, + reason: 'Board has cards on it, please move cards in order to delete', + }); + }); + }); }); diff --git a/services/apps/alcs/src/alcs/board/board.service.spec.ts b/services/apps/alcs/src/alcs/board/board.service.spec.ts index 95c34efe0d..2fcdfb4e75 100644 --- a/services/apps/alcs/src/alcs/board/board.service.spec.ts +++ b/services/apps/alcs/src/alcs/board/board.service.spec.ts @@ -4,6 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ApplicationService } from '../application/application.service'; +import { CARD_STATUS } from '../card/card-status/card-status.entity'; import { Card } from '../card/card.entity'; import { CardService } from '../card/card.service'; import { BoardStatus } from './board-status.entity'; @@ -121,4 +122,53 @@ describe('BoardsService', () => { expect(mockBoardStatusRepository.delete).toHaveBeenCalledTimes(1); }); + + it('should call through for getOneOrFail', async () => { + mockBoardRepository.findOneOrFail.mockResolvedValue({} as any); + + await service.getOneOrFail({}); + + expect(mockBoardRepository.findOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should call through for getBoardsWithStatus', async () => { + mockBoardRepository.find.mockResolvedValue([]); + + await service.getBoardsWithStatus(CARD_STATUS.READY_FOR_REVIEW); + + expect(mockBoardRepository.find).toHaveBeenCalledTimes(1); + }); + + it('should delete the status links then remove the board for delete', async () => { + mockBoardRepository.findOneOrFail.mockResolvedValue(new Board()); + mockBoardRepository.remove.mockResolvedValue({} as any); + mockBoardStatusRepository.delete.mockResolvedValue({} as any); + + await service.delete('board-code'); + + expect(mockBoardRepository.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockBoardStatusRepository.delete).toHaveBeenCalledTimes(1); + expect(mockBoardRepository.remove).toHaveBeenCalledTimes(1); + }); + + it('should save both the board and updated status when creating or updating', async () => { + cardService.getCardTypes.mockResolvedValue([]); + mockBoardRepository.save.mockResolvedValue(new Board()); + mockBoardStatusRepository.delete.mockResolvedValue({} as any); + mockBoardStatusRepository.save.mockResolvedValue([] as any); + + await service.create({ + allowedCardTypes: [], + code: '', + createCardTypes: [], + showOnSchedule: false, + statuses: [], + title: '', + }); + + expect(cardService.getCardTypes).toHaveBeenCalledTimes(1); + expect(mockBoardRepository.save).toHaveBeenCalledTimes(1); + expect(mockBoardStatusRepository.delete).toHaveBeenCalledTimes(1); + expect(mockBoardStatusRepository.save).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/alcs/card/card.service.spec.ts b/services/apps/alcs/src/alcs/card/card.service.spec.ts index 44ba8c745d..4e7bd197ce 100644 --- a/services/apps/alcs/src/alcs/card/card.service.spec.ts +++ b/services/apps/alcs/src/alcs/card/card.service.spec.ts @@ -14,6 +14,7 @@ import { import { User } from '../../user/user.entity'; import { Board } from '../board/board.entity'; import { NotificationService } from '../notification/notification.service'; +import { CardSubtask } from './card-subtask/card-subtask.entity'; import { CardSubtaskService } from './card-subtask/card-subtask.service'; import { CARD_TYPE, CardType } from './card-type/card-type.entity'; import { CardUpdateServiceDto } from './card.dto'; @@ -161,6 +162,15 @@ describe('CardService', () => { expect(cardTypeRepositoryMock.find).toBeCalledTimes(1); }); + it('should call the repo for listing portal card types', async () => { + cardTypeRepositoryMock.find.mockResolvedValue([new CardType({})]); + + const types = await service.getPortalCardTypes(); + expect(types.length).toEqual(1); + + expect(cardTypeRepositoryMock.find).toBeCalledTimes(1); + }); + it('should call the repo for getWithBoard', async () => { cardRepositoryMock.findOne.mockResolvedValue(new Card()); @@ -169,6 +179,32 @@ describe('CardService', () => { expect(cardRepositoryMock.findOne).toBeCalledTimes(1); }); + it('should call the repo for getByBoard', async () => { + cardRepositoryMock.find.mockResolvedValue([new Card()]); + + const card = await service.getByBoard(''); + expect(card).toBeDefined(); + expect(cardRepositoryMock.find).toBeCalledTimes(1); + }); + + it('should call the repo for getByCardStatus', async () => { + cardRepositoryMock.find.mockResolvedValue([new Card()]); + + const card = await service.getByCardStatus(''); + expect(card).toBeDefined(); + expect(cardRepositoryMock.find).toBeCalledTimes(1); + }); + + it('should call the repo for save', async () => { + cardRepositoryMock.findOne.mockResolvedValue(new Card()); + cardRepositoryMock.save.mockResolvedValue(new Card()); + + const card = await service.save(new Card()); + expect(card).toBeDefined(); + expect(cardRepositoryMock.findOne).toBeCalledTimes(1); + expect(cardRepositoryMock.save).toHaveBeenCalledTimes(1); + }); + it('should call notification service when assignee is changed', async () => { const mockUserUuid = 'fake-user'; const mockUpdate = { @@ -194,4 +230,46 @@ describe('CardService', () => { ); expect(createNotificationServiceDto.targetType).toStrictEqual('card'); }); + + it('should delete the subtasks then call softRemove for Archive', async () => { + cardRepositoryMock.findOneOrFail.mockResolvedValue( + new Card({ + subtasks: [ + new CardSubtask({ + uuid: 'subtask-uuid', + }), + ], + }), + ); + cardRepositoryMock.save.mockResolvedValue(new Card()); + cardRepositoryMock.softRemove.mockResolvedValue({} as any); + mockSubtaskService.deleteMany.mockResolvedValue(); + + await service.archive('uuid'); + expect(cardRepositoryMock.findOneOrFail).toBeCalledTimes(1); + expect(cardRepositoryMock.save).toHaveBeenCalledTimes(1); + expect(cardRepositoryMock.softRemove).toHaveBeenCalledTimes(1); + expect(mockSubtaskService.deleteMany).toHaveBeenCalledTimes(1); + }); + + it('should recover the subtasks then call recover for recover', async () => { + cardRepositoryMock.findOneOrFail.mockResolvedValue( + new Card({ + subtasks: [ + new CardSubtask({ + uuid: 'subtask-uuid', + }), + ], + }), + ); + cardRepositoryMock.save.mockResolvedValue(new Card()); + cardRepositoryMock.recover.mockResolvedValue({} as any); + mockSubtaskService.recoverMany.mockResolvedValue(); + + await service.recover('uuid'); + expect(cardRepositoryMock.findOneOrFail).toBeCalledTimes(1); + expect(cardRepositoryMock.save).toHaveBeenCalledTimes(1); + expect(cardRepositoryMock.recover).toHaveBeenCalledTimes(1); + expect(mockSubtaskService.recoverMany).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.spec.ts index 30483de2fb..6f9ec113dc 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.spec.ts @@ -70,6 +70,36 @@ describe('NoticeOfIntentController', () => { expect(mockService.mapToDtos).toHaveBeenCalledTimes(1); }); + it('should call through to service for get', async () => { + mockService.getByFileNumber.mockResolvedValue(new NoticeOfIntent()); + mockService.mapToDtos.mockResolvedValue([]); + + await controller.get('fileNumber'); + + expect(mockService.getByFileNumber).toHaveBeenCalledTimes(1); + expect(mockService.mapToDtos).toHaveBeenCalledTimes(1); + }); + + it('should call through to service for search', async () => { + mockService.searchByFileNumber.mockResolvedValue([new NoticeOfIntent()]); + mockService.mapToDtos.mockResolvedValue([]); + + await controller.search('fileNumber'); + + expect(mockService.searchByFileNumber).toHaveBeenCalledTimes(1); + expect(mockService.mapToDtos).toHaveBeenCalledTimes(1); + }); + + it('should call through to service for update', async () => { + mockService.update.mockResolvedValue(new NoticeOfIntent()); + mockService.mapToDtos.mockResolvedValue([]); + + await controller.update({}, 'fileNumber'); + + expect(mockService.update).toHaveBeenCalledTimes(1); + expect(mockService.mapToDtos).toHaveBeenCalledTimes(1); + }); + it('should call through to service for get card', async () => { mockService.getByCardUuid.mockResolvedValue(new NoticeOfIntent()); mockService.mapToDtos.mockResolvedValue([]); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.ts index ee68803698..ad759a6dc7 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.controller.ts @@ -88,7 +88,7 @@ export class NoticeOfIntentController { @Get('/search/:fileNumber') @UserRoles(...ROLES_ALLOWED_APPLICATIONS) - async searchApplications(@Param('fileNumber') fileNumber: string) { + async search(@Param('fileNumber') fileNumber: string) { const noticeOfIntents = await this.noticeOfIntentService.searchByFileNumber( fileNumber, ); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts index 2160673de2..13436a77b9 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts @@ -4,7 +4,10 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { ServiceValidationException } from '../../../../../libs/common/src/exceptions/base.exception'; +import { + ServiceNotFoundException, + ServiceValidationException, +} from '../../../../../libs/common/src/exceptions/base.exception'; import { NoticeOfIntentProfile } from '../../common/automapper/notice-of-intent.automapper.profile'; import { FileNumberService } from '../../file-number/file-number.service'; import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; @@ -172,6 +175,19 @@ describe('NoticeOfIntentService', () => { expect(mockRepository.find.mock.calls[0][0]!.where).toEqual(mockFilter); }); + it('should call throw an exception when getOrFailByUuid fails', async () => { + mockRepository.findOne.mockResolvedValue(null); + const promise = service.getOrFailByUuid('uuid'); + + await expect(promise).rejects.toMatchObject( + new ServiceNotFoundException( + `Failed to find notice of intent with uuid uuid`, + ), + ); + + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + }); + it('should call through to the repo for getByFileNumber', async () => { mockRepository.findOneOrFail.mockResolvedValue(new NoticeOfIntent()); await service.getByFileNumber('file'); @@ -179,6 +195,38 @@ describe('NoticeOfIntentService', () => { expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); }); + it('should call through to the repo for searchByFileNumber', async () => { + mockRepository.find.mockResolvedValue([new NoticeOfIntent()]); + const res = await service.searchByFileNumber('file'); + + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(res.length).toEqual(1); + }); + + it('should call through to the repo for getFileNumber', async () => { + mockRepository.findOneOrFail.mockResolvedValue( + new NoticeOfIntent({ + fileNumber: 'fileNumber', + }), + ); + const res = await service.getFileNumber('file'); + + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); + expect(res).toEqual('fileNumber'); + }); + + it('should call through to the repo for getUuid', async () => { + mockRepository.findOneOrFail.mockResolvedValue( + new NoticeOfIntent({ + uuid: 'uuid', + }), + ); + const res = await service.getUuid('file'); + + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); + expect(res).toEqual('uuid'); + }); + it('should set values and call save for update', async () => { const notice = new NoticeOfIntent({ summary: 'old-summary', @@ -328,4 +376,29 @@ describe('NoticeOfIntentService', () => { expect(res[0].pausedDays).toEqual(5); expect(res[0].paused).toEqual(true); }); + + it('should create a card and save it for submit', async () => { + const mockNoi = new NoticeOfIntent(); + mockRepository.findOne.mockResolvedValue(mockNoi); + mockRepository.findOneOrFail.mockResolvedValue(mockNoi); + mockCodeService.fetchRegion.mockResolvedValue(new ApplicationRegion()); + mockRepository.save.mockResolvedValue({} as any); + + await service.submit({ + applicant: 'Bruce Wayne', + typeCode: 'CAT', + fileNumber: 'fileNumber', + localGovernmentUuid: 'governmentUuid', + regionCode: 'REGION', + }); + + expect(mockNoi.fileNumber).toEqual('fileNumber'); + expect(mockNoi.region).toBeDefined(); + expect(mockNoi.card).toBeDefined(); + expect(mockCodeService.fetchRegion).toHaveBeenCalledTimes(1); + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + 0; + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.ts deleted file mode 100644 index 32a3c1264a..0000000000 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { NoticeOfIntentController } from './notice-of-intent.controller'; - -describe('NoiController', () => { - let controller: NoticeOfIntentController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [NoticeOfIntentController], - }).compile(); - - controller = module.get(NoticeOfIntentController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); From 0e315588c088f6aff136f161bd9b2a952bf60c86 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Mon, 28 Aug 2023 15:04:07 -0700 Subject: [PATCH 312/954] Send NOI decision released status emails (#917) * Send noi decision released email (wip masked date) * Rename variable to reflect removed masked date and add to noi * Add noi decision email unit test * Update noi service method and add unit test --- ...application-decision-v2.controller.spec.ts | 2 +- .../application-decision-v2.controller.ts | 2 +- ...e-of-intent-decision-v2.controller.spec.ts | 105 ++++++++++++++++++ ...notice-of-intent-decision-v2.controller.ts | 50 +++++++++ .../notice-of-intent-decision.module.ts | 2 + ...otice-of-intent-submission.service.spec.ts | 9 ++ .../notice-of-intent-submission.service.ts | 18 +-- .../alcs/src/providers/email/email.service.ts | 5 +- .../decision-released/application.template.ts | 4 +- .../notice-of-intent.template.ts | 4 +- 10 files changed, 186 insertions(+), 15 deletions(-) diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts index 47fbe6ff88..b9b93ddd3e 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts @@ -328,7 +328,7 @@ describe('ApplicationDecisionV2Controller', () => { parentType: 'application', primaryContact: mockOwner, ccGovernment: true, - decisionReleaseMaskedDate: new Date().toLocaleDateString('en-CA', { + decisionDate: new Date().toLocaleDateString('en-CA', { weekday: 'long', year: 'numeric', month: 'long', diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts index 8dfc2386b4..877430a398 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts @@ -327,7 +327,7 @@ export class ApplicationDecisionV2Controller { parentType: PARENT_TYPE.APPLICATION, primaryContact, ccGovernment: true, - decisionReleaseMaskedDate: date.toLocaleDateString('en-CA', options), + decisionDate: date.toLocaleDateString('en-CA', options), }); } } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts index 2f0906fa24..74a21fc925 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts @@ -20,6 +20,12 @@ import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; import { NoticeOfIntentModificationService } from '../notice-of-intent-modification/notice-of-intent-modification.service'; import { NoticeOfIntentDecisionV2Controller } from './notice-of-intent-decision-v2.controller'; import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2.service'; +import { NoticeOfIntentSubmissionService } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentOwner } from '../../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { LocalGovernment } from '../../local-government/local-government.entity'; +import { generateALCDNoticeOfIntentHtml } from '../../../../../../templates/emails/decision-released'; +import { NOI_SUBMISSION_STATUS } from '../../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; describe('NoticeOfIntentDecisionV2Controller', () => { let controller: NoticeOfIntentDecisionV2Controller; @@ -27,6 +33,7 @@ describe('NoticeOfIntentDecisionV2Controller', () => { let mockNoticeOfIntentService: DeepMocked; let mockCodeService: DeepMocked; let mockModificationService: DeepMocked; + let mockNoticeOfIntentSubmissionService: DeepMocked; let mockEmailService: DeepMocked; let mockNoticeOfintent; @@ -37,6 +44,7 @@ describe('NoticeOfIntentDecisionV2Controller', () => { mockNoticeOfIntentService = createMock(); mockCodeService = createMock(); mockModificationService = createMock(); + mockNoticeOfIntentSubmissionService = createMock(); mockEmailService = createMock(); mockNoticeOfintent = new NoticeOfIntent(); @@ -71,6 +79,10 @@ describe('NoticeOfIntentDecisionV2Controller', () => { provide: NoticeOfIntentModificationService, useValue: mockModificationService, }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockNoticeOfIntentSubmissionService, + }, { provide: EmailService, useValue: mockEmailService, @@ -256,4 +268,97 @@ describe('NoticeOfIntentDecisionV2Controller', () => { expect(mockDecisionService.generateResolutionNumber).toBeCalledTimes(1); expect(mockDecisionService.generateResolutionNumber).toBeCalledWith(2023); }); + + it('should send status email after the first release of any decisions', async () => { + const fileNumber = 'fake-file-number'; + const primaryContactOwnerUuid = 'primary-contact'; + const mockOwner = new NoticeOfIntentOwner({ + uuid: primaryContactOwnerUuid, + }); + const localGovernmentUuid = 'fake-government'; + const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); + const mockNoticeOfIntentSubmission = new NoticeOfIntentSubmission({ + fileNumber, + primaryContactOwnerUuid, + owners: [mockOwner], + localGovernmentUuid, + }); + + mockNoticeOfIntentService.getFileNumber.mockResolvedValue(fileNumber); + mockDecisionService.get.mockResolvedValue( + new NoticeOfIntentDecision({ wasReleased: false }), + ); + mockDecisionService.update.mockResolvedValue(mockDecision); + mockNoticeOfIntentSubmissionService.getOrFailByFileNumber.mockResolvedValue( + mockNoticeOfIntentSubmission, + ); + mockEmailService.getNoticeOfIntentEmailData.mockResolvedValue({ + primaryContact: mockOwner, + submissionGovernment: mockGovernment, + }); + mockEmailService.sendNoticeOfIntentStatusEmail.mockResolvedValue(); + + const updates = { + outcome: 'New Outcome', + date: new Date(2023, 3, 3, 3, 3, 3, 3).valueOf(), + isDraft: false, + } as UpdateNoticeOfIntentDecisionDto; + + await controller.update('fake-uuid', updates); + + expect(mockDecisionService.update).toBeCalledTimes(1); + expect(mockDecisionService.update).toBeCalledWith( + 'fake-uuid', + { + outcome: 'New Outcome', + date: updates.date, + isDraft: false, + }, + undefined, + ); + expect(mockEmailService.sendNoticeOfIntentStatusEmail).toBeCalledTimes(1); + expect(mockEmailService.sendNoticeOfIntentStatusEmail).toBeCalledWith({ + generateStatusHtml: generateALCDNoticeOfIntentHtml, + status: NOI_SUBMISSION_STATUS.ALC_DECISION, + noticeOfIntentSubmission: mockNoticeOfIntentSubmission, + government: mockGovernment, + parentType: 'notice-of-intent', + primaryContact: mockOwner, + ccGovernment: true, + decisionDate: new Date().toLocaleDateString('en-CA', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }), + }); + }); + + it('should not send status email on subsequent decision releases', async () => { + mockDecisionService.get.mockResolvedValue( + new NoticeOfIntentDecision({ wasReleased: true }), + ); + mockDecisionService.update.mockResolvedValue(mockDecision); + mockEmailService.sendNoticeOfIntentStatusEmail.mockResolvedValue(); + + const updates = { + outcome: 'New Outcome', + date: new Date(2023, 3, 3, 3, 3, 3, 3).valueOf(), + isDraft: false, + } as UpdateNoticeOfIntentDecisionDto; + + await controller.update('fake-uuid', updates); + + expect(mockDecisionService.update).toBeCalledTimes(1); + expect(mockDecisionService.update).toBeCalledWith( + 'fake-uuid', + { + outcome: 'New Outcome', + date: updates.date, + isDraft: false, + }, + undefined, + ); + expect(mockEmailService.sendNoticeOfIntentStatusEmail).toBeCalledTimes(0); + }); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts index f243e2211f..0e4f59b396 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts @@ -33,6 +33,11 @@ import { import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; import { NoticeOfIntentModificationService } from '../notice-of-intent-modification/notice-of-intent-modification.service'; import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2.service'; +import { generateALCDNoticeOfIntentHtml } from '../../../../../../templates/emails/decision-released'; +import { NOI_SUBMISSION_STATUS } from '../../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { PARENT_TYPE } from '../../card/card-subtask/card-subtask.dto'; +import { NoticeOfIntentSubmissionService } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('notice-of-intent-decision/v2') @@ -41,6 +46,7 @@ export class NoticeOfIntentDecisionV2Controller { constructor( private noticeOfIntentDecisionV2Service: NoticeOfIntentDecisionV2Service, private noticeOfIntentService: NoticeOfIntentService, + private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, private emailService: EmailService, private modificationService: NoticeOfIntentModificationService, @InjectMapper() private mapper: Mapper, @@ -137,12 +143,18 @@ export class NoticeOfIntentDecisionV2Controller { modifies = null; } + const decision = await this.noticeOfIntentDecisionV2Service.get(uuid); + const updatedDecision = await this.noticeOfIntentDecisionV2Service.update( uuid, updateDto, modifies, ); + if (!decision.wasReleased && updateDto.isDraft === false) { + this.sendDecisionReleasedEmail(updatedDecision); + } + return this.mapper.mapAsync( updatedDecision, NoticeOfIntentDecision, @@ -238,4 +250,42 @@ export class NoticeOfIntentDecisionV2Controller { resolutionYear, ); } + + private async sendDecisionReleasedEmail(decision: NoticeOfIntentDecision) { + const fileNumber = await this.noticeOfIntentService.getFileNumber( + decision.noticeOfIntentUuid, + ); + + const noticeOfIntentSubmission = + await this.noticeOfIntentSubmissionService.getOrFailByFileNumber( + fileNumber, + ); + + const { primaryContact, submissionGovernment } = + await this.emailService.getNoticeOfIntentEmailData( + noticeOfIntentSubmission, + ); + + const date = decision.date ? new Date(decision.date) : new Date(); + + const options: Intl.DateTimeFormatOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }; + + if (primaryContact) { + await this.emailService.sendNoticeOfIntentStatusEmail({ + generateStatusHtml: generateALCDNoticeOfIntentHtml, + status: NOI_SUBMISSION_STATUS.ALC_DECISION, + noticeOfIntentSubmission, + government: submissionGovernment, + parentType: PARENT_TYPE.NOTICE_OF_INTENT, + primaryContact, + ccGovernment: true, + decisionDate: date.toLocaleDateString('en-CA', options), + }); + } + } } diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts index bed848ef83..706500e5cc 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -25,6 +25,7 @@ import { NoticeOfIntentModificationOutcomeType } from './notice-of-intent-modifi import { NoticeOfIntentModificationController } from './notice-of-intent-modification/notice-of-intent-modification.controller'; import { NoticeOfIntentModification } from './notice-of-intent-modification/notice-of-intent-modification.entity'; import { NoticeOfIntentModificationService } from './notice-of-intent-modification/notice-of-intent-modification.service'; +import { NoticeOfIntentSubmissionModule } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.module'; @Module({ imports: [ @@ -44,6 +45,7 @@ import { NoticeOfIntentModificationService } from './notice-of-intent-modificati DocumentModule, forwardRef(() => NoticeOfIntentModule), NoticeOfIntentSubmissionStatusModule, + NoticeOfIntentSubmissionModule, ], providers: [ //These are in the same module, so be careful to import the correct one diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts index 962e1482b6..5cb5e32961 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -347,4 +347,13 @@ describe('NoticeOfIntentSubmissionService', () => { expect(mockRepository.save).toBeCalledTimes(1); expect(mockRepository.findOneOrFail).toBeCalledTimes(2); }); + + it('should return the fetched notice of intent when fetching with file number', async () => { + const noiSubmission = new NoticeOfIntentSubmission(); + mockRepository.findOneOrFail.mockResolvedValue(noiSubmission); + + const app = await service.getOrFailByFileNumber(''); + + expect(app).toBe(noiSubmission); + }); }); diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts index 10d6854aef..a0b97f9cb2 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.ts @@ -61,6 +61,16 @@ export class NoticeOfIntentSubmissionService { @InjectMapper() private mapper: Mapper, ) {} + async getOrFailByFileNumber(fileNumber: string) { + return await this.noticeOfIntentSubmissionRepository.findOneOrFail({ + where: { + fileNumber, + isDraft: false, + }, + relations: this.DEFAULT_RELATIONS, + }); + } + async create(type: string, createdBy: User) { const fileNumber = await this.fileNumberService.generateNextFileNumber(); @@ -155,13 +165,7 @@ export class NoticeOfIntentSubmissionService { user.clientRoles!.includes(value), ); if (overlappingRoles.length > 0) { - return await this.noticeOfIntentSubmissionRepository.findOneOrFail({ - where: { - fileNumber, - isDraft: false, - }, - relations: this.DEFAULT_RELATIONS, - }); + return await this.getOrFailByFileNumber(fileNumber); } const whereClauses = await this.generateWhereClauses( diff --git a/services/apps/alcs/src/providers/email/email.service.ts b/services/apps/alcs/src/providers/email/email.service.ts index 74e62eace8..cf698dfaaa 100644 --- a/services/apps/alcs/src/providers/email/email.service.ts +++ b/services/apps/alcs/src/providers/email/email.service.ts @@ -35,7 +35,7 @@ type BaseStatusEmailData = { government: LocalGovernment | null; parentType: PARENT_TYPE; ccGovernment?: boolean; - decisionReleaseMaskedDate?: string; + decisionDate?: string; }; type ApplicationEmailData = BaseStatusEmailData & { applicationSubmission: ApplicationSubmission; @@ -296,7 +296,7 @@ export class EmailService { governmentName: data.government?.name, status: status.label, parentTypeLabel: parentTypeLabel[data.parentType], - decisionReleaseMaskedDate: data?.decisionReleaseMaskedDate, + decisionDate: data?.decisionDate, }); const parentId = await this.applicationService.getUuid(fileNumber); @@ -336,6 +336,7 @@ export class EmailService { governmentName: data.government?.name, status: status.label, parentTypeLabel: parentTypeLabel[data.parentType], + decisionDate: data?.decisionDate, }); const parentId = await this.noticeOfIntentService.getUuid(fileNumber); diff --git a/services/templates/emails/decision-released/application.template.ts b/services/templates/emails/decision-released/application.template.ts index 4ef596f6bf..050496fb37 100644 --- a/services/templates/emails/decision-released/application.template.ts +++ b/services/templates/emails/decision-released/application.template.ts @@ -4,7 +4,7 @@ import { header, footer, notificationOnly, portalButton } from '../partials'; import { StatusUpdateEmail } from '../../../apps/alcs/src/providers/email/email.service'; type DecisionReleasedStatusEmail = StatusUpdateEmail & { - decisionReleaseMaskedDate: number; + decisionDate: number; }; const template = ` @@ -28,7 +28,7 @@ const template = ` This email is to advise that the Reasons for Decision for the above noted application has been released. - Please log into the ALC Portal to view the Reasons for Decision. The document can be found by clicking 'View' from the Inbox table and then navigating to the 'ALC Review and Decision' tab. The Reasons for Decision will be available to the public on {{ decisionReleaseMaskedDate }}. + Please log into the ALC Portal to view the Reasons for Decision. The document can be found by clicking 'View' from the Inbox table and then navigating to the 'ALC Review and Decision' tab. The Reasons for Decision will be available to the public on {{ decisionDate }}. Further correspondence with respect to this application should be directed to the ALC Land Use Planner for your region, found on the ALC website Contact Us page. diff --git a/services/templates/emails/decision-released/notice-of-intent.template.ts b/services/templates/emails/decision-released/notice-of-intent.template.ts index ade548924c..e8d0db1b2f 100644 --- a/services/templates/emails/decision-released/notice-of-intent.template.ts +++ b/services/templates/emails/decision-released/notice-of-intent.template.ts @@ -4,7 +4,7 @@ import { header, footer, notificationOnly, portalButton } from '../partials'; import { StatusUpdateEmail } from '../../../apps/alcs/src/providers/email/email.service'; type DecisionReleasedStatusEmail = StatusUpdateEmail & { - decisionReleaseMaskedDate: number; + decisionDate: number; }; const template = ` @@ -28,7 +28,7 @@ const template = ` The decision for the above noted Notice of Intent (NOI) has been released on the the ALC Portal. - The decision document can be found by clicking 'View' from the NOI Inbox table in the ALC Portal, and then navigating to the 'ALC Review and Decision' tab. The decision will be available to the public on {{ decisionReleaseMaskedDate }}. + The decision document can be found by clicking 'View' from the NOI Inbox table in the ALC Portal, and then navigating to the 'ALC Review and Decision' tab. The decision will be available to the public on {{ decisionDate }}. Further correspondence with respect to this NOI should be directed to ALC.Soil@gov.bc.ca. From 88cea3c5ef7363bba12d90ff8e9e51958e1369e9 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 28 Aug 2023 16:20:54 -0700 Subject: [PATCH 313/954] commit before cleaning --- .../submissions/app_submissions.py | 24 +++++++---- .../submissions/submap/subdiv_plot.py | 41 +++++++++++++------ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 43bb52bfcd..0cb7a0c233 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -10,6 +10,8 @@ get_directions_rows, get_subdiv_rows, create_subdiv_dict, + add_subdiv, + map_subdiv_lots, ) from db import inject_conn_pool from constants import BATCH_UPLOAD_SIZE @@ -62,17 +64,16 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): if not rows: break try: - # print(rows) adj_rows = get_directions_rows(rows, cursor) direction_data = create_direction_dict(adj_rows) subdiv_rows = get_subdiv_rows(rows, cursor) - print(subdiv_rows) + # print(subdiv_rows) subdiv_data = create_subdiv_dict(subdiv_rows) - print(subdiv_data) + # print(subdiv_data) submissions_to_be_inserted_count = len(rows) - insert_app_sub_records(conn, batch_size, cursor, rows, direction_data) + insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdiv_data) successful_inserts_count = ( successful_inserts_count + submissions_to_be_inserted_count @@ -96,7 +97,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): print("Total failed inserts:", failed_inserts) log_end(etl_name) -def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data): +def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdiv_data): """ Function to insert submission records in batches. @@ -114,7 +115,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data): nfu_data_list, other_data_list, inc_exc_data_list, - ) = prepare_app_sub_data(rows, direction_data) + ) = prepare_app_sub_data(rows, direction_data, subdiv_data) if len(nfu_data_list) > 0: execute_batch( @@ -142,7 +143,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data): conn.commit() -def prepare_app_sub_data(app_sub_raw_data_list, direction_data): +def prepare_app_sub_data(app_sub_raw_data_list, direction_data, subdiv_data): """ This function prepares different lists of data based on the 'alr_change_code' field of each data dict in 'app_sub_raw_data_list'. @@ -164,6 +165,9 @@ def prepare_app_sub_data(app_sub_raw_data_list, direction_data): for row in app_sub_raw_data_list: data = dict(row) data = add_direction_field(data) + data = add_subdiv(data,json) + if data['alr_appl_component_id'] in subdiv_data: + data = map_subdiv_lots(data, subdiv_data, json) if data["alr_application_id"] in direction_data: data = map_direction_values(data, direction_data) if data["alr_change_code"] == ALRChangeCode.NFU.value: @@ -193,7 +197,8 @@ def get_insert_query(unique_fields,unique_values): east_land_use_type, west_land_use_type, north_land_use_type, - south_land_use_type + south_land_use_type, + subd_proposed_lots {unique_fields} ) VALUES ( @@ -210,7 +215,8 @@ def get_insert_query(unique_fields,unique_values): %(east_land_use_type)s, %(west_land_use_type)s, %(north_land_use_type)s, - %(south_land_use_type)s + %(south_land_use_type)s, + %(subd_proposed_lots)s {unique_values} ) """ diff --git a/bin/migrate-oats-data/submissions/submap/subdiv_plot.py b/bin/migrate-oats-data/submissions/submap/subdiv_plot.py index a2a7bf4dab..c822ea0bc2 100644 --- a/bin/migrate-oats-data/submissions/submap/subdiv_plot.py +++ b/bin/migrate-oats-data/submissions/submap/subdiv_plot.py @@ -2,7 +2,7 @@ def get_subdiv_rows(rows, cursor): # fetches subdivision_data, component_ids = [dict(item)["alr_appl_component_id"] for item in rows] component_ids_string = ', '.join(str(item) for item in component_ids) - print(component_ids_string) + # print(component_ids_string) subdiv_rows_query = f""" SELECT spi.alr_appl_component_id, spi.parcel_area, road_dedication_area, spi.subdiv_design_parcel_id @@ -15,6 +15,7 @@ def get_subdiv_rows(rows, cursor): # print(subdiv_rows_query) cursor.execute(subdiv_rows_query) subdiv_rows = cursor.fetchall() + # print(subdiv_rows) return subdiv_rows def create_subdiv_dict(subdiv_rows): @@ -23,24 +24,38 @@ def create_subdiv_dict(subdiv_rows): parcel_design_id = 'subdiv_design_parcel_id' area = 'parcel_area' lot = 'Lot' - type = 'type' + parcel_type = 'type' size = 'size' subdiv_dict = {} for row in subdiv_rows: app_component_id = row[alr_id] - + if app_component_id in subdiv_dict: - if row[parcel_design_id] not in subdiv_dict: - subdiv_dict[app_component_id][size] = row[area] - subdiv_dict[app_component_id][type] = lot + # if row[parcel_design_id] not in subdiv_dict[app_component_id]: + subdiv_dict[app_component_id].append({size: row[area], parcel_type: lot}) + # print("in the loop") + # print(subdiv_dict[app_component_id]) + # print("updated row") else: - subdiv_dict[app_component_id] = {} - subdiv_dict[app_component_id][size] = row[area] - subdiv_dict[app_component_id][type] = lot - subdiv_dict[app_component_id][size] = row['road_dedication_area'] - subdiv_dict[app_component_id][type] = 'Road_Dedication' - + subdiv_dict[app_component_id] = [] + if row['road_dedication_area'] is not None: + subdiv_dict[app_component_id].append({size: row['road_dedication_area'], parcel_type: 'Road Dedication'}) + subdiv_dict[app_component_id].append({size: row[area], parcel_type: lot}) return subdiv_dict - \ No newline at end of file + +def add_subdiv(data,json): + insert_string = [] + json_string = json.dumps(insert_string) + data['subd_proposed_lots'] = json_string + return data + +def map_subdiv_lots(data, subdiv_data, json): + insert_string = subdiv_data[data['alr_appl_component_id']] + # print(insert_string) + json_string = json.dumps(insert_string) + data['subd_proposed_lots'] = json_string + # print('to json') + # print(data['subd_proposed_lots']) + return data \ No newline at end of file From 38c6b6101e638d6f4a0d3d973b550154a7176251 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Mon, 28 Aug 2023 16:45:12 -0700 Subject: [PATCH 314/954] ready for MR --- .../submissions/app_submissions.py | 4 +-- .../submissions/submap/subdiv_plot.py | 25 +++++-------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 0cb7a0c233..bdbea88c6e 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -67,9 +67,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): adj_rows = get_directions_rows(rows, cursor) direction_data = create_direction_dict(adj_rows) subdiv_rows = get_subdiv_rows(rows, cursor) - # print(subdiv_rows) subdiv_data = create_subdiv_dict(subdiv_rows) - # print(subdiv_data) submissions_to_be_inserted_count = len(rows) @@ -107,6 +105,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdi cursor (obj): Cursor object to execute queries. rows (list): Rows of data to insert in the database. direction_data (dict): Dictionary of adjacent parcel data + subdiv_data: list of subdivision data Returns: None: Commits the changes to the database. @@ -149,6 +148,7 @@ def prepare_app_sub_data(app_sub_raw_data_list, direction_data, subdiv_data): :param app_sub_raw_data_list: A list of raw data dictionaries. :param direction_data: A dictionary of adjacent parcel data. + :param subdiv_data: list of subdivision data :return: Five lists, each containing dictionaries from 'app_sub_raw_data_list' and 'direction_data' grouped based on the 'alr_change_code' field Detailed Workflow: diff --git a/bin/migrate-oats-data/submissions/submap/subdiv_plot.py b/bin/migrate-oats-data/submissions/submap/subdiv_plot.py index c822ea0bc2..0298291c81 100644 --- a/bin/migrate-oats-data/submissions/submap/subdiv_plot.py +++ b/bin/migrate-oats-data/submissions/submap/subdiv_plot.py @@ -1,27 +1,21 @@ def get_subdiv_rows(rows, cursor): - # fetches subdivision_data, + # fetches subdivision_data component_ids = [dict(item)["alr_appl_component_id"] for item in rows] - component_ids_string = ', '.join(str(item) for item in component_ids) - # print(component_ids_string) + component_ids_string = ', '.join(str(item) for item in component_ids) subdiv_rows_query = f""" SELECT - spi.alr_appl_component_id, spi.parcel_area, road_dedication_area, spi.subdiv_design_parcel_id + spi.alr_appl_component_id, spi.parcel_area, spi.subdiv_design_parcel_id FROM oats.oats_subdiv_parcel_intents spi - LEFT JOIN oats.oats_subdivision_designs sd ON spi.alr_appl_component_id = sd.alr_appl_component_id - LEFT JOIN oats.oats_subdiv_design_parcels sdp ON spi.subdiv_design_parcel_id = sdp.subdiv_design_parcel_id WHERE spi.alr_appl_component_id in ({component_ids_string}) """ - # print(subdiv_rows_query) cursor.execute(subdiv_rows_query) subdiv_rows = cursor.fetchall() - # print(subdiv_rows) return subdiv_rows def create_subdiv_dict(subdiv_rows): - # creates dictionary of adjacent land use data with all directions attributed to one application id + # creates dictionary of subdivision parcel data with all subdivision attributed to one application component id alr_id = 'alr_appl_component_id' - parcel_design_id = 'subdiv_design_parcel_id' area = 'parcel_area' lot = 'Lot' parcel_type = 'type' @@ -32,30 +26,23 @@ def create_subdiv_dict(subdiv_rows): app_component_id = row[alr_id] if app_component_id in subdiv_dict: - # if row[parcel_design_id] not in subdiv_dict[app_component_id]: subdiv_dict[app_component_id].append({size: row[area], parcel_type: lot}) - # print("in the loop") - # print(subdiv_dict[app_component_id]) - # print("updated row") else: subdiv_dict[app_component_id] = [] - if row['road_dedication_area'] is not None: - subdiv_dict[app_component_id].append({size: row['road_dedication_area'], parcel_type: 'Road Dedication'}) subdiv_dict[app_component_id].append({size: row[area], parcel_type: lot}) return subdiv_dict def add_subdiv(data,json): + # inserts null valued json string insert_string = [] json_string = json.dumps(insert_string) data['subd_proposed_lots'] = json_string return data def map_subdiv_lots(data, subdiv_data, json): + # updates json string for applications with subdivision data insert_string = subdiv_data[data['alr_appl_component_id']] - # print(insert_string) json_string = json.dumps(insert_string) data['subd_proposed_lots'] = json_string - # print('to json') - # print(data['subd_proposed_lots']) return data \ No newline at end of file From 59c38e62656520cbc6a3624fe552261306394ac9 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Mon, 28 Aug 2023 16:33:21 -0700 Subject: [PATCH 315/954] Move timeline calculations to back end, add subtasks * Move all timeline logic to backend, clean up front-end, add unit tests * Load and add subtasks to events --- .../overview/overview.component.spec.ts | 26 +- .../overview/overview.component.ts | 302 +------------ .../info-requests/info-requests.component.ts | 2 +- .../overview/overview.component.spec.ts | 20 +- .../overview/overview.component.ts | 172 +------- .../application-timeline.dto.ts | 7 + .../application-timeline.service.spec.ts | 45 ++ .../application-timeline.service.ts | 25 ++ .../notice-of-intent-meeting.service.ts | 4 +- .../notice-of-intent-timeline.dto.ts | 7 + .../notice-of-intent-timeline.service.spec.ts | 45 ++ .../notice-of-intent-timeline.service.ts | 25 ++ .../shared/timeline/timeline.component.html | 7 +- .../app/shared/timeline/timeline.component.ts | 11 +- services/apps/alcs/src/alcs/alcs.module.ts | 6 + ...plication-decision-meeting.service.spec.ts | 10 +- .../application-decision-meeting.service.ts | 2 +- ...ation-submission-status.controller.spec.ts | 6 +- ...pplication-submission-status.controller.ts | 2 +- ...lication-submission-status.service.spec.ts | 8 +- .../application-submission-status.service.ts | 6 +- .../application-timeline.controller.spec.ts | 49 +++ .../application-timeline.controller.ts | 20 + .../application-timeline.module.ts | 26 ++ .../application-timeline.service.spec.ts | 294 +++++++++++++ .../application-timeline.service.ts | 406 ++++++++++++++++++ ...otice-of-intent-meeting.controller.spec.ts | 6 +- .../notice-of-intent-meeting.controller.ts | 12 +- .../notice-of-intent-meeting.service.spec.ts | 5 +- .../notice-of-intent-meeting.service.ts | 22 +- ...tice-of-intent-timeline.controller.spec.ts | 49 +++ .../notice-of-intent-timeline.controller.ts | 20 + .../notice-of-intent-timeline.module.ts | 22 + .../notice-of-intent-timeline.service.spec.ts | 187 ++++++++ .../notice-of-intent-timeline.service.ts | 267 ++++++++++++ .../notice-of-intent.module.ts | 20 +- services/apps/alcs/src/main.module.ts | 2 +- 37 files changed, 1582 insertions(+), 563 deletions(-) create mode 100644 alcs-frontend/src/app/services/application/application-timeline/application-timeline.dto.ts create mode 100644 alcs-frontend/src/app/services/application/application-timeline/application-timeline.service.spec.ts create mode 100644 alcs-frontend/src/app/services/application/application-timeline/application-timeline.service.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.dto.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.spec.ts create mode 100644 alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts create mode 100644 services/apps/alcs/src/alcs/application/application-timeline/application-timeline.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/application/application-timeline/application-timeline.controller.ts create mode 100644 services/apps/alcs/src/alcs/application/application-timeline/application-timeline.module.ts create mode 100644 services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.controller.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.module.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts diff --git a/alcs-frontend/src/app/features/application/overview/overview.component.spec.ts b/alcs-frontend/src/app/features/application/overview/overview.component.spec.ts index 260dcac99c..ae137fca84 100644 --- a/alcs-frontend/src/app/features/application/overview/overview.component.spec.ts +++ b/alcs-frontend/src/app/features/application/overview/overview.component.spec.ts @@ -4,13 +4,9 @@ import { MatDialog } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; -import { ApplicationMeetingService } from '../../../services/application/application-meeting/application-meeting.service'; -import { ApplicationModificationService } from '../../../services/application/application-modification/application-modification.service'; -import { ApplicationReconsiderationService } from '../../../services/application/application-reconsideration/application-reconsideration.service'; -import { ApplicationReviewService } from '../../../services/application/application-review/application-review.service'; import { ApplicationSubmissionStatusService } from '../../../services/application/application-submission-status/application-submission-status.service'; +import { ApplicationTimelineService } from '../../../services/application/application-timeline/application-timeline.service'; import { ApplicationDto } from '../../../services/application/application.dto'; -import { ApplicationDecisionService } from '../../../services/application/decision/application-decision-v1/application-decision.service'; import { OverviewComponent } from './overview.component'; @@ -30,27 +26,9 @@ describe('OverviewComponent', () => { useValue: mockAppDetailService, }, { - provide: ApplicationDecisionService, + provide: ApplicationTimelineService, useValue: {}, }, - { - provide: ApplicationReconsiderationService, - useValue: {}, - }, - { - provide: ApplicationModificationService, - useValue: {}, - }, - { - provide: ApplicationReviewService, - useValue: {}, - }, - { - provide: ApplicationMeetingService, - useValue: { - fetch: jest.fn(), - }, - }, { provide: MatDialog, useValue: {}, diff --git a/alcs-frontend/src/app/features/application/overview/overview.component.ts b/alcs-frontend/src/app/features/application/overview/overview.component.ts index 0749c04df4..04655eb1aa 100644 --- a/alcs-frontend/src/app/features/application/overview/overview.component.ts +++ b/alcs-frontend/src/app/features/application/overview/overview.component.ts @@ -1,45 +1,15 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { BehaviorSubject, combineLatestWith, Subject, takeUntil, tap } from 'rxjs'; +import { Subject, takeUntil } from 'rxjs'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; -import { ApplicationMeetingDto } from '../../../services/application/application-meeting/application-meeting.dto'; -import { ApplicationMeetingService } from '../../../services/application/application-meeting/application-meeting.service'; -import { ApplicationModificationDto } from '../../../services/application/application-modification/application-modification.dto'; -import { ApplicationModificationService } from '../../../services/application/application-modification/application-modification.service'; -import { ApplicationReconsiderationDto } from '../../../services/application/application-reconsideration/application-reconsideration.dto'; -import { ApplicationReconsiderationService } from '../../../services/application/application-reconsideration/application-reconsideration.service'; import { ApplicationSubmissionToSubmissionStatusDto } from '../../../services/application/application-submission-status/application-submission-status.dto'; import { ApplicationSubmissionStatusService } from '../../../services/application/application-submission-status/application-submission-status.service'; +import { ApplicationTimelineService } from '../../../services/application/application-timeline/application-timeline.service'; import { ApplicationDto, SUBMISSION_STATUS } from '../../../services/application/application.dto'; -import { ApplicationDecisionDto } from '../../../services/application/decision/application-decision-v1/application-decision.dto'; -import { ApplicationDecisionService } from '../../../services/application/decision/application-decision-v1/application-decision.service'; +import { TimelineEventDto } from '../../../services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.dto'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { TimelineEvent } from '../../../shared/timeline/timeline.component'; import { UncancelApplicationDialogComponent } from './uncancel-application-dialog/uncancel-application-dialog.component'; -const editLink = new Map([ - ['IR', './info-request'], - ['AM', './site-visit-meeting'], - ['SV', './site-visit-meeting'], -]); - -const SORTING_ORDER = { - //high comes first, 1 shows at bottom - MODIFICATION_REVIEW: 13, - MODIFICATION_REQUEST: 12, - RECON_REVIEW: 11, - RECON_REQUEST: 10, - CHAIR_REVIEW_DECISION: 9, - AUDITED_DECISION: 8, - DECISION_MADE: 7, - VISIT_REPORTS: 6, - VISIT_REQUESTS: 5, - ACKNOWLEDGE_COMPLETE: 4, - FEE_RECEIVED: 3, - ACKNOWLEDGED_INCOMPLETE: 2, - SUBMITTED: 1, -}; - @Component({ selector: 'app-overview', templateUrl: './overview.component.html', @@ -48,77 +18,33 @@ const SORTING_ORDER = { export class OverviewComponent implements OnInit, OnDestroy { $destroy = new Subject(); application?: ApplicationDto; - private $decisions = new BehaviorSubject([]); - private $statusHistory = new BehaviorSubject([]); - events: TimelineEvent[] = []; + events: TimelineEventDto[] = []; summary = ''; isCancelled = false; constructor( private applicationDetailService: ApplicationDetailService, - private meetingService: ApplicationMeetingService, - private decisionService: ApplicationDecisionService, - private reconsiderationService: ApplicationReconsiderationService, - private modificationService: ApplicationModificationService, private confirmationDialogService: ConfirmationDialogService, private applicationSubmissionStatusService: ApplicationSubmissionStatusService, + private applicationTimelineService: ApplicationTimelineService, private dialog: MatDialog ) {} ngOnInit(): void { - this.applicationDetailService.$application - .pipe(takeUntil(this.$destroy)) - .pipe( - tap((app) => { - if (app) { - this.clearComponentData(); - - this.loadStatusHistory(app.fileNumber); - this.meetingService.fetch(app.fileNumber); - this.decisionService.fetchByApplication(app.fileNumber).then((res) => { - this.$decisions.next(res); - }); - } - }) - ) - .pipe( - combineLatestWith( - this.meetingService.$meetings, - this.$decisions, - this.reconsiderationService.$reconsiderations, - this.modificationService.$modifications, - this.$statusHistory - ) - ) - .subscribe(([application, meetings, decisions, reconsiderations, modifications, statusHistory]) => { - if (application) { - this.summary = application.summary || ''; - this.application = application; - - this.events = this.mapApplicationToEvents( - application, - meetings, - decisions, - reconsiderations, - modifications, - statusHistory - ); - } - }); + this.applicationDetailService.$application.pipe(takeUntil(this.$destroy)).subscribe(async (application) => { + if (application) { + this.application = application; + this.events = await this.applicationTimelineService.fetchByFileNumber(application.fileNumber); + this.loadStatusHistory(this.application.fileNumber); + } + }); } async ngOnDestroy() { - await this.clearComponentData(); - this.$destroy.next(); this.$destroy.complete(); } - private async clearComponentData() { - this.$decisions.next([]); - this.$statusHistory.next([]); - } - async onCancelApplication() { this.confirmationDialogService .openDialog({ @@ -127,6 +53,7 @@ export class OverviewComponent implements OnInit, OnDestroy { title: 'Cancel Application', }) .subscribe(async (didConfirm) => { + debugger; if (didConfirm && this.application) { await this.applicationDetailService.cancelApplication(this.application.fileNumber); await this.loadStatusHistory(this.application.fileNumber); @@ -152,207 +79,9 @@ export class OverviewComponent implements OnInit, OnDestroy { } } - private mapApplicationToEvents( - application: ApplicationDto, - meetings: ApplicationMeetingDto[], - decisions: ApplicationDecisionDto[], - reconsiderations: ApplicationReconsiderationDto[], - modifications: ApplicationModificationDto[], - statusHistory: ApplicationSubmissionToSubmissionStatusDto[] - ): TimelineEvent[] { - const mappedEvents: TimelineEvent[] = []; - - const statusesToInclude = statusHistory.filter( - (status) => ![SUBMISSION_STATUS.IN_REVIEW_BY_ALC].includes(status.status.code) - ); - for (const status of statusesToInclude) { - if (status.effectiveDate) { - let htmlText = `${status.status.label}`; - - if (status.status.code === SUBMISSION_STATUS.RECEIVED_BY_ALC) { - htmlText = 'Received All Items - Received by ALC'; - } - - if (status.status.code === SUBMISSION_STATUS.IN_PROGRESS) { - htmlText = 'Created - In Progress'; - } - - if (status.status.code === SUBMISSION_STATUS.SUBMITTED_TO_ALC_INCOMPLETE) { - htmlText = 'Acknowledged Incomplete - Submitted to ALC - Incomplete'; - } - - mappedEvents.push({ - htmlText, - startDate: new Date(status.effectiveDate + status.status.weight), - isFulfilled: true, - }); - } - } - - if (application.dateAcknowledgedComplete) { - mappedEvents.push({ - htmlText: 'Acknowledged Complete', - startDate: new Date(application.dateAcknowledgedComplete + SORTING_ORDER.ACKNOWLEDGE_COMPLETE), - isFulfilled: true, - }); - } - - if (application.feePaidDate) { - mappedEvents.push({ - htmlText: 'Fee Received Date', - startDate: new Date(application.feePaidDate + SORTING_ORDER.FEE_RECEIVED), - isFulfilled: true, - }); - } - - const events: TimelineEvent[] = application.decisionMeetings - .sort((a, b) => a.date - b.date) - .map((meeting, index) => { - let htmlText = `Review Discussion #${index + 1}`; - if (index === 0) { - htmlText += ` - Under Review by ALC`; - } - - return { - htmlText, - startDate: new Date(meeting.date), - isFulfilled: true, - }; - }); - mappedEvents.push(...events); - - for (const [index, decision] of decisions.entries()) { - if (decision.isDraft) { - continue; - } - - if (decision.auditDate) { - mappedEvents.push({ - htmlText: `Audited Decision #${decisions.length - index}`, - startDate: new Date(decision.auditDate + SORTING_ORDER.AUDITED_DECISION), - isFulfilled: true, - }); - } - - if (decision.chairReviewDate) { - mappedEvents.push({ - htmlText: `Chair Reviewed Decision #${decisions.length - index}`, - startDate: new Date(decision.chairReviewDate + SORTING_ORDER.CHAIR_REVIEW_DECISION), - isFulfilled: true, - }); - } - - mappedEvents.push({ - htmlText: `Decision #${decisions.length - index} Made${ - decisions.length - 1 === index ? ` - Active Days: ${application.activeDays}` : '' - }`, - startDate: new Date(decision.date + SORTING_ORDER.DECISION_MADE), - isFulfilled: true, - }); - } - - if (application.notificationSentDate) { - mappedEvents.push({ - htmlText: "'Ready for Review' Notification Sent to Applicant", - startDate: new Date(application.notificationSentDate), - isFulfilled: true, - }); - } - - meetings.sort((a, b) => a.meetingStartDate - b.meetingStartDate); - const typeCount = new Map(); - meetings.forEach((meeting) => { - const count = typeCount.get(meeting.meetingType.code) || 0; - - mappedEvents.push({ - htmlText: `${meeting.meetingType.label} #${count + 1}`, - startDate: new Date(meeting.meetingStartDate + SORTING_ORDER.VISIT_REQUESTS), - fulfilledDate: meeting.meetingEndDate ? new Date(meeting.meetingEndDate) : undefined, - isFulfilled: !!meeting.meetingEndDate, - link: editLink.get(meeting.meetingType.code), - }); - - if (meeting.reportStartDate) { - mappedEvents.push({ - htmlText: `${meeting.meetingType.label} #${count + 1} Report Sent to Applicant`, - startDate: new Date(meeting.reportStartDate + SORTING_ORDER.VISIT_REPORTS), - fulfilledDate: meeting.reportEndDate ? new Date(meeting.reportEndDate) : undefined, - isFulfilled: !!meeting.reportEndDate, - link: editLink.get(meeting.meetingType.code), - }); - } - - typeCount.set(meeting.meetingType.code, count + 1); - }); - - const mappedReconsiderations = this.mapReconsiderationsToEvents(reconsiderations); - mappedEvents.push(...mappedReconsiderations); - - const mappedModifications = this.mapModificationsToEvents(modifications); - mappedEvents.push(...mappedModifications); - - mappedEvents.sort((a, b) => b.startDate.getTime() - a.startDate.getTime()); - - return mappedEvents; - } - - private mapReconsiderationsToEvents(reconsiderations: ApplicationReconsiderationDto[]) { - const events: TimelineEvent[] = []; - for (const [index, reconsideration] of reconsiderations - .sort((a, b) => b.submittedDate - a.submittedDate) - .entries()) { - if (reconsideration.type.code === '33.1') { - events.push({ - htmlText: `Reconsideration Request #${reconsiderations.length - index} - ${reconsideration.type.code}`, - startDate: new Date(reconsideration.submittedDate + SORTING_ORDER.RECON_REQUEST), - isFulfilled: true, - }); - } else { - events.push({ - htmlText: `Reconsideration Requested #${reconsiderations.length - index} - ${reconsideration.type.code}`, - startDate: new Date(reconsideration.submittedDate + SORTING_ORDER.RECON_REQUEST), - isFulfilled: true, - }); - if (reconsideration.reviewDate) { - events.push({ - htmlText: `Reconsideration Request Reviewed #${reconsiderations.length - index} - ${ - reconsideration.reviewOutcome?.label - }`, - startDate: new Date(reconsideration.reviewDate + SORTING_ORDER.RECON_REVIEW), - isFulfilled: true, - }); - } - } - } - return events; - } - - private mapModificationsToEvents(modifications: ApplicationModificationDto[]) { - const events: TimelineEvent[] = []; - for (const [index, modification] of modifications.sort((a, b) => b.submittedDate - a.submittedDate).entries()) { - events.push({ - htmlText: `Modification Requested #${modifications.length - index} - ${ - modification.isTimeExtension ? 'Time Extension' : 'Other' - }`, - startDate: new Date(modification.submittedDate + SORTING_ORDER.MODIFICATION_REQUEST), - isFulfilled: true, - }); - if (modification.reviewDate) { - events.push({ - htmlText: `Modification Request Reviewed #${modifications.length - index} - ${ - modification.reviewOutcome?.label - }`, - startDate: new Date(modification.reviewDate + SORTING_ORDER.MODIFICATION_REVIEW), - isFulfilled: true, - }); - } - } - return events; - } - - onSaveSummary(updatedSummary: string) { + async onSaveSummary(updatedSummary: string) { if (this.application) { - this.applicationDetailService.updateApplication(this.application.fileNumber, { + await this.applicationDetailService.updateApplication(this.application.fileNumber, { summary: updatedSummary ?? null, }); } @@ -372,6 +101,5 @@ export class OverviewComponent implements OnInit, OnDestroy { this.isCancelled = statusHistory.filter((status) => status.effectiveDate && status.statusTypeCode === SUBMISSION_STATUS.CANCELLED) .length > 0; - this.$statusHistory.next(statusHistory); } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/info-requests/info-requests.component.ts b/alcs-frontend/src/app/features/notice-of-intent/info-requests/info-requests.component.ts index 4e08e6e54b..174de6eddc 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/info-requests/info-requests.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/info-requests/info-requests.component.ts @@ -36,7 +36,7 @@ export class InfoRequestsComponent implements OnInit, OnDestroy { if (noi) { this.fileNumber = noi.fileNumber; this.uuid = noi.uuid; - this.meetingService.fetch(noi.uuid); + this.meetingService.fetch(noi.fileNumber); } }); diff --git a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.spec.ts index 1175ddba9a..ecfae7257b 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.spec.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.spec.ts @@ -8,6 +8,7 @@ import { NoticeOfIntentMeetingService } from '../../../services/notice-of-intent import { NoticeOfIntentDetailService } from '../../../services/notice-of-intent/notice-of-intent-detail.service'; import { NoticeOfIntentModificationDto } from '../../../services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.dto'; import { NoticeOfIntentModificationService } from '../../../services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.service'; +import { NoticeOfIntentTimelineService } from '../../../services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service'; import { NoticeOfIntentDto } from '../../../services/notice-of-intent/notice-of-intent.dto'; import { OverviewComponent } from './overview.component'; @@ -16,8 +17,6 @@ describe('OverviewComponent', () => { let component: OverviewComponent; let fixture: ComponentFixture; let mockNOIDetailService: DeepMocked; - let mockNoticeOfIntentMeetingService: DeepMocked; - let mockNOIModificationService: DeepMocked; let mockNOIDecisionService: DeepMocked; beforeEach(async () => { @@ -25,28 +24,13 @@ describe('OverviewComponent', () => { mockNOIDetailService = createMock(); mockNOIDetailService.$noticeOfIntent = new BehaviorSubject(undefined); - - mockNoticeOfIntentMeetingService = createMock(); - mockNoticeOfIntentMeetingService.$meetings = new BehaviorSubject([]); - - mockNOIModificationService = createMock(); - mockNOIModificationService.$modifications = new BehaviorSubject([]); - await TestBed.configureTestingModule({ providers: [ { provide: NoticeOfIntentDetailService, useValue: mockNOIDetailService, }, - { provide: NoticeOfIntentMeetingService, useValue: mockNoticeOfIntentMeetingService }, - { - provide: NoticeOfIntentModificationService, - useValue: mockNOIModificationService, - }, - { - provide: NoticeOfIntentDecisionService, - useValue: mockNOIDecisionService, - }, + { provide: NoticeOfIntentTimelineService, useValue: {} }, ], declarations: [OverviewComponent], schemas: [NO_ERRORS_SCHEMA], diff --git a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.ts b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.ts index facf0fe847..240cb4c737 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/overview/overview.component.ts @@ -1,34 +1,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { BehaviorSubject, combineLatestWith, Subject, takeUntil, tap } from 'rxjs'; -import { ApplicationModificationDto } from '../../../services/application/application-modification/application-modification.dto'; -import { ApplicationDecisionDto } from '../../../services/application/decision/application-decision-v1/application-decision.dto'; -import { NoticeOfIntentDecisionDto } from '../../../services/notice-of-intent/decision/notice-of-intent-decision.dto'; -import { NoticeOfIntentDecisionService } from '../../../services/notice-of-intent/decision/notice-of-intent-decision.service'; -import { NoticeOfIntentMeetingDto } from '../../../services/notice-of-intent/meeting/notice-of-intent-meeting.dto'; -import { NoticeOfIntentMeetingService } from '../../../services/notice-of-intent/meeting/notice-of-intent-meeting.service'; +import { Subject, takeUntil } from 'rxjs'; import { NoticeOfIntentDetailService } from '../../../services/notice-of-intent/notice-of-intent-detail.service'; -import { NoticeOfIntentModificationDto } from '../../../services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.dto'; -import { NoticeOfIntentModificationService } from '../../../services/notice-of-intent/notice-of-intent-modification/notice-of-intent-modification.service'; +import { TimelineEventDto } from '../../../services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.dto'; +import { NoticeOfIntentTimelineService } from '../../../services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service'; import { NoticeOfIntentDto } from '../../../services/notice-of-intent/notice-of-intent.dto'; -import { TimelineEvent } from '../../../shared/timeline/timeline.component'; - -const editLink = new Map([['IR', './info-request']]); - -const SORTING_ORDER = { - //high comes first, 1 shows at bottom - MODIFICATION_REVIEW: 12, - MODIFICATION_REQUEST: 11, - CHAIR_REVIEW_DECISION: 10, - AUDITED_DECISION: 9, - DECISION_MADE: 8, - VISIT_REPORTS: 7, - VISIT_REQUESTS: 6, - ACKNOWLEDGE_COMPLETE: 5, - RECEIVED_ALL_ITEMS: 4, - FEE_RECEIVED: 3, - ACKNOWLEDGED_INCOMPLETE: 2, - SUBMITTED: 1, -}; @Component({ selector: 'app-overview', @@ -38,43 +13,20 @@ const SORTING_ORDER = { export class OverviewComponent implements OnInit, OnDestroy { $destroy = new Subject(); noticeOfIntent?: NoticeOfIntentDto; - events: TimelineEvent[] = []; + events: TimelineEventDto[] = []; summary = ''; - private $decisions = new BehaviorSubject([]); - constructor( private noticeOfIntentDetailService: NoticeOfIntentDetailService, - private noticeOfIntentMeetingService: NoticeOfIntentMeetingService, - private noticeOfIntentDecisionService: NoticeOfIntentDecisionService, - private noticeOfIntentModificationService: NoticeOfIntentModificationService + private noticeOfIntentTimelineService: NoticeOfIntentTimelineService ) {} ngOnInit(): void { this.noticeOfIntentDetailService.$noticeOfIntent .pipe(takeUntil(this.$destroy)) - .pipe( - tap((noi) => { - if (noi) { - this.noticeOfIntentMeetingService.fetch(noi.uuid); - this.noticeOfIntentDecisionService.fetchByFileNumber(noi.fileNumber).then((res) => { - this.$decisions.next(res); - }); - } - }) - ) - .pipe( - combineLatestWith( - this.noticeOfIntentMeetingService.$meetings, - this.noticeOfIntentModificationService.$modifications, - this.$decisions - ) - ) - .subscribe(([noticeOfIntent, meetings, modifications, decisions]) => { + .subscribe(async (noticeOfIntent) => { if (noticeOfIntent) { - this.summary = noticeOfIntent.summary || ''; - this.noticeOfIntent = noticeOfIntent; - this.populateEvents(noticeOfIntent, meetings, modifications, decisions); + this.events = await this.noticeOfIntentTimelineService.fetchByFileNumber(noticeOfIntent.fileNumber); } }); } @@ -91,114 +43,4 @@ export class OverviewComponent implements OnInit, OnDestroy { }); } } - - private populateEvents( - noticeOfIntent: NoticeOfIntentDto, - meetings: NoticeOfIntentMeetingDto[], - modifications: NoticeOfIntentModificationDto[], - decisions: NoticeOfIntentDecisionDto[] - ) { - const mappedEvents: TimelineEvent[] = []; - if (noticeOfIntent.dateSubmittedToAlc) { - mappedEvents.push({ - htmlText: 'Submitted to ALC', - startDate: new Date(noticeOfIntent.dateSubmittedToAlc + SORTING_ORDER.SUBMITTED), - isFulfilled: true, - }); - } - - if (noticeOfIntent.dateAcknowledgedIncomplete) { - mappedEvents.push({ - htmlText: 'Acknowledged Incomplete', - startDate: new Date(noticeOfIntent.dateAcknowledgedIncomplete + SORTING_ORDER.ACKNOWLEDGED_INCOMPLETE), - isFulfilled: true, - }); - } - - if (noticeOfIntent.dateAcknowledgedComplete) { - mappedEvents.push({ - htmlText: 'Acknowledged Complete', - startDate: new Date(noticeOfIntent.dateAcknowledgedComplete + SORTING_ORDER.ACKNOWLEDGE_COMPLETE), - isFulfilled: true, - }); - } - - if (noticeOfIntent.feePaidDate) { - mappedEvents.push({ - htmlText: 'Fee Received Date', - startDate: new Date(noticeOfIntent.feePaidDate + SORTING_ORDER.FEE_RECEIVED), - isFulfilled: true, - }); - } - - if (noticeOfIntent.dateReceivedAllItems) { - mappedEvents.push({ - htmlText: 'Received All Items', - startDate: new Date(noticeOfIntent.dateReceivedAllItems + SORTING_ORDER.FEE_RECEIVED), - isFulfilled: true, - }); - } - - for (const [index, decision] of decisions.entries()) { - if (decision.auditDate) { - mappedEvents.push({ - htmlText: `Audited Decision #${decisions.length - index}`, - startDate: new Date(decision.auditDate + SORTING_ORDER.AUDITED_DECISION), - isFulfilled: true, - }); - } - - mappedEvents.push({ - htmlText: `Decision #${decisions.length - index} Made${ - decisions.length - 1 === index ? ` - Active Days: ${noticeOfIntent.activeDays}` : '' - }`, - startDate: new Date(decision.date! + SORTING_ORDER.DECISION_MADE), - isFulfilled: true, - }); - } - - meetings.sort((a, b) => a.meetingStartDate - b.meetingStartDate); - const typeCount = new Map(); - meetings.forEach((meeting) => { - const count = typeCount.get(meeting.meetingType.code) || 0; - mappedEvents.push({ - htmlText: `${meeting.meetingType.label} #${count + 1}`, - startDate: new Date(meeting.meetingStartDate + SORTING_ORDER.VISIT_REQUESTS), - fulfilledDate: meeting.meetingEndDate ? new Date(meeting.meetingEndDate) : undefined, - isFulfilled: !!meeting.meetingEndDate, - link: editLink.get(meeting.meetingType.code), - }); - - typeCount.set(meeting.meetingType.code, count + 1); - }); - - const mappedModifications = this.mapModificationsToEvents(modifications); - mappedEvents.push(...mappedModifications); - - mappedEvents.sort((a, b) => b.startDate.getTime() - a.startDate.getTime()); - - this.events = mappedEvents; - } - - private mapModificationsToEvents(modifications: NoticeOfIntentModificationDto[]) { - const events: TimelineEvent[] = []; - for (const [index, modification] of modifications.sort((a, b) => b.submittedDate - a.submittedDate).entries()) { - events.push({ - htmlText: `Modification Requested #${modifications.length - index}`, - startDate: new Date(modification.submittedDate + SORTING_ORDER.MODIFICATION_REQUEST), - isFulfilled: true, - }); - - if (modification.outcomeNotificationDate) { - events.push({ - htmlText: `Modification Request Reviewed #${modifications.length - index} - ${ - modification.reviewOutcome.label - }`, - startDate: new Date(modification.submittedDate + SORTING_ORDER.MODIFICATION_REQUEST), - isFulfilled: true, - }); - } - } - return events; - } } diff --git a/alcs-frontend/src/app/services/application/application-timeline/application-timeline.dto.ts b/alcs-frontend/src/app/services/application/application-timeline/application-timeline.dto.ts new file mode 100644 index 0000000000..cc1519a556 --- /dev/null +++ b/alcs-frontend/src/app/services/application/application-timeline/application-timeline.dto.ts @@ -0,0 +1,7 @@ +export interface TimelineEventDto { + htmlText: string; + startDate: number; + fulfilledDate: number | null; + isFulfilled: boolean; + link: string | null; +} diff --git a/alcs-frontend/src/app/services/application/application-timeline/application-timeline.service.spec.ts b/alcs-frontend/src/app/services/application/application-timeline/application-timeline.service.spec.ts new file mode 100644 index 0000000000..9f6dabcb8c --- /dev/null +++ b/alcs-frontend/src/app/services/application/application-timeline/application-timeline.service.spec.ts @@ -0,0 +1,45 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of } from 'rxjs'; +import { ToastService } from '../../toast/toast.service'; + +import { ApplicationTimelineService } from './application-timeline.service'; + +describe('ApplicationTimelineService', () => { + let service: ApplicationTimelineService; + let httpClient: DeepMocked; + let toastService: DeepMocked; + + beforeEach(() => { + httpClient = createMock(); + toastService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: httpClient, + }, + { + provide: ToastService, + useValue: toastService, + }, + ], + }); + service = TestBed.inject(ApplicationTimelineService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call get to fetch timeline events', async () => { + httpClient.get.mockReturnValue(of([])); + + const res = await service.fetchByFileNumber('1'); + + expect(httpClient.get).toHaveBeenCalledTimes(1); + expect(res.length).toEqual(0); + }); +}); diff --git a/alcs-frontend/src/app/services/application/application-timeline/application-timeline.service.ts b/alcs-frontend/src/app/services/application/application-timeline/application-timeline.service.ts new file mode 100644 index 0000000000..f352192b1b --- /dev/null +++ b/alcs-frontend/src/app/services/application/application-timeline/application-timeline.service.ts @@ -0,0 +1,25 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { ToastService } from '../../toast/toast.service'; +import { TimelineEventDto } from './application-timeline.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class ApplicationTimelineService { + private url = `${environment.apiUrl}/application-timeline`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetchByFileNumber(fileNumber: string) { + try { + return await firstValueFrom(this.http.get(`${this.url}/${fileNumber}`)); + } catch (err) { + console.error(err); + this.toastService.showErrorToast('Failed to timeline events'); + } + return []; + } +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/meeting/notice-of-intent-meeting.service.ts b/alcs-frontend/src/app/services/notice-of-intent/meeting/notice-of-intent-meeting.service.ts index aa316067c1..46dda0e04c 100644 --- a/alcs-frontend/src/app/services/notice-of-intent/meeting/notice-of-intent-meeting.service.ts +++ b/alcs-frontend/src/app/services/notice-of-intent/meeting/notice-of-intent-meeting.service.ts @@ -19,12 +19,12 @@ export class NoticeOfIntentMeetingService { constructor(private http: HttpClient, private toastService: ToastService) {} - async fetch(uuid: string) { + async fetch(fileNumber: string) { this.clearMeetings(); let meetings: NoticeOfIntentMeetingDto[] = []; try { - meetings = await firstValueFrom(this.http.get(`${this.url}/${uuid}`)); + meetings = await firstValueFrom(this.http.get(`${this.url}/${fileNumber}`)); } catch (err) { this.toastService.showErrorToast('Failed to fetch meetings'); } diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.dto.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.dto.ts new file mode 100644 index 0000000000..cc1519a556 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.dto.ts @@ -0,0 +1,7 @@ +export interface TimelineEventDto { + htmlText: string; + startDate: number; + fulfilledDate: number | null; + isFulfilled: boolean; + link: string | null; +} diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.spec.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.spec.ts new file mode 100644 index 0000000000..7a5d06ee20 --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.spec.ts @@ -0,0 +1,45 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of } from 'rxjs'; +import { ToastService } from '../../toast/toast.service'; + +import { NoticeOfIntentTimelineService } from './notice-of-intent-timeline.service'; + +describe('NoticeOfIntentTimelineService', () => { + let service: NoticeOfIntentTimelineService; + let httpClient: DeepMocked; + let toastService: DeepMocked; + + beforeEach(() => { + httpClient = createMock(); + toastService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: httpClient, + }, + { + provide: ToastService, + useValue: toastService, + }, + ], + }); + service = TestBed.inject(NoticeOfIntentTimelineService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should call get to fetch timeline events', async () => { + httpClient.get.mockReturnValue(of([])); + + const res = await service.fetchByFileNumber('1'); + + expect(httpClient.get).toHaveBeenCalledTimes(1); + expect(res.length).toEqual(0); + }); +}); diff --git a/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts new file mode 100644 index 0000000000..56d28e709c --- /dev/null +++ b/alcs-frontend/src/app/services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts @@ -0,0 +1,25 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../../environments/environment'; +import { ToastService } from '../../toast/toast.service'; +import { TimelineEventDto } from './notice-of-intent-timeline.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NoticeOfIntentTimelineService { + private url = `${environment.apiUrl}/notice-of-intent-timeline`; + + constructor(private http: HttpClient, private toastService: ToastService) {} + + async fetchByFileNumber(fileNumber: string) { + try { + return await firstValueFrom(this.http.get(`${this.url}/${fileNumber}`)); + } catch (err) { + console.error(err); + this.toastService.showErrorToast('Failed to fetch timeline events'); + } + return []; + } +} diff --git a/alcs-frontend/src/app/shared/timeline/timeline.component.html b/alcs-frontend/src/app/shared/timeline/timeline.component.html index b4f3721b49..54ed9c9704 100644 --- a/alcs-frontend/src/app/shared/timeline/timeline.component.html +++ b/alcs-frontend/src/app/shared/timeline/timeline.component.html @@ -18,9 +18,10 @@
diff --git a/alcs-frontend/src/app/shared/timeline/timeline.component.ts b/alcs-frontend/src/app/shared/timeline/timeline.component.ts index f0303d3d6c..bbd79500ae 100644 --- a/alcs-frontend/src/app/shared/timeline/timeline.component.ts +++ b/alcs-frontend/src/app/shared/timeline/timeline.component.ts @@ -1,12 +1,5 @@ import { Component, Input } from '@angular/core'; - -export interface TimelineEvent { - startDate: Date; - fulfilledDate?: Date; - isFulfilled: boolean; - htmlText: string; - link?: string; -} +import { TimelineEventDto } from '../../services/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.dto'; @Component({ selector: 'app-timeline[events]', @@ -14,7 +7,7 @@ export interface TimelineEvent { styleUrls: ['./timeline.component.scss'], }) export class TimelineComponent { - @Input() events: TimelineEvent[] = []; + @Input() events: TimelineEventDto[] = []; constructor() {} } diff --git a/services/apps/alcs/src/alcs/alcs.module.ts b/services/apps/alcs/src/alcs/alcs.module.ts index 7092c9b22c..f54beb3aaa 100644 --- a/services/apps/alcs/src/alcs/alcs.module.ts +++ b/services/apps/alcs/src/alcs/alcs.module.ts @@ -3,6 +3,7 @@ import { RouterModule } from '@nestjs/core'; import { ApplicationSubmissionStatusModule } from './application/application-submission-status/application-submission-status.module'; import { AdminModule } from './admin/admin.module'; import { ApplicationDecisionModule } from './application-decision/application-decision.module'; +import { ApplicationTimelineModule } from './application/application-timeline/application-timeline.module'; import { ApplicationModule } from './application/application.module'; import { BoardModule } from './board/board.module'; import { CardModule } from './card/card.module'; @@ -14,6 +15,7 @@ import { HomeModule } from './home/home.module'; import { ImportModule } from './import/import.module'; import { LocalGovernmentModule } from './local-government/local-government.module'; import { NoticeOfIntentDecisionModule } from './notice-of-intent-decision/notice-of-intent-decision.module'; +import { NoticeOfIntentTimelineModule } from './notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.module'; import { NoticeOfIntentSubmissionStatusModule } from './notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentModule } from './notice-of-intent/notice-of-intent.module'; import { NotificationModule } from './notification/notification.module'; @@ -39,6 +41,8 @@ import { StaffJournalModule } from './staff-journal/staff-journal.module'; NoticeOfIntentModule, StaffJournalModule, NoticeOfIntentDecisionModule, + ApplicationTimelineModule, + NoticeOfIntentTimelineModule, SearchModule, LocalGovernmentModule, RouterModule.register([ @@ -61,6 +65,8 @@ import { StaffJournalModule } from './staff-journal/staff-journal.module'; { path: 'alcs', module: ApplicationSubmissionStatusModule }, { path: 'alcs', module: NoticeOfIntentSubmissionStatusModule }, { path: 'alcs', module: LocalGovernmentModule }, + { path: 'alcs', module: ApplicationTimelineModule }, + { path: 'alcs', module: NoticeOfIntentTimelineModule }, ]), ], controllers: [], diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.service.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.service.spec.ts index 93bf9457ea..f272c8e8ae 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.service.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.service.spec.ts @@ -77,7 +77,7 @@ describe('ApplicationDecisionMeetingService', () => { mockApplicationSubmissionStatusService.setStatusDateByFileNumber.mockResolvedValue( {} as any, ); - mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber.mockResolvedValue( + mockApplicationSubmissionStatusService.getStatusesByFileNumber.mockResolvedValue( [mockSubmissionStatus], ); }); @@ -125,10 +125,10 @@ describe('ApplicationDecisionMeetingService', () => { expect(mockAppDecisionMeetingRepository.findOne).toBeCalledTimes(0); expect(mockAppDecisionMeetingRepository.save).toBeCalledTimes(1); expect( - mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber, + mockApplicationSubmissionStatusService.getStatusesByFileNumber, ).toBeCalledTimes(1); expect( - mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber, + mockApplicationSubmissionStatusService.getStatusesByFileNumber, ).toBeCalledWith(mockApplication.fileNumber); expect( mockApplicationSubmissionStatusService.setStatusDate, @@ -156,10 +156,10 @@ describe('ApplicationDecisionMeetingService', () => { }); expect(mockAppDecisionMeetingRepository.save).toBeCalledTimes(1); expect( - mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber, + mockApplicationSubmissionStatusService.getStatusesByFileNumber, ).toBeCalledTimes(1); expect( - mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber, + mockApplicationSubmissionStatusService.getStatusesByFileNumber, ).toBeCalledWith(mockApplication.fileNumber); expect( mockApplicationSubmissionStatusService.setStatusDate, diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.service.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.service.ts index 2dc7958735..59906bd274 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.service.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v1/application-decision-meeting/application-decision-meeting.service.ts @@ -76,7 +76,7 @@ export class ApplicationDecisionMeetingService { ); const currentStatuses = - await this.applicationSubmissionStatusService.getCurrentStatusesByFileNumber( + await this.applicationSubmissionStatusService.getStatusesByFileNumber( application.fileNumber, ); diff --git a/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.controller.spec.ts b/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.controller.spec.ts index 44519583c8..0a57bd3bfd 100644 --- a/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.controller.spec.ts @@ -49,17 +49,17 @@ describe('ApplicationSubmissionStatusController', () => { it('should call service to get statuses by file number', async () => { const fakeFileNumber = 'fake'; - mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber.mockResolvedValue( + mockApplicationSubmissionStatusService.getStatusesByFileNumber.mockResolvedValue( [new ApplicationSubmissionToSubmissionStatus()], ); const result = await controller.getStatusesByFileNumber(fakeFileNumber); expect( - mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber, + mockApplicationSubmissionStatusService.getStatusesByFileNumber, ).toBeCalledTimes(1); expect( - mockApplicationSubmissionStatusService.getCurrentStatusesByFileNumber, + mockApplicationSubmissionStatusService.getStatusesByFileNumber, ).toBeCalledWith(fakeFileNumber); expect(result.length).toEqual(1); expect(result).toBeDefined(); diff --git a/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.controller.ts b/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.controller.ts index a97e38a951..9556b5a140 100644 --- a/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.controller.ts +++ b/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.controller.ts @@ -18,7 +18,7 @@ export class ApplicationSubmissionStatusController { @Get('/:fileNumber') async getStatusesByFileNumber(@Param('fileNumber') fileNumber) { const statuses = - await this.applicationSubmissionStatusService.getCurrentStatusesByFileNumber( + await this.applicationSubmissionStatusService.getStatusesByFileNumber( fileNumber, ); diff --git a/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.service.spec.ts b/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.service.spec.ts index ca1f35abc3..db4f84dafe 100644 --- a/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.service.spec.ts +++ b/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.service.spec.ts @@ -116,7 +116,7 @@ describe('ApplicationSubmissionStatusService', () => { mockStatuses, ); - const statuses = await service.getCurrentStatusesBy(fakeSubmissionUuid); + const statuses = await service.getStatusesByUuid(fakeSubmissionUuid); expect( mockApplicationSubmissionToSubmissionStatusRepository.findBy, @@ -150,9 +150,7 @@ describe('ApplicationSubmissionStatusService', () => { }), ); - const statuses = await service.getCurrentStatusesByFileNumber( - fakeFileNumber, - ); + const statuses = await service.getStatusesByFileNumber(fakeFileNumber); expect( mockApplicationSubmissionToSubmissionStatusRepository.findBy, @@ -188,7 +186,7 @@ describe('ApplicationSubmissionStatusService', () => { mockApplicationSubmissionRepository.findOneBy.mockResolvedValue(null); await expect( - service.getCurrentStatusesByFileNumber(fakeFileNumber), + service.getStatusesByFileNumber(fakeFileNumber), ).rejects.toMatchObject( new ServiceNotFoundException( `Submission does not exist for provided application ${fakeFileNumber}. Only applications originated in portal have statuses.`, diff --git a/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.service.ts b/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.service.ts index 44fbccc982..5e3d8ffb15 100644 --- a/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.service.ts +++ b/services/apps/alcs/src/alcs/application/application-submission-status/application-submission-status.service.ts @@ -86,11 +86,11 @@ export class ApplicationSubmissionStatusService { ); } - async getCurrentStatusesBy(submissionUuid: string) { + async getStatusesByUuid(submissionUuid: string) { return await this.statusesRepository.findBy({ submissionUuid }); } - async getCurrentStatusesByFileNumber(fileNumber: string) { + async getStatusesByFileNumber(fileNumber: string) { const submission = await this.getSubmission(fileNumber); return await this.statusesRepository.findBy({ @@ -121,7 +121,7 @@ export class ApplicationSubmissionStatusService { //Note: do not use fileNumber as identifier since there maybe multiple submissions with // the same fileNumber due to isDraft flag async removeStatuses(submissionUuid: string) { - const statusesToRemove = await this.getCurrentStatusesBy(submissionUuid); + const statusesToRemove = await this.getStatusesByUuid(submissionUuid); return await this.statusesRepository.remove(statusesToRemove); } diff --git a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.controller.spec.ts b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.controller.spec.ts new file mode 100644 index 0000000000..bb8f864850 --- /dev/null +++ b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.controller.spec.ts @@ -0,0 +1,49 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { ApplicationTimelineController } from './application-timeline.controller'; +import { ApplicationTimelineService } from './application-timeline.service'; + +describe('ApplicationTimelineController', () => { + let controller: ApplicationTimelineController; + let mockTimelineService: DeepMocked; + + beforeEach(async () => { + mockTimelineService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ApplicationTimelineService, + useValue: mockTimelineService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + controllers: [ApplicationTimelineController], + }).compile(); + + controller = module.get( + ApplicationTimelineController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call through to get timeline events', async () => { + mockTimelineService.getTimelineEvents.mockResolvedValue([]); + const res = await controller.fetchTimelineEvents('fileNumber'); + + expect(res).toBeDefined(); + expect(mockTimelineService.getTimelineEvents).toHaveBeenCalledTimes(1); + expect(mockTimelineService.getTimelineEvents).toHaveBeenCalledWith( + 'fileNumber', + ); + }); +}); diff --git a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.controller.ts b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.controller.ts new file mode 100644 index 0000000000..5661ed1358 --- /dev/null +++ b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { ApplicationTimelineService } from './application-timeline.service'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@Controller('application-timeline') +@UseGuards(RolesGuard) +export class ApplicationTimelineController { + constructor(private applicationTimelineService: ApplicationTimelineService) {} + + @Get('/:fileNumber') + @UserRoles(AUTH_ROLE.ADMIN) + async fetchTimelineEvents(@Param('fileNumber') fileNumber: string) { + return await this.applicationTimelineService.getTimelineEvents(fileNumber); + } +} diff --git a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.module.ts b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.module.ts new file mode 100644 index 0000000000..4ed61a3a95 --- /dev/null +++ b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ApplicationDecision } from '../../application-decision/application-decision.entity'; +import { ApplicationModification } from '../../application-decision/application-modification/application-modification.entity'; +import { ApplicationReconsideration } from '../../application-decision/application-reconsideration/application-reconsideration.entity'; +import { ApplicationSubmissionStatusModule } from '../application-submission-status/application-submission-status.module'; +import { Application } from '../application.entity'; +import { ApplicationModule } from '../application.module'; +import { ApplicationTimelineController } from './application-timeline.controller'; +import { ApplicationTimelineService } from './application-timeline.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + Application, + ApplicationModification, + ApplicationReconsideration, + ApplicationDecision, + ]), + ApplicationModule, + ApplicationSubmissionStatusModule, + ], + providers: [ApplicationTimelineService], + controllers: [ApplicationTimelineController], +}) +export class ApplicationTimelineModule {} diff --git a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.spec.ts b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.spec.ts new file mode 100644 index 0000000000..b675cd35f1 --- /dev/null +++ b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.spec.ts @@ -0,0 +1,294 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ApplicationDecision } from '../../application-decision/application-decision.entity'; +import { ApplicationModificationOutcomeType } from '../../application-decision/application-modification/application-modification-outcome-type/application-modification-outcome-type.entity'; +import { ApplicationModification } from '../../application-decision/application-modification/application-modification.entity'; +import { ApplicationReconsideration } from '../../application-decision/application-reconsideration/application-reconsideration.entity'; +import { ApplicationReconsiderationOutcomeType } from '../../application-decision/application-reconsideration/reconsideration-outcome-type/application-reconsideration-outcome-type.entity'; +import { ApplicationReconsiderationType } from '../../application-decision/application-reconsideration/reconsideration-type/application-reconsideration-type.entity'; +import { NoticeOfIntentMeetingType } from '../../notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting-type.entity'; +import { NoticeOfIntentMeeting } from '../../notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.entity'; +import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; +import { ApplicationMeeting } from '../application-meeting/application-meeting.entity'; +import { ApplicationMeetingService } from '../application-meeting/application-meeting.service'; +import { ApplicationPaused } from '../application-paused.entity'; +import { ApplicationSubmissionStatusService } from '../application-submission-status/application-submission-status.service'; +import { SUBMISSION_STATUS } from '../application-submission-status/submission-status.dto'; +import { ApplicationSubmissionToSubmissionStatus } from '../application-submission-status/submission-status.entity'; +import { Application } from '../application.entity'; +import { ApplicationService } from '../application.service'; +import { ApplicationTimelineService } from './application-timeline.service'; + +describe('ApplicationTimelineService', () => { + let service: ApplicationTimelineService; + let mockAppRepo: DeepMocked>; + let mockAppModificationRepo: DeepMocked>; + let mockAppReconsiderationRepo: DeepMocked< + Repository + >; + let mockAppDecisionRepo: DeepMocked>; + let mockAppService: DeepMocked; + let mockAppMeetingService: DeepMocked; + let mockAppStatusService: DeepMocked; + + beforeEach(async () => { + mockAppRepo = createMock(); + mockAppModificationRepo = createMock(); + mockAppReconsiderationRepo = createMock(); + mockAppDecisionRepo = createMock(); + mockAppService = createMock(); + mockAppMeetingService = createMock(); + mockAppStatusService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: getRepositoryToken(Application), + useValue: mockAppRepo, + }, + { + provide: getRepositoryToken(ApplicationReconsideration), + useValue: mockAppReconsiderationRepo, + }, + { + provide: getRepositoryToken(ApplicationModification), + useValue: mockAppModificationRepo, + }, + { + provide: getRepositoryToken(ApplicationDecision), + useValue: mockAppDecisionRepo, + }, + { + provide: ApplicationService, + useValue: mockAppService, + }, + { + provide: ApplicationMeetingService, + useValue: mockAppMeetingService, + }, + { + provide: ApplicationSubmissionStatusService, + useValue: mockAppStatusService, + }, + ApplicationTimelineService, + ], + }).compile(); + + service = module.get( + ApplicationTimelineService, + ); + + mockAppRepo.findOneOrFail.mockResolvedValue(new Application()); + mockAppModificationRepo.find.mockResolvedValue([]); + mockAppReconsiderationRepo.find.mockResolvedValue([]); + mockAppDecisionRepo.find.mockResolvedValue([]); + mockAppMeetingService.getByAppFileNumber.mockResolvedValue([]); + mockAppStatusService.getStatusesByFileNumber.mockResolvedValue([]); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should return nothing for empty Application', async () => { + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + }); + + it('should map base Application events in the correct order', async () => { + const sameDate = new Date(); + mockAppRepo.findOneOrFail.mockResolvedValue( + new Application({ + feePaidDate: sameDate, + dateAcknowledgedComplete: sameDate, + notificationSentDate: sameDate, + }), + ); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(3); + expect(res[2].htmlText).toEqual('Fee Received Date'); + expect(res[1].htmlText).toEqual('Acknowledged Complete'); + expect(res[0].htmlText).toEqual( + "'Ready for Review' Notification Sent to Applicant", + ); + }); + + it('should map decision events in the correct order', async () => { + const sameDate = new Date(); + mockAppService.mapToDtos.mockResolvedValue([ + { + activeDays: 6, + } as any, + ]); + + mockAppDecisionRepo.find.mockResolvedValue([ + new ApplicationDecision({ + auditDate: new Date(sameDate.getTime() + 1000), + chairReviewDate: new Date(sameDate.getTime() + 1000), + date: new Date(sameDate.getTime() + 1000), + }), + new ApplicationDecision({ + auditDate: sameDate, + chairReviewDate: sameDate, + date: sameDate, + }), + ]); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(6); + expect(res[5].htmlText).toEqual('Decision #1 Made - Active Days: 6'); + expect(res[4].htmlText).toEqual('Audited Decision #1'); + expect(res[3].htmlText).toEqual('Chair Reviewed Decision #1'); + expect(res[2].htmlText).toEqual('Decision #2 Made'); + expect(res[1].htmlText).toEqual('Audited Decision #2'); + expect(res[0].htmlText).toEqual('Chair Reviewed Decision #2'); + }); + + it('should map reconsideration events in the correct order', async () => { + const sameDate = new Date(); + mockAppReconsiderationRepo.find.mockResolvedValue([ + new ApplicationReconsideration({ + type: { + code: '33', + } as ApplicationReconsiderationType, + submittedDate: new Date(sameDate.getTime() + 100), + reviewDate: new Date(sameDate.getTime() + 100), + reviewOutcome: { + label: 'CATS', + } as ApplicationReconsiderationOutcomeType, + }), + new ApplicationReconsideration({ + type: { + code: '33.1', + } as ApplicationReconsiderationType, + submittedDate: sameDate, + }), + ]); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(3); + expect(res[2].htmlText).toEqual('Reconsideration Request #1 - 33.1'); + expect(res[1].htmlText).toEqual('Reconsideration Requested #2 - 33'); + expect(res[0].htmlText).toEqual( + 'Reconsideration Request Reviewed #2 - CATS', + ); + }); + + it('should map modification events in the correct order', async () => { + const sameDate = new Date(); + mockAppModificationRepo.find.mockResolvedValue([ + new ApplicationModification({ + isTimeExtension: true, + submittedDate: new Date(sameDate.getTime() + 100), + reviewDate: new Date(sameDate.getTime() + 100), + reviewOutcome: { + label: 'CATS', + } as ApplicationModificationOutcomeType, + }), + new ApplicationModification({ + isTimeExtension: false, + submittedDate: sameDate, + }), + ]); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(3); + expect(res[2].htmlText).toEqual('Modification Requested #1 - Other'); + expect(res[1].htmlText).toEqual( + 'Modification Requested #2 - Time Extension', + ); + expect(res[0].htmlText).toEqual('Modification Request Reviewed #2 - CATS'); + }); + + it('should map Meeting Events', async () => { + const sameDate = new Date(); + mockAppMeetingService.getByAppFileNumber.mockResolvedValue([ + new ApplicationMeeting({ + meetingPause: new ApplicationPaused({ + startDate: new Date(sameDate.getTime() + 1000), + endDate: new Date(sameDate.getTime() + 1000), + }), + reportPause: new ApplicationPaused({ + startDate: new Date(sameDate.getTime() + 1000), + endDate: null, + }), + type: { + label: 'CATS', + } as any, + }), + new ApplicationMeeting({ + meetingPause: new ApplicationPaused({ + startDate: sameDate, + endDate: sameDate, + }), + reportPause: new ApplicationPaused({ + startDate: sameDate, + endDate: sameDate, + }), + type: { + label: 'CATS', + } as any, + }), + ]); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(4); + expect(res[3].htmlText).toEqual('CATS #1'); + expect(res[2].htmlText).toEqual('CATS #1 Report Sent to Applicant'); + expect(res[1].htmlText).toEqual('CATS #2'); + expect(res[0].htmlText).toEqual('CATS #2 Report Sent to Applicant'); + }); + + it('should map Status Events', async () => { + const sameDate = new Date(); + mockAppStatusService.getStatusesByFileNumber.mockResolvedValue([ + new ApplicationSubmissionToSubmissionStatus({ + statusType: { + code: SUBMISSION_STATUS.RECEIVED_BY_ALC, + weight: 2, + } as any, + effectiveDate: sameDate, + }), + new ApplicationSubmissionToSubmissionStatus({ + statusType: { + code: SUBMISSION_STATUS.IN_PROGRESS, + weight: 0, + } as any, + effectiveDate: sameDate, + }), + new ApplicationSubmissionToSubmissionStatus({ + statusType: { + code: SUBMISSION_STATUS.SUBMITTED_TO_ALC_INCOMPLETE, + weight: 1, + } as any, + effectiveDate: sameDate, + }), + ]); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(3); + expect(res[2].htmlText).toEqual('Created - In Progress'); + expect(res[1].htmlText).toEqual( + 'Acknowledged Incomplete - Submitted to ALC - Incomplete', + ); + expect(res[0].htmlText).toEqual( + 'Received All Items - Received by ALC', + ); + }); +}); diff --git a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts new file mode 100644 index 0000000000..b3bad2061d --- /dev/null +++ b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts @@ -0,0 +1,406 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ApplicationDecision } from '../../application-decision/application-decision.entity'; +import { ApplicationModification } from '../../application-decision/application-modification/application-modification.entity'; +import { ApplicationReconsideration } from '../../application-decision/application-reconsideration/application-reconsideration.entity'; +import { CardSubtask } from '../../card/card-subtask/card-subtask.entity'; +import { ApplicationMeetingService } from '../application-meeting/application-meeting.service'; +import { ApplicationSubmissionStatusService } from '../application-submission-status/application-submission-status.service'; +import { SUBMISSION_STATUS } from '../application-submission-status/submission-status.dto'; +import { Application } from '../application.entity'; +import { ApplicationService } from '../application.service'; + +export interface TimelineEvent { + htmlText: string; + startDate: number; + fulfilledDate: number | null; + isFulfilled: boolean; + link?: string | null; +} + +const SORTING_ORDER = { + //high comes first, 1 shows at bottom + MODIFICATION_REVIEW: 14, + MODIFICATION_REQUEST: 13, + RECON_REVIEW: 12, + RECON_REQUEST: 11, + CHAIR_REVIEW_DECISION: 10, + AUDITED_DECISION: 9, + READY_FOR_REVIEW_NOTIFICATION: 8, + DECISION_MADE: 7, + VISIT_REPORTS: 6, + VISIT_REQUESTS: 5, + ACKNOWLEDGE_COMPLETE: 4, + FEE_RECEIVED: 3, + ACKNOWLEDGED_INCOMPLETE: 2, + SUBMITTED: 1, +}; + +const editLink = new Map([ + ['IR', './info-request'], + ['AM', './site-visit-meeting'], + ['SV', './site-visit-meeting'], +]); + +@Injectable() +export class ApplicationTimelineService { + constructor( + @InjectRepository(Application) + private applicationRepo: Repository, + @InjectRepository(ApplicationModification) + private applicationModificationRepo: Repository, + @InjectRepository(ApplicationReconsideration) + private applicationReconsiderationRepo: Repository, + @InjectRepository(ApplicationDecision) + private applicationDecisionRepo: Repository, + private applicationService: ApplicationService, + private applicationMeetingService: ApplicationMeetingService, + private applicationSubmissionStatusService: ApplicationSubmissionStatusService, + ) {} + + async getTimelineEvents(fileNumber: string) { + const events: TimelineEvent[] = []; + const application = await this.applicationRepo.findOneOrFail({ + where: { + fileNumber, + }, + relations: { + decisionMeetings: true, + type: true, + card: { + type: true, + subtasks: { + type: true, + }, + }, + }, + withDeleted: true, + }); + + this.addApplicationEvents(application, events); + await this.addDecisionEvents(application, events); + await this.addReconsiderationEvents(application, events); + await this.addModificationEvents(application, events); + await this.addMeetingEvents(application, events); + await this.addStatusEvents(application, events); + + if (application.card) { + for (const subtask of application.card.subtasks) { + const mappedEvent = this.mapSubtaskToEvent(subtask); + events.push(mappedEvent); + } + } + + events.sort((a, b) => b.startDate - a.startDate); + return events; + } + + private addApplicationEvents( + application: Application, + events: TimelineEvent[], + ) { + if (application.feePaidDate) { + events.push({ + htmlText: 'Fee Received Date', + startDate: + application.feePaidDate.getTime() + SORTING_ORDER.FEE_RECEIVED, + fulfilledDate: null, + isFulfilled: true, + }); + } + + if (application.dateAcknowledgedComplete) { + events.push({ + htmlText: 'Acknowledged Complete', + startDate: + application.dateAcknowledgedComplete.getTime() + + SORTING_ORDER.ACKNOWLEDGE_COMPLETE, + isFulfilled: true, + fulfilledDate: null, + }); + } + + if (application.notificationSentDate) { + events.push({ + htmlText: "'Ready for Review' Notification Sent to Applicant", + startDate: + application.notificationSentDate.getTime() + + SORTING_ORDER.READY_FOR_REVIEW_NOTIFICATION, + fulfilledDate: null, + isFulfilled: true, + }); + } + } + + private mapSubtaskToEvent(subtask: CardSubtask): TimelineEvent { + return { + htmlText: `${subtask.type.label} Subtask`, + fulfilledDate: subtask.completedAt?.getTime() ?? null, + startDate: subtask.createdAt.getTime(), + isFulfilled: !!subtask.completedAt, + }; + } + + private async addDecisionEvents( + application: Application, + events: TimelineEvent[], + ) { + const decisions = await this.applicationDecisionRepo.find({ + where: { + applicationUuid: application.uuid, + isDraft: false, + }, + order: { + date: 'DESC', + }, + }); + + if (decisions.length > 0) { + const mappedApplications = await this.applicationService.mapToDtos([ + application, + ]); + const mappedApplication = mappedApplications[0]; + + for (const [index, decision] of decisions.entries()) { + if (decision.auditDate) { + events.push({ + htmlText: `Audited Decision #${decisions.length - index}`, + startDate: + decision.auditDate.getTime() + SORTING_ORDER.AUDITED_DECISION, + fulfilledDate: null, + isFulfilled: true, + }); + } + + if (decision.chairReviewDate) { + events.push({ + htmlText: `Chair Reviewed Decision #${decisions.length - index}`, + startDate: + decision.chairReviewDate.getTime() + + SORTING_ORDER.CHAIR_REVIEW_DECISION, + fulfilledDate: null, + isFulfilled: true, + }); + } + + events.push({ + htmlText: `Decision #${decisions.length - index} Made${ + decisions.length - 1 === index + ? ` - Active Days: ${mappedApplication.activeDays}` + : '' + }`, + startDate: decision.date!.getTime() + SORTING_ORDER.DECISION_MADE, + fulfilledDate: null, + isFulfilled: true, + }); + } + } + } + + private async addReconsiderationEvents( + application: Application, + events: TimelineEvent[], + ) { + const reconsiderations = await this.applicationReconsiderationRepo.find({ + where: { + applicationUuid: application.uuid, + }, + relations: { + card: { + subtasks: { + type: true, + }, + }, + }, + order: { + submittedDate: 'DESC', + }, + }); + + for (const [index, reconsideration] of reconsiderations.entries()) { + if (reconsideration.type.code === '33.1') { + events.push({ + htmlText: `Reconsideration Request #${ + reconsiderations.length - index + } - ${reconsideration.type.code}`, + startDate: + reconsideration.submittedDate.getTime() + + SORTING_ORDER.RECON_REQUEST, + fulfilledDate: null, + isFulfilled: true, + }); + } else { + events.push({ + htmlText: `Reconsideration Requested #${ + reconsiderations.length - index + } - ${reconsideration.type.code}`, + startDate: + reconsideration.submittedDate.getTime() + + SORTING_ORDER.RECON_REQUEST, + fulfilledDate: null, + isFulfilled: true, + }); + if (reconsideration.reviewDate) { + events.push({ + htmlText: `Reconsideration Request Reviewed #${ + reconsiderations.length - index + } - ${reconsideration.reviewOutcome?.label}`, + startDate: + reconsideration.reviewDate.getTime() + SORTING_ORDER.RECON_REVIEW, + fulfilledDate: null, + isFulfilled: true, + }); + } + } + } + } + + private async addModificationEvents( + application: Application, + events: TimelineEvent[], + ) { + const modifications = await this.applicationModificationRepo.find({ + where: { + applicationUuid: application.uuid, + }, + relations: { + card: { + subtasks: { + type: true, + }, + }, + }, + order: { + submittedDate: 'DESC', + }, + }); + + for (const [index, modification] of modifications.entries()) { + events.push({ + htmlText: `Modification Requested #${modifications.length - index} - ${ + modification.isTimeExtension ? 'Time Extension' : 'Other' + }`, + startDate: + modification.submittedDate.getTime() + + SORTING_ORDER.MODIFICATION_REQUEST, + fulfilledDate: null, + isFulfilled: true, + }); + + if (modification.card) { + for (const subtask of modification.card.subtasks) { + const mappedEvent = this.mapSubtaskToEvent(subtask); + events.push(mappedEvent); + } + } + + if (modification.reviewDate) { + events.push({ + htmlText: `Modification Request Reviewed #${ + modifications.length - index + } - ${modification.reviewOutcome.label}`, + startDate: + modification.reviewDate.getTime() + + SORTING_ORDER.MODIFICATION_REVIEW, + fulfilledDate: null, + isFulfilled: true, + }); + } + } + } + + private async addMeetingEvents( + noticeOfIntent: Application, + events: TimelineEvent[], + ) { + const meetings = await this.applicationMeetingService.getByAppFileNumber( + noticeOfIntent.fileNumber, + ); + + meetings.sort( + (a, b) => + a.meetingPause!.startDate.getTime() - + b.meetingPause!.startDate.getTime(), + ); + + const typeCount = new Map(); + meetings.forEach((meeting) => { + const count = typeCount.get(meeting.type.code) || 0; + + const meetingStartDate = meeting.meetingPause?.startDate.getTime(); + const meetingEndDate = meeting.meetingPause?.endDate?.getTime(); + + if (meetingStartDate) { + events.push({ + htmlText: `${meeting.type.label} #${count + 1}`, + startDate: meetingStartDate + SORTING_ORDER.VISIT_REQUESTS, + fulfilledDate: meetingEndDate ?? null, + isFulfilled: !!meetingEndDate, + link: editLink.get(meeting.type.code), + }); + } + + const reportStartDate = meeting.reportPause?.startDate.getTime(); + const reportEndDate = meeting.reportPause?.endDate?.getTime(); + if (reportStartDate) { + events.push({ + htmlText: `${meeting.type.label} #${ + count + 1 + } Report Sent to Applicant`, + startDate: reportStartDate + SORTING_ORDER.VISIT_REPORTS, + fulfilledDate: reportEndDate ?? null, + isFulfilled: !!reportEndDate, + link: editLink.get(meeting.type.code), + }); + } + + typeCount.set(meeting.type.code, count + 1); + }); + } + + private async addStatusEvents( + application: Application, + events: TimelineEvent[], + ) { + const statusHistory = + await this.applicationSubmissionStatusService.getStatusesByFileNumber( + application.fileNumber, + ); + + const statusesToInclude = statusHistory.filter( + (status) => + ![SUBMISSION_STATUS.IN_REVIEW_BY_ALC].includes( + status.statusType.code, + ), + ); + for (const status of statusesToInclude) { + if (status.effectiveDate) { + let htmlText = `${status.statusType.label}`; + + if (status.statusType.code === SUBMISSION_STATUS.RECEIVED_BY_ALC) { + htmlText = 'Received All Items - Received by ALC'; + } + + if (status.statusType.code === SUBMISSION_STATUS.IN_PROGRESS) { + htmlText = 'Created - In Progress'; + } + + if ( + status.statusType.code === + SUBMISSION_STATUS.SUBMITTED_TO_ALC_INCOMPLETE + ) { + htmlText = + 'Acknowledged Incomplete - Submitted to ALC - Incomplete'; + } + + events.push({ + htmlText, + startDate: status.effectiveDate.getTime() + status.statusType.weight, + fulfilledDate: null, + isFulfilled: true, + }); + } + } + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.spec.ts index 39cec3a79b..d06047a6fc 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.spec.ts @@ -80,14 +80,12 @@ describe('NoticeOfIntentMeetingController', () => { }); it('should get all for notice of intent', async () => { - mockNoticeOfIntentMeetingService.getByAppFileNumber.mockResolvedValue([ + mockNoticeOfIntentMeetingService.getByFileNumber.mockResolvedValue([ mockMeeting, ]); const result = await controller.getAllForApplication('fake-number'); - expect(mockNoticeOfIntentMeetingService.getByAppFileNumber).toBeCalledTimes( - 1, - ); + expect(mockNoticeOfIntentMeetingService.getByFileNumber).toBeCalledTimes(1); expect(result[0].uuid).toStrictEqual(mockMeeting.uuid); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.ts index f0d00b7ef8..6c82c8202a 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.controller.ts @@ -1,17 +1,17 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; import { Body, Controller, Delete, Get, + Logger, NotFoundException, Param, Patch, Post, + UseGuards, } from '@nestjs/common'; - -import { Mapper } from '@automapper/core'; -import { InjectMapper } from '@automapper/nestjs'; -import { Logger, UseGuards } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; import { ServiceNotFoundException } from '../../../../../../libs/common/src/exceptions/base.exception'; @@ -44,9 +44,7 @@ export class NoticeOfIntentMeetingController { async getAllForApplication( @Param('fileNumber') fileNumber, ): Promise { - const meetings = await this.noiMeetingService.getByAppFileNumber( - fileNumber, - ); + const meetings = await this.noiMeetingService.getByFileNumber(fileNumber); return this.mapper.mapArrayAsync( meetings, diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.service.spec.ts index 1e6066fa42..da700a985b 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.service.spec.ts @@ -62,6 +62,7 @@ describe('NoticeOfIntentMeetingService', () => { mockNoiMeetingRepository.findOne.mockResolvedValue(mockMeeting); mockNoiMeetingRepository.save.mockResolvedValue(mockMeeting); mockNoiService.getOrFailByUuid.mockResolvedValue(mockNoi); + mockNoiService.getUuid.mockResolvedValue(mockNoi.uuid); }); it('should be defined', () => { @@ -69,14 +70,14 @@ describe('NoticeOfIntentMeetingService', () => { }); it('should get meetings for noi', async () => { - const result = await service.getByAppFileNumber(mockNoi.fileNumber); + const result = await service.getByFileNumber(mockNoi.fileNumber); expect(result).toStrictEqual([mockMeeting]); }); it('should return empty array if no meetings for noi', async () => { mockNoiMeetingRepository.find.mockResolvedValue([]); - const result = await service.getByAppFileNumber('non-existing number'); + const result = await service.getByFileNumber('non-existing number'); expect(result).toStrictEqual([]); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.service.ts index 5c7828d596..b1c8962791 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-meeting/notice-of-intent-meeting.service.ts @@ -22,22 +22,22 @@ const DEFAULT_RELATIONS: FindOptionsRelations = { export class NoticeOfIntentMeetingService { constructor( @InjectRepository(NoticeOfIntentMeeting) - private noiRepository: Repository, + private noticeOfIntentMeetingRepository: Repository, @InjectRepository(NoticeOfIntentMeetingType) private noiMeetingTypeRepository: Repository, - private noiService: NoticeOfIntentService, + private noticeOfIntentService: NoticeOfIntentService, ) {} - async getByAppFileNumber(number: string): Promise { - const noi = await this.noiService.getOrFailByUuid(number); - return this.noiRepository.find({ - where: { noticeOfIntentUuid: noi.uuid }, + async getByFileNumber(number: string): Promise { + const uuid = await this.noticeOfIntentService.getUuid(number); + return await this.noticeOfIntentMeetingRepository.find({ + where: { noticeOfIntentUuid: uuid }, relations: DEFAULT_RELATIONS, }); } get(uuid) { - return this.noiRepository.findOne({ + return this.noticeOfIntentMeetingRepository.findOne({ where: { uuid }, relations: DEFAULT_RELATIONS, }); @@ -68,7 +68,7 @@ export class NoticeOfIntentMeetingService { : existingMeeting.startDate; existingMeeting.endDate = formatIncomingDate(updateDto.meetingEndDate); - await this.noiRepository.save(existingMeeting); + await this.noticeOfIntentMeetingRepository.save(existingMeeting); return this.get(uuid); } @@ -83,13 +83,15 @@ export class NoticeOfIntentMeetingService { createMeeting.typeCode = meeting.meetingTypeCode; createMeeting.noticeOfIntentUuid = meeting.noticeOfIntentUuid; - const savedMeeting = await this.noiRepository.save(createMeeting); + const savedMeeting = await this.noticeOfIntentMeetingRepository.save( + createMeeting, + ); return this.get(savedMeeting.uuid); } async remove(meeting: NoticeOfIntentMeeting) { - return this.noiRepository.softRemove(meeting); + return this.noticeOfIntentMeetingRepository.softRemove(meeting); } async fetNoticeOfIntentMeetingTypes() { diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.controller.spec.ts new file mode 100644 index 0000000000..16dc3e6044 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.controller.spec.ts @@ -0,0 +1,49 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentTimelineController } from './notice-of-intent-timeline.controller'; +import { NoticeOfIntentTimelineService } from './notice-of-intent-timeline.service'; + +describe('NoticeOfIntentTimelineController', () => { + let controller: NoticeOfIntentTimelineController; + let mockTimelineService: DeepMocked; + + beforeEach(async () => { + mockTimelineService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: NoticeOfIntentTimelineService, + useValue: mockTimelineService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + controllers: [NoticeOfIntentTimelineController], + }).compile(); + + controller = module.get( + NoticeOfIntentTimelineController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call through to get timeline events', async () => { + mockTimelineService.getTimelineEvents.mockResolvedValue([]); + const res = await controller.fetchTimelineEvents('fileNumber'); + + expect(res).toBeDefined(); + expect(mockTimelineService.getTimelineEvents).toHaveBeenCalledTimes(1); + expect(mockTimelineService.getTimelineEvents).toHaveBeenCalledWith( + 'fileNumber', + ); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.controller.ts new file mode 100644 index 0000000000..12ecc93e33 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.controller.ts @@ -0,0 +1,20 @@ +import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { NoticeOfIntentTimelineService } from './notice-of-intent-timeline.service'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@Controller('notice-of-intent-timeline') +@UseGuards(RolesGuard) +export class NoticeOfIntentTimelineController { + constructor(private noiTimelineService: NoticeOfIntentTimelineService) {} + + @Get('/:fileNumber') + @UserRoles(AUTH_ROLE.ADMIN) + async fetchTimelineEvents(@Param('fileNumber') fileNumber: string) { + return await this.noiTimelineService.getTimelineEvents(fileNumber); + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.module.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.module.ts new file mode 100644 index 0000000000..45ed940a72 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NoticeOfIntentDecision } from '../../notice-of-intent-decision/notice-of-intent-decision.entity'; +import { NoticeOfIntentModification } from '../../notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.entity'; +import { NoticeOfIntent } from '../notice-of-intent.entity'; +import { NoticeOfIntentModule } from '../notice-of-intent.module'; +import { NoticeOfIntentTimelineController } from './notice-of-intent-timeline.controller'; +import { NoticeOfIntentTimelineService } from './notice-of-intent-timeline.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + NoticeOfIntent, + NoticeOfIntentModification, + NoticeOfIntentDecision, + ]), + NoticeOfIntentModule, + ], + providers: [NoticeOfIntentTimelineService], + controllers: [NoticeOfIntentTimelineController], +}) +export class NoticeOfIntentTimelineModule {} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.spec.ts new file mode 100644 index 0000000000..6bed075791 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.spec.ts @@ -0,0 +1,187 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NoticeOfIntentDecision } from '../../notice-of-intent-decision/notice-of-intent-decision.entity'; +import { NoticeOfIntentModificationOutcomeType } from '../../notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification-outcome-type/notice-of-intent-modification-outcome-type.entity'; +import { NoticeOfIntentModification } from '../../notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.entity'; +import { NoticeOfIntentMeetingType } from '../notice-of-intent-meeting/notice-of-intent-meeting-type.entity'; +import { NoticeOfIntentMeeting } from '../notice-of-intent-meeting/notice-of-intent-meeting.entity'; +import { NoticeOfIntentMeetingService } from '../notice-of-intent-meeting/notice-of-intent-meeting.service'; +import { NoticeOfIntent } from '../notice-of-intent.entity'; +import { NoticeOfIntentService } from '../notice-of-intent.service'; +import { NoticeOfIntentTimelineService } from './notice-of-intent-timeline.service'; + +describe('NoticeOfIntentTimelineService', () => { + let service: NoticeOfIntentTimelineService; + let mockNOIRepo: DeepMocked>; + let mockNOIModificationRepo: DeepMocked< + Repository + >; + let mockNOIDecisionRepo: DeepMocked>; + let mockNOIService: DeepMocked; + let mockNOIMeetingService: DeepMocked; + + beforeEach(async () => { + mockNOIRepo = createMock(); + mockNOIModificationRepo = createMock(); + mockNOIDecisionRepo = createMock(); + mockNOIService = createMock(); + mockNOIMeetingService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: getRepositoryToken(NoticeOfIntent), + useValue: mockNOIRepo, + }, + { + provide: getRepositoryToken(NoticeOfIntentModification), + useValue: mockNOIModificationRepo, + }, + { + provide: getRepositoryToken(NoticeOfIntentDecision), + useValue: mockNOIDecisionRepo, + }, + { + provide: NoticeOfIntentService, + useValue: mockNOIService, + }, + { + provide: NoticeOfIntentMeetingService, + useValue: mockNOIMeetingService, + }, + NoticeOfIntentTimelineService, + ], + }).compile(); + + service = module.get( + NoticeOfIntentTimelineService, + ); + + mockNOIRepo.findOneOrFail.mockResolvedValue(new NoticeOfIntent()); + mockNOIDecisionRepo.find.mockResolvedValue([]); + mockNOIModificationRepo.find.mockResolvedValue([]); + mockNOIMeetingService.getByFileNumber.mockResolvedValue([]); + mockNOIService.mapToDtos.mockResolvedValue([]); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should return nothing for empty NOI', async () => { + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + }); + + it('should map NOI events in the correct order', async () => { + const sameDate = new Date(); + mockNOIRepo.findOneOrFail.mockResolvedValue( + new NoticeOfIntent({ + dateReceivedAllItems: sameDate, + feePaidDate: sameDate, + dateAcknowledgedComplete: sameDate, + dateAcknowledgedIncomplete: sameDate, + dateSubmittedToAlc: sameDate, + }), + ); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(5); + expect(res[0].htmlText).toEqual('Acknowledged Complete'); + expect(res[1].htmlText).toEqual('Fee Received Date'); + expect(res[2].htmlText).toEqual('Received All Items'); + expect(res[3].htmlText).toEqual('Acknowledged Incomplete'); + expect(res[4].htmlText).toEqual('Submitted to ALC'); + }); + + it('should map Decision Events', async () => { + const sameDate = new Date(); + mockNOIDecisionRepo.find.mockResolvedValue([ + new NoticeOfIntentDecision({ + date: new Date(sameDate.getTime() + 1000), + auditDate: new Date(sameDate.getTime() + 1000), + }), + new NoticeOfIntentDecision({ + date: sameDate, + auditDate: sameDate, + }), + ]); + mockNOIService.mapToDtos.mockResolvedValue([ + { + activeDays: 5, + } as any, + ]); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(4); + expect(res[3].htmlText).toEqual('Decision #1 Made - Active Days: 5'); + expect(res[2].htmlText).toEqual('Audited Decision #1'); + expect(res[1].htmlText).toEqual('Decision #2 Made'); + expect(res[0].htmlText).toEqual('Audited Decision #2'); + }); + + it('should map Modification Events', async () => { + const sameDate = new Date(); + mockNOIModificationRepo.find.mockResolvedValue([ + new NoticeOfIntentModification({ + submittedDate: new Date(sameDate.getTime() + 1000), + outcomeNotificationDate: new Date(sameDate.getTime() + 1000), + reviewOutcome: { + label: 'CATS', + } as NoticeOfIntentModificationOutcomeType, + }), + new NoticeOfIntentModification({ + submittedDate: sameDate, + outcomeNotificationDate: sameDate, + reviewOutcome: { + label: 'CATS', + } as NoticeOfIntentModificationOutcomeType, + }), + ]); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(4); + expect(res[3].htmlText).toEqual('Modification Requested #1'); + expect(res[2].htmlText).toEqual('Modification Request Reviewed #1 - CATS'); + expect(res[1].htmlText).toEqual('Modification Requested #2'); + expect(res[0].htmlText).toEqual('Modification Request Reviewed #2 - CATS'); + }); + + it('should map Meeting Events', async () => { + const sameDate = new Date(); + mockNOIMeetingService.getByFileNumber.mockResolvedValue([ + new NoticeOfIntentMeeting({ + startDate: new Date(sameDate.getTime() + 1000), + endDate: new Date(sameDate.getTime() + 1000), + type: { + label: 'Meeting', + code: 'CATS', + } as NoticeOfIntentMeetingType, + }), + new NoticeOfIntentMeeting({ + startDate: sameDate, + endDate: sameDate, + type: { + label: 'Meeting', + code: 'CATS', + } as NoticeOfIntentMeetingType, + }), + ]); + + const res = await service.getTimelineEvents('file-number'); + + expect(res).toBeDefined(); + expect(res.length).toEqual(2); + expect(res[1].htmlText).toEqual('Meeting #1'); + expect(res[0].htmlText).toEqual('Meeting #2'); + }); +}); diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts new file mode 100644 index 0000000000..1f538aebd3 --- /dev/null +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts @@ -0,0 +1,267 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CardSubtask } from '../../card/card-subtask/card-subtask.entity'; +import { NoticeOfIntentDecision } from '../../notice-of-intent-decision/notice-of-intent-decision.entity'; +import { NoticeOfIntentModification } from '../../notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.entity'; +import { NoticeOfIntentMeetingService } from '../notice-of-intent-meeting/notice-of-intent-meeting.service'; +import { NoticeOfIntent } from '../notice-of-intent.entity'; +import { NoticeOfIntentService } from '../notice-of-intent.service'; + +export interface TimelineEvent { + htmlText: string; + startDate: number; + fulfilledDate: number | null; + isFulfilled: boolean; + link?: string | null; +} + +const SORTING_ORDER = { + //high comes first, 1 shows at bottom + MODIFICATION_REVIEW: 12, + MODIFICATION_REQUEST: 11, + CHAIR_REVIEW_DECISION: 10, + AUDITED_DECISION: 9, + DECISION_MADE: 8, + VISIT_REPORTS: 7, + VISIT_REQUESTS: 6, + ACKNOWLEDGE_COMPLETE: 5, + RECEIVED_ALL_ITEMS: 4, + FEE_RECEIVED: 3, + ACKNOWLEDGED_INCOMPLETE: 2, + SUBMITTED: 1, +}; + +const editLink = new Map([['IR', './info-request']]); + +@Injectable() +export class NoticeOfIntentTimelineService { + constructor( + @InjectRepository(NoticeOfIntent) + private noticeOfIntentRepo: Repository, + @InjectRepository(NoticeOfIntentModification) + private noticeOfIntentModificationRepo: Repository, + @InjectRepository(NoticeOfIntentDecision) + private noticeOfIntentDecisionRepo: Repository, + private noticeOfIntentService: NoticeOfIntentService, + private noticeOfIntentMeetingService: NoticeOfIntentMeetingService, + ) {} + + async getTimelineEvents(fileNumber: string) { + const events: TimelineEvent[] = []; + const noticeOfIntent = await this.noticeOfIntentRepo.findOneOrFail({ + where: { + fileNumber, + }, + relations: { + card: { + type: true, + subtasks: { + type: true, + }, + }, + }, + withDeleted: true, + }); + this.addNoticeOfIntentEvents(noticeOfIntent, events); + await this.addDecisionEvents(noticeOfIntent, events); + await this.addModificationEvents(noticeOfIntent, events); + await this.addMeetingEvents(noticeOfIntent, events); + + if (noticeOfIntent.card) { + for (const subtask of noticeOfIntent.card.subtasks) { + const mappedEvent = this.mapSubtaskToEvent(subtask); + events.push(mappedEvent); + } + } + + events.sort((a, b) => b.startDate - a.startDate); + return events; + } + + private addNoticeOfIntentEvents( + noticeOfIntent: NoticeOfIntent, + events: TimelineEvent[], + ) { + if (noticeOfIntent.dateSubmittedToAlc) { + events.push({ + htmlText: 'Submitted to ALC', + startDate: + noticeOfIntent.dateSubmittedToAlc.getTime() + SORTING_ORDER.SUBMITTED, + isFulfilled: true, + fulfilledDate: null, + }); + } + + if (noticeOfIntent.dateAcknowledgedIncomplete) { + events.push({ + htmlText: 'Acknowledged Incomplete', + startDate: + noticeOfIntent.dateAcknowledgedIncomplete.getTime() + + SORTING_ORDER.ACKNOWLEDGED_INCOMPLETE, + isFulfilled: true, + fulfilledDate: null, + }); + } + + if (noticeOfIntent.dateAcknowledgedComplete) { + events.push({ + htmlText: 'Acknowledged Complete', + startDate: + noticeOfIntent.dateAcknowledgedComplete.getTime() + + SORTING_ORDER.ACKNOWLEDGE_COMPLETE, + isFulfilled: true, + fulfilledDate: null, + }); + } + + if (noticeOfIntent.feePaidDate) { + events.push({ + htmlText: 'Fee Received Date', + startDate: + noticeOfIntent.feePaidDate.getTime() + SORTING_ORDER.FEE_RECEIVED, + isFulfilled: true, + fulfilledDate: null, + }); + } + + if (noticeOfIntent.dateReceivedAllItems) { + events.push({ + htmlText: 'Received All Items', + startDate: + noticeOfIntent.dateReceivedAllItems.getTime() + + SORTING_ORDER.FEE_RECEIVED, + isFulfilled: true, + fulfilledDate: null, + }); + } + } + + private mapSubtaskToEvent(subtask: CardSubtask): TimelineEvent { + return { + htmlText: `${subtask.type.label} Subtask`, + fulfilledDate: subtask.completedAt?.getTime() ?? null, + startDate: subtask.createdAt.getTime(), + isFulfilled: !!subtask.completedAt, + }; + } + + private async addDecisionEvents( + noticeOfIntent: NoticeOfIntent, + events: TimelineEvent[], + ) { + const decisions = await this.noticeOfIntentDecisionRepo.find({ + where: { + noticeOfIntentUuid: noticeOfIntent.uuid, + isDraft: false, + }, + order: { + date: 'DESC', + }, + }); + + const mappedNOIs = await this.noticeOfIntentService.mapToDtos([ + noticeOfIntent, + ]); + const mappedNOI = mappedNOIs[0]; + + for (const [index, decision] of decisions.entries()) { + if (decision.auditDate) { + events.push({ + htmlText: `Audited Decision #${decisions.length - index}`, + startDate: + decision.auditDate.getTime() + SORTING_ORDER.AUDITED_DECISION, + fulfilledDate: null, + isFulfilled: true, + }); + } + + events.push({ + htmlText: `Decision #${decisions.length - index} Made${ + decisions.length - 1 === index + ? ` - Active Days: ${mappedNOI.activeDays}` + : '' + }`, + startDate: decision.date!.getTime() + SORTING_ORDER.DECISION_MADE, + fulfilledDate: null, + isFulfilled: true, + }); + } + } + + private async addModificationEvents( + noticeOfIntent: NoticeOfIntent, + events: TimelineEvent[], + ) { + const modifications = await this.noticeOfIntentModificationRepo.find({ + where: { + noticeOfIntentUuid: noticeOfIntent.uuid, + }, + relations: { + card: { + subtasks: { + type: true, + }, + }, + }, + order: { + submittedDate: 'DESC', + }, + }); + + for (const [index, modification] of modifications.entries()) { + events.push({ + htmlText: `Modification Requested #${modifications.length - index}`, + startDate: + modification.submittedDate.getTime() + + SORTING_ORDER.MODIFICATION_REQUEST, + fulfilledDate: null, + isFulfilled: true, + }); + + if (modification.card) { + for (const subtask of modification.card.subtasks) { + const mappedEvent = this.mapSubtaskToEvent(subtask); + events.push(mappedEvent); + } + } + + if (modification.outcomeNotificationDate) { + events.push({ + htmlText: `Modification Request Reviewed #${ + modifications.length - index + } - ${modification.reviewOutcome.label}`, + startDate: + modification.outcomeNotificationDate.getTime() + + SORTING_ORDER.MODIFICATION_REVIEW, + fulfilledDate: null, + isFulfilled: true, + }); + } + } + } + + private async addMeetingEvents( + noticeOfIntent: NoticeOfIntent, + events: TimelineEvent[], + ) { + const meetings = await this.noticeOfIntentMeetingService.getByFileNumber( + noticeOfIntent.fileNumber, + ); + + meetings.sort((a, b) => a.startDate.getTime() - b.startDate.getTime()); + const typeCount = new Map(); + meetings.forEach((meeting) => { + const count = typeCount.get(meeting.type.code) || 0; + events.push({ + htmlText: `${meeting.type.label} #${count + 1}`, + startDate: meeting.startDate.getTime() + SORTING_ORDER.VISIT_REQUESTS, + fulfilledDate: meeting.endDate?.getTime() ?? null, + isFulfilled: !!meeting.endDate, + link: editLink.get(meeting.type.code), + }); + + typeCount.set(meeting.type.code, count + 1); + }); + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts index 48f54d1834..5c73a57a42 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts @@ -1,9 +1,12 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { NoticeOfIntentParcelProfile } from '../../common/automapper/notice-of-intent-parcel.automapper.profile'; import { NoticeOfIntentProfile } from '../../common/automapper/notice-of-intent.automapper.profile'; import { DocumentCode } from '../../document/document-code.entity'; import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; +import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionModule } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.module'; import { BoardModule } from '../board/board.module'; import { CardModule } from '../card/card.module'; import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; @@ -12,26 +15,19 @@ import { LocalGovernmentModule } from '../local-government/local-government.modu import { NoticeOfIntentDocumentController } from './notice-of-intent-document/notice-of-intent-document.controller'; import { NoticeOfIntentDocument } from './notice-of-intent-document/notice-of-intent-document.entity'; import { NoticeOfIntentDocumentService } from './notice-of-intent-document/notice-of-intent-document.service'; - import { NoticeOfIntentMeetingType } from './notice-of-intent-meeting/notice-of-intent-meeting-type.entity'; import { NoticeOfIntentMeetingController } from './notice-of-intent-meeting/notice-of-intent-meeting.controller'; import { NoticeOfIntentMeeting } from './notice-of-intent-meeting/notice-of-intent-meeting.entity'; import { NoticeOfIntentMeetingService } from './notice-of-intent-meeting/notice-of-intent-meeting.service'; +import { NoticeOfIntentParcelController } from './notice-of-intent-parcel/notice-of-intent-parcel.controller'; +import { NoticeOfIntentSubmissionStatusType } from './notice-of-intent-submission-status/notice-of-intent-status-type.entity'; import { NoticeOfIntentSubmissionStatusModule } from './notice-of-intent-submission-status/notice-of-intent-submission-status.module'; -import { NoticeOfIntentController } from './notice-of-intent.controller'; - +import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission/notice-of-intent-submission.controller'; +import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission/notice-of-intent-submission.service'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; - +import { NoticeOfIntentController } from './notice-of-intent.controller'; import { NoticeOfIntent } from './notice-of-intent.entity'; import { NoticeOfIntentService } from './notice-of-intent.service'; -import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission/notice-of-intent-submission.service'; -import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission/notice-of-intent-submission.controller'; -import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; -import { NoticeOfIntentSubmissionStatusType } from './notice-of-intent-submission-status/notice-of-intent-status-type.entity'; -import { NoticeOfIntentParcelController } from './notice-of-intent-parcel/notice-of-intent-parcel.controller'; -import { NoticeOfIntentSubmissionModule } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.module'; -import { NoticeOfIntentParcel } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; -import { NoticeOfIntentParcelProfile } from '../../common/automapper/notice-of-intent-parcel.automapper.profile'; @Module({ imports: [ diff --git a/services/apps/alcs/src/main.module.ts b/services/apps/alcs/src/main.module.ts index dcfb85af1c..552a4ed0b2 100644 --- a/services/apps/alcs/src/main.module.ts +++ b/services/apps/alcs/src/main.module.ts @@ -20,10 +20,10 @@ import { HealthCheck } from './healthcheck/healthcheck.entity'; import { LogoutController } from './logout/logout.controller'; import { MainController } from './main.controller'; import { MainService } from './main.service'; +import { NoticeOfIntentSubmissionModule } from './portal/notice-of-intent-submission/notice-of-intent-submission.module'; import { PortalModule } from './portal/portal.module'; import { TypeormConfigService } from './providers/typeorm/typeorm.service'; import { UserModule } from './user/user.module'; -import { NoticeOfIntentSubmissionModule } from './portal/notice-of-intent-submission/notice-of-intent-submission.module'; @Module({ imports: [ From 540f2702f9f0dec4ef3d56c39a0eb2ecbb5f21f8 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Tue, 29 Aug 2023 10:49:33 -0700 Subject: [PATCH 316/954] Rename Application -> Notice of Intent (#919) --- .../notice-of-intent-details.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.html b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.html index 4034ed7785..8f7549b022 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/applicant-info/notice-of-intent-details/notice-of-intent-details.component.html @@ -66,7 +66,7 @@

Primary Contact Information

Land Use

-

Land Use of Parcel(s) under Application

+

Land Use of Parcel(s) under Notice of Intent

Quantify and describe in detail all agriculture that currently takes place on the parcel(s). From 137f711ad5d15715963eb46165a813c03aec5aaa Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 29 Aug 2023 11:29:51 -0700 Subject: [PATCH 317/954] MR feedback --- .../submissions/app_submissions.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index bdbea88c6e..99c096b66c 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -64,10 +64,9 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): if not rows: break try: - adj_rows = get_directions_rows(rows, cursor) - direction_data = create_direction_dict(adj_rows) - subdiv_rows = get_subdiv_rows(rows, cursor) - subdiv_data = create_subdiv_dict(subdiv_rows) + + direction_data = get_direction_data(rows, cursor) + subdiv_data = get_subdiv_data(rows, cursor) submissions_to_be_inserted_count = len(rows) @@ -105,7 +104,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdi cursor (obj): Cursor object to execute queries. rows (list): Rows of data to insert in the database. direction_data (dict): Dictionary of adjacent parcel data - subdiv_data: list of subdivision data + subdiv_data: dictionary of subdivision data lists Returns: None: Commits the changes to the database. @@ -148,7 +147,7 @@ def prepare_app_sub_data(app_sub_raw_data_list, direction_data, subdiv_data): :param app_sub_raw_data_list: A list of raw data dictionaries. :param direction_data: A dictionary of adjacent parcel data. - :param subdiv_data: list of subdivision data + :param subdiv_data: dictionary of subdivision data lists :return: Five lists, each containing dictionaries from 'app_sub_raw_data_list' and 'direction_data' grouped based on the 'alr_change_code' field Detailed Workflow: @@ -232,6 +231,18 @@ def get_insert_query_for_inc_exc(): unique_values = ", %(alr_area)s" return get_insert_query(unique_fields,unique_values) +def get_direction_data(rows, cursor): + # runs query to get direction data and creates a dict based on alr_application_id + adj_rows = get_directions_rows(rows, cursor) + direction_data = create_direction_dict(adj_rows) + return direction_data + +def get_subdiv_data(rows, cursor): + # runs query to get subdivision data and creates a dictionaly based on alr_appl_component_id with a list of plots + subdiv_rows = get_subdiv_rows(rows, cursor) + subdiv_data = create_subdiv_dict(subdiv_rows) + return subdiv_data + @inject_conn_pool def clean_application_submission(conn=None): From 645be838b13b0888b0106b97b16f9ff5b2f46a18 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 29 Aug 2023 11:46:42 -0700 Subject: [PATCH 318/954] Fix Edit Submission Updated banners * Add special exception to local government uuid * Pass original submission UUID to app-details so it can be used by parcels --- .../src/app/features/application/overview/overview.component.ts | 1 - .../alcs-edit-submission/alcs-edit-submission.component.ts | 2 +- .../select-government/select-government.component.ts | 1 - .../alcs-edit-submission/alcs-edit-submission.component.html | 2 ++ .../alcs-edit-submission/alcs-edit-submission.component.ts | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) diff --git a/alcs-frontend/src/app/features/application/overview/overview.component.ts b/alcs-frontend/src/app/features/application/overview/overview.component.ts index 04655eb1aa..435265d389 100644 --- a/alcs-frontend/src/app/features/application/overview/overview.component.ts +++ b/alcs-frontend/src/app/features/application/overview/overview.component.ts @@ -53,7 +53,6 @@ export class OverviewComponent implements OnInit, OnDestroy { title: 'Cancel Application', }) .subscribe(async (didConfirm) => { - debugger; if (didConfirm && this.application) { await this.applicationDetailService.cancelApplication(this.application.fileNumber); await this.loadStatusHistory(this.application.fileNumber); diff --git a/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts index 3fdefdbced..4ba097f60f 100644 --- a/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts +++ b/portal-frontend/src/app/features/applications/alcs-edit-submission/alcs-edit-submission.component.ts @@ -129,7 +129,7 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView const changedFields = new Set(); for (const diff of diffResult) { const fullPath = diff.path.join('.'); - if (!fullPath.toLowerCase().includes('uuid')) { + if (!fullPath.toLowerCase().includes('uuid') || fullPath === 'localGovernmentUuid') { changedFields.add(diff.path.join('.')); changedFields.add(diff.path[0].toString()); } diff --git a/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts index 2fb8d05845..522ca5f4c6 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts @@ -1,7 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; -import { Router } from '@angular/router'; import { map, Observable, startWith, takeUntil } from 'rxjs'; import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; import { LocalGovernmentDto } from '../../../../services/code/code.dto'; diff --git a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.html b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.html index 00dacfed36..16a75b21e2 100644 --- a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.html @@ -135,10 +135,12 @@
Notice of Intent ID: {{ noiSubmission.fileNumber }} |
diff --git a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts index 553bf9cf93..f9ab4ae5c9 100644 --- a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/alcs-edit-submission.component.ts @@ -230,7 +230,7 @@ export class AlcsEditSubmissionComponent implements OnInit, OnDestroy, AfterView const changedFields = new Set(); for (const diff of diffResult) { const fullPath = diff.path.join('.'); - if (!fullPath.toLowerCase().includes('uuid')) { + if (!fullPath.toLowerCase().includes('uuid') || fullPath === 'localGovernmentUuid') { changedFields.add(diff.path.join('.')); changedFields.add(diff.path[0].toString()); } From a26c998edeb6dc8747b4467d6e31644a0c153b53 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:13:15 -0700 Subject: [PATCH 319/954] Update font styling and date format (#922) --- .../decision-documents.component.html | 2 +- .../shared/lots-table/lots-table-form.component.html | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html index 351641d7a0..d71013bc1c 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.html @@ -49,7 +49,7 @@

Documents

- + diff --git a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html index 3b6f7159b9..0807552a36 100644 --- a/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html +++ b/alcs-frontend/src/app/shared/lots-table/lots-table-form.component.html @@ -1,5 +1,5 @@
-
+
Total Number of Proposed Lots Approved Lot Areas
{{ event.startDate | momentFormat }} - {{ event.fulfilledDate | momentFormat }} - - Add date + {{ event.fulfilledDate | momentFormat }} + + Add date + - Upload Date{{ element.uploadedAt | date }}{{ element.uploadedAt | momentFormat }}
- + - + + + diff --git a/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.ts b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.ts index e833e82712..027fbdbfea 100644 --- a/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.ts +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.ts @@ -36,6 +36,21 @@ export class SubmissionDocumentsComponent implements OnInit, OnDestroy { } } + async downloadFile(uuid: string) { + const res = await this.applicationDocumentService.downloadFile(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/notice-of-intents/edit-submission/select-government/select-government.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.html index 33dcb4ec8d..3428d17d01 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.html @@ -45,11 +45,6 @@

Government

submit, you will need to contact the ALC directly:  ALC.Portal@gov.bc.ca / 236-468-3342 - - You're logged in with a Business BCeID that is associated with the government selected above. You will have the - opportunity to complete the local or first nation government review form immediately after this notice of intent is - submitted. -

Please Note: If your Local or First Nation Government is not listed, please contact the ALC directly. ALC.Portal@gov.bc.ca / 236-468-3342 diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.ts index 8188a55e2f..9cef1882b8 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/select-government/select-government.component.ts @@ -21,7 +21,6 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, localGovernment = new FormControl('', [Validators.required]); showWarning = false; - selectedOwnGovernment = false; selectGovernmentUuid = ''; localGovernments: LocalGovernmentDto[] = []; filteredLocalGovernments!: Observable; @@ -74,8 +73,6 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, } else { this.localGovernment.setErrors({ invalid: localGovernment.hasGuid }); } - - this.selectedOwnGovernment = localGovernment.matchesUserGuid; } } } @@ -141,7 +138,6 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, if (!lg.hasGuid) { this.localGovernment.setErrors({ invalid: true }); } - this.selectedOwnGovernment = lg.matchesUserGuid; } } } diff --git a/portal-frontend/src/app/services/application-document/application-document.service.ts b/portal-frontend/src/app/services/application-document/application-document.service.ts index 41067bf26d..0a2b78c846 100644 --- a/portal-frontend/src/app/services/application-document/application-document.service.ts +++ b/portal-frontend/src/app/services/application-document/application-document.service.ts @@ -54,6 +54,16 @@ export class ApplicationDocumentService { return undefined; } + async downloadFile(fileUuid: string) { + try { + return await firstValueFrom(this.httpClient.get<{ url: string }>(`${this.serviceUrl}/${fileUuid}/download`)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to download the document, please try again'); + } + return undefined; + } + async deleteExternalFile(fileUuid: string) { try { this.overlayService.showSpinner(); diff --git a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts index 6a2f91c2b8..a13110717f 100644 --- a/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-decision-v2.automapper.profile.ts @@ -265,6 +265,10 @@ export class ApplicationDecisionProfile extends AutomapperProfile { mapper, ApplicationDecision, ApplicationPortalDecisionDto, + forMember( + (ad) => ad.date, + mapFrom((a) => a.date?.getTime()), + ), forMember( (a) => a.reconsiders, mapFrom((dec) => diff --git a/services/apps/alcs/src/portal/application-decision/application-decision.dto.ts b/services/apps/alcs/src/portal/application-decision/application-decision.dto.ts index 755da4b7ca..2c1a9f6014 100644 --- a/services/apps/alcs/src/portal/application-decision/application-decision.dto.ts +++ b/services/apps/alcs/src/portal/application-decision/application-decision.dto.ts @@ -9,7 +9,6 @@ export class ApplicationPortalDecisionDto { @AutoMap() uuid: string; - @AutoMap() date: number; @AutoMap(() => ApplicationDecisionOutcomeCodeDto) diff --git a/services/apps/alcs/src/portal/application-document/application-document.controller.spec.ts b/services/apps/alcs/src/portal/application-document/application-document.controller.spec.ts index 1df45859b0..27622efa17 100644 --- a/services/apps/alcs/src/portal/application-document/application-document.controller.spec.ts +++ b/services/apps/alcs/src/portal/application-document/application-document.controller.spec.ts @@ -117,7 +117,7 @@ describe('ApplicationDocumentController', () => { ); }); - it('should call through for download', async () => { + it('should call through for open', async () => { const fakeUrl = 'fake-url'; appDocumentService.getInlineUrl.mockResolvedValue(fakeUrl); appDocumentService.get.mockResolvedValue(mockDocument); @@ -131,6 +131,20 @@ describe('ApplicationDocumentController', () => { expect(res.url).toEqual(fakeUrl); }); + it('should call through for download', async () => { + const fakeUrl = 'fake-url'; + appDocumentService.getDownloadUrl.mockResolvedValue(fakeUrl); + appDocumentService.get.mockResolvedValue(mockDocument); + + const res = await controller.download('fake-uuid', { + user: { + entity: {}, + }, + }); + + expect(res.url).toEqual(fakeUrl); + }); + it('should call out to service to attach external document', async () => { const user = { user: { entity: 'Bruce' } }; const fakeUuid = 'fakeUuid'; diff --git a/services/apps/alcs/src/portal/application-document/application-document.controller.ts b/services/apps/alcs/src/portal/application-document/application-document.controller.ts index 239388c06c..23fd7567fe 100644 --- a/services/apps/alcs/src/portal/application-document/application-document.controller.ts +++ b/services/apps/alcs/src/portal/application-document/application-document.controller.ts @@ -73,6 +73,20 @@ export class ApplicationDocumentController { return { url }; } + @Get('/:uuid/download') + async download(@Param('uuid') fileUuid: string, @Req() req) { + const document = await this.applicationDocumentService.get(fileUuid); + + //TODO: How do we know which documents applicant can access? + // await this.applicationSubmissionService.verifyAccess( + // document.applicationUuid, + // req.user.entity, + // ); + + const url = await this.applicationDocumentService.getDownloadUrl(document); + return { url }; + } + @Patch('/application/:fileNumber') async update( @Param('fileNumber') fileNumber: string, From 8359e374e0a1797deebeb09cbc4dfaf3ac7ec2f0 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 31 Aug 2023 10:35:09 -0700 Subject: [PATCH 325/954] Bug Fixes Pt3 * Change componentLabels -> componentLabelsStr to prevent the labels from disappearing on both NOI and Application Conditions pages when updating * Add an extra check to allow 33.1 recons to not require approval to be used as links * Mark the decision components as touched when validating so they show red error borders * Load components attached to conditions so they show on the NOI conditions page --- .../decision/conditions/condition/condition.component.ts | 4 ++-- .../decision-input/decision-input-v2.component.ts | 8 ++++++-- .../decision/conditions/condition/condition.component.ts | 8 ++------ .../decision/conditions/conditions.component.ts | 2 -- .../decision-components/decision-components.component.ts | 4 ++++ .../decision-input/decision-input-v2.component.ts | 3 +++ .../notice-of-intent-decision-v2.controller.ts | 1 - .../notice-of-intent-decision-v2.service.ts | 1 + 8 files changed, 18 insertions(+), 13 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts index c445216b08..982dbf0370 100644 --- a/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/application/decision/conditions/condition/condition.component.ts @@ -134,8 +134,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { [field]: value, }); - const labels = this.condition.componentLabels; - this.condition = { ...update, componentLabels: labels } as Condition; + const labels = this.condition.componentLabelsStr; + this.condition = { ...update, componentLabelsStr: labels } as Condition; this.updateStatus(); } diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts index 672049a355..073a1059cf 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -8,7 +8,10 @@ import { combineLatestWith, Subject, takeUntil } from 'rxjs'; import { ApplicationDetailService } from '../../../../../services/application/application-detail.service'; import { ApplicationModificationDto } from '../../../../../services/application/application-modification/application-modification.dto'; import { ApplicationModificationService } from '../../../../../services/application/application-modification/application-modification.service'; -import { ApplicationReconsiderationDto } from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; +import { + ApplicationReconsiderationDto, + RECONSIDERATION_TYPE, +} from '../../../../../services/application/application-reconsideration/application-reconsideration.dto'; import { ApplicationReconsiderationService } from '../../../../../services/application/application-reconsideration/application-reconsideration.service'; import { ApplicationDecisionConditionDto, @@ -260,7 +263,8 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { .filter( (reconsideration) => (existingDecision && existingDecision.reconsiders?.uuid === reconsideration.uuid) || - (reconsideration.reviewOutcome?.code === 'PRC' && !reconsideration.resultingDecision) + (reconsideration.reviewOutcome?.code === 'PRC' && !reconsideration.resultingDecision) || + (reconsideration.type.code === RECONSIDERATION_TYPE.T_33_1 && !reconsideration.resultingDecision) ) .map((reconsideration, index) => ({ label: `Reconsideration Request #${reconsiderations.length - index} - ${reconsideration.reconsideredDecisions diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts index 975703b4f6..2f5e487808 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/condition/condition.component.ts @@ -61,8 +61,8 @@ export class ConditionComponent implements OnInit, AfterViewInit { [field]: value, }); - const labels = this.condition.componentLabels; - this.condition = { ...update, componentLabels: labels } as Condition; + const labels = this.condition.componentLabelsStr; + this.condition = { ...update, componentLabelsStr: labels } as Condition; this.updateStatus(); } @@ -93,8 +93,4 @@ export class ConditionComponent implements OnInit, AfterViewInit { this.conditionStatus = CONDITION_STATUS.INCOMPLETE; } } - - getComponentLabel(componentUuid: string) { - return this.condition.conditionComponentsLabels?.find((e) => e.componentUuid === componentUuid)?.label; - } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts index 0d64ea2980..174497db5a 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/conditions/conditions.component.ts @@ -13,7 +13,6 @@ import { NoticeOfIntentDto } from '../../../../services/notice-of-intent/notice- import { DRAFT_DECISION_TYPE_LABEL, MODIFICATION_TYPE_LABEL, - RECON_TYPE_LABEL, RELEASED_DECISION_TYPE_LABEL, } from '../../../../shared/application-type-pill/application-type-pill.constants'; @@ -56,7 +55,6 @@ export class ConditionsComponent implements OnInit { dratDecisionLabel = DRAFT_DECISION_TYPE_LABEL; releasedDecisionLabel = RELEASED_DECISION_TYPE_LABEL; - reconLabel = RECON_TYPE_LABEL; modificationLabel = MODIFICATION_TYPE_LABEL; constructor( diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index f8f325536f..259a47b70d 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -212,4 +212,8 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView isDisabled: this.components.some((c) => c.noticeOfIntentDecisionComponentTypeCode === e.code), })); } + + onValidate() { + this.childComponents.forEach((component) => component.form.markAllAsTouched()); + } } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts index 4b1294a72b..2b2987090e 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.ts @@ -380,6 +380,9 @@ export class DecisionInputV2Component implements OnInit, OnDestroy { if (this.decisionConditionsComponent) { this.decisionConditionsComponent.onValidate(); } + if (this.decisionComponentsComponent) { + this.decisionComponentsComponent.onValidate(); + } if ( !this.form.valid || diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts index 0e4f59b396..38edcfad1c 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts @@ -37,7 +37,6 @@ import { generateALCDNoticeOfIntentHtml } from '../../../../../../templates/emai import { NOI_SUBMISSION_STATUS } from '../../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { PARENT_TYPE } from '../../card/card-subtask/card-subtask.dto'; import { NoticeOfIntentSubmissionService } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.service'; -import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @Controller('notice-of-intent-decision/v2') diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts index 98c7b0b662..ed7a81cec8 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.service.ts @@ -98,6 +98,7 @@ export class NoticeOfIntentDecisionV2Service { }, conditions: { type: true, + components: true, }, }, }); From 7e012b8fe3c91b0e094b46653b068b28b36c9ce6 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 31 Aug 2023 11:27:48 -0700 Subject: [PATCH 326/954] Change valueChange -> Change This prevents calling form.patchvalue from immediately calling the hooked functions, which we don't want. --- .../proposal/pfrs-proposal/pfrs-proposal.component.html | 6 +++--- .../proposal/roso-proposal/roso-proposal.component.html | 2 +- .../proposal/pfrs/pfrs-proposal.component.html | 8 ++++---- .../proposal/pofo/pofo-proposal.component.html | 4 ++-- .../proposal/roso/roso-proposal.component.html | 8 ++++---- .../proposal/roso/roso-proposal.component.ts | 2 ++ 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html index d795b5f5f6..a2ef832778 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html @@ -32,7 +32,7 @@

Proposal

@@ -351,7 +351,7 @@

Project Duration

@@ -385,7 +385,7 @@

Project Duration

diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html index 6364510f0f..7f4871db55 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html @@ -32,7 +32,7 @@

Proposal

diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.html index a97858ac91..602553c586 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.html @@ -32,7 +32,7 @@

Proposal

@@ -270,7 +270,7 @@

Project Duration

@@ -299,7 +299,7 @@

Project Duration

@@ -373,7 +373,7 @@

Project Duration

diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.html index 2598f55750..f70fd5e041 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.html @@ -32,7 +32,7 @@

Proposal

@@ -214,7 +214,7 @@

Project Duration

diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html index 00e3d6ec48..eaa73a7444 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html @@ -32,7 +32,7 @@

Proposal

@@ -201,7 +201,7 @@

Project Duration

@@ -275,9 +275,9 @@

Project Duration

Date: Thu, 31 Aug 2023 13:26:29 -0700 Subject: [PATCH 327/954] Change L/FNG Review to only call save when components are dirty * Also don't reload application / documents / etc when changing steps --- .../review-contact-information.component.html | 1 + .../review-contact-information.component.ts | 2 +- .../review-ocp/review-ocp.component.ts | 2 +- .../review-resolution.component.ts | 2 +- .../review-submission.component.ts | 33 ++++++++++--------- .../review-zoning/review-zoning.component.ts | 2 +- ...pplication-submission-review.controller.ts | 4 ++- .../typeorm/datasource.cli.orm.config.ts | 1 + 8 files changed, 27 insertions(+), 20 deletions(-) diff --git a/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.html b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.html index 72c324e640..fc0977ce8c 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.html +++ b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.html @@ -70,6 +70,7 @@

Contact Information

placeholder="Type phone number" formControlName="phoneNumber" mask="(000) 000-0000" + type="tel" />
diff --git a/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.ts index 40644c8efe..657196a7bd 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-contact-information/review-contact-information.component.ts @@ -70,7 +70,7 @@ export class ReviewContactInformationComponent implements OnInit, OnDestroy { } private async saveProgress() { - if (this.fileId) { + if (this.fileId && this.contactForm.dirty) { await this.applicationReviewService.update(this.fileId, { localGovernmentFileNumber: this.lgFileNumber.getRawValue(), firstName: this.firstName.getRawValue(), diff --git a/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.ts index 76305b82c6..043b41b517 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-ocp/review-ocp.component.ts @@ -69,7 +69,7 @@ export class ReviewOcpComponent implements OnInit, OnDestroy { } private async saveProgress() { - if (this.fileId) { + if (this.fileId && this.ocpForm.dirty) { const ocpDesignation = this.isOCPDesignation.getRawValue(); let ocpDesignationResult = null; if (ocpDesignation !== null) { diff --git a/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.ts index ada417fcd3..ecf6b13591 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-resolution/review-resolution.component.ts @@ -61,7 +61,7 @@ export class ReviewResolutionComponent implements OnInit, OnDestroy { } private async saveProgress() { - if (this.fileId) { + if (this.fileId && this.resolutionForm.dirty) { if (this.isAuthorized.getRawValue() !== null) { if (this.isFirstNationGovernment || this.isSubjectToZoning || this.isOCPDesignation) { await this.applicationReviewService.update(this.fileId, { diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submission.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.ts index 8948e8625a..e9360b815d 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submission.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.ts @@ -82,6 +82,12 @@ export class ReviewSubmissionComponent implements OnInit, OnDestroy { .subscribe(([queryParamMap, paramMap]) => { const fileId = paramMap.get('fileId'); if (fileId) { + if (this.fileId !== fileId) { + this.loadApplication(fileId); + this.loadApplicationDocuments(fileId); + this.loadApplicationReview(fileId); + } + this.fileId = fileId; const stepInd = paramMap.get('stepInd'); @@ -90,27 +96,24 @@ export class ReviewSubmissionComponent implements OnInit, OnDestroy { this.showValidationErrors = showErrors === 't'; } - this.loadApplication(fileId); - this.loadApplicationDocuments(fileId); - this.loadApplicationReview(fileId).then(() => { - if (stepInd) { - // setTimeout is required for stepper to be initialized - setTimeout(() => { - const stepInt = parseInt(stepInd); - this.customStepper.navigateToStep(stepInt, true); - - this.showDownloadPdf = this.isFirstNationGovernment - ? stepInt === ReviewApplicationFngSteps.ReviewAndSubmitFng - : stepInt === ReviewApplicationSteps.ReviewAndSubmit; - }); - } - }); + if (stepInd) { + // setTimeout is required for stepper to be initialized + setTimeout(() => { + const stepInt = parseInt(stepInd); + this.customStepper.navigateToStep(stepInt, true); + + this.showDownloadPdf = this.isFirstNationGovernment + ? stepInt === ReviewApplicationFngSteps.ReviewAndSubmitFng + : stepInt === ReviewApplicationSteps.ReviewAndSubmit; + }); + } } }); this.$application.pipe(takeUntil(this.$destroy)).subscribe((application) => { this.application = application; }); + this.applicationReviewService.$applicationReview.pipe(takeUntil(this.$destroy)).subscribe((appReview) => { this.isFirstNationGovernment = appReview?.isFirstNationGovernment ?? false; }); diff --git a/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.ts index 7da567bed9..dd84e353e9 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-zoning/review-zoning.component.ts @@ -73,7 +73,7 @@ export class ReviewZoningComponent implements OnInit, OnDestroy { } private async saveProgress() { - if (this.fileId) { + if (this.fileId && this.zoningForm.dirty) { const isSubjectToZoning = this.isSubjectToZoning.getRawValue(); let subjectToZoningResult = null; if (isSubjectToZoning !== null) { diff --git a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts index 777229b76c..12da1943ed 100644 --- a/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts +++ b/services/apps/alcs/src/portal/application-submission-review/application-submission-review.controller.ts @@ -265,7 +265,9 @@ export class ApplicationSubmissionReviewController { if (!validationResult.application) { throw new BaseServiceException( - `Invalid application found during LG Submission ${application.fileNumber}`, + `Invalid application found during LG Submission ${ + application.fileNumber + } ${validationResult.errors.toString()}`, ); } diff --git a/services/apps/alcs/src/providers/typeorm/datasource.cli.orm.config.ts b/services/apps/alcs/src/providers/typeorm/datasource.cli.orm.config.ts index c4ce58f6a0..32f17e8c50 100644 --- a/services/apps/alcs/src/providers/typeorm/datasource.cli.orm.config.ts +++ b/services/apps/alcs/src/providers/typeorm/datasource.cli.orm.config.ts @@ -20,4 +20,5 @@ export const connectionSource = new DataSource({ migrations: [join(__dirname, '**', 'migrations/*{.ts,.js}')], namingStrategy: new SnakeNamingStrategy(), uuidExtension: 'pgcrypto', + maxQueryExecutionTime: 500, }); From 67d4b9e73a7e26d162fd7c5bbc2d31f5beb95344 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Thu, 31 Aug 2023 13:42:49 -0700 Subject: [PATCH 328/954] Feature/alcs 957-part-1: applications and nois (#925) advanced search page status search advanced search support for applications advanced search support for NOIs --- alcs-frontend/src/app/app-routing.module.ts | 14 +- alcs-frontend/src/app/app.module.ts | 11 +- .../application-search-table.component.html | 73 ++++ .../application-search-table.component.scss | 0 ...application-search-table.component.spec.ts | 31 ++ .../application-search-table.component.ts | 122 ++++++ ...tice-of-intent-search-table.component.html | 73 ++++ ...tice-of-intent-search-table.component.scss | 0 ...e-of-intent-search-table.component.spec.ts | 33 ++ ...notice-of-intent-search-table.component.ts | 123 ++++++ .../app/features/search/search.component.html | 263 +++++++++++++ .../app/features/search/search.component.scss | 154 ++++++++ .../search/search.component.spec.ts | 33 +- .../app/features/search/search.component.ts | 305 +++++++++++++++ .../app/features/search/search.interface.ts | 7 + .../src/app/features/search/search.module.ts | 22 ++ .../src/app/services/search/search.dto.ts | 51 +++ .../services/search/search.service.spec.ts | 98 +++++ .../src/app/services/search/search.service.ts | 49 ++- .../header/search-bar/search-bar.component.ts | 1 - .../header/search/search.component.html | 44 --- .../header/search/search.component.scss | 49 --- .../shared/header/search/search.component.ts | 105 ----- .../pipes/table-column-no-data.pipe.spec.ts | 8 + .../shared/pipes/table-column-no-data.pipe.ts | 10 + alcs-frontend/src/app/shared/shared.module.ts | 5 +- ...pplication-advanced-search.service.spec.ts | 116 ++++++ .../application-advanced-search.service.ts | 366 ++++++++++++++++++ .../application-search-view.entity.ts | 104 +++++ ...-of-intent-advanced-search.service.spec.ts | 113 ++++++ ...otice-of-intent-advanced-search.service.ts | 336 ++++++++++++++++ .../notice-of-intent-search-view.entity.ts | 100 +++++ .../src/alcs/search/search.controller.spec.ts | 99 ++++- .../alcs/src/alcs/search/search.controller.ts | 152 +++++++- .../apps/alcs/src/alcs/search/search.dto.ts | 122 ++++++ .../alcs/src/alcs/search/search.module.ts | 15 +- .../src/alcs/search/search.service.spec.ts | 16 + ...92985798191-get_current_status_function.ts | 41 ++ .../1693352687970-app_search_view.ts | 30 ++ ...9968-rename_get_current_status_function.ts | 15 + ...169-get_current_status_for_noi_function.ts | 41 ++ .../1693416651488-noi_search_view.ts | 30 ++ .../apps/alcs/src/utils/search-helper.spec.ts | 35 ++ services/apps/alcs/src/utils/search-helper.ts | 18 + 44 files changed, 3207 insertions(+), 226 deletions(-) create mode 100644 alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.html create mode 100644 alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.scss create mode 100644 alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.spec.ts create mode 100644 alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.ts create mode 100644 alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.html create mode 100644 alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.scss create mode 100644 alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.spec.ts create mode 100644 alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.ts create mode 100644 alcs-frontend/src/app/features/search/search.component.html create mode 100644 alcs-frontend/src/app/features/search/search.component.scss rename alcs-frontend/src/app/{shared/header => features}/search/search.component.spec.ts (52%) create mode 100644 alcs-frontend/src/app/features/search/search.component.ts create mode 100644 alcs-frontend/src/app/features/search/search.interface.ts create mode 100644 alcs-frontend/src/app/features/search/search.module.ts delete mode 100644 alcs-frontend/src/app/shared/header/search/search.component.html delete mode 100644 alcs-frontend/src/app/shared/header/search/search.component.scss delete mode 100644 alcs-frontend/src/app/shared/header/search/search.component.ts create mode 100644 alcs-frontend/src/app/shared/pipes/table-column-no-data.pipe.spec.ts create mode 100644 alcs-frontend/src/app/shared/pipes/table-column-no-data.pipe.ts create mode 100644 services/apps/alcs/src/alcs/search/application/application-advanced-search.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts create mode 100644 services/apps/alcs/src/alcs/search/application/application-search-view.entity.ts create mode 100644 services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts create mode 100644 services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-search-view.entity.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1692985798191-get_current_status_function.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693352687970-app_search_view.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693410289968-rename_get_current_status_function.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693410408169-get_current_status_for_noi_function.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693416651488-noi_search_view.ts create mode 100644 services/apps/alcs/src/utils/search-helper.spec.ts create mode 100644 services/apps/alcs/src/utils/search-helper.ts diff --git a/alcs-frontend/src/app/app-routing.module.ts b/alcs-frontend/src/app/app-routing.module.ts index b63b57b267..e3d3768667 100644 --- a/alcs-frontend/src/app/app-routing.module.ts +++ b/alcs-frontend/src/app/app-routing.module.ts @@ -7,7 +7,6 @@ import { ProvisionComponent } from './features/provision/provision.component'; import { AuthGuard } from './services/authentication/auth.guard'; import { ALL_ROLES, ROLES } from './services/authentication/authentication.service'; import { HasRolesGuard } from './services/authentication/hasRoles.guard'; -import { SearchComponent } from './shared/header/search/search.component'; export const ROLES_ALLOWED_APPLICATIONS = [ROLES.ADMIN, ROLES.LUP, ROLES.APP_SPECIALIST, ROLES.GIS, ROLES.SOIL_OFFICER]; export const ROLES_ALLOWED_BOARDS = ROLES_ALLOWED_APPLICATIONS; @@ -70,6 +69,14 @@ const routes: Routes = [ }, loadChildren: () => import('./features/admin/admin.module').then((m) => m.AdminModule), }, + { + path: 'search', + canActivate: [HasRolesGuard], + data: { + roles: ALL_ROLES, + }, + loadChildren: () => import('./features/search/search.module').then((m) => m.SearchModule), + }, { path: 'login', component: LoginComponent, @@ -83,11 +90,6 @@ const routes: Routes = [ component: ProvisionComponent, canActivate: [AuthGuard], }, - { - path: 'search', - component: SearchComponent, - canActivate: [AuthGuard], - }, { path: '', redirectTo: '/login', pathMatch: 'full' }, { path: '**', component: NotFoundComponent }, ]; diff --git a/alcs-frontend/src/app/app.module.ts b/alcs-frontend/src/app/app.module.ts index 03a4d798a7..a92968e50b 100644 --- a/alcs-frontend/src/app/app.module.ts +++ b/alcs-frontend/src/app/app.module.ts @@ -12,13 +12,13 @@ import { AuthorizationComponent } from './features/authorization/authorization.c import { NotFoundComponent } from './features/errors/not-found/not-found.component'; import { LoginComponent } from './features/login/login.component'; import { ProvisionComponent } from './features/provision/provision.component'; +import { SearchModule } from './features/search/search.module'; import { AuthInterceptorService } from './services/authentication/auth-interceptor.service'; import { TokenRefreshService } from './services/authentication/token-refresh.service'; import { ConfirmationDialogComponent } from './shared/confirmation-dialog/confirmation-dialog.component'; import { HeaderComponent } from './shared/header/header.component'; import { NotificationsComponent } from './shared/header/notifications/notifications.component'; import { SearchBarComponent } from './shared/header/search-bar/search-bar.component'; -import { SearchComponent } from './shared/header/search/search.component'; import { SharedModule } from './shared/shared.module'; @NgModule({ @@ -32,9 +32,14 @@ import { SharedModule } from './shared/shared.module'; ConfirmationDialogComponent, NotificationsComponent, SearchBarComponent, - SearchComponent, ], - imports: [BrowserModule, BrowserAnimationsModule, SharedModule.forRoot(), AppRoutingModule, MomentDateModule], + imports: [ + BrowserModule, + BrowserAnimationsModule, + SharedModule.forRoot(), + AppRoutingModule, + MomentDateModule, + ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true }, { provide: MAT_DIALOG_DEFAULT_OPTIONS, useValue: { panelClass: 'mat-dialog-override' } }, diff --git a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.html b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.html new file mode 100644 index 0000000000..bd7ef5c617 --- /dev/null +++ b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.html @@ -0,0 +1,73 @@ +
## {{ i + 1 }} TypeType @@ -37,7 +37,7 @@ - Size (ha)*Size (ha)* - Alr Area (ha)*ALR Area (ha)* Date: Tue, 29 Aug 2023 13:25:48 -0700 Subject: [PATCH 320/954] Fix circular import error breaking api (#923) --- .../notice-of-intent-decision.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts index 706500e5cc..fe7cf9c6c6 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision.module.ts @@ -45,7 +45,7 @@ import { NoticeOfIntentSubmissionModule } from '../../portal/notice-of-intent-su DocumentModule, forwardRef(() => NoticeOfIntentModule), NoticeOfIntentSubmissionStatusModule, - NoticeOfIntentSubmissionModule, + forwardRef(() => NoticeOfIntentSubmissionModule), ], providers: [ //These are in the same module, so be careful to import the correct one From 29aaa7fb78505c4cde8d6fce03ab239424c67680 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 29 Aug 2023 14:58:53 -0700 Subject: [PATCH 321/954] Force deploy --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44f336520a..1872b126fe 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This project contains the frontend and backend for the ALCS used for tracking various permits and other ALC related documents -## Getting Help or Reporting an Issue +## Getting Help or Reporting an Issue To report bugs/issues/features requests, please file an [issue](https://github.com/bcgov/alcs/issues). From 3f814288cce9ff73baef3be61392678e83fa63ed Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 29 Aug 2023 16:39:29 -0700 Subject: [PATCH 322/954] Bug Fixes * Only prepare components after the NOI / App are loaded * Load codes/subtypes for Timeline mapping --- .../decision-components/decision-components.component.ts | 3 +-- .../decision-components/decision-components.component.ts | 3 +-- .../application-timeline/application-timeline.service.ts | 3 +++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index 6b4ef02f93..09429cc6d5 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -59,11 +59,10 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView if (application) { this.application = application; this.application.submittedApplication = await this.submissionService.fetchSubmission(application.fileNumber); + await this.prepareDecisionComponentTypes(this.codes); } }); - this.prepareDecisionComponentTypes(this.codes); - // validate components on load setTimeout(() => this.onChange(), 0); } diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts index ef7853468e..f8f325536f 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-components/decision-components.component.ts @@ -68,11 +68,10 @@ export class DecisionComponentsComponent implements OnInit, OnDestroy, AfterView if (noticeOfIntent) { this.noticeOfIntent = noticeOfIntent; this.noticeOfIntentSubmission = await this.submissionService.fetchSubmission(noticeOfIntent.fileNumber); + await this.prepareDecisionComponentTypes(this.codes); } }); - this.prepareDecisionComponentTypes(this.codes); - // validate components on load setTimeout(() => this.onChange(), 0); } diff --git a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts index b3bad2061d..d74fd9633d 100644 --- a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts +++ b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts @@ -207,6 +207,8 @@ export class ApplicationTimelineService { applicationUuid: application.uuid, }, relations: { + type: true, + reviewOutcome: true, card: { subtasks: { type: true, @@ -270,6 +272,7 @@ export class ApplicationTimelineService { type: true, }, }, + reviewOutcome: true, }, order: { submittedDate: 'DESC', From aae85510de5d8d5016ee860dbab95f6129f5613e Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 30 Aug 2023 16:20:50 -0700 Subject: [PATCH 323/954] Bug Fixes Pt1 * Filter to only application parcels on app prep page * Save POFO and ROSO components if child components are dirty * Allow updating submissions in draft mode regardless of their status --- .../proposal/parcel-prep/parcel-prep.component.ts | 14 ++++++++------ .../pofo-proposal/pofo-proposal.component.ts | 2 +- .../roso-proposal/roso-proposal.component.ts | 2 +- .../application-submission.controller.ts | 13 +++++++------ 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.ts b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.ts index 5bf3bce19a..335c6ec886 100644 --- a/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.ts +++ b/alcs-frontend/src/app/features/application/proposal/parcel-prep/parcel-prep.component.ts @@ -26,12 +26,14 @@ export class ParcelPrepComponent implements OnChanges { async loadParcels(fileNumber: string) { const parcels = await this.parcelService.fetchParcels(fileNumber); - this.parcels = parcels.map((parcel) => ({ - ...parcel, - owners: `${parcel.owners[0].displayName} ${parcel.owners.length > 1 ? ' et al.' : ''}`, - fullOwners: parcel.owners.map((owner) => owner.displayName).join(', '), - hasManyOwners: parcel.owners.length > 1, - })); + this.parcels = parcels + .filter((parcel) => parcel.parcelType === 'application') + .map((parcel) => ({ + ...parcel, + owners: `${parcel.owners[0].displayName} ${parcel.owners.length > 1 ? ' et al.' : ''}`, + fullOwners: parcel.owners.map((owner) => owner.displayName).join(', '), + hasManyOwners: parcel.owners.length > 1, + })); } async saveParcel(uuid: string, alrArea: string | null) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts index 2548b6d2b2..986baff612 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts @@ -113,7 +113,7 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId && this.form.dirty) { + if (this.fileId && (this.form.dirty || this.areComponentsDirty)) { const isFollowUp = this.isFollowUp.getRawValue(); const soilFollowUpIDs = this.followUpIDs.getRawValue(); const purpose = this.purpose.getRawValue(); diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts index 4860d4b274..cd4a416290 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts @@ -111,7 +111,7 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId && this.form.dirty) { + if (this.fileId && (this.form.dirty || this.areComponentsDirty)) { const isNOIFollowUp = this.isFollowUp.getRawValue(); const soilFollowUpIDs = this.followUpIds.getRawValue(); const purpose = this.purpose.getRawValue(); diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts index d495f331e1..b443e74b01 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.controller.ts @@ -155,12 +155,13 @@ export class ApplicationSubmissionController { ); if ( - !submission.status || - ![ - SUBMISSION_STATUS.INCOMPLETE.toString(), - SUBMISSION_STATUS.WRONG_GOV.toString(), - SUBMISSION_STATUS.IN_PROGRESS.toString(), - ].includes(submission.status.statusTypeCode) + !submission.isDraft && + (!submission.status || + ![ + SUBMISSION_STATUS.INCOMPLETE.toString(), + SUBMISSION_STATUS.WRONG_GOV.toString(), + SUBMISSION_STATUS.IN_PROGRESS.toString(), + ].includes(submission.status.statusTypeCode)) ) { throw new ServiceValidationException('Not allowed to update submission'); } From 961160cbf6d12cbfe6e398858b54191ac487db69 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 31 Aug 2023 09:14:30 -0700 Subject: [PATCH 324/954] Bug Fixes Pt 2 * Add download to controller and hook up to download on front end * Disable government selector when in draft mode * Remove government review banner and variable from NOIs * Properly map date on Application decisions --- .../select-government.component.ts | 4 ++++ .../submission-documents.component.html | 2 +- .../submission-documents.component.ts | 15 +++++++++++++++ .../select-government.component.html | 5 ----- .../select-government.component.ts | 4 ---- .../application-document.service.ts | 10 ++++++++++ ...application-decision-v2.automapper.profile.ts | 4 ++++ .../application-decision.dto.ts | 1 - .../application-document.controller.spec.ts | 16 +++++++++++++++- .../application-document.controller.ts | 14 ++++++++++++++ 10 files changed, 63 insertions(+), 12 deletions(-) diff --git a/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts index 522ca5f4c6..b5b264eb7a 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/select-government/select-government.component.ts @@ -47,6 +47,10 @@ export class SelectGovernmentComponent extends StepComponent implements OnInit, } }); + if (this.draftMode) { + this.localGovernment.disable(); + } + this.filteredLocalGovernments = this.localGovernment.valueChanges.pipe( startWith(''), map((value) => this.filter(value || '')) diff --git a/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.html b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.html index 437bbd407e..8dc409f48a 100644 --- a/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.html +++ b/portal-frontend/src/app/features/applications/view-submission/alc-review/submission-documents/submission-documents.component.html @@ -28,7 +28,7 @@

Application Documents

Actions -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File ID + {{ element.fileNumber | emptyColumn }} + Date Submitted + {{ element.dateSubmitted | date | emptyColumn }} + Owner Name + {{ element.ownerName | emptyColumn }} + Type + + + - + Local/First Nation Government + {{ element.localGovernmentName | emptyColumn }} + Portal Status + +
+
No Search results.
+
Please adjust criteria and try again.
+
+ diff --git a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.scss b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.spec.ts b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.spec.ts new file mode 100644 index 0000000000..769348d3ca --- /dev/null +++ b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { DeepMocked } from '@golevelup/ts-jest'; + +import { ApplicationSearchTableComponent } from './application-search-table.component'; + +describe('ApplicationSearchTableComponent', () => { + let component: ApplicationSearchTableComponent; + let fixture: ComponentFixture; + let mockRouter: DeepMocked; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ApplicationSearchTableComponent], + providers: [ + { + provide: Router, + useValue: mockRouter, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ApplicationSearchTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.ts b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.ts new file mode 100644 index 0000000000..25f980db36 --- /dev/null +++ b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.ts @@ -0,0 +1,122 @@ +import { AfterViewInit, Component, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { Router } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { ApplicationRegionDto, ApplicationTypeDto } from '../../../services/application/application-code.dto'; +import { ApplicationStatusDto } from '../../../services/application/application-submission-status/application-submission-status.dto'; +import { ApplicationSearchResultDto } from '../../../services/search/search.dto'; +import { ApplicationSubmissionStatusPill } from '../../../shared/application-submission-status-type-pill/application-submission-status-type-pill.component'; +import { defaultStatusBackgroundColour, defaultStatusColour } from '../search.component'; +import { TableChange } from '../search.interface'; + +interface SearchResult { + fileNumber: string; + dateSubmitted: number; + ownerName: string; + type?: ApplicationTypeDto; + localGovernmentName?: string; + portalStatus?: string; + referenceId: string; + board?: string; + class: string; + status?: ApplicationSubmissionStatusPill; +} + +@Component({ + selector: 'app-application-search-table', + templateUrl: './application-search-table.component.html', + styleUrls: ['./application-search-table.component.scss'], +}) +export class ApplicationSearchTableComponent implements AfterViewInit, OnDestroy { + $destroy = new Subject(); + + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatSort) sort?: MatSort; + + _applications: ApplicationSearchResultDto[] = []; + @Input() set applications(applications: ApplicationSearchResultDto[]) { + this._applications = applications; + this.dataSource = this.mapApplications(applications); + } + + @Input() totalCount = 0; + @Input() statuses: ApplicationStatusDto[] = []; + @Input() regions: ApplicationRegionDto[] = []; + + @Output() tableChange = new EventEmitter(); + + displayedColumns = ['fileId', 'dateSubmitted', 'ownerName', 'type', 'government', 'portalStatus']; + dataSource: SearchResult[] = []; + pageIndex = 0; + itemsPerPage = 20; + total = 0; + sortDirection = 'DESC'; + sortField = 'dateSubmitted'; + + constructor(private router: Router) {} + + ngAfterViewInit() { + if (this.sort) { + this.sort.sortChange.pipe(takeUntil(this.$destroy)).subscribe(async (sortObj) => { + this.paginator.pageIndex = 0; + this.pageIndex = 0; + this.sortDirection = sortObj.direction.toUpperCase(); + this.sortField = sortObj.active; + + await this.onTableChange(); + }); + } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + async onTableChange() { + this.tableChange.emit({ + pageIndex: this.pageIndex, + itemsPerPage: this.itemsPerPage, + sortDirection: this.sortDirection, + sortField: this.sortField, + tableType: 'APP', + }); + } + + async onPageChange($event: PageEvent) { + this.pageIndex = $event.pageIndex; + this.itemsPerPage = $event.pageSize; + + await this.onTableChange(); + } + + async onSelectRecord(record: SearchResult) { + await this.router.navigateByUrl(`/application/${record.referenceId}`); + } + + private mapApplications(applications: ApplicationSearchResultDto[]): SearchResult[] { + return applications.map((e) => { + const status = this.statuses.find((st) => st.code === e.status); + + return { + fileNumber: e.fileNumber, + dateSubmitted: e.dateSubmitted, + ownerName: e.ownerName, + type: e.type, + localGovernmentName: e.localGovernmentName, + portalStatus: e.portalStatus, + referenceId: e.referenceId, + board: e.boardCode, + class: e.class, + status: { + backgroundColor: status?.portalBackgroundColor ?? defaultStatusBackgroundColour, + textColor: status?.portalColor ?? defaultStatusColour, + borderColor: status?.portalBackgroundColor, + label: status?.label, + shortLabel: status?.label, + }, + }; + }); + } +} diff --git a/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.html b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.html new file mode 100644 index 0000000000..bd7ef5c617 --- /dev/null +++ b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.html @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File ID + {{ element.fileNumber | emptyColumn }} + Date Submitted + {{ element.dateSubmitted | date | emptyColumn }} + Owner Name + {{ element.ownerName | emptyColumn }} + Type + + + - + Local/First Nation Government + {{ element.localGovernmentName | emptyColumn }} + Portal Status + +
+
No Search results.
+
Please adjust criteria and try again.
+
+ diff --git a/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.scss b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.spec.ts b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.spec.ts new file mode 100644 index 0000000000..8f73bb0ba8 --- /dev/null +++ b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; + +import { NoticeOfIntentSearchTableComponent } from './notice-of-intent-search-table.component'; + +describe('NoticeOfIntentSearchTableComponent', () => { + let component: NoticeOfIntentSearchTableComponent; + let fixture: ComponentFixture; + let mockRouter: DeepMocked; + + beforeEach(async () => { + mockRouter = createMock(); + + await TestBed.configureTestingModule({ + declarations: [NoticeOfIntentSearchTableComponent], + providers: [ + { + provide: Router, + useValue: mockRouter, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(NoticeOfIntentSearchTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.ts b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.ts new file mode 100644 index 0000000000..536701d449 --- /dev/null +++ b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.ts @@ -0,0 +1,123 @@ +import { AfterViewInit, Component, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { Router } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { ApplicationRegionDto } from '../../../services/application/application-code.dto'; +import { ApplicationStatusDto } from '../../../services/application/application-submission-status/application-submission-status.dto'; +import { NoticeOfIntentTypeDto } from '../../../services/notice-of-intent/notice-of-intent.dto'; +import { NoticeOfIntentSearchResultDto } from '../../../services/search/search.dto'; +import { ApplicationSubmissionStatusPill } from '../../../shared/application-submission-status-type-pill/application-submission-status-type-pill.component'; +import { TableChange } from '../search.interface'; + +interface SearchResult { + fileNumber: string; + dateSubmitted: number; + ownerName: string; + type?: NoticeOfIntentTypeDto; + government?: string; + portalStatus?: string; + referenceId: string; + board?: string; + class: string; + status?: ApplicationSubmissionStatusPill; +} + +@Component({ + selector: 'app-notice-of-intent-search-table', + templateUrl: './notice-of-intent-search-table.component.html', + styleUrls: ['./notice-of-intent-search-table.component.scss'], +}) +export class NoticeOfIntentSearchTableComponent implements AfterViewInit, OnDestroy { + $destroy = new Subject(); + + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatSort) sort?: MatSort; + + _noticeOfIntents: NoticeOfIntentSearchResultDto[] = []; + @Input() set noticeOfIntents(noticeOfIntents: NoticeOfIntentSearchResultDto[]) { + this._noticeOfIntents = noticeOfIntents; + this.dataSource = this.mapNoticeOfIntent(noticeOfIntents); + } + + @Input() totalCount = 0; + @Input() statuses: ApplicationStatusDto[] = []; + @Input() regions: ApplicationRegionDto[] = []; + + @Output() tableChange = new EventEmitter(); + + displayedColumns = ['fileId', 'dateSubmitted', 'ownerName', 'type', 'government', 'portalStatus']; + dataSource: SearchResult[] = []; + pageIndex = 0; + itemsPerPage = 20; + total = 0; + sortDirection = 'DESC'; + sortField = 'dateSubmitted'; + + constructor(private router: Router) {} + + ngAfterViewInit() { + if (this.sort) { + this.sort.sortChange.pipe(takeUntil(this.$destroy)).subscribe(async (sortObj) => { + this.paginator.pageIndex = 0; + this.pageIndex = 0; + this.sortDirection = sortObj.direction.toUpperCase(); + this.sortField = sortObj.active; + + await this.onTableChange(); + }); + } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + async onTableChange() { + this.tableChange.emit({ + pageIndex: this.pageIndex, + itemsPerPage: this.itemsPerPage, + sortDirection: this.sortDirection, + sortField: this.sortField, + tableType: 'APP', + }); + } + + async onPageChange($event: PageEvent) { + this.pageIndex = $event.pageIndex; + this.itemsPerPage = $event.pageSize; + + await this.onTableChange(); + } + + async onSelectRecord(record: SearchResult) { + await this.router.navigateByUrl(`/notice-of-intent/${record.referenceId}`); + } + + private mapNoticeOfIntent(applications: NoticeOfIntentSearchResultDto[]) { + return applications.map((e) => { + const status = this.statuses.find((st) => st.code === e.status); + + return { + fileNumber: e.fileNumber, + dateSubmitted: e.dateSubmitted, + ownerName: e.ownerName, + type: e.type, + localGovernmentName: e.localGovernmentName, + portalStatus: e.portalStatus, + referenceId: e.referenceId, + board: e.boardCode, + class: e.class, + status: { + // TODO there no pills for NOI status yet. This needs to be addressed once noi statuses are done + backgroundColor: '#fcba19', + textColor: '#002f17', + borderColor: '#fcba19', + label: status?.label, + shortLabel: status?.label, + }, + }; + }); + } +} diff --git a/alcs-frontend/src/app/features/search/search.component.html b/alcs-frontend/src/app/features/search/search.component.html new file mode 100644 index 0000000000..3a4b561a2f --- /dev/null +++ b/alcs-frontend/src/app/features/search/search.component.html @@ -0,0 +1,263 @@ +
+

Advanced Search

+
+ +
+
Provide one or more of the following criteria:
+
+ +
+
+
+
+ + File ID + + +
+
+ + Name + + +
Search by Primary Contact, Parcel Owner, Organization, Ministry or Department
+
+
+
+ +
+
+ + PID + + + + Civic Address + + +
+ +
+
+ info +
+
+ Property details change over time. Use both ALCS and iMap to + confirm ALC history on a property +
+
+ + Expand search to include 'Other Parcels in the Community' from applicant submission +
+ + +
+
+
+
+
+ + Resolution Number + + +
+
/
+
+ +
+
+
+
+ + Legacy ID + + +
+
+
+
+ + + + {{ status.label }} + + + +
+ +
+ + File Type (To be implemented) + + +
Note: This field searches both proposal and decision component type
+
+
+ +
+
+ + + + + {{ option.name }} + + + +
+ +
+ + +
{{ item.label }}
+
+
+
+
+
Date Submitted
+
+
+ + From + + + + +
+ +
+ + To + + + + +
+
+
Date Decided
+
+
+ + From + + + + +
+ +
+ + To + + + + +
+
+
+
+
+ + +
+ +
+

Search Results:

+ + + + Applications: {{ applicationTotal }} + + + + + Notice of Intent: {{ noticeOfIntentTotal }} + + + + + Non-Applications: 0 + + +
diff --git a/alcs-frontend/src/app/features/search/search.component.scss b/alcs-frontend/src/app/features/search/search.component.scss new file mode 100644 index 0000000000..f2b6428f48 --- /dev/null +++ b/alcs-frontend/src/app/features/search/search.component.scss @@ -0,0 +1,154 @@ +@use '../../../styles/colors'; +h3, +div, +span { + color: colors.$black; +} + +.search-title { + width: 100%; + margin-top: 42px; + margin-bottom: 24px; + display: flex; + justify-content: left; + align-items: center; + + h4 { + margin-right: 12px !important; + } +} + +:host::ng-deep { + .table { + width: 100%; + margin-top: 12px; + + td, + th { + font-size: 16px; + width: 25%; + } + + .type-cell { + width: 90px; + } + + tr.mdc-data-table__row:hover { + box-shadow: 0 0 0 2px rgba(colors.$primary-color, 0.9); + cursor: pointer; + transform: scale(1); + } + + tr.no-data { + border: none; + background-color: colors.$grey-light; + align-items: center; + justify-content: center; + cursor: auto; + box-shadow: none !important; + height: 32px; + } + } +} + +.search-fields-wrapper { + padding: 0 80px; +} + +.row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 24px; + width: 100%; + margin: 24px 0; + + &.date-row { + margin: 0 0 12px 0; + } + + &.date-row-label { + margin: 0 0 4px 0; + gap: 4px; + } + + .column { + display: flex; + flex-direction: column; + flex-basis: 100%; + flex: 1; + } + + .resolution-wrapper { + gap: 12px; + margin: 0; + } +} + +.address-search-fields-wrapper { + background-color: #fee9b580; + padding: 12px 80px; + + .row { + margin: 4px 0; + } + .column { + background-color: white; + } +} + +.expand-search { + display: flex; + align-items: center; + justify-content: right; + + .expand-search-btn { + display: flex; + align-items: center; + cursor: pointer; + color: #1a5a96; + + span { + color: #1a5a96; + } + } +} + +.btn-controls { + padding: 12px 80px; + display: flex; + justify-content: space-between; +} + +.title { + margin: 36px 80px; +} + +.subtitle { + margin: 18px 80px; +} + +.info-banner { + display: flex; + align-items: center; + + .info-description { + min-width: 0; + word-break: break-word; + } + + .icon { + display: flex; + justify-content: center; + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + } +} + +.search-result-wrapper { + margin-top: 60px; + margin-bottom: 60px; +} diff --git a/alcs-frontend/src/app/shared/header/search/search.component.spec.ts b/alcs-frontend/src/app/features/search/search.component.spec.ts similarity index 52% rename from alcs-frontend/src/app/shared/header/search/search.component.spec.ts rename to alcs-frontend/src/app/features/search/search.component.spec.ts index 25ddceaee6..1e5903ebfe 100644 --- a/alcs-frontend/src/app/shared/header/search/search.component.spec.ts +++ b/alcs-frontend/src/app/features/search/search.component.spec.ts @@ -1,10 +1,14 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Observable } from 'rxjs'; -import { SearchService } from '../../../services/search/search.service'; -import { ToastService } from '../../../services/toast/toast.service'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { ApplicationRegionDto } from '../../services/application/application-code.dto'; +import { ApplicationLocalGovernmentService } from '../../services/application/application-local-government/application-local-government.service'; +import { ApplicationService } from '../../services/application/application.service'; +import { SearchService } from '../../services/search/search.service'; +import { ToastService } from '../../services/toast/toast.service'; import { SearchComponent } from './search.component'; @@ -12,13 +16,17 @@ describe('SearchComponent', () => { let component: SearchComponent; let fixture: ComponentFixture; let mockSearchService: DeepMocked; - let mockRouter: DeepMocked; let mockToastService: DeepMocked; + let mockLocalGovernmentService: DeepMocked; + let mockApplicationService: DeepMocked; beforeEach(async () => { mockSearchService = createMock(); - mockRouter = createMock(); mockToastService = createMock(); + mockLocalGovernmentService = createMock(); + mockApplicationService = createMock(); + + mockApplicationService.$applicationRegions = new BehaviorSubject([]); await TestBed.configureTestingModule({ providers: [ @@ -26,22 +34,27 @@ describe('SearchComponent', () => { provide: SearchService, useValue: mockSearchService, }, - { - provide: Router, - useValue: mockRouter, - }, { provide: ActivatedRoute, useValue: { - queryParamMap: new Observable + queryParamMap: new Observable(), }, }, { provide: ToastService, useValue: mockToastService, }, + { + provide: ApplicationLocalGovernmentService, + useValue: mockLocalGovernmentService, + }, + { + provide: ApplicationService, + useValue: mockApplicationService, + }, ], declarations: [SearchComponent], + imports: [MatAutocompleteModule], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/alcs-frontend/src/app/features/search/search.component.ts b/alcs-frontend/src/app/features/search/search.component.ts new file mode 100644 index 0000000000..57c8e400ca --- /dev/null +++ b/alcs-frontend/src/app/features/search/search.component.ts @@ -0,0 +1,305 @@ +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { ActivatedRoute } from '@angular/router'; +import moment from 'moment'; +import { combineLatestWith, map, Observable, startWith, Subject, takeUntil } from 'rxjs'; +import { ApplicationRegionDto } from '../../services/application/application-code.dto'; +import { ApplicationLocalGovernmentDto } from '../../services/application/application-local-government/application-local-government.dto'; +import { ApplicationLocalGovernmentService } from '../../services/application/application-local-government/application-local-government.service'; +import { ApplicationStatusDto } from '../../services/application/application-submission-status/application-submission-status.dto'; +import { ApplicationService } from '../../services/application/application.service'; +import { + AdvancedSearchResponseDto, + ApplicationSearchResultDto, + NoticeOfIntentSearchResultDto, + SearchRequestDto, +} from '../../services/search/search.dto'; +import { SearchService } from '../../services/search/search.service'; +import { ToastService } from '../../services/toast/toast.service'; +import { formatDateForApi } from '../../shared/utils/api-date-formatter'; +import { TableChange } from './search.interface'; + +export const defaultStatusBackgroundColour = '#ffffff'; +export const defaultStatusColour = '#313132'; +@Component({ + selector: 'app-search', + templateUrl: './search.component.html', + styleUrls: ['./search.component.scss'], +}) +export class SearchComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatSort) sort?: MatSort; + + applications: ApplicationSearchResultDto[] = []; + applicationTotal = 0; + + noticeOfIntents: NoticeOfIntentSearchResultDto[] = []; + noticeOfIntentTotal = 0; + + isSearchExpanded = false; + pageIndex = 0; + itemsPerPage = 20; + sortDirection = 'DESC'; + sortField = 'dateSubmitted'; + + localGovernmentControl = new FormControl(undefined); + portalStatusControl = new FormControl(undefined); + searchForm = new FormGroup({ + fileNumber: new FormControl(undefined), + name: new FormControl(undefined), + pid: new FormControl(undefined), + civicAddress: new FormControl(undefined), + isIncludeOtherParcels: new FormControl(false), + resolutionNumber: new FormControl(undefined), + resolutionYear: new FormControl(undefined), + legacyId: new FormControl(undefined), + portalStatus: this.portalStatusControl, + componentType: new FormControl(undefined), + government: this.localGovernmentControl, + region: new FormControl(undefined), + dateSubmittedFrom: new FormControl(undefined), + dateSubmittedTo: new FormControl(undefined), + dateDecidedFrom: new FormControl(undefined), + dateDecidedTo: new FormControl(undefined), + }); + resolutionYears: number[] = []; + localGovernments: ApplicationLocalGovernmentDto[] = []; + filteredLocalGovernments!: Observable; + regions: ApplicationRegionDto[] = []; + statuses: ApplicationStatusDto[] = []; + + constructor( + private searchService: SearchService, + private activatedRoute: ActivatedRoute, + private localGovernmentService: ApplicationLocalGovernmentService, + private applicationService: ApplicationService, + private toastService: ToastService + ) {} + + ngOnInit(): void { + this.setup(); + + this.applicationService.$applicationRegions + .pipe(takeUntil(this.$destroy)) + .pipe(combineLatestWith(this.applicationService.$applicationStatuses, this.activatedRoute.queryParamMap)) + .subscribe(([regions, statuses, queryParamMap]) => { + this.regions = regions; + this.statuses = statuses; + + const searchText = queryParamMap.get('searchText'); + + if (searchText) { + this.searchForm.controls.fileNumber.setValue(searchText); + + this.searchService + .advancedSearchFetch({ + fileNumber: searchText, + isIncludeOtherParcels: false, + pageSize: this.itemsPerPage, + page: this.pageIndex + 1, + sortDirection: this.sortDirection, + sortField: this.sortField, + applicationFileTypes: [], + }) + .then((result) => this.mapSearchResults(result)); + } + }); + } + + private setup() { + const startingYear = moment('1950'); + const currentYear = moment().year(); + while (startingYear.year() <= currentYear) { + this.resolutionYears.push(startingYear.year()); + startingYear.add(1, 'year'); + } + this.resolutionYears.reverse(); + this.loadGovernments(); + this.applicationService.setup(); + + this.filteredLocalGovernments = this.localGovernmentControl.valueChanges.pipe( + startWith(''), + map((value) => this.filterLocalGovernment(value || '')) + ); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + // TODO: remove this once the search is complete + // async onSelectCard(record: SearchResult) { + // switch (record.class) { + // case 'APP': + // await this.router.navigateByUrl(`/application/${record.referenceId}`); + // break; + // case 'NOI': + // await this.router.navigateByUrl(`/notice-of-intent/${record.referenceId}`); + // break; + // case 'COV': + // case 'PLAN': + // await this.router.navigateByUrl(`/board/${record.board}?card=${record.referenceId}&type=${record.type}`); + // break; + // default: + // this.toastService.showErrorToast(`Unable to navigate to ${record.referenceId}`); + // } + // } + + async onSubmit() { + await this.onSearch(); + } + + expandSearchClicked() { + this.isSearchExpanded = !this.isSearchExpanded; + } + + onGovernmentChange($event: MatAutocompleteSelectedEvent) { + const localGovernmentName = $event.option.value; + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + if (localGovernment) { + this.localGovernmentControl.setValue(localGovernment.name); + } + } + } + + onBlur() { + //Blur will fire before onChange above, so use setTimeout to delay it + setTimeout(() => { + const localGovernmentName = this.localGovernmentControl.getRawValue(); + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + if (!localGovernment) { + this.localGovernmentControl.setValue(null); + console.log('Clearing Local Government field'); + } + } + }, 500); + } + + onReset() { + this.searchForm.reset(); + } + + async onSearch() { + const searchParams = this.getSearchParams(); + const result = await this.searchService.advancedSearchFetch(searchParams); + this.mapSearchResults(result); + } + + getSearchParams(): SearchRequestDto { + return { + // pagination + pageSize: this.itemsPerPage, + page: this.pageIndex + 1, + // sorting + sortField: this.sortField, + sortDirection: this.sortDirection, + // TODO move condition into helper function? + fileNumber: + this.searchForm.controls.fileNumber.value && this.searchForm.controls.fileNumber.value !== '' + ? this.searchForm.controls.fileNumber.value + : undefined, + legacyId: this.searchForm.controls.legacyId.value ?? undefined, + name: this.searchForm.controls.name.value ?? undefined, + civicAddress: this.searchForm.controls.civicAddress.value ?? undefined, + pid: this.searchForm.controls.pid.value ?? undefined, + isIncludeOtherParcels: this.searchForm.controls.isIncludeOtherParcels.value ?? false, + resolutionNumber: this.searchForm.controls.resolutionNumber.value + ? parseInt(this.searchForm.controls.resolutionNumber.value) + : undefined, + resolutionYear: this.searchForm.controls.resolutionYear.value ?? undefined, + portalStatusCode: this.searchForm.controls.portalStatus.value ?? undefined, + governmentName: this.searchForm.controls.government.value ?? undefined, + regionCode: this.searchForm.controls.region.value ?? undefined, + dateSubmittedFrom: this.searchForm.controls.dateSubmittedFrom.value + ? formatDateForApi(this.searchForm.controls.dateSubmittedFrom.value) + : undefined, + dateSubmittedTo: this.searchForm.controls.dateSubmittedTo.value + ? formatDateForApi(this.searchForm.controls.dateSubmittedTo.value) + : undefined, + dateDecidedFrom: this.searchForm.controls.dateDecidedFrom.value + ? formatDateForApi(this.searchForm.controls.dateDecidedFrom.value) + : undefined, + dateDecidedTo: this.searchForm.controls.dateDecidedTo.value + ? formatDateForApi(this.searchForm.controls.dateDecidedTo.value) + : undefined, + // TODO this will be reworked in later tickets + applicationFileTypes: this.searchForm.controls.componentType.value + ? this.searchForm.controls.componentType.value.split(',') + : [], + }; + } + + async onApplicationSearch() { + const searchParams = this.getSearchParams(); + const result = await this.searchService.advancedSearchApplicationsFetch(searchParams); + + this.applications = result?.data ?? []; + this.applicationTotal = result?.total ?? 0; + } + + async onNoticeOfIntentSearch() { + const searchParams = this.getSearchParams(); + const result = await this.searchService.advancedSearchNoticeOfIntentsFetch(searchParams); + + this.noticeOfIntents = result?.data ?? []; + this.noticeOfIntentTotal = result?.total ?? 0; + } + + async onTableChange(event: TableChange) { + this.pageIndex = event.pageIndex; + this.itemsPerPage = event.itemsPerPage; + this.sortDirection = event.sortDirection; + this.sortField = event.sortField; + + switch (event.tableType) { + case 'APP': + await this.onApplicationSearch(); + break; + case 'NOI': + await this.onApplicationSearch(); + break; + default: + this.toastService.showErrorToast('Not implemented'); + } + } + + private async loadGovernments() { + const governments = await this.localGovernmentService.list(); + this.localGovernments = governments.sort((a, b) => (a.name > b.name ? 1 : -1)); + } + + private filterLocalGovernment(value: string): ApplicationLocalGovernmentDto[] { + if (this.localGovernments) { + const filterValue = value.toLowerCase(); + return this.localGovernments.filter((localGovernment) => + localGovernment.name.toLowerCase().includes(filterValue) + ); + } + return []; + } + + private mapSearchResults(searchResult?: AdvancedSearchResponseDto) { + if (!searchResult) { + searchResult = { + applications: [], + noticeOfIntents: [], + totalApplications: 0, + totalNoticeOfIntents: 0, + }; + } + + this.applicationTotal = searchResult.totalApplications; + this.applications = searchResult.applications; + + this.noticeOfIntentTotal = searchResult.totalNoticeOfIntents; + this.noticeOfIntents = searchResult.noticeOfIntents; + } +} diff --git a/alcs-frontend/src/app/features/search/search.interface.ts b/alcs-frontend/src/app/features/search/search.interface.ts new file mode 100644 index 0000000000..f254745a09 --- /dev/null +++ b/alcs-frontend/src/app/features/search/search.interface.ts @@ -0,0 +1,7 @@ +export interface TableChange { + pageIndex: number; + itemsPerPage: number; + sortDirection: string; + sortField: string; + tableType: string; +} diff --git a/alcs-frontend/src/app/features/search/search.module.ts b/alcs-frontend/src/app/features/search/search.module.ts new file mode 100644 index 0000000000..48530f0a02 --- /dev/null +++ b/alcs-frontend/src/app/features/search/search.module.ts @@ -0,0 +1,22 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatTabsModule } from '@angular/material/tabs'; +import { RouterModule, Routes } from '@angular/router'; +import { SharedModule } from '../../shared/shared.module'; +import { SearchComponent } from './search.component'; +import { ApplicationSearchTableComponent } from './application-search-table/application-search-table.component'; +import { NoticeOfIntentSearchTableComponent } from './notice-of-intent-search-table/notice-of-intent-search-table.component'; + +const routes: Routes = [ + { + path: '', + component: SearchComponent, + }, +]; + +@NgModule({ + declarations: [SearchComponent, ApplicationSearchTableComponent, NoticeOfIntentSearchTableComponent], + imports: [CommonModule, SharedModule.forRoot(), RouterModule.forChild(routes), MatTabsModule, MatPaginatorModule], +}) +export class SearchModule {} diff --git a/alcs-frontend/src/app/services/search/search.dto.ts b/alcs-frontend/src/app/services/search/search.dto.ts index 797c619778..2cb7223441 100644 --- a/alcs-frontend/src/app/services/search/search.dto.ts +++ b/alcs-frontend/src/app/services/search/search.dto.ts @@ -1,5 +1,56 @@ import { ApplicationTypeDto } from '../application/application-code.dto'; +export interface ApplicationSearchResultDto { + fileNumber: string; + type: ApplicationTypeDto; + referenceId: string; + applicant?: string; + localGovernmentName: string; + boardCode?: string; + ownerName: string; + dateSubmitted: number; + portalStatus?: string; + class: string; + status: string; +} + +export interface NoticeOfIntentSearchResultDto extends ApplicationSearchResultDto {} + +export interface AdvancedSearchResponseDto { + applications: ApplicationSearchResultDto[]; + noticeOfIntents: NoticeOfIntentSearchResultDto[]; + totalApplications: number; + totalNoticeOfIntents: number; +} + +export interface AdvancedSearchEntityResponseDto { + data: T[]; + total: number; +} + +export interface SearchRequestDto { + pageSize: number; + page: number; + sortField: string; + sortDirection: string; + fileNumber?: string; + legacyId?: string; + name?: string; + pid?: string; + civicAddress?: string; + isIncludeOtherParcels: boolean; + resolutionNumber?: number; + resolutionYear?: number; + portalStatusCode?: string; + governmentName?: string; + regionCode?: string; + dateSubmittedFrom?: number; + dateSubmittedTo?: number; + dateDecidedFrom?: number; + dateDecidedTo?: number; + applicationFileTypes: string[]; +} + export interface SearchResultDto { fileNumber: string; type: string; diff --git a/alcs-frontend/src/app/services/search/search.service.spec.ts b/alcs-frontend/src/app/services/search/search.service.spec.ts index ecd881e5cd..2ce634722d 100644 --- a/alcs-frontend/src/app/services/search/search.service.spec.ts +++ b/alcs-frontend/src/app/services/search/search.service.spec.ts @@ -36,6 +36,27 @@ describe('SearchService', () => { }, ]; + const mockAdvancedSearchEntityResult = { + total: 0, + data: [], + }; + + const mockAdvancedSearchResult = { + applications: [], + noticeOfIntents: [], + totalApplications: 0, + totalNoticeOfIntents: 0, + }; + + const mockSearchRequestDto = { + pageSize: 1, + page: 1, + sortField: '1', + sortDirection: 'ASC', + isIncludeOtherParcels: false, + applicationFileTypes: [], + }; + beforeEach(() => { mockHttpClient = createMock(); mockToastService = createMock(); @@ -82,4 +103,81 @@ describe('SearchService', () => { expect(res).toBeUndefined(); expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); }); + + it('should fetch advanced search results by AdvancedSearchRequestDto', async () => { + mockHttpClient.post.mockReturnValue(of(mockAdvancedSearchResult)); + + const res = await service.advancedSearchFetch(mockSearchRequestDto); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res?.totalApplications).toEqual(0); + expect(res?.applications).toEqual([]); + expect(res?.totalNoticeOfIntents).toEqual(0); + expect(res?.noticeOfIntents).toEqual([]); + }); + + it('should show an error toast message if search fails', async () => { + mockHttpClient.post.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.advancedSearchFetch(mockSearchRequestDto); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeUndefined(); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should fetch application advanced search results by AdvancedSearchRequestDto', async () => { + mockHttpClient.post.mockReturnValue(of(mockAdvancedSearchEntityResult)); + + const res = await service.advancedSearchApplicationsFetch(mockSearchRequestDto); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res?.total).toEqual(0); + expect(res?.data).toEqual([]); + }); + + it('should show an error toast message if application advanced search fails', async () => { + mockHttpClient.post.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.advancedSearchApplicationsFetch(mockSearchRequestDto); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeUndefined(); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should fetch NOI advanced search results by AdvancedSearchRequestDto', async () => { + mockHttpClient.post.mockReturnValue(of(mockAdvancedSearchEntityResult)); + + const res = await service.advancedSearchNoticeOfIntentsFetch(mockSearchRequestDto); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res?.total).toEqual(0); + expect(res?.data).toEqual([]); + }); + + it('should show an error toast message if NOI advanced search fails', async () => { + mockHttpClient.post.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.advancedSearchNoticeOfIntentsFetch(mockSearchRequestDto); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeUndefined(); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); }); diff --git a/alcs-frontend/src/app/services/search/search.service.ts b/alcs-frontend/src/app/services/search/search.service.ts index 615f8610ce..77ed377de3 100644 --- a/alcs-frontend/src/app/services/search/search.service.ts +++ b/alcs-frontend/src/app/services/search/search.service.ts @@ -3,7 +3,14 @@ import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; import { ToastService } from '../toast/toast.service'; -import { SearchResultDto } from './search.dto'; +import { + AdvancedSearchEntityResponseDto, + AdvancedSearchResponseDto, + ApplicationSearchResultDto, + NoticeOfIntentSearchResultDto, + SearchRequestDto, + SearchResultDto, +} from './search.dto'; @Injectable({ providedIn: 'root', @@ -13,6 +20,16 @@ export class SearchService { constructor(private http: HttpClient, private toastService: ToastService) {} + async advancedSearchFetch(searchDto: SearchRequestDto) { + try { + return await firstValueFrom(this.http.post(`${this.baseUrl}/advanced`, searchDto)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast(`Search failed. Please refresh the page and try again`); + return undefined; + } + } + async fetch(searchTerm: string) { try { return await firstValueFrom(this.http.get(`${this.baseUrl}/${searchTerm}`)); @@ -22,4 +39,34 @@ export class SearchService { return undefined; } } + + async advancedSearchApplicationsFetch(searchDto: SearchRequestDto) { + try { + return await firstValueFrom( + this.http.post>( + `${this.baseUrl}/advanced/application`, + searchDto + ) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast(`Search failed. Please refresh the page and try again`); + return undefined; + } + } + + async advancedSearchNoticeOfIntentsFetch(searchDto: SearchRequestDto) { + try { + return await firstValueFrom( + this.http.post>( + `${this.baseUrl}/advanced/notice-of-intent`, + searchDto + ) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast(`Search failed. Please refresh the page and try again`); + return undefined; + } + } } diff --git a/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.ts b/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.ts index bb2b548751..cd1278ee1e 100644 --- a/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.ts +++ b/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.ts @@ -18,7 +18,6 @@ export class SearchBarComponent { async onSearch() { try { const searchResult = await this.searchService.fetch(this.searchText); - if (!searchResult || searchResult.length < 1) { this.toastService.showWarningToast(`File ID ${this.searchText} not found, try again`); return; diff --git a/alcs-frontend/src/app/shared/header/search/search.component.html b/alcs-frontend/src/app/shared/header/search/search.component.html deleted file mode 100644 index 15638122a9..0000000000 --- a/alcs-frontend/src/app/shared/header/search/search.component.html +++ /dev/null @@ -1,44 +0,0 @@ -
-
-

Search Results:

-

File ID: {{ searchText }}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
File ID - {{ element.title }} - Class - {{ element.class }} - Type - - - Local/First Nation Government - {{ element.government }} -
No Search Results
-
diff --git a/alcs-frontend/src/app/shared/header/search/search.component.scss b/alcs-frontend/src/app/shared/header/search/search.component.scss deleted file mode 100644 index 5c9eded809..0000000000 --- a/alcs-frontend/src/app/shared/header/search/search.component.scss +++ /dev/null @@ -1,49 +0,0 @@ -@use '../../../../styles/colors.scss'; - -.layout { - padding: 0 80px; -} - -.search-title { - width: 100%; - margin-top: 42px; - margin-bottom: 24px; - display: flex; - justify-content: left; - align-items: center; - - h4 { - margin-right: 12px !important; - } -} - -.table { - width: 100%; - margin-top: 12px; - - td, - th { - font-size: 16px; - width: 25%; - } - - .type-cell { - width: 90px; - } - - tr.mdc-data-table__row:hover { - box-shadow: 0 0 0 2px rgba(colors.$primary-color, 0.9); - cursor: pointer; - transform: scale(1); - } - - tr.no-data { - border: none; - background-color: colors.$grey-light; - align-items: center; - justify-content: center; - cursor: auto; - box-shadow: none !important; - height: 32px; - } -} diff --git a/alcs-frontend/src/app/shared/header/search/search.component.ts b/alcs-frontend/src/app/shared/header/search/search.component.ts deleted file mode 100644 index 53a7ec8c89..0000000000 --- a/alcs-frontend/src/app/shared/header/search/search.component.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Subject, takeUntil } from 'rxjs'; -import { ApplicationTypeDto } from '../../../services/application/application-code.dto'; -import { SearchResultDto } from '../../../services/search/search.dto'; -import { SearchService } from '../../../services/search/search.service'; -import { ToastService } from '../../../services/toast/toast.service'; -import { COVENANT_TYPE_LABEL, PLANNING_TYPE_LABEL } from '../../application-type-pill/application-type-pill.constants'; - -interface SearchResult { - title: string; - class: string; - type?: any; - government?: string; - referenceId: string; - board?: string; - label?: ApplicationTypeDto; -} - -@Component({ - selector: 'app-search', - templateUrl: './search.component.html', - styleUrls: ['./search.component.scss'], -}) -export class SearchComponent implements OnInit, OnDestroy { - $destroy = new Subject(); - searchText?: string; - - searchResults: SearchResult[] = []; - displayedColumns = ['fileId', 'class', 'type', 'government']; - - constructor( - private searchService: SearchService, - private router: Router, - private activatedRoute: ActivatedRoute, - private toastService: ToastService - ) {} - - ngOnInit(): void { - this.activatedRoute.queryParamMap.pipe(takeUntil(this.$destroy)).subscribe((queryParamMap) => { - const searchText = queryParamMap.get('searchText'); - - if (searchText) { - this.searchText = searchText; - - this.searchService - .fetch(searchText) - .then((result) => (this.searchResults = this.mapSearchResults(result ?? []))); - } - }); - } - - ngOnDestroy(): void { - this.$destroy.next(); - this.$destroy.complete(); - } - - async onSelectCard(record: SearchResult) { - switch (record.type) { - case 'APP': - await this.router.navigateByUrl(`/application/${record.referenceId}`); - break; - case 'NOI': - await this.router.navigateByUrl(`/notice-of-intent/${record.referenceId}`); - break; - case 'COV': - case 'PLAN': - await this.router.navigateByUrl(`/board/${record.board}?card=${record.referenceId}&type=${record.type}`); - break; - default: - this.toastService.showErrorToast(`Unable to navigate to ${record.referenceId}`); - } - } - - private mapSearchResults(data: SearchResultDto[]) { - return data.map((e) => { - const { classType, label } = this.mapClassAndLabels(e); - - return { - title: `${e.fileNumber} ${e.applicant ?? ''}`, - class: classType, - type: e.type, - label: label, - government: e.localGovernmentName, - referenceId: e.referenceId, - board: e.boardCode, - }; - }); - } - - private mapClassAndLabels(data: SearchResultDto) { - switch (data.type) { - case 'APP': - return { classType: 'Application', label: data.label }; - case 'NOI': - return { classType: 'NOI' }; - case 'COV': - return { classType: 'Non-App', label: COVENANT_TYPE_LABEL }; - case 'PLAN': - return { classType: 'Non-App', label: PLANNING_TYPE_LABEL }; - default: - return { classType: 'Unknown' }; - } - } -} diff --git a/alcs-frontend/src/app/shared/pipes/table-column-no-data.pipe.spec.ts b/alcs-frontend/src/app/shared/pipes/table-column-no-data.pipe.spec.ts new file mode 100644 index 0000000000..9af20ee335 --- /dev/null +++ b/alcs-frontend/src/app/shared/pipes/table-column-no-data.pipe.spec.ts @@ -0,0 +1,8 @@ +import { TableColumnNoDataPipe } from './table-column-no-data.pipe'; + +describe('TableColumnNoDataPipe', () => { + it('create an instance', () => { + const pipe = new TableColumnNoDataPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/shared/pipes/table-column-no-data.pipe.ts b/alcs-frontend/src/app/shared/pipes/table-column-no-data.pipe.ts new file mode 100644 index 0000000000..e6d078a8c8 --- /dev/null +++ b/alcs-frontend/src/app/shared/pipes/table-column-no-data.pipe.ts @@ -0,0 +1,10 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'emptyColumn' +}) +export class TableColumnNoDataPipe implements PipeTransform { + transform(value: any): any { + return value !== null && value !== undefined && value !== '' ? value : '-'; + } +} \ No newline at end of file diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index 6eef90b2d6..acf4f6e46a 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -34,6 +34,7 @@ import { NgOptionHighlightModule } from '@ng-select/ng-option-highlight'; import { NgSelectModule } from '@ng-select/ng-select'; import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; import { ApplicationDocumentComponent } from './application-document/application-document.component'; +import { ApplicationLegacyIdComponent } from './application-legacy-id/application-legacy-id.component'; import { ApplicationSubmissionStatusTypePillComponent } from './application-submission-status-type-pill/application-submission-status-type-pill.component'; import { ApplicationTypePillComponent } from './application-type-pill/application-type-pill.component'; import { AvatarCircleComponent } from './avatar-circle/avatar-circle.component'; @@ -58,6 +59,7 @@ import { FileSizePipe } from './pipes/fileSize.pipe'; import { MomentPipe } from './pipes/moment.pipe'; import { SafePipe } from './pipes/safe.pipe'; import { StartOfDayPipe } from './pipes/startOfDay.pipe'; +import { TableColumnNoDataPipe } from './pipes/table-column-no-data.pipe'; import { StaffJournalNoteInputComponent } from './staff-journal/staff-journal-note-input/staff-journal-note-input.component'; import { StaffJournalNoteComponent } from './staff-journal/staff-journal-note/staff-journal-note.component'; import { StaffJournalComponent } from './staff-journal/staff-journal.component'; @@ -66,7 +68,6 @@ import { TimelineComponent } from './timeline/timeline.component'; import { DATE_FORMATS } from './utils/date-format'; import { ExtensionsDatepickerFormatter } from './utils/extensions-datepicker-formatter'; import { WarningBannerComponent } from './warning-banner/warning-banner.component'; -import { ApplicationLegacyIdComponent } from './application-legacy-id/application-legacy-id.component'; @NgModule({ declarations: [ @@ -102,6 +103,7 @@ import { ApplicationLegacyIdComponent } from './application-legacy-id/applicatio LotsTableFormComponent, InlineNgSelectComponent, ApplicationLegacyIdComponent, + TableColumnNoDataPipe, ], imports: [ CommonModule, @@ -193,6 +195,7 @@ import { ApplicationLegacyIdComponent } from './application-legacy-id/applicatio ErrorMessageComponent, LotsTableFormComponent, ApplicationLegacyIdComponent, + TableColumnNoDataPipe, ], }) export class SharedModule { diff --git a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.spec.ts b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.spec.ts new file mode 100644 index 0000000000..5ff132c11d --- /dev/null +++ b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.spec.ts @@ -0,0 +1,116 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { LocalGovernment } from '../../local-government/local-government.entity'; +import { SearchRequestDto } from '../search.dto'; +import { ApplicationAdvancedSearchService } from './application-advanced-search.service'; +import { ApplicationSubmissionSearchView } from './application-search-view.entity'; + +describe('ApplicationAdvancedSearchService', () => { + let service: ApplicationAdvancedSearchService; + let mockApplicationSubmissionSearchViewRepository: DeepMocked< + Repository + >; + let mockLocalGovernmentRepository: DeepMocked>; + + const mockSearchRequestDto: SearchRequestDto = { + fileNumber: '123', + legacyId: '123', + portalStatusCode: 'A', + governmentName: 'B', + regionCode: 'C', + name: 'D', + pid: 'E', + civicAddress: 'F', + isIncludeOtherParcels: true, + dateSubmittedFrom: new Date('2020-10-10').getTime(), + dateSubmittedTo: new Date('2021-10-10').getTime(), + dateDecidedFrom: new Date('2020-11-10').getTime(), + dateDecidedTo: new Date('2021-11-10').getTime(), + resolutionNumber: 123, + resolutionYear: 2021, + applicationFileTypes: ['type1', 'type2'], + page: 1, + pageSize: 10, + sortField: 'ownerName', + sortDirection: 'ASC', + }; + + let mockQuery: any = {}; + + beforeEach(async () => { + mockApplicationSubmissionSearchViewRepository = createMock(); + mockLocalGovernmentRepository = createMock(); + + mockQuery = { + getManyAndCount: jest.fn().mockResolvedValue([[], 0]), + orderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + innerJoinAndMapOne: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + setParameters: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ApplicationAdvancedSearchService, + { + provide: getRepositoryToken(ApplicationSubmissionSearchView), + useValue: mockApplicationSubmissionSearchViewRepository, + }, + { + provide: getRepositoryToken(LocalGovernment), + useValue: mockLocalGovernmentRepository, + }, + ], + }).compile(); + + service = module.get( + ApplicationAdvancedSearchService, + ); + + mockLocalGovernmentRepository.findOneByOrFail.mockResolvedValue( + new LocalGovernment(), + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should successfully build a query using all search parameters defined', async () => { + mockApplicationSubmissionSearchViewRepository.createQueryBuilder.mockReturnValue( + mockQuery as any, + ); + + const result = await service.searchApplications(mockSearchRequestDto); + + expect(result).toEqual({ data: [], total: 0 }); + expect( + mockApplicationSubmissionSearchViewRepository.createQueryBuilder, + ).toBeCalledTimes(1); + expect(mockQuery.andWhere).toBeCalledTimes(15); + expect(mockQuery.where).toBeCalledTimes(1); + }); + + it('should call compileApplicationSearchQuery method correctly', async () => { + const compileApplicationSearchQuerySpy = jest + .spyOn(service as any, 'compileApplicationSearchQuery') + .mockResolvedValue(mockQuery); + + const result = await service.searchApplications(mockSearchRequestDto); + + expect(result).toEqual({ data: [], total: 0 }); + expect(compileApplicationSearchQuerySpy).toBeCalledWith( + mockSearchRequestDto, + ); + expect(mockQuery.orderBy).toHaveBeenCalledTimes(1); + expect(mockQuery.offset).toHaveBeenCalledTimes(1); + expect(mockQuery.limit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts new file mode 100644 index 0000000000..82d2199802 --- /dev/null +++ b/services/apps/alcs/src/alcs/search/application/application-advanced-search.service.ts @@ -0,0 +1,366 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Brackets, Repository } from 'typeorm'; +import { ApplicationOwner } from '../../../portal/application-submission/application-owner/application-owner.entity'; +import { ApplicationParcel } from '../../../portal/application-submission/application-parcel/application-parcel.entity'; +import { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { formatStringToPostgresSearchStringArrayWithWildCard } from '../../../utils/search-helper'; +import { ApplicationDecisionComponent } from '../../application-decision/application-decision-v2/application-decision/component/application-decision-component.entity'; +import { ApplicationDecision } from '../../application-decision/application-decision.entity'; +import { LocalGovernment } from '../../local-government/local-government.entity'; +import { AdvancedSearchResultDto, SearchRequestDto } from '../search.dto'; +import { ApplicationSubmissionSearchView } from './application-search-view.entity'; + +@Injectable() +export class ApplicationAdvancedSearchService { + constructor( + @InjectRepository(ApplicationSubmissionSearchView) + private applicationSearchRepository: Repository, + @InjectRepository(LocalGovernment) + private governmentRepository: Repository, + ) {} + + async searchApplications( + searchDto: SearchRequestDto, + ): Promise> { + let query = await this.compileApplicationSearchQuery(searchDto); + + query = this.compileApplicationGroupBySearchQuery(query); + + const sortQuery = this.compileSortQuery(searchDto); + + query = query + .orderBy(sortQuery, searchDto.sortDirection) + .offset((searchDto.page - 1) * searchDto.pageSize) + .limit(searchDto.pageSize); + + const result = await query.getManyAndCount(); + + return { + data: result[0], + total: result[1], + }; + } + + private compileSortQuery(searchDto: SearchRequestDto) { + switch (searchDto.sortField) { + case 'fileId': + return '"appSearch"."file_number"'; + + case 'ownerName': + return '"appSearch"."applicant"'; + + case 'type': + return '"appSearch"."application_type_code"'; + + case 'government': + return '"appSearch"."local_government_name"'; + + case 'portalStatus': + return `"appSearch"."status" ->> 'label' `; + + default: + case 'dateSubmitted': + return '"appSearch"."date_submitted_to_alc"'; + } + } + + private compileApplicationGroupBySearchQuery(query) { + query = query + .innerJoinAndMapOne( + 'appSearch.applicationType', + 'appSearch.applicationType', + 'applicationType', + ) + .groupBy( + ` + "appSearch"."uuid" + , "appSearch"."application_uuid" + , "appSearch"."application_region_code" + , "appSearch"."file_number" + , "appSearch"."applicant" + , "appSearch"."local_government_uuid" + , "appSearch"."local_government_name" + , "appSearch"."application_type_code" + , "appSearch"."legacy_id" + , "appSearch"."status" + , "appSearch"."date_submitted_to_alc" + , "appSearch"."decision_date" + , "applicationType"."audit_deleted_date_at" + , "applicationType"."audit_created_at" + , "applicationType"."audit_updated_by" + , "applicationType"."audit_updated_at" + , "applicationType"."audit_created_by" + , "applicationType"."short_label" + , "applicationType"."label" + , "applicationType"."code" + , "applicationType"."background_color" + , "applicationType"."text_color" + , "applicationType"."html_description" + , "applicationType"."portal_label" + , "appSearch"."is_draft" + `, + ); + return query; + } + + private async compileApplicationSearchQuery(searchDto: SearchRequestDto) { + let query = this.applicationSearchRepository + .createQueryBuilder('appSearch') + .where('appSearch.is_draft = false'); + + if (searchDto.fileNumber) { + query = query + .andWhere('appSearch.file_number = :fileNumber') + .setParameters({ fileNumber: searchDto.fileNumber ?? null }); + } + + if (searchDto.legacyId) { + query = query.andWhere('appSearch.legacy_id = :legacyId', { + legacyId: searchDto.legacyId, + }); + } + + if (searchDto.portalStatusCode) { + query = query.andWhere( + "alcs.get_current_status_for_application_submission_by_uuid(appSearch.uuid) ->> 'status_type_code' = :status", + { + status: searchDto.portalStatusCode, + }, + ); + } + + if (searchDto.governmentName) { + const government = await this.governmentRepository.findOneByOrFail({ + name: searchDto.governmentName, + }); + + query = query.andWhere( + 'appSearch.local_government_uuid = :local_government_uuid', + { + local_government_uuid: government.uuid, + }, + ); + } + + if (searchDto.regionCode) { + query = query.andWhere( + 'appSearch.application_region_code = :application_region_code', + { + application_region_code: searchDto.regionCode, + }, + ); + } + + query = this.compileApplicationSearchByNameQuery(searchDto, query); + + query = this.compileApplicationParcelSearchQuery(searchDto, query); + + query = this.compileApplicationDecisionSearchQuery(searchDto, query); + + query = this.compileApplicationFileTypeSearchQuery(searchDto, query); + + query = this.compileApplicationDateRangeSearchQuery(searchDto, query); + + return query; + } + + private compileApplicationDateRangeSearchQuery( + searchDto: SearchRequestDto, + query, + ) { + // TODO check dates toIsoString + if (searchDto.dateSubmittedFrom) { + query = query.andWhere( + 'appSearch.date_submitted_to_alc >= :date_submitted_to_alc', + { + date_submitted_to_alc: formatIncomingDate( + searchDto.dateSubmittedFrom, + )!.toISOString(), + }, + ); + } + + if (searchDto.dateSubmittedTo) { + query = query.andWhere( + 'appSearch.date_submitted_to_alc <= :date_submitted_to_alc', + { + date_submitted_to_alc: formatIncomingDate( + searchDto.dateSubmittedTo, + )!.toISOString(), + }, + ); + } + + if (searchDto.dateDecidedFrom) { + query = query.andWhere('appSearch.decision_date >= :decision_date', { + decision_date: formatIncomingDate( + searchDto.dateDecidedFrom, + )!.toISOString(), + }); + } + + if (searchDto.dateDecidedTo) { + query = query.andWhere('appSearch.decision_date <= :decision_date_to', { + decision_date_to: formatIncomingDate( + searchDto.dateDecidedTo, + )!.toISOString(), + }); + } + return query; + } + + private compileApplicationDecisionSearchQuery( + searchDto: SearchRequestDto, + query, + ) { + if ( + searchDto.resolutionNumber !== undefined || + searchDto.resolutionYear !== undefined + ) { + query = this.joinApplicationDecision(query); + + if (searchDto.resolutionNumber !== undefined) { + query = query.andWhere( + 'decision.resolution_number = :resolution_number', + { + resolution_number: searchDto.resolutionNumber, + }, + ); + } + + if (searchDto.resolutionYear !== undefined) { + query = query.andWhere('decision.resolution_year = :resolution_year', { + resolution_year: searchDto.resolutionYear, + }); + } + } + return query; + } + + private joinApplicationDecision(query: any) { + query = query.leftJoin( + ApplicationDecision, + 'decision', + 'decision.application_uuid = "appSearch"."application_uuid"', + ); + return query; + } + + private compileApplicationParcelSearchQuery( + searchDto: SearchRequestDto, + query, + ) { + if ( + (searchDto.pid || searchDto.civicAddress) && + searchDto.isIncludeOtherParcels + ) { + query = query.leftJoin( + ApplicationParcel, + 'parcel', + "parcel.application_submission_uuid = appSearch.uuid AND parcel.parcel_type IN ('application', 'other')", + ); + } else { + query = query.leftJoin( + ApplicationParcel, + 'parcel', + "parcel.application_submission_uuid = appSearch.uuid AND parcel.parcel_type = 'application'", + ); + } + + if (searchDto.pid) { + query = query.andWhere('parcel.pid = :pid', { pid: searchDto.pid }); + } + + if (searchDto.civicAddress) { + query = query.andWhere('parcel.civic_address like :civic_address', { + civic_address: `%${searchDto.civicAddress}%`, + }); + } + return query; + } + + private compileApplicationSearchByNameQuery( + searchDto: SearchRequestDto, + query, + ) { + if (searchDto.name) { + const formattedSearchString = + formatStringToPostgresSearchStringArrayWithWildCard(searchDto.name!); + + query = query + .leftJoin( + ApplicationOwner, + 'application_owner', + 'application_owner.application_submission_uuid = appSearch.uuid', + ) + .andWhere( + new Brackets((qb) => + qb + .where( + "LOWER(application_owner.first_name || ' ' || application_owner.last_name) LIKE ANY (:names)", + { + names: formattedSearchString, + }, + ) + .orWhere( + 'LOWER(application_owner.first_name) LIKE ANY (:names)', + { + names: formattedSearchString, + }, + ) + .orWhere('LOWER(application_owner.last_name) LIKE ANY (:names)', { + names: formattedSearchString, + }) + .orWhere( + 'LOWER(application_owner.organization_name) LIKE ANY (:names)', + { + names: formattedSearchString, + }, + ), + ), + ); + } + return query; + } + + private compileApplicationFileTypeSearchQuery( + searchDto: SearchRequestDto, + query, + ) { + query = query; + + if (searchDto.applicationFileTypes.length > 0) { + // if decision is not joined yet -> join it. The join of decision happens in compileApplicationDecisionSearchQuery + if ( + searchDto.resolutionNumber === undefined && + searchDto.resolutionYear === undefined + ) { + query = this.joinApplicationDecision(query); + } + + query = query.leftJoin( + ApplicationDecisionComponent, + 'decisionComponent', + 'decisionComponent.application_decision_uuid = decision.uuid', + ); + + query = query.andWhere( + new Brackets((qb) => + qb + .where('appSearch.application_type_code IN (:...typeCodes)', { + typeCodes: searchDto.applicationFileTypes, + }) + .orWhere( + 'decisionComponent.application_decision_component_type_code IN (:...typeCodes)', + { + typeCodes: searchDto.applicationFileTypes, + }, + ), + ), + ); + } + + return query; + } +} diff --git a/services/apps/alcs/src/alcs/search/application/application-search-view.entity.ts b/services/apps/alcs/src/alcs/search/application/application-search-view.entity.ts new file mode 100644 index 0000000000..3da99ed8cb --- /dev/null +++ b/services/apps/alcs/src/alcs/search/application/application-search-view.entity.ts @@ -0,0 +1,104 @@ +import { + DataSource, + JoinColumn, + ManyToOne, + PrimaryColumn, + ViewColumn, + ViewEntity, +} from 'typeorm'; +import { ApplicationSubmission } from '../../../portal/application-submission/application-submission.entity'; +import { Application } from '../../application/application.entity'; +import { ApplicationType } from '../../code/application-code/application-type/application-type.entity'; +import { LocalGovernment } from '../../local-government/local-government.entity'; + +// typeorm does not transform property names for the status +export class SearchApplicationSubmissionStatusType { + submission_uuid: string; + + status_type_code: string; + + effective_date: Date; + + label: string; +} + +@ViewEntity({ + expression: (datasource: DataSource) => + datasource + .createQueryBuilder() + .select('as2.uuid', 'uuid') + .addSelect('as2.file_number', 'file_number') + .addSelect('as2.applicant', 'applicant') + .addSelect('as2.local_government_uuid', 'local_government_uuid') + .addSelect('localGovernment.name', 'local_government_name') + .addSelect('as2.type_code', 'application_type_code') + .addSelect('as2.is_draft', 'is_draft') + .addSelect('a.legacy_id', 'legacy_id') + .addSelect('a.date_submitted_to_alc', 'date_submitted_to_alc') + .addSelect('a.decision_date', 'decision_date') + .addSelect('a.uuid', 'application_uuid') + .addSelect('a.region_code', 'application_region_code') + .addSelect( + 'alcs.get_current_status_for_application_submission_by_uuid(as2.uuid)', + 'status', + ) + .from(ApplicationSubmission, 'as2') + .innerJoin(Application, 'a', 'a.file_number = as2.file_number') + .innerJoinAndSelect( + ApplicationType, + 'applicationType', + 'as2.type_code = applicationType.code', + ) + .leftJoin( + LocalGovernment, + 'localGovernment', + 'as2.local_government_uuid = localGovernment.uuid', + ), +}) +export class ApplicationSubmissionSearchView { + @ViewColumn() + @PrimaryColumn() + uuid: string; + + @ViewColumn() + applicationUuid: string; + + @ViewColumn() + isDraft: boolean; + + @ViewColumn() + applicationRegionCode?: string; + + @ViewColumn() + fileNumber: string; + + @ViewColumn() + applicant?: string; + + @ViewColumn() + localGovernmentUuid?: string; + + @ViewColumn() + localGovernmentName?: string; + + @ViewColumn() + applicationTypeCode: string; + + @ViewColumn() + legacyId?: string; + + @ViewColumn() + status: SearchApplicationSubmissionStatusType; + + @ViewColumn() + dateSubmittedToAlc: Date | null; + + @ViewColumn() + decisionDate: Date | null; + + @ManyToOne(() => ApplicationType, { + nullable: false, + }) + @JoinColumn({ name: 'application_type_code' }) + applicationType: ApplicationType; +} diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts new file mode 100644 index 0000000000..ce29c4b358 --- /dev/null +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts @@ -0,0 +1,113 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { LocalGovernment } from '../../local-government/local-government.entity'; +import { SearchRequestDto } from '../search.dto'; +import { NoticeOfIntentAdvancedSearchService } from './notice-of-intent-advanced-search.service'; +import { NoticeOfIntentSubmissionSearchView } from './notice-of-intent-search-view.entity'; + +describe('NoticeOfIntentService', () => { + let service: NoticeOfIntentAdvancedSearchService; + let mockNoticeOfIntentSubmissionSearchViewRepository: DeepMocked< + Repository + >; + let mockLocalGovernmentRepository: DeepMocked>; + + const mockSearchDto: SearchRequestDto = { + fileNumber: '123', + portalStatusCode: 'A', + governmentName: 'B', + regionCode: 'C', + name: 'D', + pid: 'E', + civicAddress: 'F', + isIncludeOtherParcels: false, + dateSubmittedFrom: new Date('2020-10-10').getTime(), + dateSubmittedTo: new Date('2021-10-10').getTime(), + dateDecidedFrom: new Date('2020-11-10').getTime(), + dateDecidedTo: new Date('2021-11-10').getTime(), + resolutionNumber: 123, + resolutionYear: 2021, + applicationFileTypes: ['type1', 'type2'], + page: 1, + pageSize: 10, + sortField: 'ownerName', + sortDirection: 'ASC', + }; + + let mockQuery: any = {}; + + beforeEach(async () => { + mockNoticeOfIntentSubmissionSearchViewRepository = createMock(); + mockLocalGovernmentRepository = createMock(); + + mockQuery = { + getManyAndCount: jest.fn().mockResolvedValue([[], 0]), + orderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + innerJoinAndMapOne: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + setParameters: jest.fn().mockReturnThis(), + leftJoin: jest.fn().mockReturnThis(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NoticeOfIntentAdvancedSearchService, + { + provide: getRepositoryToken(NoticeOfIntentSubmissionSearchView), + useValue: mockNoticeOfIntentSubmissionSearchViewRepository, + }, + { + provide: getRepositoryToken(LocalGovernment), + useValue: mockLocalGovernmentRepository, + }, + ], + }).compile(); + + service = module.get( + NoticeOfIntentAdvancedSearchService, + ); + + mockLocalGovernmentRepository.findOneByOrFail.mockResolvedValue( + new LocalGovernment(), + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should successfully build a query using all search parameters defined', async () => { + mockNoticeOfIntentSubmissionSearchViewRepository.createQueryBuilder.mockReturnValue( + mockQuery as any, + ); + + const result = await service.searchNoticeOfIntents(mockSearchDto); + + expect(result).toEqual({ data: [], total: 0 }); + expect( + mockNoticeOfIntentSubmissionSearchViewRepository.createQueryBuilder, + ).toBeCalledTimes(1); + expect(mockQuery.andWhere).toBeCalledTimes(14); + expect(mockQuery.where).toBeCalledTimes(1); + }); + + it('should call compileNoticeOfIntentSearchQuery method correctly', async () => { + const compileApplicationSearchQuerySpy = jest + .spyOn(service as any, 'compileNoticeOfIntentSearchQuery') + .mockResolvedValue(mockQuery); + + const result = await service.searchNoticeOfIntents(mockSearchDto); + + expect(result).toEqual({ data: [], total: 0 }); + expect(compileApplicationSearchQuerySpy).toBeCalledWith(mockSearchDto); + expect(mockQuery.orderBy).toHaveBeenCalledTimes(1); + expect(mockQuery.offset).toHaveBeenCalledTimes(1); + expect(mockQuery.limit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts new file mode 100644 index 0000000000..4ba8d3fbdf --- /dev/null +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts @@ -0,0 +1,336 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Brackets, Repository } from 'typeorm'; +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 { formatIncomingDate } from '../../../utils/incoming-date.formatter'; +import { formatStringToPostgresSearchStringArrayWithWildCard } from '../../../utils/search-helper'; +import { LocalGovernment } from '../../local-government/local-government.entity'; +import { NoticeOfIntentDecisionComponent } from '../../notice-of-intent-decision/notice-of-intent-decision-component/notice-of-intent-decision-component.entity'; +import { NoticeOfIntentDecision } from '../../notice-of-intent-decision/notice-of-intent-decision.entity'; +import { AdvancedSearchResultDto, SearchRequestDto } from '../search.dto'; +import { NoticeOfIntentSubmissionSearchView } from './notice-of-intent-search-view.entity'; + +@Injectable() +export class NoticeOfIntentAdvancedSearchService { + constructor( + @InjectRepository(NoticeOfIntentSubmissionSearchView) + private noiSearchRepository: Repository, + @InjectRepository(LocalGovernment) + private governmentRepository: Repository, + ) {} + + async searchNoticeOfIntents( + searchDto: SearchRequestDto, + ): Promise> { + let query = await this.compileNoticeOfIntentSearchQuery(searchDto); + + query = this.compileGroupBySearchQuery(query); + + const sortQuery = this.compileSortQuery(searchDto); + + query = query + .orderBy(sortQuery, searchDto.sortDirection) + .offset((searchDto.page - 1) * searchDto.pageSize) + .limit(searchDto.pageSize); + + const result = await query.getManyAndCount(); + + return { + data: result[0], + total: result[1], + }; + } + + private compileSortQuery(searchDto: SearchRequestDto) { + switch (searchDto.sortField) { + case 'fileId': + return '"noiSearch"."file_number"'; + + case 'ownerName': + return '"noiSearch"."applicant"'; + + case 'type': + return '"noiSearch"."notice_of_intent_type_code"'; + + case 'government': + return '"noiSearch"."local_government_name"'; + + case 'portalStatus': + return `"noiSearch"."status" ->> 'label' `; + + default: + case 'dateSubmitted': + return '"noiSearch"."date_submitted_to_alc"'; + } + } + + private compileGroupBySearchQuery(query) { + query = query + .innerJoinAndMapOne( + 'noiSearch.noticeOfIntentType', + 'noiSearch.noticeOfIntentType', + 'noticeOfIntentType', + ) + .groupBy( + ` + "noiSearch"."uuid" + , "noiSearch"."notice_of_intent_uuid" + , "noiSearch"."notice_of_intent_region_code" + , "noiSearch"."file_number" + , "noiSearch"."applicant" + , "noiSearch"."local_government_uuid" + , "noiSearch"."local_government_name" + , "noiSearch"."notice_of_intent_type_code" + , "noiSearch"."status" + , "noiSearch"."date_submitted_to_alc" + , "noiSearch"."decision_date" + , "noticeOfIntentType"."audit_deleted_date_at" + , "noticeOfIntentType"."audit_created_at" + , "noticeOfIntentType"."audit_updated_by" + , "noticeOfIntentType"."audit_updated_at" + , "noticeOfIntentType"."audit_created_by" + , "noticeOfIntentType"."short_label" + , "noticeOfIntentType"."label" + , "noticeOfIntentType"."code" + , "noticeOfIntentType"."html_description" + , "noticeOfIntentType"."portal_label" + , "noiSearch"."is_draft" + `, + ); + return query; + } + + private async compileNoticeOfIntentSearchQuery(searchDto: SearchRequestDto) { + let query = this.noiSearchRepository + .createQueryBuilder('noiSearch') + .where('noiSearch.is_draft = false'); + + if (searchDto.fileNumber) { + query = query + .andWhere('noiSearch.file_number = :fileNumber') + .setParameters({ fileNumber: searchDto.fileNumber ?? null }); + } + + if (searchDto.portalStatusCode) { + query = query.andWhere( + "alcs.get_current_status_for_notice_of_intent_submission_by_uuid(noiSearch.uuid) ->> 'status_type_code' = :status", + { + status: searchDto.portalStatusCode, + }, + ); + } + + if (searchDto.governmentName) { + const government = await this.governmentRepository.findOneByOrFail({ + name: searchDto.governmentName, + }); + + query = query.andWhere( + 'noiSearch.local_government_uuid = :local_government_uuid', + { + local_government_uuid: government.uuid, + }, + ); + } + + if (searchDto.regionCode) { + query = query.andWhere( + 'noiSearch.notice_of_intent_region_code = :noi_region_code', + { + noi_region_code: searchDto.regionCode, + }, + ); + } + + query = this.compileSearchByNameQuery(searchDto, query); + + query = this.compileParcelSearchQuery(searchDto, query); + + query = this.compileDecisionSearchQuery(searchDto, query); + + query = this.compileFileTypeSearchQuery(searchDto, query); + + query = this.compileDateRangeSearchQuery(searchDto, query); + + return query; + } + + private compileDateRangeSearchQuery(searchDto: SearchRequestDto, query) { + // TODO check dates toIsoString + if (searchDto.dateSubmittedFrom) { + query = query.andWhere( + 'noiSearch.date_submitted_to_alc >= :date_submitted_to_alc', + { + date_submitted_to_alc: formatIncomingDate( + searchDto.dateSubmittedFrom, + )!.toISOString(), + }, + ); + } + + if (searchDto.dateSubmittedTo) { + query = query.andWhere( + 'noiSearch.date_submitted_to_alc <= :date_submitted_to_alc', + { + date_submitted_to_alc: formatIncomingDate( + searchDto.dateSubmittedTo, + )!.toISOString(), + }, + ); + } + + if (searchDto.dateDecidedFrom) { + query = query.andWhere('noiSearch.decision_date >= :decision_date', { + decision_date: formatIncomingDate( + searchDto.dateDecidedFrom, + )!.toISOString(), + }); + } + + if (searchDto.dateDecidedTo) { + query = query.andWhere('noiSearch.decision_date <= :decision_date_to', { + decision_date_to: formatIncomingDate( + searchDto.dateDecidedTo, + )!.toISOString(), + }); + } + return query; + } + + private compileDecisionSearchQuery(searchDto: SearchRequestDto, query) { + if ( + searchDto.resolutionNumber !== undefined || + searchDto.resolutionYear !== undefined + ) { + query = this.joinDecision(query); + + if (searchDto.resolutionNumber !== undefined) { + query = query.andWhere( + 'decision.resolution_number = :resolution_number', + { + resolution_number: searchDto.resolutionNumber, + }, + ); + } + + if (searchDto.resolutionYear !== undefined) { + query = query.andWhere('decision.resolution_year = :resolution_year', { + resolution_year: searchDto.resolutionYear, + }); + } + } + return query; + } + + private joinDecision(query: any) { + query = query.leftJoin( + NoticeOfIntentDecision, + 'decision', + 'decision.notice_of_intent_uuid = "noiSearch"."notice_of_intent_uuid"', + ); + return query; + } + + private compileParcelSearchQuery(searchDto: SearchRequestDto, query) { + if (searchDto.pid || searchDto.civicAddress) { + query = query.leftJoin( + NoticeOfIntentParcel, + 'parcel', + 'parcel.notice_of_intent_submission_uuid = noiSearch.uuid', + ); + } + + if (searchDto.pid) { + query = query.andWhere('parcel.pid = :pid', { pid: searchDto.pid }); + } + + if (searchDto.civicAddress) { + query = query.andWhere('parcel.civic_address like :civic_address', { + civic_address: `%${searchDto.civicAddress}%`, + }); + } + return query; + } + + private compileSearchByNameQuery(searchDto: SearchRequestDto, query) { + if (searchDto.name) { + const formattedSearchString = + formatStringToPostgresSearchStringArrayWithWildCard(searchDto.name!); + + query = query + .leftJoin( + NoticeOfIntentOwner, + 'notice_of_intent_owner', + 'notice_of_intent_owner.notice_of_intent_submission_uuid = noiSearch.uuid', + ) + .andWhere( + new Brackets((qb) => + qb + .where( + "LOWER(notice_of_intent_owner.first_name || ' ' || notice_of_intent_owner.last_name) LIKE ANY (:names)", + { + names: formattedSearchString, + }, + ) + .orWhere( + 'LOWER(notice_of_intent_owner.first_name) LIKE ANY (:names)', + { + names: formattedSearchString, + }, + ) + .orWhere( + 'LOWER(notice_of_intent_owner.last_name) LIKE ANY (:names)', + { + names: formattedSearchString, + }, + ) + .orWhere( + 'LOWER(notice_of_intent_owner.organization_name) LIKE ANY (:names)', + { + names: formattedSearchString, + }, + ), + ), + ); + } + return query; + } + + private compileFileTypeSearchQuery(searchDto: SearchRequestDto, query) { + query = query; + + if (searchDto.applicationFileTypes.length > 0) { + // if decision is not joined yet -> join it. The join of decision happens in compileApplicationDecisionSearchQuery + if ( + searchDto.resolutionNumber === undefined && + searchDto.resolutionYear === undefined + ) { + query = this.joinDecision(query); + } + + query = query.leftJoin( + NoticeOfIntentDecisionComponent, + 'decisionComponent', + 'decisionComponent.notice_of_intent_decision_uuid = decision.uuid', + ); + + query = query.andWhere( + new Brackets((qb) => + qb + .where('noiSearch.notice_of_intent_type_code IN (:...typeCodes)', { + typeCodes: searchDto.applicationFileTypes, + }) + .orWhere( + 'decisionComponent.notice_of_intent_decision_component_type_code IN (:...typeCodes)', + { + typeCodes: searchDto.applicationFileTypes, + }, + ), + ), + ); + } + + return query; + } +} diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-search-view.entity.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-search-view.entity.ts new file mode 100644 index 0000000000..8c1c397a93 --- /dev/null +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-search-view.entity.ts @@ -0,0 +1,100 @@ +import { + DataSource, + JoinColumn, + ManyToOne, + PrimaryColumn, + ViewColumn, + ViewEntity, +} from 'typeorm'; +import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentType } from '../../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { LocalGovernment } from '../../local-government/local-government.entity'; +import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; + +// typeorm does not transform property names for the status +export class SearchNoticeOfIntentSubmissionStatusType { + submission_uuid: string; + + status_type_code: string; + + effective_date: Date; + + label: string; +} + +@ViewEntity({ + expression: (datasource: DataSource) => + datasource + .createQueryBuilder() + .select('nois.uuid', 'uuid') + .addSelect('nois.file_number', 'file_number') + .addSelect('nois.applicant', 'applicant') + .addSelect('nois.local_government_uuid', 'local_government_uuid') + .addSelect('localGovernment.name', 'local_government_name') + .addSelect('nois.type_code', 'notice_of_intent_type_code') + .addSelect('nois.is_draft', 'is_draft') + .addSelect('noi.date_submitted_to_alc', 'date_submitted_to_alc') + .addSelect('noi.decision_date', 'decision_date') + .addSelect('noi.uuid', 'notice_of_intent_uuid') + .addSelect('noi.region_code', 'notice_of_intent_region_code') + .addSelect( + 'alcs.get_current_status_for_notice_of_intent_submission_by_uuid(nois.uuid)', + 'status', + ) + .from(NoticeOfIntentSubmission, 'nois') + .innerJoin(NoticeOfIntent, 'noi', 'noi.file_number = nois.file_number') + .innerJoinAndSelect( + NoticeOfIntentType, + 'noticeOfIntentType', + 'nois.type_code = noticeOfIntentType.code', + ) + .leftJoin( + LocalGovernment, + 'localGovernment', + 'nois.local_government_uuid = localGovernment.uuid', + ), +}) +export class NoticeOfIntentSubmissionSearchView { + @ViewColumn() + @PrimaryColumn() + uuid: string; + + @ViewColumn() + noticeOfIntentUuid: string; + + @ViewColumn() + isDraft: boolean; + + @ViewColumn() + noticeOfIntentRegionCode?: string; + + @ViewColumn() + fileNumber: string; + + @ViewColumn() + applicant?: string; + + @ViewColumn() + localGovernmentUuid?: string; + + @ViewColumn() + localGovernmentName?: string; + + @ViewColumn() + noticeOfIntentTypeCode: string; + + @ViewColumn() + status: SearchNoticeOfIntentSubmissionStatusType; + + @ViewColumn() + dateSubmittedToAlc: Date | null; + + @ViewColumn() + decisionDate: Date | null; + + @ManyToOne(() => NoticeOfIntentType, { + nullable: false, + }) + @JoinColumn({ name: 'notice_of_intent_type_code' }) + noticeOfIntentType: NoticeOfIntentType; +} diff --git a/services/apps/alcs/src/alcs/search/search.controller.spec.ts b/services/apps/alcs/src/alcs/search/search.controller.spec.ts index 278ef3034e..41d2380130 100644 --- a/services/apps/alcs/src/alcs/search/search.controller.spec.ts +++ b/services/apps/alcs/src/alcs/search/search.controller.spec.ts @@ -10,15 +10,24 @@ import { Card } from '../card/card.entity'; import { Covenant } from '../covenant/covenant.entity'; import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; import { PlanningReview } from '../planning-review/planning-review.entity'; +import { ApplicationAdvancedSearchService } from './application/application-advanced-search.service'; +import { ApplicationSubmissionSearchView } from './application/application-search-view.entity'; +import { NoticeOfIntentAdvancedSearchService } from './notice-of-intent/notice-of-intent-advanced-search.service'; +import { NoticeOfIntentSubmissionSearchView } from './notice-of-intent/notice-of-intent-search-view.entity'; import { SearchController } from './search.controller'; +import { AdvancedSearchResultDto, SearchRequestDto } from './search.dto'; import { SearchService } from './search.service'; describe('SearchController', () => { let controller: SearchController; let mockSearchService: DeepMocked; + let mockNoticeOfIntentAdvancedSearchService: DeepMocked; + let mockApplicationAdvancedSearchService: DeepMocked; beforeEach(async () => { mockSearchService = createMock(); + mockNoticeOfIntentAdvancedSearchService = createMock(); + mockApplicationAdvancedSearchService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -31,6 +40,14 @@ describe('SearchController', () => { provide: SearchService, useValue: mockSearchService, }, + { + provide: NoticeOfIntentAdvancedSearchService, + useValue: mockNoticeOfIntentAdvancedSearchService, + }, + { + provide: ApplicationAdvancedSearchService, + useValue: mockApplicationAdvancedSearchService, + }, { provide: ClsService, useValue: {}, @@ -62,13 +79,33 @@ describe('SearchController', () => { } as Card, }), ); + + const mockNoiResult = new AdvancedSearchResultDto< + NoticeOfIntentSubmissionSearchView[] + >(); + mockNoiResult.data = new Array(); + mockNoiResult.total = 0; + + mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents.mockResolvedValue( + mockNoiResult, + ); + + const mockApplicationResult = new AdvancedSearchResultDto< + ApplicationSubmissionSearchView[] + >(); + mockApplicationResult.data = new Array(); + mockApplicationResult.total = 0; + + mockApplicationAdvancedSearchService.searchApplications.mockResolvedValue( + mockApplicationResult, + ); }); it('should be defined', () => { expect(controller).toBeDefined(); }); - it('should call service to retrieve Application, Noi, Planning, Covenant', async () => { + it('should call service to retrieve Application, Noi, Planning, Covenant by file number', async () => { const searchString = 'fake'; const result = await controller.search(searchString); @@ -83,4 +120,64 @@ describe('SearchController', () => { expect(result).toBeDefined(); expect(result.length).toBe(4); }); + + it('should call applications advanced search to retrieve Applications', async () => { + const mockSearchRequestDto = { + pageSize: 1, + page: 1, + sortField: '1', + sortDirection: 'ASC', + isIncludeOtherParcels: false, + applicationFileTypes: [], + }; + + mockApplicationAdvancedSearchService.searchApplications.mockResolvedValue({ + data: [], + total: 0, + }); + + const result = await controller.advancedSearch( + mockSearchRequestDto as SearchRequestDto, + ); + + expect( + mockApplicationAdvancedSearchService.searchApplications, + ).toBeCalledTimes(1); + expect( + mockApplicationAdvancedSearchService.searchApplications, + ).toBeCalledWith(mockSearchRequestDto); + expect(result.applications).toBeDefined(); + expect(result.totalApplications).toBe(0); + }); + + it('should call NOI advanced search to retrieve NOIs', async () => { + const mockSearchRequestDto = { + pageSize: 1, + page: 1, + sortField: '1', + sortDirection: 'ASC', + isIncludeOtherParcels: false, + applicationFileTypes: [], + }; + + mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents.mockResolvedValue( + { + data: [], + total: 0, + }, + ); + + const result = await controller.advancedSearch( + mockSearchRequestDto as SearchRequestDto, + ); + + expect( + mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents, + ).toBeCalledTimes(1); + expect( + mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents, + ).toBeCalledWith(mockSearchRequestDto); + expect(result.applications).toBeDefined(); + expect(result.totalApplications).toBe(0); + }); }); diff --git a/services/apps/alcs/src/alcs/search/search.controller.ts b/services/apps/alcs/src/alcs/search/search.controller.ts index 24f7398346..9d6f5e753c 100644 --- a/services/apps/alcs/src/alcs/search/search.controller.ts +++ b/services/apps/alcs/src/alcs/search/search.controller.ts @@ -1,6 +1,6 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; -import { Controller, Get, Param, UseGuards } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common'; import { ApiOAuth2 } from '@nestjs/swagger'; import * as config from 'config'; import { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; @@ -13,7 +13,18 @@ import { ApplicationType } from '../code/application-code/application-type/appli import { Covenant } from '../covenant/covenant.entity'; import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; import { PlanningReview } from '../planning-review/planning-review.entity'; -import { SearchResultDto } from './search.dto'; +import { ApplicationAdvancedSearchService } from './application/application-advanced-search.service'; +import { ApplicationSubmissionSearchView } from './application/application-search-view.entity'; +import { NoticeOfIntentAdvancedSearchService } from './notice-of-intent/notice-of-intent-advanced-search.service'; +import { NoticeOfIntentSubmissionSearchView } from './notice-of-intent/notice-of-intent-search-view.entity'; +import { + AdvancedSearchResponseDto, + AdvancedSearchResultDto, + ApplicationSearchResultDto, + NoticeOfIntentSearchResultDto, + SearchRequestDto, + SearchResultDto, +} from './search.dto'; import { SearchService } from './search.service'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) @@ -23,10 +34,12 @@ export class SearchController { constructor( private searchService: SearchService, @InjectMapper() private mapper: Mapper, + private noticeOfIntentSearchService: NoticeOfIntentAdvancedSearchService, + private applicationSearchService: ApplicationAdvancedSearchService, ) {} - @Get('/:searchTerm') @UserRoles(...ROLES_ALLOWED_APPLICATIONS) + @Get('/:searchTerm') async search(@Param('searchTerm') searchTerm) { const application = await this.searchService.getApplication(searchTerm); @@ -122,4 +135,137 @@ export class SearchController { return result; } + + @Post('/advanced') + @UserRoles(...ROLES_ALLOWED_APPLICATIONS) + async advancedSearch(@Body() searchDto: SearchRequestDto) { + const applicationSearchResult = + await this.applicationSearchService.searchApplications(searchDto); + + const noticeOfIntentSearchService = + await this.noticeOfIntentSearchService.searchNoticeOfIntents(searchDto); + + const mappedSearchResult = this.mapAdvancedSearchResults( + applicationSearchResult, + noticeOfIntentSearchService, + ); + + return mappedSearchResult; + } + + @Post('/advanced/application') + @UserRoles(...ROLES_ALLOWED_APPLICATIONS) + async advancedSearchApplications( + @Body() searchDto: SearchRequestDto, + ): Promise> { + const applicationSearchResult = + await this.applicationSearchService.searchApplications(searchDto); + + const mappedSearchResult = this.mapAdvancedSearchResults( + applicationSearchResult, + null, + ); + + return { + total: mappedSearchResult.totalApplications, + data: mappedSearchResult.applications, + }; + } + + @Post('/advanced/notice-of-intent') + @UserRoles(...ROLES_ALLOWED_APPLICATIONS) + async advancedSearchNoticeOfIntents( + @Body() searchDto: SearchRequestDto, + ): Promise> { + const noticeOfIntentSearchService = + await this.noticeOfIntentSearchService.searchNoticeOfIntents(searchDto); + + const mappedSearchResult = this.mapAdvancedSearchResults( + null, + noticeOfIntentSearchService, + ); + + return { + total: mappedSearchResult.totalNoticeOfIntents, + data: mappedSearchResult.noticeOfIntents, + }; + } + + private mapAdvancedSearchResults( + applications: AdvancedSearchResultDto< + ApplicationSubmissionSearchView[] + > | null, + noticeOfIntents: AdvancedSearchResultDto< + NoticeOfIntentSubmissionSearchView[] + > | null, + ) { + const response = new AdvancedSearchResponseDto(); + + const mappedApplications: ApplicationSearchResultDto[] = []; + if (applications && applications.data.length > 0) { + mappedApplications.push( + ...applications.data.map((app) => + this.mapApplicationToAdvancedSearchResult(app), + ), + ); + } + + const mappedNoticeOfIntents: NoticeOfIntentSearchResultDto[] = []; + if (noticeOfIntents && noticeOfIntents.data.length > 0) { + mappedNoticeOfIntents.push( + ...noticeOfIntents.data.map((noi) => + this.mapNoticeOfIntentToAdvancedSearchResult(noi), + ), + ); + } + + response.applications = mappedApplications; + response.totalApplications = applications?.total ?? 0; + response.noticeOfIntents = mappedNoticeOfIntents; + response.totalNoticeOfIntents = noticeOfIntents?.total ?? 0; + + return response; + } + + private mapApplicationToAdvancedSearchResult( + application: ApplicationSubmissionSearchView, + ) { + const result = { + referenceId: application.fileNumber, + fileNumber: application.fileNumber, + dateSubmitted: application.dateSubmittedToAlc, + type: this.mapper.map( + application.applicationType, + ApplicationType, + ApplicationTypeDto, + ), + localGovernmentName: application.localGovernmentName, + ownerName: application.applicant, + class: 'APP', + status: application.status.status_type_code, + } as ApplicationSearchResultDto; + + return result; + } + + private mapNoticeOfIntentToAdvancedSearchResult( + noi: NoticeOfIntentSubmissionSearchView, + ) { + const result = { + referenceId: noi.fileNumber, + fileNumber: noi.fileNumber, + dateSubmitted: noi.dateSubmittedToAlc, + type: this.mapper.map( + noi.noticeOfIntentType, + ApplicationType, + ApplicationTypeDto, + ), + localGovernmentName: noi.localGovernmentName, + ownerName: noi.applicant, + class: 'NOI', + status: noi.status.status_type_code, + } as NoticeOfIntentSearchResultDto; + + return result; + } } diff --git a/services/apps/alcs/src/alcs/search/search.dto.ts b/services/apps/alcs/src/alcs/search/search.dto.ts index 36d0c58de1..0a3ff0db2d 100644 --- a/services/apps/alcs/src/alcs/search/search.dto.ts +++ b/services/apps/alcs/src/alcs/search/search.dto.ts @@ -1,3 +1,11 @@ +import { + IsArray, + IsBoolean, + IsNumber, + IsOptional, + IsString, + MinLength, +} from 'class-validator'; import { ApplicationTypeDto } from '../code/application-code/application-type/application-type.dto'; export class SearchResultDto { @@ -9,3 +17,117 @@ export class SearchResultDto { boardCode?: string; label?: ApplicationTypeDto; } + +export class ApplicationSearchResultDto { + type: ApplicationTypeDto; + referenceId: string; + ownerName?: string; + localGovernmentName: string; + fileNumber: string; + boardCode?: string; + status: string; +} + +export class NoticeOfIntentSearchResultDto { + type: ApplicationTypeDto; + referenceId: string; + ownerName?: string; + localGovernmentName: string; + fileNumber: string; + boardCode?: string; + status: string; +} + +export class AdvancedSearchResponseDto { + applications: ApplicationSearchResultDto[]; + noticeOfIntents: NoticeOfIntentSearchResultDto[]; + totalApplications: number; + totalNoticeOfIntents: number; +} + +export class AdvancedSearchResultDto { + data: T; + total: number; +} + +export class SearchRequestDto { + @IsString() + @IsOptional() + @MinLength(3) + fileNumber?: string; + + @IsString() + @IsOptional() + @MinLength(3) + legacyId?: string; + + @IsString() + @IsOptional() + @MinLength(3) + name?: string; + + @IsString() + @IsOptional() + @MinLength(9) + pid?: string; + + @IsString() + @IsOptional() + @MinLength(3) + civicAddress?: string; + + @IsBoolean() + @IsOptional() + isIncludeOtherParcels = false; + + @IsNumber() + @IsOptional() + resolutionNumber?: number; + + @IsNumber() + @IsOptional() + resolutionYear?: number; + + @IsString() + @IsOptional() + portalStatusCode?: string; + + @IsString() + @IsOptional() + governmentName?: string; + + @IsString() + @IsOptional() + regionCode?: string; + + @IsNumber() + @IsOptional() + dateSubmittedFrom?: number; + + @IsNumber() + @IsOptional() + dateSubmittedTo?: number; + + @IsNumber() + @IsOptional() + dateDecidedFrom?: number; + + @IsNumber() + @IsOptional() + dateDecidedTo?: number; + + @IsArray() + applicationFileTypes: string[]; + + @IsNumber() + page: number; + + @IsNumber() + pageSize: number; + + @IsString() + sortField: string; + + @IsString() + sortDirection: 'ASC' | 'DESC'; +} diff --git a/services/apps/alcs/src/alcs/search/search.module.ts b/services/apps/alcs/src/alcs/search/search.module.ts index 38c9051ac4..b3fee1cb95 100644 --- a/services/apps/alcs/src/alcs/search/search.module.ts +++ b/services/apps/alcs/src/alcs/search/search.module.ts @@ -1,11 +1,15 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationProfile } from '../../common/automapper/application.automapper.profile'; -import { LocalGovernment } from '../local-government/local-government.entity'; import { Application } from '../application/application.entity'; import { Covenant } from '../covenant/covenant.entity'; +import { LocalGovernment } from '../local-government/local-government.entity'; import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; import { PlanningReview } from '../planning-review/planning-review.entity'; +import { ApplicationAdvancedSearchService } from './application/application-advanced-search.service'; +import { ApplicationSubmissionSearchView } from './application/application-search-view.entity'; +import { NoticeOfIntentAdvancedSearchService } from './notice-of-intent/notice-of-intent-advanced-search.service'; +import { NoticeOfIntentSubmissionSearchView } from './notice-of-intent/notice-of-intent-search-view.entity'; import { SearchController } from './search.controller'; import { SearchService } from './search.service'; @@ -17,9 +21,16 @@ import { SearchService } from './search.service'; PlanningReview, Covenant, LocalGovernment, + ApplicationSubmissionSearchView, + NoticeOfIntentSubmissionSearchView, ]), ], - providers: [SearchService, ApplicationProfile], + providers: [ + SearchService, + ApplicationProfile, + ApplicationAdvancedSearchService, + NoticeOfIntentAdvancedSearchService, + ], controllers: [SearchController], }) export class SearchModule {} diff --git a/services/apps/alcs/src/alcs/search/search.service.spec.ts b/services/apps/alcs/src/alcs/search/search.service.spec.ts index 497ae813d8..a75070366e 100644 --- a/services/apps/alcs/src/alcs/search/search.service.spec.ts +++ b/services/apps/alcs/src/alcs/search/search.service.spec.ts @@ -4,8 +4,10 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Application } from '../application/application.entity'; import { Covenant } from '../covenant/covenant.entity'; +import { LocalGovernment } from '../local-government/local-government.entity'; import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; import { PlanningReview } from '../planning-review/planning-review.entity'; +import { ApplicationSubmissionSearchView } from './application/application-search-view.entity'; import { SearchService } from './search.service'; describe('SearchService', () => { @@ -14,6 +16,10 @@ describe('SearchService', () => { let mockNoiRepository: DeepMocked>; let mockPlanningReviewRepository: DeepMocked>; let mockCovenantRepository: DeepMocked>; + let mockApplicationSubmissionSearchView: DeepMocked< + Repository + >; + let mockLocalGovernment: DeepMocked>; const fakeFileNumber = 'fake'; @@ -22,6 +28,8 @@ describe('SearchService', () => { mockNoiRepository = createMock(); mockPlanningReviewRepository = createMock(); mockCovenantRepository = createMock(); + mockApplicationSubmissionSearchView = createMock(); + mockLocalGovernment = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -42,6 +50,14 @@ describe('SearchService', () => { provide: getRepositoryToken(Covenant), useValue: mockCovenantRepository, }, + { + provide: getRepositoryToken(ApplicationSubmissionSearchView), + useValue: mockApplicationSubmissionSearchView, + }, + { + provide: getRepositoryToken(LocalGovernment), + useValue: mockLocalGovernment, + }, ], }).compile(); diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1692985798191-get_current_status_function.ts b/services/apps/alcs/src/providers/typeorm/migrations/1692985798191-get_current_status_function.ts new file mode 100644 index 0000000000..542e0bf519 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1692985798191-get_current_status_function.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class getCurrentStatusFunction1692985798191 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE OR REPLACE FUNCTION alcs.get_current_status_for_submission_by_uuid(application_submission_uuid uuid) + RETURNS jsonb + LANGUAGE plpgsql + AS $function$ + DECLARE + utc_timestamp_tomorrow timestamptz; + result RECORD; + BEGIN + -- TODO adjust the date according to api + utc_timestamp_tomorrow = timezone('utc', (now() - INTERVAL '-1 DAY')); + + SELECT + astss.submission_uuid , astss.status_type_code ,astss.effective_date, asst."label" + FROM alcs.application_submission_status_type asst + JOIN alcs.application_submission_to_submission_status astss + ON asst.code = astss.status_type_code + AND astss.submission_uuid = application_submission_uuid + AND astss.effective_date IS NOT NULL + WHERE + astss.effective_date < utc_timestamp_tomorrow + ORDER BY astss.effective_date desc, weight desc + LIMIT 1 INTO result; + + RETURN row_to_json(RESULT); + + END;$function$ + ;`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // nope + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693352687970-app_search_view.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693352687970-app_search_view.ts new file mode 100644 index 0000000000..668d9746cf --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693352687970-app_search_view.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class appSearchView1693352687970 implements MigrationInterface { + name = 'appSearchView1693352687970'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE VIEW "alcs"."application_submission_search_view" AS SELECT "as2"."uuid" AS "uuid", "as2"."applicant" AS "applicant", "a"."uuid" AS "application_uuid", "applicationType"."audit_deleted_date_at" AS "applicationType_audit_deleted_date_at", "applicationType"."audit_created_at" AS "applicationType_audit_created_at", "applicationType"."audit_updated_at" AS "applicationType_audit_updated_at", "applicationType"."audit_created_by" AS "applicationType_audit_created_by", "applicationType"."audit_updated_by" AS "applicationType_audit_updated_by", "applicationType"."label" AS "applicationType_label", "applicationType"."code" AS "applicationType_code", "applicationType"."description" AS "applicationType_description", "applicationType"."short_label" AS "applicationType_short_label", "applicationType"."background_color" AS "applicationType_background_color", "applicationType"."text_color" AS "applicationType_text_color", "applicationType"."html_description" AS "applicationType_html_description", "applicationType"."portal_label" AS "applicationType_portal_label", "localGovernment"."name" AS "local_government_name", "as2"."file_number" AS "file_number", "as2"."local_government_uuid" AS "local_government_uuid", "as2"."type_code" AS "application_type_code", "as2"."is_draft" AS "is_draft", "a"."legacy_id" AS "legacy_id", "a"."date_submitted_to_alc" AS "date_submitted_to_alc", "a"."decision_date" AS "decision_date", "a"."region_code" AS "application_region_code", alcs.get_current_status_for_submission_by_uuid("as2"."uuid") AS "status" FROM "alcs"."application_submission" "as2" INNER JOIN "alcs"."application" "a" ON "a"."file_number" = "as2"."file_number" AND "a"."audit_deleted_date_at" IS NULL INNER JOIN "alcs"."application_type" "applicationType" ON "as2"."type_code" = "applicationType"."code" AND "applicationType"."audit_deleted_date_at" IS NULL LEFT JOIN "alcs"."local_government" "localGovernment" ON "as2"."local_government_uuid" = "localGovernment"."uuid" AND "localGovernment"."audit_deleted_date_at" IS NULL WHERE "as2"."audit_deleted_date_at" IS NULL`, + ); + await queryRunner.query( + `INSERT INTO "alcs"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'alcs', + 'VIEW', + 'application_submission_search_view', + 'SELECT "as2"."uuid" AS "uuid", "as2"."applicant" AS "applicant", "a"."uuid" AS "application_uuid", "applicationType"."audit_deleted_date_at" AS "applicationType_audit_deleted_date_at", "applicationType"."audit_created_at" AS "applicationType_audit_created_at", "applicationType"."audit_updated_at" AS "applicationType_audit_updated_at", "applicationType"."audit_created_by" AS "applicationType_audit_created_by", "applicationType"."audit_updated_by" AS "applicationType_audit_updated_by", "applicationType"."label" AS "applicationType_label", "applicationType"."code" AS "applicationType_code", "applicationType"."description" AS "applicationType_description", "applicationType"."short_label" AS "applicationType_short_label", "applicationType"."background_color" AS "applicationType_background_color", "applicationType"."text_color" AS "applicationType_text_color", "applicationType"."html_description" AS "applicationType_html_description", "applicationType"."portal_label" AS "applicationType_portal_label", "localGovernment"."name" AS "local_government_name", "as2"."file_number" AS "file_number", "as2"."local_government_uuid" AS "local_government_uuid", "as2"."type_code" AS "application_type_code", "as2"."is_draft" AS "is_draft", "a"."legacy_id" AS "legacy_id", "a"."date_submitted_to_alc" AS "date_submitted_to_alc", "a"."decision_date" AS "decision_date", "a"."region_code" AS "application_region_code", alcs.get_current_status_for_submission_by_uuid("as2"."uuid") AS "status" FROM "alcs"."application_submission" "as2" INNER JOIN "alcs"."application" "a" ON "a"."file_number" = "as2"."file_number" AND "a"."audit_deleted_date_at" IS NULL INNER JOIN "alcs"."application_type" "applicationType" ON "as2"."type_code" = "applicationType"."code" AND "applicationType"."audit_deleted_date_at" IS NULL LEFT JOIN "alcs"."local_government" "localGovernment" ON "as2"."local_government_uuid" = "localGovernment"."uuid" AND "localGovernment"."audit_deleted_date_at" IS NULL WHERE "as2"."audit_deleted_date_at" IS NULL', + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "alcs"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'application_submission_search_view', 'alcs'], + ); + await queryRunner.query( + `DROP VIEW "alcs"."application_submission_search_view"`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693410289968-rename_get_current_status_function.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693410289968-rename_get_current_status_function.ts new file mode 100644 index 0000000000..10c1ff185a --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693410289968-rename_get_current_status_function.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class renameGetCurrentStatusFunction1693410289968 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER FUNCTION alcs.get_current_status_for_submission_by_uuid(uuid) RENAME TO get_current_status_for_application_submission_by_uuid; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // nope + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693410408169-get_current_status_for_noi_function.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693410408169-get_current_status_for_noi_function.ts new file mode 100644 index 0000000000..6110dfb108 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693410408169-get_current_status_for_noi_function.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class getCurrentStatusForNoiFunction1693410408169 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE OR REPLACE FUNCTION alcs.get_current_status_for_notice_of_intent_submission_by_uuid(application_submission_uuid uuid) + RETURNS jsonb + LANGUAGE plpgsql + AS $function$ + DECLARE + utc_timestamp_tomorrow timestamptz; + result RECORD; + BEGIN + -- TODO adjust the date according to api + utc_timestamp_tomorrow = timezone('utc', (now() - INTERVAL '-1 DAY')); + + SELECT + noistss.submission_uuid , noistss.status_type_code ,noistss.effective_date, noisst."label" + FROM alcs.notice_of_intent_submission_status_type noisst + JOIN alcs.notice_of_intent_submission_to_submission_status noistss + ON noisst.code = noistss.status_type_code + AND noistss.submission_uuid = application_submission_uuid + AND noistss.effective_date IS NOT NULL + WHERE + noistss.effective_date < utc_timestamp_tomorrow + ORDER BY noistss.effective_date desc, weight desc + LIMIT 1 INTO result; + + RETURN row_to_json(RESULT); + + END;$function$ + ;`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // nope + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693416651488-noi_search_view.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693416651488-noi_search_view.ts new file mode 100644 index 0000000000..c0fee51a94 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693416651488-noi_search_view.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class noiSearchView1693416651488 implements MigrationInterface { + name = 'noiSearchView1693416651488'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE VIEW "alcs"."notice_of_intent_submission_search_view" AS SELECT "nois"."uuid" AS "uuid", "nois"."applicant" AS "applicant", "noi"."uuid" AS "notice_of_intent_uuid", "noticeOfIntentType"."audit_deleted_date_at" AS "noticeOfIntentType_audit_deleted_date_at", "noticeOfIntentType"."audit_created_at" AS "noticeOfIntentType_audit_created_at", "noticeOfIntentType"."audit_updated_at" AS "noticeOfIntentType_audit_updated_at", "noticeOfIntentType"."audit_created_by" AS "noticeOfIntentType_audit_created_by", "noticeOfIntentType"."audit_updated_by" AS "noticeOfIntentType_audit_updated_by", "noticeOfIntentType"."label" AS "noticeOfIntentType_label", "noticeOfIntentType"."code" AS "noticeOfIntentType_code", "noticeOfIntentType"."description" AS "noticeOfIntentType_description", "noticeOfIntentType"."short_label" AS "noticeOfIntentType_short_label", "noticeOfIntentType"."html_description" AS "noticeOfIntentType_html_description", "noticeOfIntentType"."portal_label" AS "noticeOfIntentType_portal_label", "localGovernment"."name" AS "local_government_name", "nois"."file_number" AS "file_number", "nois"."local_government_uuid" AS "local_government_uuid", "nois"."type_code" AS "notice_of_intent_type_code", "nois"."is_draft" AS "is_draft", "noi"."date_submitted_to_alc" AS "date_submitted_to_alc", "noi"."decision_date" AS "decision_date", "noi"."region_code" AS "notice_of_intent_region_code", alcs.get_current_status_for_notice_of_intent_submission_by_uuid("nois"."uuid") AS "status" FROM "alcs"."notice_of_intent_submission" "nois" INNER JOIN "alcs"."notice_of_intent" "noi" ON "noi"."file_number" = "nois"."file_number" AND "noi"."audit_deleted_date_at" IS NULL INNER JOIN "alcs"."notice_of_intent_type" "noticeOfIntentType" ON "nois"."type_code" = "noticeOfIntentType"."code" AND "noticeOfIntentType"."audit_deleted_date_at" IS NULL LEFT JOIN "alcs"."local_government" "localGovernment" ON "nois"."local_government_uuid" = "localGovernment"."uuid" AND "localGovernment"."audit_deleted_date_at" IS NULL WHERE "nois"."audit_deleted_date_at" IS NULL`, + ); + await queryRunner.query( + `INSERT INTO "alcs"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'alcs', + 'VIEW', + 'notice_of_intent_submission_search_view', + 'SELECT "nois"."uuid" AS "uuid", "nois"."applicant" AS "applicant", "noi"."uuid" AS "notice_of_intent_uuid", "noticeOfIntentType"."audit_deleted_date_at" AS "noticeOfIntentType_audit_deleted_date_at", "noticeOfIntentType"."audit_created_at" AS "noticeOfIntentType_audit_created_at", "noticeOfIntentType"."audit_updated_at" AS "noticeOfIntentType_audit_updated_at", "noticeOfIntentType"."audit_created_by" AS "noticeOfIntentType_audit_created_by", "noticeOfIntentType"."audit_updated_by" AS "noticeOfIntentType_audit_updated_by", "noticeOfIntentType"."label" AS "noticeOfIntentType_label", "noticeOfIntentType"."code" AS "noticeOfIntentType_code", "noticeOfIntentType"."description" AS "noticeOfIntentType_description", "noticeOfIntentType"."short_label" AS "noticeOfIntentType_short_label", "noticeOfIntentType"."html_description" AS "noticeOfIntentType_html_description", "noticeOfIntentType"."portal_label" AS "noticeOfIntentType_portal_label", "localGovernment"."name" AS "local_government_name", "nois"."file_number" AS "file_number", "nois"."local_government_uuid" AS "local_government_uuid", "nois"."type_code" AS "notice_of_intent_type_code", "nois"."is_draft" AS "is_draft", "noi"."date_submitted_to_alc" AS "date_submitted_to_alc", "noi"."decision_date" AS "decision_date", "noi"."region_code" AS "notice_of_intent_region_code", alcs.get_current_status_for_notice_of_intent_submission_by_uuid("nois"."uuid") AS "status" FROM "alcs"."notice_of_intent_submission" "nois" INNER JOIN "alcs"."notice_of_intent" "noi" ON "noi"."file_number" = "nois"."file_number" AND "noi"."audit_deleted_date_at" IS NULL INNER JOIN "alcs"."notice_of_intent_type" "noticeOfIntentType" ON "nois"."type_code" = "noticeOfIntentType"."code" AND "noticeOfIntentType"."audit_deleted_date_at" IS NULL LEFT JOIN "alcs"."local_government" "localGovernment" ON "nois"."local_government_uuid" = "localGovernment"."uuid" AND "localGovernment"."audit_deleted_date_at" IS NULL WHERE "nois"."audit_deleted_date_at" IS NULL', + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "alcs"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'notice_of_intent_submission_search_view', 'alcs'], + ); + await queryRunner.query( + `DROP VIEW "alcs"."notice_of_intent_submission_search_view"`, + ); + } +} diff --git a/services/apps/alcs/src/utils/search-helper.spec.ts b/services/apps/alcs/src/utils/search-helper.spec.ts new file mode 100644 index 0000000000..38b1063f1b --- /dev/null +++ b/services/apps/alcs/src/utils/search-helper.spec.ts @@ -0,0 +1,35 @@ +import { formatStringToPostgresSearchStringArrayWithWildCard } from './search-helper'; + +describe('formatStringToSearchStringWithWildCard', () => { + test('should format string correctly when input contains single word', () => { + const input = 'word'; + const expectedOutput = '{%word%}'; + const actualOutput = + formatStringToPostgresSearchStringArrayWithWildCard(input); + expect(actualOutput).toBe(expectedOutput); + }); + + test('should format string correctly when input contains multiple words', () => { + const input = 'multiple words'; + const expectedOutput = '{%multiple%,%words%,%multiple words%}'; + const actualOutput = + formatStringToPostgresSearchStringArrayWithWildCard(input); + expect(actualOutput).toBe(expectedOutput); + }); + + test('should trim input and format string correctly', () => { + const input = ' trimmed word '; + const expectedOutput = '{%trimmed%,%word%,%trimmed word%}'; + const actualOutput = + formatStringToPostgresSearchStringArrayWithWildCard(input); + expect(actualOutput).toBe(expectedOutput); + }); + + it('should handle empty string correctly', () => { + const input = ''; + const expectedOutput = '{%%}'; + expect(formatStringToPostgresSearchStringArrayWithWildCard(input)).toBe( + expectedOutput, + ); + }); +}); diff --git a/services/apps/alcs/src/utils/search-helper.ts b/services/apps/alcs/src/utils/search-helper.ts new file mode 100644 index 0000000000..4ee7141d7c --- /dev/null +++ b/services/apps/alcs/src/utils/search-helper.ts @@ -0,0 +1,18 @@ +export const formatStringToPostgresSearchStringArrayWithWildCard = ( + input: string, +): string => { + input = input.trim(); + const splitString = input.split(' '); + let output = ''; + + if (splitString.length === 1) { + output = splitString.map((word) => `%${word}%`.toLowerCase()).join(','); + return `{${output}}`; + } + + output = splitString + .map((word) => `%${word}%`.trim().toLowerCase()) + .join(','); + output += `,%${input}%`; + return `{${output}}`; +}; From 607f7e86441acae1ff32b7be1fac9428b0c273c1 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Thu, 31 Aug 2023 14:07:49 -0700 Subject: [PATCH 329/954] Fix portal bugs with edit submissions and reviews (#931) * Improve dialog text readability * Add toast error message and validation for lfng review * Pass lfng review unit test --- .../remove-file-confirmation-dialog.component.html | 11 +++++++++-- .../review-submission.component.html | 5 ++++- .../review-submit-fng.component.spec.ts | 7 +++++++ .../review-submit-fng/review-submit-fng.component.ts | 4 ++++ .../review-submit/review-submit.component.spec.ts | 7 +++++++ .../review-submit/review-submit.component.ts | 4 ++++ .../remove-file-confirmation-dialog.component.html | 11 +++++++++-- 7 files changed, 44 insertions(+), 5 deletions(-) diff --git a/portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html b/portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html index e4aa159b4d..5f486cb803 100644 --- a/portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html +++ b/portal-frontend/src/app/features/applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html @@ -4,8 +4,15 @@

Remove Submission Document

- Warning: This document will be immediately deleted from ALCS and the Portal and cannot be undone. To retain, - first download a copy from the Documents tab in ALCS then return to complete this action. +
+ Warning: This document will be immediately deleted from ALCS and the Portal and cannot be undone. To retain + the document, first download a copy from the Documents tab in ALCS then return to complete this action. +
+
+
+ To replace the document on the Submission: delete current, upload new, then update the submission (via Review & + Submit). +
diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submission.component.html b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.html index 8edf89fd36..19bf444e54 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submission.component.html +++ b/portal-frontend/src/app/features/applications/review-submission/review-submission.component.html @@ -93,7 +93,10 @@
Application ID: {{ application.fileNumber }} | {{ applic
- +
diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.spec.ts index 48641df391..b860ebee32 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.spec.ts @@ -11,6 +11,7 @@ import { ApplicationSubmissionService } from '../../../../services/application-s import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; import { ReviewSubmitFngComponent } from './review-submit-fng.component'; +import { ToastService } from '../../../../services/toast/toast.service'; describe('ReviewSubmitFngComponent', () => { let component: ReviewSubmitFngComponent; @@ -18,6 +19,7 @@ describe('ReviewSubmitFngComponent', () => { let mockAppReviewService: DeepMocked; let mockAppSubmissionService: DeepMocked; let mockAppDocumentService: DeepMocked; + let mockToastService: DeepMocked; let mockPdfGenerationService: DeepMocked; let applicationPipe = new BehaviorSubject(undefined); @@ -29,6 +31,7 @@ describe('ReviewSubmitFngComponent', () => { undefined ); mockAppDocumentService = createMock(); + mockToastService = createMock(); mockPdfGenerationService = createMock(); mockAppSubmissionService = createMock(); @@ -50,6 +53,10 @@ describe('ReviewSubmitFngComponent', () => { provide: PdfGenerationService, useValue: mockPdfGenerationService, }, + { + provide: ToastService, + useValue: mockToastService, + }, ], declarations: [ReviewSubmitFngComponent], schemas: [NO_ERRORS_SCHEMA], diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts index fc165fe3c2..b2d992c0cf 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit-fng/review-submit-fng.component.ts @@ -12,6 +12,7 @@ import { CustomStepperComponent } from '../../../../shared/custom-stepper/custom import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; import { ReviewApplicationFngSteps } from '../review-submission.component'; +import { ToastService } from '../../../../services/toast/toast.service'; @Component({ selector: 'app-review-submit-fng[stepper]', @@ -42,6 +43,7 @@ export class ReviewSubmitFngComponent implements OnInit, OnDestroy { private router: Router, private applicationReviewService: ApplicationSubmissionReviewService, private applicationDocumentService: ApplicationDocumentService, + private toastService: ToastService, private pdfGenerationService: PdfGenerationService ) {} @@ -131,6 +133,8 @@ export class ReviewSubmitFngComponent implements OnInit, OnDestroy { } }, 5); + this.toastService.showErrorToast('Please correct all errors before submitting the form'); + return contactInfoValid && resolutionValid && attachmentsValid; } diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.spec.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.spec.ts index 4bfb483bb5..6baabb88a1 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.spec.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.spec.ts @@ -10,12 +10,14 @@ import { ApplicationSubmissionDto } from '../../../../services/application-submi import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; import { ReviewSubmitComponent } from './review-submit.component'; +import { ToastService } from '../../../../services/toast/toast.service'; describe('ReviewSubmitComponent', () => { let component: ReviewSubmitComponent; let fixture: ComponentFixture; let mockAppReviewService: DeepMocked; let mockAppDocumentService: DeepMocked; + let mockToastService: DeepMocked; let mockPdfGenerationService: DeepMocked; let applicationPipe = new BehaviorSubject(undefined); @@ -28,6 +30,7 @@ describe('ReviewSubmitComponent', () => { ); mockAppDocumentService = createMock(); + mockToastService = createMock(); mockPdfGenerationService = createMock(); await TestBed.configureTestingModule({ @@ -41,6 +44,10 @@ describe('ReviewSubmitComponent', () => { provide: ApplicationDocumentService, useValue: mockAppDocumentService, }, + { + provide: ToastService, + useValue: mockToastService, + }, { provide: PdfGenerationService, useValue: mockPdfGenerationService, diff --git a/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts index 0ee3796c66..b6a47e87fb 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-submit/review-submit.component.ts @@ -12,6 +12,7 @@ import { CustomStepperComponent } from '../../../../shared/custom-stepper/custom import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; import { ReviewApplicationSteps } from '../review-submission.component'; +import { ToastService } from '../../../../services/toast/toast.service'; @Component({ selector: 'app-review-submit[stepper]', @@ -46,6 +47,7 @@ export class ReviewSubmitComponent implements OnInit, OnDestroy { private router: Router, private applicationReviewService: ApplicationSubmissionReviewService, private applicationDocumentService: ApplicationDocumentService, + private toastService: ToastService, private pdfGenerationService: PdfGenerationService ) {} @@ -158,6 +160,8 @@ export class ReviewSubmitComponent implements OnInit, OnDestroy { }); } + this.toastService.showErrorToast('Please correct all errors before submitting the form'); + return contactInfoValid && ocpValid && zoningValid && authorizationValid && attachmentsValid; } return false; diff --git a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html index e4aa159b4d..5f486cb803 100644 --- a/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component.html @@ -4,8 +4,15 @@

Remove Submission Document

- Warning: This document will be immediately deleted from ALCS and the Portal and cannot be undone. To retain, - first download a copy from the Documents tab in ALCS then return to complete this action. +
+ Warning: This document will be immediately deleted from ALCS and the Portal and cannot be undone. To retain + the document, first download a copy from the Documents tab in ALCS then return to complete this action. +
+
+
+ To replace the document on the Submission: delete current, upload new, then update the submission (via Review & + Submit). +
From 83baad4712b1450214a5de28fab6188e3ec8f4ec Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Fri, 1 Sep 2023 11:08:52 -0700 Subject: [PATCH 330/954] Fix form validation to run by field state instead of button click (#934) --- .../additional-information.component.html | 3 ++- .../additional-information.component.ts | 17 +++++++---------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html index 29935d6930..40cdb0efee 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html @@ -114,7 +114,7 @@

Additional Proposal Information

Action - + @@ -137,6 +137,7 @@

Additional Proposal Information

color="primary" (click)="onStructureAdd()" [ngClass]="{ 'mat-error': proposedStructures.length < 1 && showErrors }" + type="button" > + Add Structure diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts index 1c3cccbbb5..82ff5f4a63 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { MatDialog } from '@angular/material/dialog'; @@ -146,7 +146,7 @@ export class AdditionalInformationComponent extends FilesStepComponent implement ) ) { this.isSoilStructureResidentialUseReasonVisible = true; - this.setRequiredAsync(this.soilStructureResidentialUseReason); + this.setRequired(this.soilStructureResidentialUseReason); } else { this.isSoilStructureResidentialUseReasonVisible = false; this.soilStructureResidentialUseReason.removeValidators([Validators.required]); @@ -157,7 +157,7 @@ export class AdditionalInformationComponent extends FilesStepComponent implement private setVisibilityAndValidatorsForAccessoryFields() { if (this.proposedStructures.some((structure) => structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE)) { this.isSoilStructureResidentialAccessoryUseReasonVisible = true; - this.setRequiredAsync(this.soilStructureResidentialAccessoryUseReason); + this.setRequired(this.soilStructureResidentialAccessoryUseReason); } else { this.isSoilStructureResidentialAccessoryUseReasonVisible = false; this.soilStructureResidentialAccessoryUseReason.removeValidators([Validators.required]); @@ -169,8 +169,8 @@ export class AdditionalInformationComponent extends FilesStepComponent implement if (this.proposedStructures.some((structure) => structure.type === STRUCTURE_TYPES.FARM_STRUCTURE)) { this.isSoilAgriParcelActivityVisible = true; this.isSoilStructureFarmUseReasonVisible = true; - this.setRequiredAsync(this.soilAgriParcelActivity); - this.setRequiredAsync(this.soilStructureFarmUseReason); + this.setRequired(this.soilAgriParcelActivity); + this.setRequired(this.soilStructureFarmUseReason); } else { this.isSoilAgriParcelActivityVisible = false; this.isSoilStructureFarmUseReasonVisible = false; @@ -293,10 +293,7 @@ export class AdditionalInformationComponent extends FilesStepComponent implement this.form.markAsDirty(); } - private setRequiredAsync(formControl: FormControl) { - //We set these asynchronously so they don't run immediately - setTimeout(() => { - formControl.setValidators([Validators.required]); - }); + private setRequired(formControl: FormControl) { + formControl.setValidators([Validators.required]); } } From 84c7fdffd5faec5afb31861999f006eac10a8bcc Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 1 Sep 2023 14:35:24 -0700 Subject: [PATCH 331/954] Rename Notifications -> Messages --- .../message.dto.ts} | 2 +- .../message.service.ts} | 10 ++--- .../src/app/shared/header/header.component.ts | 8 ++-- .../notifications.component.spec.ts | 4 +- .../notifications/notifications.component.ts | 4 +- services/apps/alcs/src/alcs/alcs.module.ts | 6 +-- .../application.controller.spec.ts | 12 +++--- .../alcs/application/application.module.ts | 4 +- .../apps/alcs/src/alcs/card/card.module.ts | 4 +- .../alcs/src/alcs/card/card.service.spec.ts | 10 ++--- .../apps/alcs/src/alcs/card/card.service.ts | 6 +-- .../alcs/comment/comment.controller.spec.ts | 8 ++-- .../alcs/src/alcs/comment/comment.module.ts | 4 +- .../src/alcs/comment/comment.service.spec.ts | 8 ++-- .../alcs/src/alcs/comment/comment.service.ts | 4 +- .../message.controller.spec.ts} | 30 +++++++------- .../message.controller.ts} | 36 ++++++++--------- .../message.dto.ts} | 4 +- .../message.entity.ts} | 4 +- .../alcs/src/alcs/message/message.module.ts | 14 +++++++ .../message.service.spec.ts} | 40 +++++++++---------- .../message.service.ts} | 32 +++++++-------- .../alcs/notification/notification.module.ts | 14 ------- ...ofile.ts => message.automapper.profile.ts} | 10 ++--- ...02985709-rename_notification_to_message.ts | 17 ++++++++ .../cleanUpNotifications.consumer.spec.ts | 8 ++-- .../cleanUpNotifications.consumer.ts | 4 +- .../src/queues/scheduler/scheduler.module.ts | 4 +- 28 files changed, 162 insertions(+), 149 deletions(-) rename alcs-frontend/src/app/services/{notification/notification.dto.ts => message/message.dto.ts} (79%) rename alcs-frontend/src/app/services/{notification/notification.service.ts => message/message.service.ts} (66%) rename services/apps/alcs/src/alcs/{notification/notification.controller.spec.ts => message/message.controller.spec.ts} (78%) rename services/apps/alcs/src/alcs/{notification/notification.controller.ts => message/message.controller.ts} (54%) rename services/apps/alcs/src/alcs/{notification/notification.dto.ts => message/message.dto.ts} (79%) rename services/apps/alcs/src/alcs/{notification/notification.entity.ts => message/message.entity.ts} (91%) create mode 100644 services/apps/alcs/src/alcs/message/message.module.ts rename services/apps/alcs/src/alcs/{notification/notification.service.spec.ts => message/message.service.spec.ts} (66%) rename services/apps/alcs/src/alcs/{notification/notification.service.ts => message/message.service.ts} (69%) delete mode 100644 services/apps/alcs/src/alcs/notification/notification.module.ts rename services/apps/alcs/src/common/automapper/{notification.automapper.profile.ts => message.automapper.profile.ts} (65%) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693602985709-rename_notification_to_message.ts diff --git a/alcs-frontend/src/app/services/notification/notification.dto.ts b/alcs-frontend/src/app/services/message/message.dto.ts similarity index 79% rename from alcs-frontend/src/app/services/notification/notification.dto.ts rename to alcs-frontend/src/app/services/message/message.dto.ts index 45d32b4721..2f8139b512 100644 --- a/alcs-frontend/src/app/services/notification/notification.dto.ts +++ b/alcs-frontend/src/app/services/message/message.dto.ts @@ -1,4 +1,4 @@ -export interface NotificationDto { +export interface MessageDto { uuid: string; title: string; body: string; diff --git a/alcs-frontend/src/app/services/notification/notification.service.ts b/alcs-frontend/src/app/services/message/message.service.ts similarity index 66% rename from alcs-frontend/src/app/services/notification/notification.service.ts rename to alcs-frontend/src/app/services/message/message.service.ts index c968c8ae09..3eb12d210a 100644 --- a/alcs-frontend/src/app/services/notification/notification.service.ts +++ b/alcs-frontend/src/app/services/message/message.service.ts @@ -2,23 +2,23 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { NotificationDto } from './notification.dto'; +import { MessageDto } from './message.dto'; @Injectable({ providedIn: 'root', }) -export class NotificationService { +export class MessageService { constructor(private http: HttpClient) {} fetchMyNotifications() { - return firstValueFrom(this.http.get(`${environment.apiUrl}/notification`)); + return firstValueFrom(this.http.get(`${environment.apiUrl}/message`)); } markRead(uuid: string) { - return firstValueFrom(this.http.post(`${environment.apiUrl}/notification/${uuid}`, {})); + return firstValueFrom(this.http.post(`${environment.apiUrl}/message/${uuid}`, {})); } markAllRead() { - return firstValueFrom(this.http.post(`${environment.apiUrl}/notification`, {})); + return firstValueFrom(this.http.post(`${environment.apiUrl}/message`, {})); } } diff --git a/alcs-frontend/src/app/shared/header/header.component.ts b/alcs-frontend/src/app/shared/header/header.component.ts index f6cf5e9e24..a8f0e184de 100644 --- a/alcs-frontend/src/app/shared/header/header.component.ts +++ b/alcs-frontend/src/app/shared/header/header.component.ts @@ -5,8 +5,8 @@ import { ROLES_ALLOWED_APPLICATIONS } from '../../app-routing.module'; import { ApplicationService } from '../../services/application/application.service'; import { AuthenticationService, ICurrentUser, ROLES } from '../../services/authentication/authentication.service'; import { BoardService, BoardWithFavourite } from '../../services/board/board.service'; -import { NotificationDto } from '../../services/notification/notification.dto'; -import { NotificationService } from '../../services/notification/notification.service'; +import { MessageDto } from '../../services/message/message.dto'; +import { MessageService } from '../../services/message/message.service'; import { ToastService } from '../../services/toast/toast.service'; import { UserDto } from '../../services/user/user.dto'; import { UserService } from '../../services/user/user.service'; @@ -23,7 +23,7 @@ export class HeaderComponent implements OnInit { hasRoles = false; allowedSearch = false; sortedBoards: BoardWithFavourite[] = []; - notifications: NotificationDto[] = []; + notifications: MessageDto[] = []; isCommissioner = false; isAdmin = false; @@ -34,7 +34,7 @@ export class HeaderComponent implements OnInit { private toastService: ToastService, private userService: UserService, private router: Router, - private notificationService: NotificationService + private notificationService: MessageService ) {} ngOnInit(): void { diff --git a/alcs-frontend/src/app/shared/header/notifications/notifications.component.spec.ts b/alcs-frontend/src/app/shared/header/notifications/notifications.component.spec.ts index e259514b8f..e9d79ab38e 100644 --- a/alcs-frontend/src/app/shared/header/notifications/notifications.component.spec.ts +++ b/alcs-frontend/src/app/shared/header/notifications/notifications.component.spec.ts @@ -2,7 +2,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatMenuModule } from '@angular/material/menu'; import { RouterTestingModule } from '@angular/router/testing'; -import { NotificationService } from '../../../services/notification/notification.service'; +import { MessageService } from '../../../services/message/message.service'; import { NotificationsComponent } from './notifications.component'; @@ -15,7 +15,7 @@ describe('NotificationsComponent', () => { imports: [RouterTestingModule, MatMenuModule], providers: [ { - provide: NotificationService, + provide: MessageService, useValue: {}, }, ], diff --git a/alcs-frontend/src/app/shared/header/notifications/notifications.component.ts b/alcs-frontend/src/app/shared/header/notifications/notifications.component.ts index 292f47b0bd..d96af69cff 100644 --- a/alcs-frontend/src/app/shared/header/notifications/notifications.component.ts +++ b/alcs-frontend/src/app/shared/header/notifications/notifications.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import moment from 'moment'; import { environment } from '../../../../environments/environment'; -import { NotificationService } from '../../../services/notification/notification.service'; +import { MessageService } from '../../../services/message/message.service'; type FormattedNotification = { uuid: string; @@ -24,7 +24,7 @@ export class NotificationsComponent implements OnInit { hasUnread = false; unreadCount = 0; - constructor(private router: Router, private notificationService: NotificationService) {} + constructor(private router: Router, private notificationService: MessageService) {} ngOnInit(): void { this.loadNotifications(); diff --git a/services/apps/alcs/src/alcs/alcs.module.ts b/services/apps/alcs/src/alcs/alcs.module.ts index f54beb3aaa..8a49f98dd7 100644 --- a/services/apps/alcs/src/alcs/alcs.module.ts +++ b/services/apps/alcs/src/alcs/alcs.module.ts @@ -18,7 +18,7 @@ import { NoticeOfIntentDecisionModule } from './notice-of-intent-decision/notice import { NoticeOfIntentTimelineModule } from './notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.module'; import { NoticeOfIntentSubmissionStatusModule } from './notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentModule } from './notice-of-intent/notice-of-intent.module'; -import { NotificationModule } from './notification/notification.module'; +import { MessageModule } from './message/message.module'; import { PlanningReviewModule } from './planning-review/planning-review.module'; import { SearchModule } from './search/search.module'; import { StaffJournalModule } from './staff-journal/staff-journal.module'; @@ -28,7 +28,7 @@ import { StaffJournalModule } from './staff-journal/staff-journal.module'; ImportModule, ApplicationModule, CommentModule, - NotificationModule, + MessageModule, BoardModule, CodeModule, PlanningReviewModule, @@ -48,7 +48,7 @@ import { StaffJournalModule } from './staff-journal/staff-journal.module'; RouterModule.register([ { path: 'alcs', module: ApplicationModule }, { path: 'alcs', module: CommentModule }, - { path: 'alcs', module: NotificationModule }, + { path: 'alcs', module: MessageModule }, { path: 'alcs', module: BoardModule }, { path: 'alcs', module: CodeModule }, { path: 'alcs', module: PlanningReviewModule }, diff --git a/services/apps/alcs/src/alcs/application/application.controller.spec.ts b/services/apps/alcs/src/alcs/application/application.controller.spec.ts index f091fd4ec8..d99a9356b7 100644 --- a/services/apps/alcs/src/alcs/application/application.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application/application.controller.spec.ts @@ -15,7 +15,7 @@ import { CardStatusDto } from '../card/card-status/card-status.dto'; import { CardDto } from '../card/card.dto'; import { Card } from '../card/card.entity'; import { CardService } from '../card/card.service'; -import { NotificationService } from '../notification/notification.service'; +import { MessageService } from '../message/message.service'; import { ApplicationTimeData } from './application-time-tracking.service'; import { ApplicationController } from './application.controller'; import { ApplicationDto, UpdateApplicationDto } from './application.dto'; @@ -31,7 +31,7 @@ import { SUBMISSION_STATUS } from './application-submission-status/submission-st describe('ApplicationController', () => { let controller: ApplicationController; let applicationService: DeepMocked; - let notificationService: DeepMocked; + let notificationService: DeepMocked; let cardService: DeepMocked; let emailService: DeepMocked; @@ -72,7 +72,7 @@ describe('ApplicationController', () => { beforeEach(async () => { applicationService = createMock(); - notificationService = createMock(); + notificationService = createMock(); cardService = createMock(); emailService = createMock(); @@ -86,7 +86,7 @@ describe('ApplicationController', () => { useValue: applicationService, }, { - provide: NotificationService, + provide: MessageService, useValue: notificationService, }, { @@ -119,7 +119,7 @@ describe('ApplicationController', () => { }); applicationService.mapToDtos.mockResolvedValue([mockApplicationDto]); - notificationService.createNotification.mockResolvedValue(); + notificationService.create.mockResolvedValue(); }); it('should be defined', () => { @@ -174,7 +174,7 @@ describe('ApplicationController', () => { expect(res.applicant).toEqual(mockUpdate.applicant); expect(applicationService.update).toHaveBeenCalledTimes(1); - expect(notificationService.createNotification).not.toHaveBeenCalled(); + expect(notificationService.create).not.toHaveBeenCalled(); expect(applicationService.update).toHaveBeenCalledWith( mockApplicationEntity, mockUpdate, diff --git a/services/apps/alcs/src/alcs/application/application.module.ts b/services/apps/alcs/src/alcs/application/application.module.ts index ef4937d3f9..9b75149e09 100644 --- a/services/apps/alcs/src/alcs/application/application.module.ts +++ b/services/apps/alcs/src/alcs/application/application.module.ts @@ -20,7 +20,7 @@ import { ApplicationType } from '../code/application-code/application-type/appli import { CodeModule } from '../code/code.module'; import { LocalGovernment } from '../local-government/local-government.entity'; import { LocalGovernmentModule } from '../local-government/local-government.module'; -import { NotificationModule } from '../notification/notification.module'; +import { MessageModule } from '../message/message.module'; import { LocalGovernmentController } from '../local-government/local-government.controller'; import { LocalGovernmentService } from '../local-government/local-government.service'; import { DocumentCode } from '../../document/document-code.entity'; @@ -59,7 +59,7 @@ import { ApplicationService } from './application.service'; ApplicationSubmissionToSubmissionStatus, LocalGovernment, ]), - NotificationModule, + MessageModule, DocumentModule, CardModule, CodeModule, diff --git a/services/apps/alcs/src/alcs/card/card.module.ts b/services/apps/alcs/src/alcs/card/card.module.ts index 4cb2525d26..027116f491 100644 --- a/services/apps/alcs/src/alcs/card/card.module.ts +++ b/services/apps/alcs/src/alcs/card/card.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CodeModule } from '../code/code.module'; import { CardProfile } from '../../common/automapper/card.automapper.profile'; -import { NotificationModule } from '../notification/notification.module'; +import { MessageModule } from '../message/message.module'; import { CardHistory } from './card-history/card-history.entity'; import { CardSubscriber } from './card-history/card.subscriber'; import { CardStatus } from './card-status/card-status.entity'; @@ -27,7 +27,7 @@ import { CardService } from './card.service'; CardHistory, ]), CodeModule, - NotificationModule, + MessageModule, ], controllers: [CardSubtaskController, CardController], providers: [ diff --git a/services/apps/alcs/src/alcs/card/card.service.spec.ts b/services/apps/alcs/src/alcs/card/card.service.spec.ts index 4e7bd197ce..c7556c53db 100644 --- a/services/apps/alcs/src/alcs/card/card.service.spec.ts +++ b/services/apps/alcs/src/alcs/card/card.service.spec.ts @@ -13,7 +13,7 @@ import { } from '../../../test/mocks/mockEntities'; import { User } from '../../user/user.entity'; import { Board } from '../board/board.entity'; -import { NotificationService } from '../notification/notification.service'; +import { MessageService } from '../message/message.service'; import { CardSubtask } from './card-subtask/card-subtask.entity'; import { CardSubtaskService } from './card-subtask/card-subtask.service'; import { CARD_TYPE, CardType } from './card-type/card-type.entity'; @@ -27,7 +27,7 @@ describe('CardService', () => { let cardTypeRepositoryMock: DeepMocked>; let mockCardEntity; let mockSubtaskService: DeepMocked; - let mockNotificationService: DeepMocked; + let mockNotificationService: DeepMocked; beforeEach(async () => { cardRepositoryMock = createMock>(); @@ -61,7 +61,7 @@ describe('CardService', () => { useValue: mockSubtaskService, }, { - provide: NotificationService, + provide: MessageService, useValue: mockNotificationService, }, ], @@ -217,10 +217,10 @@ describe('CardService', () => { await service.update(fakeAuthor, 'fake', mockUpdate, 'Notification Text'); - expect(mockNotificationService.createNotification).toHaveBeenCalledTimes(1); + expect(mockNotificationService.create).toHaveBeenCalledTimes(1); const createNotificationServiceDto = - mockNotificationService.createNotification.mock.calls[0][0]; + mockNotificationService.create.mock.calls[0][0]; expect(createNotificationServiceDto.actor).toStrictEqual(fakeAuthor); expect(createNotificationServiceDto.receiverUuid).toStrictEqual( mockUserUuid, diff --git a/services/apps/alcs/src/alcs/card/card.service.ts b/services/apps/alcs/src/alcs/card/card.service.ts index 68bbd49257..54e18617db 100644 --- a/services/apps/alcs/src/alcs/card/card.service.ts +++ b/services/apps/alcs/src/alcs/card/card.service.ts @@ -8,7 +8,7 @@ import { IConfig } from 'config'; import { FindOptionsRelations, Not, Repository } from 'typeorm'; import { User } from '../../user/user.entity'; import { Board } from '../board/board.entity'; -import { NotificationService } from '../notification/notification.service'; +import { MessageService } from '../message/message.service'; import { CardSubtaskService } from './card-subtask/card-subtask.service'; import { CARD_TYPE, CardType } from './card-type/card-type.entity'; import { CardDetailedDto, CardDto, CardUpdateServiceDto } from './card.dto'; @@ -31,7 +31,7 @@ export class CardService { private cardTypeRepository: Repository, @Inject(CONFIG_TOKEN) private config: IConfig, private subtaskService: CardSubtaskService, - private notificationService: NotificationService, + private notificationService: MessageService, ) {} async getCardTypes() { @@ -113,7 +113,7 @@ export class CardService { if (shouldCreateNotification) { const frontEnd = this.config.get('ALCS.FRONTEND_ROOT'); - this.notificationService.createNotification({ + this.notificationService.create({ actor: user, receiverUuid: savedCard.assigneeUuid, title: "You've been assigned", diff --git a/services/apps/alcs/src/alcs/comment/comment.controller.spec.ts b/services/apps/alcs/src/alcs/comment/comment.controller.spec.ts index 90b705dbb8..8244c111e5 100644 --- a/services/apps/alcs/src/alcs/comment/comment.controller.spec.ts +++ b/services/apps/alcs/src/alcs/comment/comment.controller.spec.ts @@ -7,14 +7,14 @@ import { ClsService } from 'nestjs-cls'; import { CommentProfile } from '../../common/automapper/comment.automapper.profile'; import { initCommentMock } from '../../../test/mocks/mockEntities'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; -import { NotificationService } from '../notification/notification.service'; +import { MessageService } from '../message/message.service'; import { CommentController } from './comment.controller'; import { CommentService } from './comment.service'; describe('CommentController', () => { let controller: CommentController; let mockCommentService: DeepMocked; - let mockNotificationService: DeepMocked; + let mockNotificationService: DeepMocked; let comment; let user; @@ -23,7 +23,7 @@ describe('CommentController', () => { beforeEach(async () => { mockCommentService = createMock(); - mockNotificationService = createMock(); + mockNotificationService = createMock(); user = { name: 'Bruce Wayne', @@ -55,7 +55,7 @@ describe('CommentController', () => { useValue: mockCommentService, }, { - provide: NotificationService, + provide: MessageService, useValue: mockNotificationService, }, { diff --git a/services/apps/alcs/src/alcs/comment/comment.module.ts b/services/apps/alcs/src/alcs/comment/comment.module.ts index 96e1932ef0..389df988c2 100644 --- a/services/apps/alcs/src/alcs/comment/comment.module.ts +++ b/services/apps/alcs/src/alcs/comment/comment.module.ts @@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationModule } from '../application/application.module'; import { CardModule } from '../card/card.module'; import { CommentProfile } from '../../common/automapper/comment.automapper.profile'; -import { NotificationModule } from '../notification/notification.module'; +import { MessageModule } from '../message/message.module'; import { CommentController } from './comment.controller'; import { Comment } from './comment.entity'; import { CommentService } from './comment.service'; @@ -14,7 +14,7 @@ import { CommentMentionService } from './mention/comment-mention.service'; imports: [ TypeOrmModule.forFeature([Comment, CommentMention]), ApplicationModule, - NotificationModule, + MessageModule, CardModule, ], controllers: [CommentController], diff --git a/services/apps/alcs/src/alcs/comment/comment.service.spec.ts b/services/apps/alcs/src/alcs/comment/comment.service.spec.ts index 23b96f1c33..25cc65fbda 100644 --- a/services/apps/alcs/src/alcs/comment/comment.service.spec.ts +++ b/services/apps/alcs/src/alcs/comment/comment.service.spec.ts @@ -9,7 +9,7 @@ import { } from '../../../test/mocks/mockEntities'; import { Card } from '../card/card.entity'; import { CardService } from '../card/card.service'; -import { NotificationService } from '../notification/notification.service'; +import { MessageService } from '../message/message.service'; import { User } from '../../user/user.entity'; import { Comment } from './comment.entity'; import { CommentService } from './comment.service'; @@ -19,7 +19,7 @@ describe('CommentService', () => { let service: CommentService; let mockCommentRepository: DeepMocked>; let mockCommentMentionService: DeepMocked; - let mockNotificationService: DeepMocked; + let mockNotificationService: DeepMocked; let mockCardService: DeepMocked; let comment; @@ -27,7 +27,7 @@ describe('CommentService', () => { beforeEach(async () => { mockCommentRepository = createMock>(); mockCommentMentionService = createMock(); - mockNotificationService = createMock(); + mockNotificationService = createMock(); mockCardService = createMock(); mockCommentMentionService.updateMentions.mockResolvedValue([]); @@ -47,7 +47,7 @@ describe('CommentService', () => { useValue: mockCommentMentionService, }, { - provide: NotificationService, + provide: MessageService, useValue: mockNotificationService, }, { provide: CardService, useValue: mockCardService }, diff --git a/services/apps/alcs/src/alcs/comment/comment.service.ts b/services/apps/alcs/src/alcs/comment/comment.service.ts index d3287a69b4..dd9ad32374 100644 --- a/services/apps/alcs/src/alcs/comment/comment.service.ts +++ b/services/apps/alcs/src/alcs/comment/comment.service.ts @@ -7,7 +7,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsRelations, Repository } from 'typeorm'; import { Card } from '../card/card.entity'; import { CardService } from '../card/card.service'; -import { NotificationService } from '../notification/notification.service'; +import { MessageService } from '../message/message.service'; import { User } from '../../user/user.entity'; import { Comment } from './comment.entity'; import { CommentMention } from './mention/comment-mention.entity'; @@ -25,7 +25,7 @@ export class CommentService { private commentRepository: Repository, private cardService: CardService, private commentMentionService: CommentMentionService, - private notificationService: NotificationService, + private notificationService: MessageService, ) {} async fetch(cardUuid: string) { diff --git a/services/apps/alcs/src/alcs/notification/notification.controller.spec.ts b/services/apps/alcs/src/alcs/message/message.controller.spec.ts similarity index 78% rename from services/apps/alcs/src/alcs/notification/notification.controller.spec.ts rename to services/apps/alcs/src/alcs/message/message.controller.spec.ts index e27f4d770a..6d38e5b12c 100644 --- a/services/apps/alcs/src/alcs/notification/notification.controller.spec.ts +++ b/services/apps/alcs/src/alcs/message/message.controller.spec.ts @@ -3,18 +3,18 @@ import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; -import { NotificationProfile } from '../../common/automapper/notification.automapper.profile'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; -import { NotificationController } from './notification.controller'; -import { NotificationService } from './notification.service'; -import { Notification } from './notification.entity'; +import { MessageProfile } from '../../common/automapper/message.automapper.profile'; +import { MessageController } from './message.controller'; +import { Message } from './message.entity'; +import { MessageService } from './message.service'; -describe('NotificationController', () => { - let controller: NotificationController; - let notificationService: DeepMocked; +describe('MessageController', () => { + let controller: MessageController; + let notificationService: DeepMocked; beforeEach(async () => { - notificationService = createMock(); + notificationService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -22,22 +22,22 @@ describe('NotificationController', () => { strategyInitializer: classes(), }), ], - controllers: [NotificationController], + controllers: [MessageController], providers: [ - NotificationProfile, + MessageProfile, { provide: ClsService, useValue: {}, }, { - provide: NotificationService, + provide: MessageService, useValue: notificationService, }, ...mockKeyCloakProviders, ], }).compile(); - controller = module.get(NotificationController); + controller = module.get(MessageController); }); it('should be defined', () => { @@ -49,7 +49,7 @@ describe('NotificationController', () => { notificationService.list.mockResolvedValue([ { createdAt: date, - } as Notification, + } as Message, ]); const res = await controller.getMyNotifications({ @@ -95,7 +95,7 @@ describe('NotificationController', () => { }); it('should call into service for markRead', async () => { - notificationService.get.mockResolvedValue({} as Notification); + notificationService.get.mockResolvedValue({} as Message); notificationService.markRead.mockResolvedValue({} as any); await controller.markRead( @@ -130,7 +130,7 @@ describe('NotificationController', () => { }, 'fake-notification', ), - ).rejects.toMatchObject(new Error(`Failed to find notification`)); + ).rejects.toMatchObject(new Error(`Failed to find message`)); expect(notificationService.markRead).not.toHaveBeenCalled(); }); diff --git a/services/apps/alcs/src/alcs/notification/notification.controller.ts b/services/apps/alcs/src/alcs/message/message.controller.ts similarity index 54% rename from services/apps/alcs/src/alcs/notification/notification.controller.ts rename to services/apps/alcs/src/alcs/message/message.controller.ts index 314eb78195..7ad91b30ee 100644 --- a/services/apps/alcs/src/alcs/notification/notification.controller.ts +++ b/services/apps/alcs/src/alcs/message/message.controller.ts @@ -14,26 +14,26 @@ import * as config from 'config'; import { RolesGuard } from '../../common/authorization/roles-guard.service'; import { ANY_AUTH_ROLE } from '../../common/authorization/roles'; import { UserRoles } from '../../common/authorization/roles.decorator'; -import { NotificationDto } from './notification.dto'; -import { Notification } from './notification.entity'; -import { NotificationService } from './notification.service'; +import { MessageDto } from './message.dto'; +import { Message } from './message.entity'; +import { MessageService } from './message.service'; @ApiOAuth2(config.get('KEYCLOAK.SCOPES')) -@Controller('notification') +@Controller('message') @UseGuards(RolesGuard) -export class NotificationController { +export class MessageController { constructor( - private notificationService: NotificationService, + private messageService: MessageService, @InjectMapper() private autoMapper: Mapper, ) {} @Get() @UserRoles(...ANY_AUTH_ROLE) - async getMyNotifications(@Req() req): Promise { + async getMyNotifications(@Req() req): Promise { const userId = req.user.entity.uuid; if (userId) { - const notifications = await this.notificationService.list(userId); - return this.mapToDto(notifications); + const messages = await this.messageService.list(userId); + return this.mapToDto(messages); } else { return []; } @@ -43,26 +43,22 @@ export class NotificationController { @UserRoles(...ANY_AUTH_ROLE) async markAllRead(@Req() req): Promise { const userId = req.user.entity.uuid; - await this.notificationService.markAllRead(userId); + await this.messageService.markAllRead(userId); } @Post('/:uuid') @UserRoles(...ANY_AUTH_ROLE) async markRead(@Req() req, @Param('uuid') id): Promise { const userId = req.user.entity.uuid; - const notification = await this.notificationService.get(id, userId); - if (notification) { - await this.notificationService.markRead(id); + const message = await this.messageService.get(id, userId); + if (message) { + await this.messageService.markRead(id); } else { - throw new NotFoundException('Failed to find notification'); + throw new NotFoundException('Failed to find message'); } } - private mapToDto(notifications: Notification[]): NotificationDto[] { - return this.autoMapper.mapArray( - notifications, - Notification, - NotificationDto, - ); + private mapToDto(notifications: Message[]): MessageDto[] { + return this.autoMapper.mapArray(notifications, Message, MessageDto); } } diff --git a/services/apps/alcs/src/alcs/notification/notification.dto.ts b/services/apps/alcs/src/alcs/message/message.dto.ts similarity index 79% rename from services/apps/alcs/src/alcs/notification/notification.dto.ts rename to services/apps/alcs/src/alcs/message/message.dto.ts index 773c11a676..40e23249f7 100644 --- a/services/apps/alcs/src/alcs/notification/notification.dto.ts +++ b/services/apps/alcs/src/alcs/message/message.dto.ts @@ -1,6 +1,6 @@ import { User } from '../../user/user.entity'; -export class NotificationDto { +export class MessageDto { uuid: string; title: string; body: string; @@ -10,7 +10,7 @@ export class NotificationDto { link: string; } -export class CreateNotificationServiceDto { +export class CreateMessageServiceDto { actor: User; receiverUuid: string; body: string; diff --git a/services/apps/alcs/src/alcs/notification/notification.entity.ts b/services/apps/alcs/src/alcs/message/message.entity.ts similarity index 91% rename from services/apps/alcs/src/alcs/notification/notification.entity.ts rename to services/apps/alcs/src/alcs/message/message.entity.ts index 0b87204227..98b5978724 100644 --- a/services/apps/alcs/src/alcs/notification/notification.entity.ts +++ b/services/apps/alcs/src/alcs/message/message.entity.ts @@ -9,8 +9,8 @@ import { import { User } from '../../user/user.entity'; @Entity() -export class Notification { - constructor(data?: Partial) { +export class Message { + constructor(data?: Partial) { if (data) { Object.assign(this, data); } diff --git a/services/apps/alcs/src/alcs/message/message.module.ts b/services/apps/alcs/src/alcs/message/message.module.ts new file mode 100644 index 0000000000..2bb8721659 --- /dev/null +++ b/services/apps/alcs/src/alcs/message/message.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { MessageProfile } from '../../common/automapper/message.automapper.profile'; +import { Message } from './message.entity'; +import { MessageService } from './message.service'; +import { MessageController } from './message.controller'; + +@Module({ + imports: [TypeOrmModule.forFeature([Message])], + providers: [MessageService, MessageProfile], + controllers: [MessageController], + exports: [MessageService], +}) +export class MessageModule {} diff --git a/services/apps/alcs/src/alcs/notification/notification.service.spec.ts b/services/apps/alcs/src/alcs/message/message.service.spec.ts similarity index 66% rename from services/apps/alcs/src/alcs/notification/notification.service.spec.ts rename to services/apps/alcs/src/alcs/message/message.service.spec.ts index cfc3a57d7e..4a1c3aacd7 100644 --- a/services/apps/alcs/src/alcs/notification/notification.service.spec.ts +++ b/services/apps/alcs/src/alcs/message/message.service.spec.ts @@ -4,18 +4,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import * as config from 'config'; import { Repository, UpdateResult } from 'typeorm'; -import { Notification } from './notification.entity'; -import { NotificationService } from './notification.service'; +import { Message } from './message.entity'; +import { MessageService } from './message.service'; -describe('NotificationService', () => { - let service: NotificationService; - let mockRepository: DeepMocked>; - let fakeNotification: Notification; +describe('MessageService', () => { + let service: MessageService; + let mockRepository: DeepMocked>; + let mockmessage: Message; beforeEach(async () => { - mockRepository = createMock>(); + mockRepository = createMock>(); - fakeNotification = { + mockmessage = new Message({ createdAt: new Date(), targetType: 'application', uuid: 'fake-uuid', @@ -24,13 +24,13 @@ describe('NotificationService', () => { link: 'link goes here', title: 'title goes here', read: false, - } as Notification; + }); const module: TestingModule = await Test.createTestingModule({ providers: [ - NotificationService, + MessageService, { - provide: getRepositoryToken(Notification), + provide: getRepositoryToken(Message), useValue: mockRepository, }, { @@ -40,28 +40,28 @@ describe('NotificationService', () => { ], }).compile(); - service = module.get(NotificationService); + service = module.get(MessageService); }); it('should be defined', () => { expect(service).toBeDefined(); }); - it('should call find when loading notifications', async () => { - mockRepository.find.mockResolvedValue([fakeNotification]); + it('should call find when loading messages', async () => { + mockRepository.find.mockResolvedValue([mockmessage]); - const notifications = await service.list('fake-user'); - expect(notifications.length).toEqual(1); - expect(notifications[0]).toEqual(fakeNotification); + const messages = await service.list('fake-user'); + expect(messages.length).toEqual(1); + expect(messages[0]).toEqual(mockmessage); }); it('should call update with correct uuid when marking read', async () => { mockRepository.update.mockResolvedValue({} as UpdateResult); - await service.markRead(fakeNotification.uuid); + await service.markRead(mockmessage.uuid); expect(mockRepository.update).toHaveBeenCalledTimes(1); expect(mockRepository.update.mock.calls[0][0]).toEqual({ - uuid: fakeNotification.uuid, + uuid: mockmessage.uuid, }); }); @@ -84,7 +84,7 @@ describe('NotificationService', () => { }); it('should call findOne when doing get', async () => { - mockRepository.findOne.mockResolvedValue({} as Notification); + mockRepository.findOne.mockResolvedValue({} as Message); await service.get('fake-uuid', 'fake-receiever'); expect(mockRepository.findOne).toHaveBeenCalledTimes(1); diff --git a/services/apps/alcs/src/alcs/notification/notification.service.ts b/services/apps/alcs/src/alcs/message/message.service.ts similarity index 69% rename from services/apps/alcs/src/alcs/notification/notification.service.ts rename to services/apps/alcs/src/alcs/message/message.service.ts index c6812798b1..8d68e22e35 100644 --- a/services/apps/alcs/src/alcs/notification/notification.service.ts +++ b/services/apps/alcs/src/alcs/message/message.service.ts @@ -6,19 +6,19 @@ import { IConfig } from 'config'; import { LessThan, Repository } from 'typeorm'; import { Card } from '../card/card.entity'; import { User } from '../../user/user.entity'; -import { CreateNotificationServiceDto } from './notification.dto'; -import { Notification } from './notification.entity'; +import { CreateMessageServiceDto } from './message.dto'; +import { Message } from './message.entity'; @Injectable() -export class NotificationService { +export class MessageService { constructor( - @InjectRepository(Notification) - private notificationRepository: Repository, + @InjectRepository(Message) + private messageRepository: Repository, @Inject(CONFIG_TOKEN) private config: IConfig, ) {} async list(userUuid: string) { - return await this.notificationRepository.find({ + return await this.messageRepository.find({ where: { receiver: { uuid: userUuid, @@ -37,7 +37,7 @@ export class NotificationService { ); } - return this.notificationRepository.findOne({ + return this.messageRepository.findOne({ where: { uuid, receiverUuid, @@ -45,15 +45,15 @@ export class NotificationService { }); } - async createNotification(notificationDto: CreateNotificationServiceDto) { - const notification = new Notification({ - ...notificationDto, + async create(messageServiceDto: CreateMessageServiceDto) { + const notification = new Message({ + ...messageServiceDto, }); - await this.notificationRepository.save(notification); + await this.messageRepository.save(notification); } async markRead(uuid: string) { - return await this.notificationRepository.update( + return await this.messageRepository.update( { uuid, }, @@ -64,7 +64,7 @@ export class NotificationService { } async markAllRead(receiverUuid: string) { - return await this.notificationRepository.update( + return await this.messageRepository.update( { receiverUuid, }, @@ -75,7 +75,7 @@ export class NotificationService { } async cleanUp(olderThan: Date, read = true) { - return await this.notificationRepository.delete({ + return await this.messageRepository.delete({ createdAt: LessThan(olderThan), read, }); @@ -94,7 +94,7 @@ export class NotificationService { throw new Error('Cannot set notifications without proper card'); } - const notification = new Notification({ + const message = new Message({ body, title: title, link: `${frontEnd}/board/${card.board.code}?card=${card.uuid}&type=${card.type.code}`, @@ -102,6 +102,6 @@ export class NotificationService { actor: author, receiverUuid, }); - await this.notificationRepository.save(notification); + await this.messageRepository.save(message); } } diff --git a/services/apps/alcs/src/alcs/notification/notification.module.ts b/services/apps/alcs/src/alcs/notification/notification.module.ts deleted file mode 100644 index a5dac3cc3a..0000000000 --- a/services/apps/alcs/src/alcs/notification/notification.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { NotificationProfile } from '../../common/automapper/notification.automapper.profile'; -import { Notification } from './notification.entity'; -import { NotificationService } from './notification.service'; -import { NotificationController } from './notification.controller'; - -@Module({ - imports: [TypeOrmModule.forFeature([Notification])], - providers: [NotificationService, NotificationProfile], - controllers: [NotificationController], - exports: [NotificationService], -}) -export class NotificationModule {} diff --git a/services/apps/alcs/src/common/automapper/notification.automapper.profile.ts b/services/apps/alcs/src/common/automapper/message.automapper.profile.ts similarity index 65% rename from services/apps/alcs/src/common/automapper/notification.automapper.profile.ts rename to services/apps/alcs/src/common/automapper/message.automapper.profile.ts index b9a023bda5..28ac30502b 100644 --- a/services/apps/alcs/src/common/automapper/notification.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/message.automapper.profile.ts @@ -1,11 +1,11 @@ import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; -import { NotificationDto } from '../../alcs/notification/notification.dto'; -import { Notification } from '../../alcs/notification/notification.entity'; +import { MessageDto } from '../../alcs/message/message.dto'; +import { Message } from '../../alcs/message/message.entity'; @Injectable() -export class NotificationProfile extends AutomapperProfile { +export class MessageProfile extends AutomapperProfile { constructor(@InjectMapper() mapper: Mapper) { super(mapper); } @@ -14,8 +14,8 @@ export class NotificationProfile extends AutomapperProfile { return (mapper) => { createMap( mapper, - Notification, - NotificationDto, + Message, + MessageDto, forMember( (n) => n.createdAt, mapFrom((n) => n.createdAt.getTime()), diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693602985709-rename_notification_to_message.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693602985709-rename_notification_to_message.ts new file mode 100644 index 0000000000..02f2445bbd --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693602985709-rename_notification_to_message.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class renameNotificationToMessage1693602985709 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "alcs"."notification" RENAME TO "message"; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "alcs"."message" RENAME TO "notification"; + `); + } +} diff --git a/services/apps/alcs/src/queues/scheduler/cleanUpNotifications.consumer.spec.ts b/services/apps/alcs/src/queues/scheduler/cleanUpNotifications.consumer.spec.ts index e9168031d3..1ff6aeb03b 100644 --- a/services/apps/alcs/src/queues/scheduler/cleanUpNotifications.consumer.spec.ts +++ b/services/apps/alcs/src/queues/scheduler/cleanUpNotifications.consumer.spec.ts @@ -2,22 +2,22 @@ import { ConfigModule } from '@app/common/config/config.module'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { DeleteResult } from 'typeorm'; -import { NotificationService } from '../../alcs/notification/notification.service'; +import { MessageService } from '../../alcs/message/message.service'; import { CleanUpNotificationsConsumer } from './cleanUpNotifications.consumer'; describe('SchedulerConsumerService', () => { let notificationCleanUpConsumer: CleanUpNotificationsConsumer; - let mockNotificationService: DeepMocked; + let mockNotificationService: DeepMocked; beforeEach(async () => { - mockNotificationService = createMock(); + mockNotificationService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ConfigModule], providers: [ CleanUpNotificationsConsumer, { - provide: NotificationService, + provide: MessageService, useValue: mockNotificationService, }, ], diff --git a/services/apps/alcs/src/queues/scheduler/cleanUpNotifications.consumer.ts b/services/apps/alcs/src/queues/scheduler/cleanUpNotifications.consumer.ts index 1225233083..0078c2234b 100644 --- a/services/apps/alcs/src/queues/scheduler/cleanUpNotifications.consumer.ts +++ b/services/apps/alcs/src/queues/scheduler/cleanUpNotifications.consumer.ts @@ -1,7 +1,7 @@ import { Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; import * as dayjs from 'dayjs'; -import { NotificationService } from '../../alcs/notification/notification.service'; +import { MessageService } from '../../alcs/message/message.service'; import { QUEUES } from './scheduler.service'; const DAYS_TO_RETAIN_READ = 30; @@ -11,7 +11,7 @@ const DAYS_TO_RETAIN_UNREAD = 365; export class CleanUpNotificationsConsumer { private logger = new Logger(CleanUpNotificationsConsumer.name); - constructor(private notificationService: NotificationService) {} + constructor(private notificationService: MessageService) {} @Process() async cleanUpNotifications() { diff --git a/services/apps/alcs/src/queues/scheduler/scheduler.module.ts b/services/apps/alcs/src/queues/scheduler/scheduler.module.ts index 77d6046237..0fe3b005c0 100644 --- a/services/apps/alcs/src/queues/scheduler/scheduler.module.ts +++ b/services/apps/alcs/src/queues/scheduler/scheduler.module.ts @@ -1,7 +1,7 @@ import { BullModule } from '@nestjs/bull'; import { Module, OnApplicationBootstrap } from '@nestjs/common'; import { ApplicationModule } from '../../alcs/application/application.module'; -import { NotificationModule } from '../../alcs/notification/notification.module'; +import { MessageModule } from '../../alcs/message/message.module'; import { EmailModule } from '../../providers/email/email.module'; import { BullConfigService } from '../bullConfig.service'; import { ApplicationExpiryConsumer } from './applicationExpiry.consumer'; @@ -21,7 +21,7 @@ import { QUEUES, SchedulerService } from './scheduler.service'; }), EmailModule, ApplicationModule, - NotificationModule, + MessageModule, ], providers: [ SchedulerService, From fa868649527c52da2ecdcce89ab3e0d2c7cd164d Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 1 Sep 2023 16:52:38 -0700 Subject: [PATCH 332/954] initial commit, functional code --- .../submissions/app_submissions.py | 40 +++++- .../submissions/submap/__init__.py | 3 +- .../submissions/submap/soil_elements.py | 129 ++++++++++++++++++ 3 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 bin/migrate-oats-data/submissions/submap/soil_elements.py diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 99c096b66c..8cbb51ba06 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -12,6 +12,10 @@ create_subdiv_dict, add_subdiv, map_subdiv_lots, + get_soil_rows, + create_soil_dict, + map_soil_data, + add_soil_field, ) from db import inject_conn_pool from constants import BATCH_UPLOAD_SIZE @@ -67,10 +71,11 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): direction_data = get_direction_data(rows, cursor) subdiv_data = get_subdiv_data(rows, cursor) + soil_data = get_soil_data(rows, cursor) submissions_to_be_inserted_count = len(rows) - insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdiv_data) + insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdiv_data, soil_data) successful_inserts_count = ( successful_inserts_count + submissions_to_be_inserted_count @@ -94,7 +99,7 @@ def process_alcs_app_submissions(conn=None, batch_size=BATCH_UPLOAD_SIZE): print("Total failed inserts:", failed_inserts) log_end(etl_name) -def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdiv_data): +def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdiv_data, soil_data): """ Function to insert submission records in batches. @@ -113,7 +118,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdi nfu_data_list, other_data_list, inc_exc_data_list, - ) = prepare_app_sub_data(rows, direction_data, subdiv_data) + ) = prepare_app_sub_data(rows, direction_data, subdiv_data, soil_data) if len(nfu_data_list) > 0: execute_batch( @@ -141,7 +146,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdi conn.commit() -def prepare_app_sub_data(app_sub_raw_data_list, direction_data, subdiv_data): +def prepare_app_sub_data(app_sub_raw_data_list, direction_data, subdiv_data, soil_data): """ This function prepares different lists of data based on the 'alr_change_code' field of each data dict in 'app_sub_raw_data_list'. @@ -165,10 +170,13 @@ def prepare_app_sub_data(app_sub_raw_data_list, direction_data, subdiv_data): data = dict(row) data = add_direction_field(data) data = add_subdiv(data,json) + data = add_soil_field(data) if data['alr_appl_component_id'] in subdiv_data: data = map_subdiv_lots(data, subdiv_data, json) if data["alr_application_id"] in direction_data: data = map_direction_values(data, direction_data) + if data["alr_appl_component_id"] in soil_data: + data = map_soil_data(data, soil_data) if data["alr_change_code"] == ALRChangeCode.NFU.value: nfu_data_list.append(data) elif data["alr_change_code"] == ALRChangeCode.EXC.value or data["alr_change_code"] == ALRChangeCode.INC.value: @@ -222,8 +230,24 @@ def get_insert_query(unique_fields,unique_values): return query.format(unique_fields=unique_fields, unique_values=unique_values) def get_insert_query_for_nfu(): - unique_fields = ", nfu_hectares" - unique_values = ", %(alr_area)s" + unique_fields = """, nfu_hectares, + nfu_will_import_fill, + nfu_fill_volume, + nfu_max_fill_depth, + nfu_project_duration_amount, + nfu_fill_type_description, + nfu_fill_origin_description, + nfu_project_duration_unit + """ + unique_values = """, %(alr_area)s, + %(import_fill)s, + %(total_fill)s, + %(max_fill_depth)s, + %(fill_duration)s, + %(fill_type)s, + %(fill_origin)s, + %(fill_duration_unit)s + """ return get_insert_query(unique_fields,unique_values) def get_insert_query_for_inc_exc(): @@ -243,6 +267,10 @@ def get_subdiv_data(rows, cursor): subdiv_data = create_subdiv_dict(subdiv_rows) return subdiv_data +def get_soil_data(rows, cursor): + soil_rows = get_soil_rows(rows, cursor) + soil_data = create_soil_dict(soil_rows) + return soil_data @inject_conn_pool def clean_application_submission(conn=None): diff --git a/bin/migrate-oats-data/submissions/submap/__init__.py b/bin/migrate-oats-data/submissions/submap/__init__.py index 39cbcf6a23..369c5be915 100644 --- a/bin/migrate-oats-data/submissions/submap/__init__.py +++ b/bin/migrate-oats-data/submissions/submap/__init__.py @@ -1,2 +1,3 @@ from .direction_mapping import * -from .subdiv_plot import * \ No newline at end of file +from .subdiv_plot import * +from .soil_elements import * \ No newline at end of file diff --git a/bin/migrate-oats-data/submissions/submap/soil_elements.py b/bin/migrate-oats-data/submissions/submap/soil_elements.py new file mode 100644 index 0000000000..6a72f36721 --- /dev/null +++ b/bin/migrate-oats-data/submissions/submap/soil_elements.py @@ -0,0 +1,129 @@ +def get_soil_rows(rows, cursor): + # fetches adjacent land use data, specifically direction, description and type code + component_ids = [dict(item)["alr_appl_component_id"] for item in rows] + component_ids_string = ', '.join(str(item) for item in component_ids) + soil_rows_query = f"""SELECT * from + oats.oats_soil_change_elements osc + WHERE osc.alr_appl_component_id in ({component_ids_string}) + """ + cursor.execute(soil_rows_query) + soil_rows = cursor.fetchall() + return soil_rows + +def create_soil_dict(soil_rows): + # creates dict contailing fill and remove data + alr_id = 'alr_appl_component_id' + area = 'project_area' + desc = 'material_desc' + origin_desc = 'material_origin_desc' + duration = 'project_duration' + volume = 'volume' + depth = 'depth' + code = 'soil_change_code' + + soil_dict = {} + for row in soil_rows: + app_component_id = row[alr_id] + if app_component_id in soil_dict: + + if row[code] == 'REMOVE': + # if soil_dict[app_component_id]['RMV'] == True: + if 'RMV' in soil_dict.get(app_component_id, {}): + print('ignored element_id:',row['soil_change_element_id']) + else: + soil_dict[app_component_id]['remove_type'] = row[desc] + soil_dict[app_component_id]['remove_origin'] = row[origin_desc] + soil_dict[app_component_id]['max_remove_depth'] = row[depth] + soil_dict[app_component_id]['total_remove'] = row[volume] + soil_dict[app_component_id]['remove_duration'] = row[duration] + soil_dict[app_component_id]['remove_area'] = row[area] + if 'import_fill' not in soil_dict.get(app_component_id, {}): + soil_dict[app_component_id]['import_fill'] = False + + soil_dict[app_component_id]['RMV'] = True + + + elif row[code] == 'ADD': + # if soil_dict[app_component_id]['ADD'] == True: + if 'ADD' in soil_dict.get(app_component_id, {}): + print('ignored element_id:',row['soil_change_element_id']) + else: + soil_dict[app_component_id]['fill_type'] = row[desc] + soil_dict[app_component_id]['fill_origin'] = row[origin_desc] + soil_dict[app_component_id]['total_fill'] = row[volume] + soil_dict[app_component_id]['max_fill_depth'] = row[depth] + soil_dict[app_component_id]['fill_duration'] = row[duration] + soil_dict[app_component_id]['fill_area'] = row[area] + soil_dict[app_component_id]['import_fill'] = True + + soil_dict[app_component_id]['ADD'] = True + + else: + print('unknown soil action') + else: + soil_dict[app_component_id] = {} + soil_dict[app_component_id][alr_id] = row[alr_id] + if row[code] == 'REMOVE': + soil_dict[app_component_id]['remove_type'] = row[desc] + soil_dict[app_component_id]['remove_origin'] = row[origin_desc] + soil_dict[app_component_id]['max_remove_depth'] = row[depth] + soil_dict[app_component_id]['total_remove'] = row[volume] + soil_dict[app_component_id]['remove_duration'] = row[duration] + soil_dict[app_component_id]['remove_area'] = row[area] + soil_dict[app_component_id]['RMV'] = True + soil_dict[app_component_id]['import_fill'] = False + + + elif row[code] == 'ADD': + soil_dict[app_component_id]['fill_type'] = row[desc] + soil_dict[app_component_id]['fill_origin'] = row[origin_desc] + soil_dict[app_component_id]['total_fill'] = row[volume] + soil_dict[app_component_id]['max_fill_depth'] = row[depth] + soil_dict[app_component_id]['fill_duration'] = row[duration] + soil_dict[app_component_id]['fill_area'] = row[area] + soil_dict[app_component_id]['import_fill'] = True + soil_dict[app_component_id]['ADD'] = True + + else: + print('unknown soil action') + return soil_dict + + +def map_soil_data(data, soil_data): + #map soil data into data + app_component_id = 'alr_appl_component_id' + data['fill_type'] = soil_data.get(data[app_component_id], {}).get('fill_type', None) + data['fill_origin'] = soil_data.get(data[app_component_id], {}).get('fill_origin', None) + data['total_fill'] = soil_data.get(data[app_component_id], {}).get('total_fill', None) + data['max_fill_depth'] = soil_data.get(data[app_component_id], {}).get('max_fill_depth', None) + data['fill_duration'] = soil_data.get(data[app_component_id], {}).get('fill_duration', None) + data['fill_area'] = soil_data.get(data[app_component_id], {}).get('fill_area', None) + data['import_fill'] = soil_data.get(data[app_component_id], {}).get('import_fill', None) + data['fill_duration_unit'] = 'duration_unit' + data['remove_duration_unit'] = 'duration_unit' + data['remove_type'] = soil_data.get(data[app_component_id], {}).get('remove_type', None) + data['remove_origin'] = soil_data.get(data[app_component_id], {}).get('remove_origin', None) + data['total_remove'] = soil_data.get(data[app_component_id], {}).get('total_remove', None) + data['max_remove_depth'] = soil_data.get(data[app_component_id], {}).get('max_remove_depth', None) + data['remove_duration'] = soil_data.get(data[app_component_id], {}).get('remove_duration', None) + data['remove_area'] = soil_data.get(data[app_component_id], {}).get('remove_area', None) + return data + +def add_soil_field(data): + # populates columns to be inserted + data['fill_type'] = None + data['fill_origin'] = None + data['total_fill'] = None + data['max_fill_depth'] = None + data['fill_duration'] = None + data['fill_area'] = None + data['import_fill'] = None + data['remove_type'] = None + data['remove_origin'] = None + data['total_remove'] = None + data['max_remove_depth'] = None + data['remove_duration'] = None + data['remove_area'] = None + data['fill_duration_unit'] = None + data['remove_duration_unit'] = None + return data From 1fe45aec128bfccb97d51dd27b97e7a01dee4385 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Tue, 5 Sep 2023 09:42:29 -0700 Subject: [PATCH 333/954] Feature/alcs 957 part 2 non applications search (#936) non-applications search --- ...application-search-table.component.spec.ts | 4 +- ...on-application-search-table.component.html | 56 +++++++++ ...on-application-search-table.component.scss | 0 ...application-search-table.component.spec.ts | 33 ++++++ .../non-application-search-table.component.ts | 105 +++++++++++++++++ .../app/features/search/search.component.html | 16 ++- .../app/features/search/search.component.ts | 91 ++++++++++----- .../src/app/features/search/search.module.ts | 3 +- .../src/app/services/search/search.dto.ts | 24 +++- .../services/search/search.service.spec.ts | 32 +++++- .../src/app/services/search/search.service.ts | 17 +++ .../non-applications-view.entity.ts | 90 +++++++++++++++ .../non-applications.service.spec.ts | 89 +++++++++++++++ .../non-applications.service.ts | 96 ++++++++++++++++ .../src/alcs/search/search.controller.spec.ts | 107 +++++++++++++++--- .../alcs/src/alcs/search/search.controller.ts | 86 +++++++++++--- .../apps/alcs/src/alcs/search/search.dto.ts | 56 +++++++-- .../alcs/src/alcs/search/search.module.ts | 4 + .../1693606128323-non_application_view.ts | 72 ++++++++++++ 19 files changed, 898 insertions(+), 83 deletions(-) create mode 100644 alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.html create mode 100644 alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.scss create mode 100644 alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.spec.ts create mode 100644 alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.ts create mode 100644 services/apps/alcs/src/alcs/search/non-applications/non-applications-view.entity.ts create mode 100644 services/apps/alcs/src/alcs/search/non-applications/non-applications.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/search/non-applications/non-applications.service.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693606128323-non_application_view.ts diff --git a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.spec.ts b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.spec.ts index 769348d3ca..4c320b829d 100644 --- a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.spec.ts +++ b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; -import { DeepMocked } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ApplicationSearchTableComponent } from './application-search-table.component'; @@ -10,6 +10,8 @@ describe('ApplicationSearchTableComponent', () => { let mockRouter: DeepMocked; beforeEach(async () => { + mockRouter = createMock(); + await TestBed.configureTestingModule({ declarations: [ApplicationSearchTableComponent], providers: [ diff --git a/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.html b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.html new file mode 100644 index 0000000000..db05dcf09b --- /dev/null +++ b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.html @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
File ID + {{ element.fileNumber | emptyColumn }} + Type + + + Owner Name + {{ element.applicant | emptyColumn }} + Local/First Nation Government + {{ element.localGovernmentName | emptyColumn }} +
+
No Search results.
+
Please adjust criteria and try again.
+
+ diff --git a/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.scss b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.spec.ts b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.spec.ts new file mode 100644 index 0000000000..b23aefd68b --- /dev/null +++ b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; + +import { NonApplicationSearchTableComponent } from './non-application-search-table.component'; + +describe('NonApplicationSearchTableComponent', () => { + let component: NonApplicationSearchTableComponent; + let fixture: ComponentFixture; + let mockRouter: DeepMocked; + + beforeEach(async () => { + mockRouter = createMock(); + + await TestBed.configureTestingModule({ + declarations: [NonApplicationSearchTableComponent], + providers: [ + { + provide: Router, + useValue: mockRouter, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(NonApplicationSearchTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.ts b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.ts new file mode 100644 index 0000000000..db05df9501 --- /dev/null +++ b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.ts @@ -0,0 +1,105 @@ +import { AfterViewInit, Component, EventEmitter, Input, OnDestroy, Output, ViewChild } from '@angular/core'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { Router } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { NonApplicationSearchResultDto } from '../../../services/search/search.dto'; +import { COVENANT_TYPE_LABEL, PLANNING_TYPE_LABEL } from '../../../shared/application-type-pill/application-type-pill.constants'; +import { TableChange } from '../search.interface'; + +interface SearchResult { + fileNumber: string; + applicant: string; + localGovernmentName?: string; + referenceId: string; + board?: string; + class: string; +} + +@Component({ + selector: 'app-non-application-search-table', + templateUrl: './non-application-search-table.component.html', + styleUrls: ['./non-application-search-table.component.scss'], +}) +export class NonApplicationSearchTableComponent implements AfterViewInit, OnDestroy { + $destroy = new Subject(); + + @ViewChild(MatPaginator) paginator!: MatPaginator; + @ViewChild(MatSort) sort?: MatSort; + + _nonApplications: NonApplicationSearchResultDto[] = []; + @Input() set nonApplications(nonApplications: NonApplicationSearchResultDto[]) { + this._nonApplications = nonApplications; + this.dataSource = this.mapNonApplications(nonApplications); + } + + @Input() totalCount = 0; + @Output() tableChange = new EventEmitter(); + + displayedColumns = ['fileId', 'type', 'applicant', 'government']; + dataSource: NonApplicationSearchResultDto[] = []; + pageIndex = 0; + itemsPerPage = 20; + total = 0; + sortDirection = 'DESC'; + sortField = 'fileId'; + + COVENANT_TYPE_LABEL = COVENANT_TYPE_LABEL + PLANNING_TYPE_LABEL = PLANNING_TYPE_LABEL + + constructor(private router: Router) {} + + ngAfterViewInit() { + if (this.sort) { + this.sort.sortChange.pipe(takeUntil(this.$destroy)).subscribe(async (sortObj) => { + this.paginator.pageIndex = 0; + this.pageIndex = 0; + this.sortDirection = sortObj.direction.toUpperCase(); + this.sortField = sortObj.active; + + await this.onTableChange(); + }); + } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + async onTableChange() { + this.tableChange.emit({ + pageIndex: this.pageIndex, + itemsPerPage: this.itemsPerPage, + sortDirection: this.sortDirection, + sortField: this.sortField, + tableType: 'NONAPP', + }); + } + + async onPageChange($event: PageEvent) { + this.pageIndex = $event.pageIndex; + this.itemsPerPage = $event.pageSize; + + await this.onTableChange(); + } + + async onSelectRecord(record: SearchResult) { + await this.router.navigateByUrl(`/board/${record.board}?card=${record.referenceId}&type=${record.class}`); + } + + private mapNonApplications(nonApplications: NonApplicationSearchResultDto[]): NonApplicationSearchResultDto[] { + return nonApplications.map((e) => { + return { + fileNumber: e.fileNumber, + type: e.type, + applicant: e.applicant, + boardCode: e.boardCode, + localGovernmentName: e.localGovernmentName, + referenceId: e.referenceId, + board: e.boardCode, + class: e.class, + }; + }); + } +} diff --git a/alcs-frontend/src/app/features/search/search.component.html b/alcs-frontend/src/app/features/search/search.component.html index 3a4b561a2f..9ab3f405f2 100644 --- a/alcs-frontend/src/app/features/search/search.component.html +++ b/alcs-frontend/src/app/features/search/search.component.html @@ -12,13 +12,13 @@
Provide one or more of the following criteria:
File ID - +
Name - +
Search by Primary Contact, Parcel Owner, Organization, Ministry or Department
@@ -29,7 +29,7 @@
Provide one or more of the following criteria:
PID - + Civic Address @@ -233,7 +233,7 @@
Provide one or more of the following criteria:

Search Results:

- + Applications: {{ applicationTotal }} Search Results:
- Non-Applications: 0 + Non-Applications: {{ nonApplicationsTotal }} + +
diff --git a/alcs-frontend/src/app/features/search/search.component.ts b/alcs-frontend/src/app/features/search/search.component.ts index 57c8e400ca..c227623a2f 100644 --- a/alcs-frontend/src/app/features/search/search.component.ts +++ b/alcs-frontend/src/app/features/search/search.component.ts @@ -3,6 +3,7 @@ import { FormControl, FormGroup } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; +import { MatTabGroup } from '@angular/material/tabs'; import { ActivatedRoute } from '@angular/router'; import moment from 'moment'; import { combineLatestWith, map, Observable, startWith, Subject, takeUntil } from 'rxjs'; @@ -14,6 +15,7 @@ import { ApplicationService } from '../../services/application/application.servi import { AdvancedSearchResponseDto, ApplicationSearchResultDto, + NonApplicationSearchResultDto, NoticeOfIntentSearchResultDto, SearchRequestDto, } from '../../services/search/search.dto'; @@ -24,6 +26,11 @@ import { TableChange } from './search.interface'; export const defaultStatusBackgroundColour = '#ffffff'; export const defaultStatusColour = '#313132'; + +const APPLICATION_TAB_INDEX = 0; +const NOTICE_OF_INTENT_TAB_INDEX = 1; +const NON_APPLICATION_TAB_INDEX = 2; + @Component({ selector: 'app-search', templateUrl: './search.component.html', @@ -34,6 +41,7 @@ export class SearchComponent implements OnInit, OnDestroy { @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort?: MatSort; + @ViewChild('searchResultTabs') tabGroup!: MatTabGroup; applications: ApplicationSearchResultDto[] = []; applicationTotal = 0; @@ -41,6 +49,9 @@ export class SearchComponent implements OnInit, OnDestroy { noticeOfIntents: NoticeOfIntentSearchResultDto[] = []; noticeOfIntentTotal = 0; + nonApplications: NonApplicationSearchResultDto[] = []; + nonApplicationsTotal = 0; + isSearchExpanded = false; pageIndex = 0; itemsPerPage = 20; @@ -133,24 +144,6 @@ export class SearchComponent implements OnInit, OnDestroy { this.$destroy.complete(); } - // TODO: remove this once the search is complete - // async onSelectCard(record: SearchResult) { - // switch (record.class) { - // case 'APP': - // await this.router.navigateByUrl(`/application/${record.referenceId}`); - // break; - // case 'NOI': - // await this.router.navigateByUrl(`/notice-of-intent/${record.referenceId}`); - // break; - // case 'COV': - // case 'PLAN': - // await this.router.navigateByUrl(`/board/${record.board}?card=${record.referenceId}&type=${record.type}`); - // break; - // default: - // this.toastService.showErrorToast(`Unable to navigate to ${record.referenceId}`); - // } - // } - async onSubmit() { await this.onSearch(); } @@ -191,9 +184,12 @@ export class SearchComponent implements OnInit, OnDestroy { const searchParams = this.getSearchParams(); const result = await this.searchService.advancedSearchFetch(searchParams); this.mapSearchResults(result); + + this.setActiveTab(); } getSearchParams(): SearchRequestDto { + const resolutionNumberString = this.formatStringSearchParam(this.searchForm.controls.resolutionNumber.value); return { // pagination pageSize: this.itemsPerPage, @@ -201,22 +197,17 @@ export class SearchComponent implements OnInit, OnDestroy { // sorting sortField: this.sortField, sortDirection: this.sortDirection, - // TODO move condition into helper function? - fileNumber: - this.searchForm.controls.fileNumber.value && this.searchForm.controls.fileNumber.value !== '' - ? this.searchForm.controls.fileNumber.value - : undefined, - legacyId: this.searchForm.controls.legacyId.value ?? undefined, - name: this.searchForm.controls.name.value ?? undefined, - civicAddress: this.searchForm.controls.civicAddress.value ?? undefined, - pid: this.searchForm.controls.pid.value ?? undefined, + // search parameters + fileNumber: this.formatStringSearchParam(this.searchForm.controls.fileNumber.value), + legacyId: this.formatStringSearchParam(this.searchForm.controls.legacyId.value), + name: this.formatStringSearchParam(this.searchForm.controls.name.value), + civicAddress: this.formatStringSearchParam(this.searchForm.controls.civicAddress.value), + pid: this.formatStringSearchParam(this.searchForm.controls.pid.value), isIncludeOtherParcels: this.searchForm.controls.isIncludeOtherParcels.value ?? false, - resolutionNumber: this.searchForm.controls.resolutionNumber.value - ? parseInt(this.searchForm.controls.resolutionNumber.value) - : undefined, + resolutionNumber: resolutionNumberString ? parseInt(resolutionNumberString) : undefined, resolutionYear: this.searchForm.controls.resolutionYear.value ?? undefined, portalStatusCode: this.searchForm.controls.portalStatus.value ?? undefined, - governmentName: this.searchForm.controls.government.value ?? undefined, + governmentName: this.formatStringSearchParam(this.searchForm.controls.government.value), regionCode: this.searchForm.controls.region.value ?? undefined, dateSubmittedFrom: this.searchForm.controls.dateSubmittedFrom.value ? formatDateForApi(this.searchForm.controls.dateSubmittedFrom.value) @@ -253,6 +244,14 @@ export class SearchComponent implements OnInit, OnDestroy { this.noticeOfIntentTotal = result?.total ?? 0; } + async onNonApplicationSearch() { + const searchParams = this.getSearchParams(); + const result = await this.searchService.advancedSearchNonApplicationsFetch(searchParams); + + this.nonApplications = result?.data ?? []; + this.nonApplicationsTotal = result?.total ?? 0; + } + async onTableChange(event: TableChange) { this.pageIndex = event.pageIndex; this.itemsPerPage = event.itemsPerPage; @@ -266,6 +265,9 @@ export class SearchComponent implements OnInit, OnDestroy { case 'NOI': await this.onApplicationSearch(); break; + case 'NONAPP': + await this.onNonApplicationSearch(); + break; default: this.toastService.showErrorToast('Not implemented'); } @@ -291,8 +293,10 @@ export class SearchComponent implements OnInit, OnDestroy { searchResult = { applications: [], noticeOfIntents: [], + nonApplications: [], totalApplications: 0, totalNoticeOfIntents: 0, + totalNonApplications: 0, }; } @@ -301,5 +305,30 @@ export class SearchComponent implements OnInit, OnDestroy { this.noticeOfIntentTotal = searchResult.totalNoticeOfIntents; this.noticeOfIntents = searchResult.noticeOfIntents; + + this.nonApplicationsTotal = searchResult.totalNonApplications; + this.nonApplications = searchResult.nonApplications; + } + + private setActiveTab() { + let maxVal = Math.max(this.applicationTotal, this.noticeOfIntentTotal, this.nonApplicationsTotal); + this.tabGroup.selectedIndex = + maxVal === this.applicationTotal + ? APPLICATION_TAB_INDEX + : maxVal === this.noticeOfIntentTotal + ? NOTICE_OF_INTENT_TAB_INDEX + : NON_APPLICATION_TAB_INDEX; + } + + private formatStringSearchParam(value: string | undefined | null) { + if (value === undefined || value === null) { + return undefined; + } + + if (value.trim() === '') { + return undefined; + } else { + return value.trim(); + } } } diff --git a/alcs-frontend/src/app/features/search/search.module.ts b/alcs-frontend/src/app/features/search/search.module.ts index 48530f0a02..a7aac6457d 100644 --- a/alcs-frontend/src/app/features/search/search.module.ts +++ b/alcs-frontend/src/app/features/search/search.module.ts @@ -7,6 +7,7 @@ import { SharedModule } from '../../shared/shared.module'; import { SearchComponent } from './search.component'; import { ApplicationSearchTableComponent } from './application-search-table/application-search-table.component'; import { NoticeOfIntentSearchTableComponent } from './notice-of-intent-search-table/notice-of-intent-search-table.component'; +import { NonApplicationSearchTableComponent } from './non-application-search-table/non-application-search-table.component'; const routes: Routes = [ { @@ -16,7 +17,7 @@ const routes: Routes = [ ]; @NgModule({ - declarations: [SearchComponent, ApplicationSearchTableComponent, NoticeOfIntentSearchTableComponent], + declarations: [SearchComponent, ApplicationSearchTableComponent, NoticeOfIntentSearchTableComponent, NonApplicationSearchTableComponent], imports: [CommonModule, SharedModule.forRoot(), RouterModule.forChild(routes), MatTabsModule, MatPaginatorModule], }) export class SearchModule {} diff --git a/alcs-frontend/src/app/services/search/search.dto.ts b/alcs-frontend/src/app/services/search/search.dto.ts index 2cb7223441..a408638827 100644 --- a/alcs-frontend/src/app/services/search/search.dto.ts +++ b/alcs-frontend/src/app/services/search/search.dto.ts @@ -16,11 +16,23 @@ export interface ApplicationSearchResultDto { export interface NoticeOfIntentSearchResultDto extends ApplicationSearchResultDto {} +export interface NonApplicationSearchResultDto { + type: string | null; + applicant: string | null; + referenceId: string | null; + localGovernmentName: string | null; + fileNumber: string; + boardCode: string | null; + class: 'PLAN' | 'COV'; +} + export interface AdvancedSearchResponseDto { applications: ApplicationSearchResultDto[]; noticeOfIntents: NoticeOfIntentSearchResultDto[]; + nonApplications: NonApplicationSearchResultDto[]; totalApplications: number; totalNoticeOfIntents: number; + totalNonApplications: number; } export interface AdvancedSearchEntityResponseDto { @@ -28,11 +40,14 @@ export interface AdvancedSearchEntityResponseDto { total: number; } -export interface SearchRequestDto { +export interface PagingRequestDto { pageSize: number; page: number; sortField: string; sortDirection: string; +} + +export interface SearchRequestDto extends PagingRequestDto { fileNumber?: string; legacyId?: string; name?: string; @@ -51,6 +66,13 @@ export interface SearchRequestDto { applicationFileTypes: string[]; } +export interface NonApplicationsSearchRequestDto extends PagingRequestDto { + fileNumber?: string; + governmentName?: string; + regionCode?: string; + name?: string; +} + export interface SearchResultDto { fileNumber: string; type: string; diff --git a/alcs-frontend/src/app/services/search/search.service.spec.ts b/alcs-frontend/src/app/services/search/search.service.spec.ts index 2ce634722d..5af45138fc 100644 --- a/alcs-frontend/src/app/services/search/search.service.spec.ts +++ b/alcs-frontend/src/app/services/search/search.service.spec.ts @@ -43,9 +43,11 @@ describe('SearchService', () => { const mockAdvancedSearchResult = { applications: [], - noticeOfIntents: [], totalApplications: 0, + noticeOfIntents: [], totalNoticeOfIntents: 0, + nonApplications: [], + totalNonApplications: 0, }; const mockSearchRequestDto = { @@ -115,6 +117,8 @@ describe('SearchService', () => { expect(res?.applications).toEqual([]); expect(res?.totalNoticeOfIntents).toEqual(0); expect(res?.noticeOfIntents).toEqual([]); + expect(res?.totalNonApplications).toEqual(0); + expect(res?.nonApplications).toEqual([]); }); it('should show an error toast message if search fails', async () => { @@ -180,4 +184,30 @@ describe('SearchService', () => { expect(res).toBeUndefined(); expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); }); + + + it('should fetch Non Applications advanced search results by AdvancedSearchRequestDto', async () => { + mockHttpClient.post.mockReturnValue(of(mockAdvancedSearchEntityResult)); + + const res = await service.advancedSearchNonApplicationsFetch(mockSearchRequestDto); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + expect(res?.total).toEqual(0); + expect(res?.data).toEqual([]); + }); + + it('should show an error toast message if NOI advanced search fails', async () => { + mockHttpClient.post.mockReturnValue( + throwError(() => { + new Error(''); + }) + ); + + const res = await service.advancedSearchNonApplicationsFetch(mockSearchRequestDto); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(res).toBeUndefined(); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); }); diff --git a/alcs-frontend/src/app/services/search/search.service.ts b/alcs-frontend/src/app/services/search/search.service.ts index 77ed377de3..f0eaa71c9f 100644 --- a/alcs-frontend/src/app/services/search/search.service.ts +++ b/alcs-frontend/src/app/services/search/search.service.ts @@ -7,6 +7,8 @@ import { AdvancedSearchEntityResponseDto, AdvancedSearchResponseDto, ApplicationSearchResultDto, + NonApplicationSearchResultDto, + NonApplicationsSearchRequestDto, NoticeOfIntentSearchResultDto, SearchRequestDto, SearchResultDto, @@ -69,4 +71,19 @@ export class SearchService { return undefined; } } + + async advancedSearchNonApplicationsFetch(searchDto: NonApplicationsSearchRequestDto) { + try { + return await firstValueFrom( + this.http.post>( + `${this.baseUrl}/advanced/non-applications`, + searchDto + ) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast(`Search failed. Please refresh the page and try again`); + return undefined; + } + } } diff --git a/services/apps/alcs/src/alcs/search/non-applications/non-applications-view.entity.ts b/services/apps/alcs/src/alcs/search/non-applications/non-applications-view.entity.ts new file mode 100644 index 0000000000..a27f5384a1 --- /dev/null +++ b/services/apps/alcs/src/alcs/search/non-applications/non-applications-view.entity.ts @@ -0,0 +1,90 @@ +import { + JoinColumn, + ManyToOne, + PrimaryColumn, + ViewColumn, + ViewEntity, +} from 'typeorm'; +import { LocalGovernment } from '../../local-government/local-government.entity'; + +@ViewEntity({ + expression: ` + SELECT + non_applications."uuid" + ,non_applications."file_number" + ,non_applications."applicant" + ,non_applications."type" + ,non_applications."class" + ,non_applications."local_government_uuid" as "local_government_uuid" + ,non_applications."card_uuid" + ,non_applications."board_code" + FROM + ( + SELECT + cov.uuid AS "uuid", + cov.file_number AS "file_number", + "applicant", + NULL AS "type", + 'COV' AS "class", + cov.local_government_uuid AS "local_government_uuid", + card.uuid AS "card_uuid", + board.code AS "board_code" + FROM + alcs.covenant cov + LEFT JOIN alcs.card card ON + cov.card_uuid = card.uuid AND card.audit_deleted_date_at IS NULL + LEFT JOIN alcs.board board ON + board.uuid = card.board_uuid AND board.audit_deleted_date_at IS NULL + WHERE cov.audit_deleted_date_at IS NULL + UNION + SELECT + planning_review.uuid AS "uuid", + planning_review.file_number AS "file_number", + NULL AS "applicant", + "type", + 'PLAN' AS "class", + planning_review.local_government_uuid AS "local_government_uuid", + card.uuid AS "card_uuid", + board.code AS "board_code" + FROM + alcs.planning_review planning_review + LEFT JOIN alcs.card card ON + planning_review.card_uuid = card.uuid AND card.audit_deleted_date_at IS NULL + LEFT JOIN alcs.board board ON + board.uuid = card.board_uuid AND board.audit_deleted_date_at IS NULL + WHERE planning_review.audit_deleted_date_at IS NULL + ) AS non_applications +`, +}) +export class NonApplicationSearchView { + @ViewColumn() + @PrimaryColumn() + uuid: string; + + @ViewColumn() + fileNumber: string; + + @ViewColumn() + applicant: string | null; + + @ViewColumn() + type: string | null; + + @ViewColumn() + localGovernmentUuid: string | null; + + @ViewColumn() + class: 'COV' | 'PLAN'; + + @ViewColumn() + cardUuid: string | null; + + @ViewColumn() + boardCode: string | null; + + @ManyToOne(() => LocalGovernment, { + nullable: true, + }) + @JoinColumn({ name: 'local_government_uuid' }) + localGovernment: LocalGovernment | null; +} diff --git a/services/apps/alcs/src/alcs/search/non-applications/non-applications.service.spec.ts b/services/apps/alcs/src/alcs/search/non-applications/non-applications.service.spec.ts new file mode 100644 index 0000000000..c09bf228f3 --- /dev/null +++ b/services/apps/alcs/src/alcs/search/non-applications/non-applications.service.spec.ts @@ -0,0 +1,89 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NonApplicationsSearchRequestDto } from '../search.dto'; +import { NonApplicationSearchView } from './non-applications-view.entity'; +import { NonApplicationsAdvancedSearchService } from './non-applications.service'; + +describe('NonApplicationsService', () => { + let service: NonApplicationsAdvancedSearchService; + let mockNonApplicationsRepository: DeepMocked< + Repository + >; + + let mockQuery: any = {}; + + const mockSearchRequestDto: NonApplicationsSearchRequestDto = { + fileNumber: '123', + governmentName: 'B', + regionCode: 'C', + name: 'D', + page: 1, + pageSize: 10, + sortField: 'applicant', + sortDirection: 'ASC', + }; + + beforeEach(async () => { + mockNonApplicationsRepository = createMock(); + + mockQuery = { + getManyAndCount: jest.fn().mockResolvedValue([[], 0]), + orderBy: jest.fn().mockReturnThis(), + offset: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + leftJoinAndMapOne: jest.fn().mockReturnThis(), + groupBy: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + andWhere: jest.fn().mockReturnThis(), + }; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NonApplicationsAdvancedSearchService, + { + provide: getRepositoryToken(NonApplicationSearchView), + useValue: mockNonApplicationsRepository, + }, + ], + }).compile(); + + service = module.get( + NonApplicationsAdvancedSearchService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should successfully build a query using all search parameters defined', async () => { + mockNonApplicationsRepository.createQueryBuilder.mockReturnValue( + mockQuery as any, + ); + + const result = await service.searchNonApplications(mockSearchRequestDto); + + expect(result).toEqual({ data: [], total: 0 }); + expect(mockNonApplicationsRepository.createQueryBuilder).toBeCalledTimes(1); + expect(mockQuery.andWhere).toBeCalledTimes(4); + expect(mockQuery.where).toBeCalledTimes(1); + }); + + it('should call compileSearchQuery method correctly', async () => { + const compileNonApplicationSearchQuerySpy = jest + .spyOn(service as any, 'compileSearchQuery') + .mockResolvedValue(mockQuery); + + const result = await service.searchNonApplications(mockSearchRequestDto); + + expect(result).toEqual({ data: [], total: 0 }); + expect(compileNonApplicationSearchQuerySpy).toBeCalledWith( + mockSearchRequestDto, + ); + expect(mockQuery.orderBy).toHaveBeenCalledTimes(1); + expect(mockQuery.offset).toHaveBeenCalledTimes(1); + expect(mockQuery.limit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/search/non-applications/non-applications.service.ts b/services/apps/alcs/src/alcs/search/non-applications/non-applications.service.ts new file mode 100644 index 0000000000..70d0c9762a --- /dev/null +++ b/services/apps/alcs/src/alcs/search/non-applications/non-applications.service.ts @@ -0,0 +1,96 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { formatStringToPostgresSearchStringArrayWithWildCard } from '../../../utils/search-helper'; +import { LocalGovernment } from '../../local-government/local-government.entity'; +import { + AdvancedSearchResultDto, + NonApplicationsSearchRequestDto, +} from '../search.dto'; +import { NonApplicationSearchView } from './non-applications-view.entity'; + +@Injectable() +export class NonApplicationsAdvancedSearchService { + constructor( + @InjectRepository(NonApplicationSearchView) + private nonApplicationSearchRepository: Repository, + ) {} + + async searchNonApplications( + searchDto: NonApplicationsSearchRequestDto, + ): Promise> { + let query = await this.compileSearchQuery(searchDto); + + const sortQuery = this.compileSortQuery(searchDto); + + query = query + .orderBy(sortQuery, searchDto.sortDirection) + .offset((searchDto.page - 1) * searchDto.pageSize) + .limit(searchDto.pageSize); + + const result = await query.getManyAndCount(); + + return { + data: result[0], + total: result[1], + }; + } + + private compileSortQuery(searchDto: NonApplicationsSearchRequestDto) { + switch (searchDto.sortField) { + case 'applicant': + return '"nonApp"."applicant"'; + + case 'government': + return '"localGovernment"."name"'; + + case 'type': + return '"nonApp"."class"'; + + default: + case 'fileId': + return '"nonApp"."file_number"'; + } + } + + private async compileSearchQuery(searchDto: NonApplicationsSearchRequestDto) { + let query = this.nonApplicationSearchRepository + .createQueryBuilder('nonApp') + .leftJoinAndMapOne( + 'nonApp.localGovernment', + LocalGovernment, + 'localGovernment', + '"nonApp"."local_government_uuid" = "localGovernment".uuid', + ) + .where('1 = 1'); + + if (searchDto.fileNumber) { + query = query.andWhere('nonApp.file_number = :fileNumber', { + fileNumber: searchDto.fileNumber ?? null, + }); + } + + if (searchDto.regionCode) { + query = query.andWhere('nonApp.region_code = :regionCode', { + regionCode: searchDto.regionCode, + }); + } + + if (searchDto.governmentName) { + query = query.andWhere('localGovernment.name = :localGovernmentName', { + localGovernmentName: searchDto.governmentName, + }); + } + + if (searchDto.name) { + const formattedSearchString = + formatStringToPostgresSearchStringArrayWithWildCard(searchDto.name!); + + query = query.andWhere('LOWER(nonApp.applicant) LIKE ANY (:names)', { + names: formattedSearchString, + }); + } + + return query; + } +} diff --git a/services/apps/alcs/src/alcs/search/search.controller.spec.ts b/services/apps/alcs/src/alcs/search/search.controller.spec.ts index 41d2380130..3e51a64d9a 100644 --- a/services/apps/alcs/src/alcs/search/search.controller.spec.ts +++ b/services/apps/alcs/src/alcs/search/search.controller.spec.ts @@ -12,10 +12,16 @@ import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; import { PlanningReview } from '../planning-review/planning-review.entity'; import { ApplicationAdvancedSearchService } from './application/application-advanced-search.service'; import { ApplicationSubmissionSearchView } from './application/application-search-view.entity'; +import { NonApplicationSearchView } from './non-applications/non-applications-view.entity'; +import { NonApplicationsAdvancedSearchService } from './non-applications/non-applications.service'; import { NoticeOfIntentAdvancedSearchService } from './notice-of-intent/notice-of-intent-advanced-search.service'; import { NoticeOfIntentSubmissionSearchView } from './notice-of-intent/notice-of-intent-search-view.entity'; import { SearchController } from './search.controller'; -import { AdvancedSearchResultDto, SearchRequestDto } from './search.dto'; +import { + AdvancedSearchResultDto, + NonApplicationsSearchRequestDto, + SearchRequestDto, +} from './search.dto'; import { SearchService } from './search.service'; describe('SearchController', () => { @@ -23,11 +29,13 @@ describe('SearchController', () => { let mockSearchService: DeepMocked; let mockNoticeOfIntentAdvancedSearchService: DeepMocked; let mockApplicationAdvancedSearchService: DeepMocked; + let mockNonApplicationsAdvancedSearchService: DeepMocked; beforeEach(async () => { mockSearchService = createMock(); mockNoticeOfIntentAdvancedSearchService = createMock(); mockApplicationAdvancedSearchService = createMock(); + mockNonApplicationsAdvancedSearchService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -48,6 +56,10 @@ describe('SearchController', () => { provide: ApplicationAdvancedSearchService, useValue: mockApplicationAdvancedSearchService, }, + { + provide: NonApplicationsAdvancedSearchService, + useValue: mockNonApplicationsAdvancedSearchService, + }, { provide: ClsService, useValue: {}, @@ -85,7 +97,6 @@ describe('SearchController', () => { >(); mockNoiResult.data = new Array(); mockNoiResult.total = 0; - mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents.mockResolvedValue( mockNoiResult, ); @@ -95,10 +106,18 @@ describe('SearchController', () => { >(); mockApplicationResult.data = new Array(); mockApplicationResult.total = 0; - mockApplicationAdvancedSearchService.searchApplications.mockResolvedValue( mockApplicationResult, ); + + const mockNonApplicationResult = new AdvancedSearchResultDto< + NonApplicationSearchView[] + >(); + mockNonApplicationResult.data = new Array(); + mockNonApplicationResult.total = 0; + mockNonApplicationsAdvancedSearchService.searchNonApplications.mockResolvedValue( + mockNonApplicationResult, + ); }); it('should be defined', () => { @@ -121,7 +140,7 @@ describe('SearchController', () => { expect(result.length).toBe(4); }); - it('should call applications advanced search to retrieve Applications', async () => { + it('should call advanced search to retrieve Applications, NOIs, PlanningReviews, Covenants', async () => { const mockSearchRequestDto = { pageSize: 1, page: 1, @@ -131,11 +150,6 @@ describe('SearchController', () => { applicationFileTypes: [], }; - mockApplicationAdvancedSearchService.searchApplications.mockResolvedValue({ - data: [], - total: 0, - }); - const result = await controller.advancedSearch( mockSearchRequestDto as SearchRequestDto, ); @@ -148,9 +162,27 @@ describe('SearchController', () => { ).toBeCalledWith(mockSearchRequestDto); expect(result.applications).toBeDefined(); expect(result.totalApplications).toBe(0); + + expect( + mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents, + ).toBeCalledTimes(1); + expect( + mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents, + ).toBeCalledWith(mockSearchRequestDto); + expect(result.noticeOfIntents).toBeDefined(); + expect(result.totalNoticeOfIntents).toBe(0); + + expect( + mockNonApplicationsAdvancedSearchService.searchNonApplications, + ).toBeCalledTimes(1); + expect( + mockNonApplicationsAdvancedSearchService.searchNonApplications, + ).toBeCalledWith(mockSearchRequestDto); + expect(result.nonApplications).toBeDefined(); + expect(result.totalNonApplications).toBe(0); }); - it('should call NOI advanced search to retrieve NOIs', async () => { + it('should call applications advanced search to retrieve Applications', async () => { const mockSearchRequestDto = { pageSize: 1, page: 1, @@ -160,14 +192,31 @@ describe('SearchController', () => { applicationFileTypes: [], }; - mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents.mockResolvedValue( - { - data: [], - total: 0, - }, + const result = await controller.advancedSearchApplications( + mockSearchRequestDto as SearchRequestDto, ); - const result = await controller.advancedSearch( + expect( + mockApplicationAdvancedSearchService.searchApplications, + ).toBeCalledTimes(1); + expect( + mockApplicationAdvancedSearchService.searchApplications, + ).toBeCalledWith(mockSearchRequestDto); + expect(result.data).toBeDefined(); + expect(result.total).toBe(0); + }); + + it('should call NOI advanced search to retrieve NOIs', async () => { + const mockSearchRequestDto = { + pageSize: 1, + page: 1, + sortField: '1', + sortDirection: 'ASC', + isIncludeOtherParcels: false, + applicationFileTypes: [], + }; + + const result = await controller.advancedSearchNoticeOfIntents( mockSearchRequestDto as SearchRequestDto, ); @@ -177,7 +226,29 @@ describe('SearchController', () => { expect( mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents, ).toBeCalledWith(mockSearchRequestDto); - expect(result.applications).toBeDefined(); - expect(result.totalApplications).toBe(0); + expect(result.data).toBeDefined(); + expect(result.total).toBe(0); + }); + + it('should call non-applications advanced search to retrieve Non-Applications', async () => { + const mockSearchRequestDto: NonApplicationsSearchRequestDto = { + pageSize: 1, + page: 1, + sortField: '1', + sortDirection: 'ASC', + }; + + const result = await controller.advancedSearchNonApplications( + mockSearchRequestDto, + ); + + expect( + mockNonApplicationsAdvancedSearchService.searchNonApplications, + ).toBeCalledTimes(1); + expect( + mockNonApplicationsAdvancedSearchService.searchNonApplications, + ).toBeCalledWith(mockSearchRequestDto); + expect(result.data).toBeDefined(); + expect(result.total).toBe(0); }); }); diff --git a/services/apps/alcs/src/alcs/search/search.controller.ts b/services/apps/alcs/src/alcs/search/search.controller.ts index 9d6f5e753c..b9141722a4 100644 --- a/services/apps/alcs/src/alcs/search/search.controller.ts +++ b/services/apps/alcs/src/alcs/search/search.controller.ts @@ -15,12 +15,16 @@ import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; import { PlanningReview } from '../planning-review/planning-review.entity'; import { ApplicationAdvancedSearchService } from './application/application-advanced-search.service'; import { ApplicationSubmissionSearchView } from './application/application-search-view.entity'; +import { NonApplicationSearchView } from './non-applications/non-applications-view.entity'; +import { NonApplicationsAdvancedSearchService } from './non-applications/non-applications.service'; import { NoticeOfIntentAdvancedSearchService } from './notice-of-intent/notice-of-intent-advanced-search.service'; import { NoticeOfIntentSubmissionSearchView } from './notice-of-intent/notice-of-intent-search-view.entity'; import { AdvancedSearchResponseDto, AdvancedSearchResultDto, ApplicationSearchResultDto, + NonApplicationSearchResultDto, + NonApplicationsSearchRequestDto, NoticeOfIntentSearchResultDto, SearchRequestDto, SearchResultDto, @@ -36,6 +40,7 @@ export class SearchController { @InjectMapper() private mapper: Mapper, private noticeOfIntentSearchService: NoticeOfIntentAdvancedSearchService, private applicationSearchService: ApplicationAdvancedSearchService, + private nonApplicationsSearchService: NonApplicationsAdvancedSearchService, ) {} @UserRoles(...ROLES_ALLOWED_APPLICATIONS) @@ -145,9 +150,13 @@ export class SearchController { const noticeOfIntentSearchService = await this.noticeOfIntentSearchService.searchNoticeOfIntents(searchDto); + const nonApplications = + await this.nonApplicationsSearchService.searchNonApplications(searchDto); + const mappedSearchResult = this.mapAdvancedSearchResults( applicationSearchResult, noticeOfIntentSearchService, + nonApplications, ); return mappedSearchResult; @@ -158,11 +167,13 @@ export class SearchController { async advancedSearchApplications( @Body() searchDto: SearchRequestDto, ): Promise> { - const applicationSearchResult = - await this.applicationSearchService.searchApplications(searchDto); + const applications = await this.applicationSearchService.searchApplications( + searchDto, + ); const mappedSearchResult = this.mapAdvancedSearchResults( - applicationSearchResult, + applications, + null, null, ); @@ -177,12 +188,13 @@ export class SearchController { async advancedSearchNoticeOfIntents( @Body() searchDto: SearchRequestDto, ): Promise> { - const noticeOfIntentSearchService = + const noticeOfIntents = await this.noticeOfIntentSearchService.searchNoticeOfIntents(searchDto); const mappedSearchResult = this.mapAdvancedSearchResults( null, - noticeOfIntentSearchService, + noticeOfIntents, + null, ); return { @@ -191,6 +203,26 @@ export class SearchController { }; } + @Post('/advanced/non-applications') + @UserRoles(...ROLES_ALLOWED_APPLICATIONS) + async advancedSearchNonApplications( + @Body() searchDto: NonApplicationsSearchRequestDto, + ): Promise> { + const nonApplications = + await this.nonApplicationsSearchService.searchNonApplications(searchDto); + + const mappedSearchResult = this.mapAdvancedSearchResults( + null, + null, + nonApplications, + ); + + return { + total: mappedSearchResult.totalNonApplications, + data: mappedSearchResult.nonApplications, + }; + } + private mapAdvancedSearchResults( applications: AdvancedSearchResultDto< ApplicationSubmissionSearchView[] @@ -198,6 +230,7 @@ export class SearchController { noticeOfIntents: AdvancedSearchResultDto< NoticeOfIntentSubmissionSearchView[] > | null, + nonApplications: AdvancedSearchResultDto | null, ) { const response = new AdvancedSearchResponseDto(); @@ -219,21 +252,32 @@ export class SearchController { ); } + const mappedNonApplications: NonApplicationSearchResultDto[] = []; + if (nonApplications?.data && nonApplications?.data.length > 0) { + mappedNonApplications.push( + ...nonApplications.data.map((nonApplication) => + this.mapNonApplicationToAdvancedSearchResult(nonApplication), + ), + ); + } + response.applications = mappedApplications; response.totalApplications = applications?.total ?? 0; response.noticeOfIntents = mappedNoticeOfIntents; response.totalNoticeOfIntents = noticeOfIntents?.total ?? 0; + response.nonApplications = mappedNonApplications; + response.totalNonApplications = nonApplications?.total ?? 0; return response; } private mapApplicationToAdvancedSearchResult( application: ApplicationSubmissionSearchView, - ) { - const result = { + ): ApplicationSearchResultDto { + return { referenceId: application.fileNumber, fileNumber: application.fileNumber, - dateSubmitted: application.dateSubmittedToAlc, + dateSubmitted: application.dateSubmittedToAlc?.getTime(), type: this.mapper.map( application.applicationType, ApplicationType, @@ -243,18 +287,16 @@ export class SearchController { ownerName: application.applicant, class: 'APP', status: application.status.status_type_code, - } as ApplicationSearchResultDto; - - return result; + }; } private mapNoticeOfIntentToAdvancedSearchResult( noi: NoticeOfIntentSubmissionSearchView, - ) { - const result = { + ): NoticeOfIntentSearchResultDto { + return { referenceId: noi.fileNumber, fileNumber: noi.fileNumber, - dateSubmitted: noi.dateSubmittedToAlc, + dateSubmitted: noi.dateSubmittedToAlc?.getTime(), type: this.mapper.map( noi.noticeOfIntentType, ApplicationType, @@ -264,8 +306,20 @@ export class SearchController { ownerName: noi.applicant, class: 'NOI', status: noi.status.status_type_code, - } as NoticeOfIntentSearchResultDto; + }; + } - return result; + private mapNonApplicationToAdvancedSearchResult( + nonApplication: NonApplicationSearchView, + ): NonApplicationSearchResultDto { + return { + referenceId: nonApplication.cardUuid, + fileNumber: nonApplication.fileNumber, + applicant: nonApplication.applicant, + boardCode: nonApplication.boardCode, + type: nonApplication.type, + localGovernmentName: nonApplication.localGovernment?.name ?? null, + class: nonApplication.class, + }; } } diff --git a/services/apps/alcs/src/alcs/search/search.dto.ts b/services/apps/alcs/src/alcs/search/search.dto.ts index 0a3ff0db2d..dce8b16d4f 100644 --- a/services/apps/alcs/src/alcs/search/search.dto.ts +++ b/services/apps/alcs/src/alcs/search/search.dto.ts @@ -18,31 +18,49 @@ export class SearchResultDto { label?: ApplicationTypeDto; } +export type SearchEntityClass = 'APP' | 'NOI' | 'PLAN' | 'COV'; + export class ApplicationSearchResultDto { type: ApplicationTypeDto; referenceId: string; ownerName?: string; - localGovernmentName: string; + localGovernmentName?: string; fileNumber: string; boardCode?: string; status: string; + dateSubmitted?: number; + class: SearchEntityClass; } export class NoticeOfIntentSearchResultDto { type: ApplicationTypeDto; referenceId: string; ownerName?: string; - localGovernmentName: string; + localGovernmentName?: string; fileNumber: string; boardCode?: string; status: string; + dateSubmitted?: number; + class: SearchEntityClass; +} + +export class NonApplicationSearchResultDto { + type: string | null; + applicant: string | null; + referenceId: string | null; + localGovernmentName: string | null; + fileNumber: string; + boardCode: string | null; + class: SearchEntityClass; } export class AdvancedSearchResponseDto { applications: ApplicationSearchResultDto[]; noticeOfIntents: NoticeOfIntentSearchResultDto[]; + nonApplications: NonApplicationSearchResultDto[]; totalApplications: number; totalNoticeOfIntents: number; + totalNonApplications: number; } export class AdvancedSearchResultDto { @@ -50,7 +68,21 @@ export class AdvancedSearchResultDto { total: number; } -export class SearchRequestDto { +export class PagingRequestDto { + @IsNumber() + page: number; + + @IsNumber() + pageSize: number; + + @IsString() + sortField: string; + + @IsString() + sortDirection: 'ASC' | 'DESC'; +} + +export class SearchRequestDto extends PagingRequestDto { @IsString() @IsOptional() @MinLength(3) @@ -118,16 +150,22 @@ export class SearchRequestDto { @IsArray() applicationFileTypes: string[]; +} - @IsNumber() - page: number; +export class NonApplicationsSearchRequestDto extends PagingRequestDto { + @IsString() + @IsOptional() + fileNumber?: string; - @IsNumber() - pageSize: number; + @IsString() + @IsOptional() + governmentName?: string; @IsString() - sortField: string; + @IsOptional() + regionCode?: string; @IsString() - sortDirection: 'ASC' | 'DESC'; + @IsOptional() + name?: string; } diff --git a/services/apps/alcs/src/alcs/search/search.module.ts b/services/apps/alcs/src/alcs/search/search.module.ts index b3fee1cb95..33145255b5 100644 --- a/services/apps/alcs/src/alcs/search/search.module.ts +++ b/services/apps/alcs/src/alcs/search/search.module.ts @@ -8,6 +8,8 @@ import { NoticeOfIntent } from '../notice-of-intent/notice-of-intent.entity'; import { PlanningReview } from '../planning-review/planning-review.entity'; import { ApplicationAdvancedSearchService } from './application/application-advanced-search.service'; import { ApplicationSubmissionSearchView } from './application/application-search-view.entity'; +import { NonApplicationSearchView } from './non-applications/non-applications-view.entity'; +import { NonApplicationsAdvancedSearchService } from './non-applications/non-applications.service'; import { NoticeOfIntentAdvancedSearchService } from './notice-of-intent/notice-of-intent-advanced-search.service'; import { NoticeOfIntentSubmissionSearchView } from './notice-of-intent/notice-of-intent-search-view.entity'; import { SearchController } from './search.controller'; @@ -23,6 +25,7 @@ import { SearchService } from './search.service'; LocalGovernment, ApplicationSubmissionSearchView, NoticeOfIntentSubmissionSearchView, + NonApplicationSearchView, ]), ], providers: [ @@ -30,6 +33,7 @@ import { SearchService } from './search.service'; ApplicationProfile, ApplicationAdvancedSearchService, NoticeOfIntentAdvancedSearchService, + NonApplicationsAdvancedSearchService, ], controllers: [SearchController], }) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693606128323-non_application_view.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693606128323-non_application_view.ts new file mode 100644 index 0000000000..344de8d29e --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693606128323-non_application_view.ts @@ -0,0 +1,72 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class nonApplicationView1693606128323 implements MigrationInterface { + name = 'nonApplicationView1693606128323'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE VIEW "alcs"."non_application_search_view" AS + SELECT + non_applications."uuid" + ,non_applications."file_number" + ,non_applications."applicant" + ,non_applications."type" + ,non_applications."class" + ,non_applications."local_government_uuid" as "local_government_uuid" + ,non_applications."card_uuid" + ,non_applications."board_code" + FROM + ( + SELECT + cov.uuid AS "uuid", + cov.file_number AS "file_number", + "applicant", + NULL AS "type", + 'COV' AS "class", + cov.local_government_uuid AS "local_government_uuid", + card.uuid AS "card_uuid", + board.code AS "board_code" + FROM + alcs.covenant cov + LEFT JOIN alcs.card card ON + cov.card_uuid = card.uuid AND card.audit_deleted_date_at IS NULL + LEFT JOIN alcs.board board ON + board.uuid = card.board_uuid AND board.audit_deleted_date_at IS NULL + WHERE cov.audit_deleted_date_at IS NULL + UNION + SELECT + planning_review.uuid AS "uuid", + planning_review.file_number AS "file_number", + NULL AS "applicant", + "type", + 'PLAN' AS "class", + planning_review.local_government_uuid AS "local_government_uuid", + card.uuid AS "card_uuid", + board.code AS "board_code" + FROM + alcs.planning_review planning_review + LEFT JOIN alcs.card card ON + planning_review.card_uuid = card.uuid AND card.audit_deleted_date_at IS NULL + LEFT JOIN alcs.board board ON + board.uuid = card.board_uuid AND board.audit_deleted_date_at IS NULL + WHERE planning_review.audit_deleted_date_at IS NULL + ) AS non_applications +`); + await queryRunner.query( + `INSERT INTO "alcs"."typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'alcs', + 'VIEW', + 'non_application_search_view', + 'SELECT\n non_applications."uuid"\n ,non_applications."file_number"\n ,non_applications."applicant" \n ,non_applications."type"\n ,non_applications."class"\n ,non_applications."local_government_uuid" as "local_government_uuid"\n ,non_applications."card_uuid"\n ,non_applications."board_code"\n FROM\n (\n SELECT\n cov.uuid AS "uuid",\n cov.file_number AS "file_number",\n "applicant",\n NULL AS "type",\n \'COV\' AS "class",\n cov.local_government_uuid AS "local_government_uuid",\n card.uuid AS "card_uuid",\n board.code AS "board_code"\n FROM\n alcs.covenant cov\n LEFT JOIN alcs.card card ON\n cov.card_uuid = card.uuid AND card.audit_deleted_date_at IS NULL\n LEFT JOIN alcs.board board ON\n board.uuid = card.board_uuid AND board.audit_deleted_date_at IS NULL\n WHERE cov.audit_deleted_date_at IS NULL\n UNION\n SELECT\n planning_review.uuid AS "uuid",\n planning_review.file_number AS "file_number",\n NULL AS "applicant",\n "type",\n \'PLAN\' AS "class",\n planning_review.local_government_uuid AS "local_government_uuid",\n card.uuid AS "card_uuid",\n board.code AS "board_code"\n FROM\n alcs.planning_review planning_review \n LEFT JOIN alcs.card card ON\n planning_review.card_uuid = card.uuid AND card.audit_deleted_date_at IS NULL\n LEFT JOIN alcs.board board ON\n board.uuid = card.board_uuid AND board.audit_deleted_date_at IS NULL\n WHERE planning_review.audit_deleted_date_at IS NULL\n ) AS non_applications', + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "alcs"."typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'non_application_search_view', 'alcs'], + ); + await queryRunner.query(`DROP VIEW "alcs"."non_application_search_view"`); + } +} From 606ff275d9ea4a2643003683b43996e5cf2d3fc6 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 5 Sep 2023 10:03:30 -0700 Subject: [PATCH 334/954] Add SRWs / Notifications * Rename AppParcelType => ParcelType and share across all 3 * Add lots of SRW stuff --- portal-frontend/src/app/app-routing.module.ts | 16 + .../create-submission-dialog.component.html | 52 ++- .../create-submission-dialog.component.scss | 9 + ...create-submission-dialog.component.spec.ts | 5 + .../create-submission-dialog.component.ts | 29 +- .../edit-submission-base.module.ts | 12 + .../edit-submission.component.html | 60 +++ .../edit-submission.component.scss | 346 ++++++++++++++++++ .../edit-submission.component.spec.ts | 46 +++ .../edit-submission.component.ts | 179 +++++++++ .../edit-submission/edit-submission.module.ts | 26 ++ .../edit-submission/files-step.partial.ts | 75 ++++ .../edit-submission/step.partial.ts | 33 ++ ...iew-notification-submission.component.html | 91 +++++ ...iew-notification-submission.component.scss | 183 +++++++++ ...-notification-submission.component.spec.ts | 46 +++ .../view-notification-submission.component.ts | 64 ++++ .../view-notification-submission.module.ts | 19 + .../notification-parcel.dto.ts | 22 ++ .../notification-parcel.service.spec.ts | 101 +++++ .../notification-parcel.service.ts | 87 +++++ .../notification-submission.dto.ts | 26 ++ .../notification-submission.service.spec.ts | 132 +++++++ .../notification-submission.service.ts | 117 ++++++ .../notification-transferee.dto.ts | 50 +++ .../notification-transferee.service.spec.ts | 189 ++++++++++ .../notification-transferee.service.ts | 143 ++++++++ .../notice-of-intent-modification.dto.ts | 2 +- .../notice-of-intent-type.dto.ts | 2 +- .../notice-of-intent-type.entity.ts | 2 +- .../notice-of-intent/notice-of-intent.dto.ts | 2 +- .../notice-of-intent.entity.ts | 2 +- .../notice-of-intent.module.ts | 2 +- .../notice-of-intent.service.spec.ts | 2 +- .../notice-of-intent.service.ts | 2 +- .../notification-type.dto.ts | 13 + .../notification-type.entity.ts | 25 ++ .../notification.controller.spec.ts | 84 +++++ .../notification/notification.controller.ts | 61 +++ .../src/alcs/notification/notification.dto.ts | 76 ++++ .../alcs/notification/notification.entity.ts | 82 +++++ .../alcs/notification/notification.module.ts | 30 ++ .../notification/notification.service.spec.ts | 265 ++++++++++++++ .../alcs/notification/notification.service.ts | 321 ++++++++++++++++ .../notice-of-intent-search-view.entity.ts | 2 +- .../application-parcel.automapper.profile.ts | 20 +- ...ice-of-intent-parcel.automapper.profile.ts | 20 +- .../notice-of-intent.automapper.profile.ts | 4 +- .../notification-parcel.automapper.profile.ts | 46 +++ ...ification-submission.automapper.profile.ts | 55 +++ ...ification-transferee.automapper.profile.ts | 29 ++ .../notification.automapper.profile.ts | 34 ++ .../parcel-ownership-type.entity.ts | 8 + .../application-submission-draft.module.ts | 4 +- ...pplication-parcel-ownership-type.entity.ts | 5 - .../application-parcel.entity.ts | 6 +- .../application-submission.module.ts | 4 +- ...-of-intent-parcel-ownership-type.entity.ts | 5 - .../notice-of-intent-parcel.dto.ts | 6 +- .../notice-of-intent-parcel.entity.ts | 6 +- .../notice-of-intent-submission.module.ts | 6 +- ...otice-of-intent-submission.service.spec.ts | 2 +- .../notification-parcel.controller.spec.ts | 158 ++++++++ .../notification-parcel.controller.ts | 95 +++++ .../notification-parcel.dto.ts | 93 +++++ .../notification-parcel.entity.ts | 90 +++++ .../notification-parcel.service.spec.ts | 199 ++++++++++ .../notification-parcel.service.ts | 92 +++++ ...notification-submission.controller.spec.ts | 264 +++++++++++++ .../notification-submission.controller.ts | 149 ++++++++ .../notification-submission.dto.ts | 59 +++ .../notification-submission.entity.ts | 88 +++++ .../notification-submission.module.ts | 58 +++ .../notification-submission.service.spec.ts | 248 +++++++++++++ .../notification-submission.service.ts | 336 +++++++++++++++++ ...notification-transferee.controller.spec.ts | 202 ++++++++++ .../notification-transferee.controller.ts | 137 +++++++ .../notification-transferee.dto.ts | 63 ++++ .../notification-transferee.entity.ts | 61 +++ .../notification-transferee.service.spec.ts | 217 +++++++++++ .../notification-transferee.service.ts | 196 ++++++++++ .../apps/alcs/src/portal/portal.module.ts | 3 + .../1693608113088-add_notifications.ts | 170 +++++++++ .../1693610085203-seed_notification_tables.ts | 17 + 84 files changed, 6312 insertions(+), 76 deletions(-) create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/edit-submission-base.module.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/step.partial.ts create mode 100644 portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html create mode 100644 portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.scss create mode 100644 portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts create mode 100644 portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts create mode 100644 portal-frontend/src/app/services/notification-parcel/notification-parcel.dto.ts create mode 100644 portal-frontend/src/app/services/notification-parcel/notification-parcel.service.spec.ts create mode 100644 portal-frontend/src/app/services/notification-parcel/notification-parcel.service.ts create mode 100644 portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts create mode 100644 portal-frontend/src/app/services/notification-submission/notification-submission.service.spec.ts create mode 100644 portal-frontend/src/app/services/notification-submission/notification-submission.service.ts create mode 100644 portal-frontend/src/app/services/notification-transferee/notification-transferee.dto.ts create mode 100644 portal-frontend/src/app/services/notification-transferee/notification-transferee.service.spec.ts create mode 100644 portal-frontend/src/app/services/notification-transferee/notification-transferee.service.ts rename services/apps/alcs/src/alcs/{code/application-code => notice-of-intent}/notice-of-intent-type/notice-of-intent-type.dto.ts (77%) rename services/apps/alcs/src/alcs/{code/application-code => notice-of-intent}/notice-of-intent-type/notice-of-intent-type.entity.ts (86%) create mode 100644 services/apps/alcs/src/alcs/notification/notification-type/notification-type.dto.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification-type/notification-type.entity.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification.controller.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification.dto.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification.entity.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification.module.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification.service.ts create mode 100644 services/apps/alcs/src/common/automapper/notification-parcel.automapper.profile.ts create mode 100644 services/apps/alcs/src/common/automapper/notification-submission.automapper.profile.ts create mode 100644 services/apps/alcs/src/common/automapper/notification-transferee.automapper.profile.ts create mode 100644 services/apps/alcs/src/common/automapper/notification.automapper.profile.ts create mode 100644 services/apps/alcs/src/common/entities/parcel-ownership-type/parcel-ownership-type.entity.ts delete mode 100644 services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel-ownership-type/application-parcel-ownership-type.entity.ts delete mode 100644 services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.dto.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.entity.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.dto.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.entity.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693608113088-add_notifications.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693610085203-seed_notification_tables.ts diff --git a/portal-frontend/src/app/app-routing.module.ts b/portal-frontend/src/app/app-routing.module.ts index 51e9d2e4d3..0a531eb326 100644 --- a/portal-frontend/src/app/app-routing.module.ts +++ b/portal-frontend/src/app/app-routing.module.ts @@ -88,6 +88,22 @@ const routes: Routes = [ loadChildren: () => import('./features/notice-of-intents/edit-submission/edit-submission.module').then((m) => m.EditSubmissionModule), }, + { + title: 'View SRW', + path: 'notification/:fileId', + canActivate: [AuthGuard], + loadChildren: () => + import('./features/notifications/view-submission/view-notification-submission.module').then( + (m) => m.ViewNotificationSubmissionModule + ), + }, + { + title: 'Edit SRW', + path: 'notification/:fileId/edit', + canActivate: [AuthGuard], + loadChildren: () => + import('./features/notifications/edit-submission/edit-submission.module').then((m) => m.EditSubmissionModule), + }, { path: '', redirectTo: '/login', pathMatch: 'full' }, ]; diff --git a/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html index c263c8a6cf..6b82c5c50b 100644 --- a/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html +++ b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.html @@ -10,7 +10,11 @@

Select an option to learn more about the submission type.
- + + +
+ Submitting a notification of Statutory Right of Way (SRW) only applies to a SRW described in + s. 218 of the Land Title Act. +
+
+
+ Before you create a notification of SRW: +
+
+
    +
  1. + Permitted Uses: If you intend to use, construct works, or remove soil or place fill (including gravel) + within the SRW beyond what is permitted in the ALR Use Regulation, application or notice of intent to the + ALC is required. Creating a notification of SRW in no way constitutes such an approval. +
  2. +
  3. + ALC Decision Condition Compliance: This notification of SRW is not meant to submit or confirm ALC decision + condition compliance (e.g. subdivision plan), please refer back to the ALC decision letter for + instructions. +
  4. +
  5. + Submitting a Notification of SRW: Ensure that you are ready to submit your SRW package to the Land Title + Survey Authority and all necessary documents including the ‘Terms of SRW’ and survey plan (if any) have + been signed and finalized. You will not be able to submit this notification of SRW to the ALC without + attaching the finalized Terms of SRW. +
  6. +
+ The ALC's automatically generated notification response will need to be attached as a supporting document to + your Land Title Survey Authority SRW package. +

+ If you have any questions, please contact the ALC.
+ Email: ALC.Portal@gov.bc.ca
+ Phone: 236-468-3342 or 1-800-663-7867 +
+ Read {{ readMoreClicked ? 'Less' : 'More' }} +
+

@@ -125,6 +171,10 @@

create

+
+ + +
+
+ + + help_outline + +
+
+
+ + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.scss new file mode 100644 index 0000000000..8125565891 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.scss @@ -0,0 +1,346 @@ +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; + +.header { + margin: rem(24) 0; + display: flex; + justify-content: space-between; + flex-flow: row wrap; + + h6 { + display: flex; + align-items: center; + justify-content: center; + } + + .header-btn-wrapper { + display: flex; + align-items: center; + flex-wrap: wrap; + margin-top: rem(16); + width: 100%; + + button { + width: 100%; + } + } + + .change-app-type-btn-wrapper { + display: flex; + align-items: center; + margin-top: rem(16); + margin-left: 0; + width: 100%; + + button { + width: 100%; + } + + .mat-icon { + margin-left: rem(8); + width: rem(22); + height: rem(22); + font-size: rem(22); + } + } + + @media screen and (min-width: $tabletBreakpoint) { + .header-btn-wrapper { + margin-top: 0; + width: unset; + + button { + width: auto; + } + + .change-app-type-btn-wrapper { + width: auto; + display: flex; + align-items: center; + margin-top: 0; + margin-left: rem(16); + + button { + width: auto; + } + + .mat-icon { + margin-left: rem(16); + font-size: rem(24); + } + } + } + } +} + +:host::ng-deep { + .step-description { + display: grid; + grid-template-columns: 1fr; + grid-row-gap: rem(24); + margin-bottom: rem(32); + } + + .step-documents { + border: rem(1) solid colors.$secondary-color; + border-radius: rem(4); + padding: rem(8); + + h6 { + color: colors.$secondary-color; + margin-top: unset !important; + } + } + + .mat-icon.mat-icon-inline { + line-height: initial; + } + + .mat-horizontal-content-container { + padding: 0; + min-height: 100%; + } + + .mat-horizontal-stepper-wrapper { + min-height: 100%; + } + + .subtext { + margin-bottom: rem(8) !important; + } + + .mat-step-header .mat-step-icon-selected { + color: #fff; + } + + .mat-step-header .mat-step-label.mat-step-label-active { + display: none; + } + + .mat-step-label { + display: none; + } + + .section { + margin: rem(24) 0; + } + + .date-picker { + ::ng-deep.mat-form-field-infix { + display: flex; + padding: rem(3) 0; + } + } + + .mat-button-wrapper { + white-space: break-spaces; + } + + .parcel-buttons-wrappers button { + margin-top: 1.2rem !important; + } + + @media screen and (min-width: $tabletBreakpoint) { + .mat-step-header .mat-step-label.mat-step-label-active { + display: inherit; + } + + .mat-step-label { + display: inherit; + } + } + + .review-step { + min-height: 100%; + } + + .mobile-hidden { + display: none !important; + + @media screen and (min-width: $tabletBreakpoint) { + display: initial !important; + } + } + + .tablet-hidden { + display: initial !important; + + @media screen and (min-width: $tabletBreakpoint) { + display: none !important; + } + } + + .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { + line-height: rem(33); + } + + .mat-button-toggle-checked.mat-button-toggle-appearance-standard { + background-color: colors.$primary-color-light; + } + + .edit-application { + .description { + margin-top: rem(12); + margin-bottom: rem(20); + display: flex; + flex-direction: column; + + div { + display: flex; + align-items: center; + } + + .save-button { + margin-top: rem(16) !important; + width: 100%; + } + + @media screen and (min-width: $tabletBreakpoint) { + flex-direction: row; + justify-content: space-between; + + .save-button { + margin-left: rem(12) !important; + margin-top: unset !important; + width: unset; + } + } + } + + .button-container { + margin-top: rem(40); + margin-bottom: rem(24); + display: flex; + flex-direction: column-reverse; + justify-content: space-between; + + @media screen and (min-width: $tabletBreakpoint) { + flex-direction: row; + justify-content: space-between; + } + + div { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: rem(8); + + @media screen and (min-width: $tabletBreakpoint) { + display: flex; + justify-content: space-between; + } + + button { + margin-bottom: rem(24) !important; + + @media screen and (min-width: $tabletBreakpoint) { + margin-left: rem(24) !important; + margin-bottom: 0 !important; + } + } + } + } + } + + // parcel entry details + .float-right { + float: right; + } + + .container { + margin: rem(20) 0 rem(20) 0; + } + + .parcel-checkbox { + margin: rem(20) 0 0 0; + } + + .type { + margin-bottom: rem(30); + } + + .pmbc-search { + border: 1px solid colors.$primary-color-dark; + border-radius: rem(4); + background-color: rgba(colors.$accent-color-light, 0.2); + padding: rem(16); + } + + .field-error { + color: colors.$error-color; + font-size: rem(15); + font-weight: 700; + display: flex; + align-items: center; + margin-top: rem(4); + } + + .form-row { + margin-top: rem(16); + display: grid; + grid-template-columns: 1fr; + grid-column-gap: rem(30); + + @media screen and (min-width: $desktopBreakpoint) { + & { + grid-template-columns: 1fr 1fr; + } + + .full-row { + grid-column: 1/3; + } + } + } + + .radio-row { + margin: rem(10) 0; + display: block; + + .mat-radio-button { + margin-right: rem(12); + } + } + + .full-width-input { + width: 100%; + } + + .mat-expansion-panel { + margin-bottom: rem(16); + } + + .mat-checkbox { + display: flex; + justify-content: center; + .mat-checkbox-layout { + white-space: break-spaces; + } + } + + .flex-center-wrap { + display: flex; + justify-content: center; + flex-wrap: wrap; + } + + .flex-evenly-wrap { + display: flex; + justify-content: space-evenly; + flex-wrap: wrap; + } + + .flex-space-between-wrap { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + } + + .owner-table-wrapper { + display: block; + overflow: auto; + width: 100%; + } + + .display-block { + display: block; + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.spec.ts new file mode 100644 index 0000000000..cc48c1cd01 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.spec.ts @@ -0,0 +1,46 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute } from '@angular/router'; +import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; +import { ToastService } from '../../../services/toast/toast.service'; + +import { EditSubmissionComponent } from './edit-submission.component'; + +describe('EditSubmissionComponent', () => { + let component: EditSubmissionComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [EditSubmissionComponent], + providers: [ + { + provide: NotificationSubmissionService, + useValue: {}, + }, + { + provide: ToastService, + useValue: {}, + }, + { + provide: ActivatedRoute, + useValue: {}, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(EditSubmissionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts new file mode 100644 index 0000000000..42bf3c4530 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts @@ -0,0 +1,179 @@ +import { StepperSelectionEvent } from '@angular/cdk/stepper'; +import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { + NOI_SUBMISSION_STATUS, + NoticeOfIntentSubmissionDetailedDto, +} from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NotificationSubmissionDetailedDto } from '../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; +import { ToastService } from '../../../services/toast/toast.service'; +import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; +import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; +import { scrollToElement } from '../../../shared/utils/scroll-helper'; + +export enum EditNotificationSteps { + Parcel = 0, + Transferees = 1, + PrimaryContact = 2, + Government = 3, + Proposal = 4, + Attachments = 5, + ReviewAndSubmit = 6, +} + +@Component({ + selector: 'app-edit-submission', + templateUrl: './edit-submission.component.html', + styleUrls: ['./edit-submission.component.scss'], +}) +export class EditSubmissionComponent implements OnDestroy, AfterViewInit { + fileId = ''; + + $destroy = new Subject(); + $notificationSubmission = new BehaviorSubject(undefined); + $notificationDocuments = new BehaviorSubject([]); + notificationSubmission: NotificationSubmissionDetailedDto | undefined; + + steps = EditNotificationSteps; + expandedParcelUuid?: string; + showValidationErrors = false; + + @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; + + constructor( + private notificationSubmissionService: NotificationSubmissionService, + private activatedRoute: ActivatedRoute, + private dialog: MatDialog, + private toastService: ToastService, + private overlayService: OverlaySpinnerService, + private router: Router + ) { + this.expandedParcelUuid = undefined; + + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + this.notificationSubmission = submission; + }); + } + + ngAfterViewInit(): void { + combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.paramMap]) + .pipe(takeUntil(this.$destroy)) + .subscribe(([queryParamMap, paramMap]) => { + const fileId = paramMap.get('fileId'); + if (fileId) { + this.loadSubmission(fileId).then(() => { + const stepInd = paramMap.get('stepInd'); + const parcelUuid = queryParamMap.get('parcelUuid'); + const showErrors = queryParamMap.get('errors'); + if (showErrors) { + this.showValidationErrors = showErrors === 't'; + } + + if (stepInd) { + // setTimeout is required for stepper to be initialized + setTimeout(() => { + this.customStepper.navigateToStep(parseInt(stepInd), true); + + if (parcelUuid) { + this.expandedParcelUuid = parcelUuid; + } + }); + } + }); + } + }); + } + + async onExit() { + await this.router.navigateByUrl(`/notification/${this.fileId}`); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + // this gets fired whenever applicant navigates away from edit page + async canDeactivate(): Promise> { + await this.saveSubmission(this.customStepper.selectedIndex); + + return of(true); + } + + async onStepChange($event: StepperSelectionEvent) { + // scrolls to step if step selected programmatically + scrollToElement({ id: `stepWrapper_${$event.selectedIndex}`, center: false }); + } + + async switchStep(index: number) { + // navigation to url will cause step change based on the index (index starts from 0) + // The save will be triggered using canDeactivate guard + this.showValidationErrors = this.customStepper.selectedIndex === EditNotificationSteps.ReviewAndSubmit; + await this.router.navigateByUrl(`notification/${this.fileId}/edit/${index}`); + } + + async saveSubmission(step: number) { + switch (step) { + case EditNotificationSteps.Parcel: + case EditNotificationSteps.Transferees: + case EditNotificationSteps.PrimaryContact: + case EditNotificationSteps.Government: + case EditNotificationSteps.Proposal: + case EditNotificationSteps.Attachments: + case EditNotificationSteps.ReviewAndSubmit: + //DO NOTHING + break; + default: + this.toastService.showErrorToast('Error updating SRW.'); + } + } + + async onDownloadPdf(fileNumber: string | undefined) { + if (fileNumber) { + //TODO: Hook this up later + } + } + + onChangeSubmissionType() { + //TODO + } + + async onSubmit() { + //TODO + } + + private async submit() { + const submission = this.notificationSubmission; + if (submission) { + const didSubmit = await this.notificationSubmissionService.submitToAlcs(submission.uuid); + if (didSubmit) { + await this.router.navigateByUrl(`/notification/${submission?.fileNumber}`); + } + } + } + + private async loadSubmission(fileId: string, reload = false) { + if (!this.notificationSubmission || reload) { + this.overlayService.showSpinner(); + this.notificationSubmission = await this.notificationSubmissionService.getByFileId(fileId); + this.fileId = fileId; + + // if (this.notificationSubmission?.status.code !== NOI_SUBMISSION_STATUS.IN_PROGRESS) { + // this.toastService.showErrorToast('Unable to edit Notice of Intent'); + // await this.router.navigateByUrl(`/home`); + // } + + const documents: NoticeOfIntentDocumentDto[] = []; //TODO await this.noticeOfIntentDocumentService.getByFileId(fileId); + if (documents) { + this.$notificationDocuments.next(documents); + } + + this.$notificationSubmission.next(this.notificationSubmission); + this.overlayService.hideSpinner(); + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts new file mode 100644 index 0000000000..ba969b4f33 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; +import { SharedModule } from '../../../shared/shared.module'; +import { EditSubmissionBaseModule } from './edit-submission-base.module'; +import { EditSubmissionComponent } from './edit-submission.component'; +import { StepComponent } from './step.partial'; + +const routes: Routes = [ + { + path: '', + component: EditSubmissionComponent, + }, + { + path: ':stepInd', + component: EditSubmissionComponent, + canDeactivate: [CanDeactivateGuard], + }, +]; + +@NgModule({ + declarations: [StepComponent], + imports: [CommonModule, SharedModule, RouterModule.forChild(routes), EditSubmissionBaseModule], +}) +export class EditSubmissionModule {} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts new file mode 100644 index 0000000000..5c5cdcf681 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts @@ -0,0 +1,75 @@ +import { Component, Input } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { BehaviorSubject } from 'rxjs'; +import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; +import { FileHandle } from '../../../shared/file-drag-drop/drag-drop.directive'; +import { RemoveFileConfirmationDialogComponent } from '../../applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; +import { StepComponent } from './step.partial'; + +@Component({ + selector: 'app-file-step', + template: '

', + styleUrls: [], +}) +export abstract class FilesStepComponent extends StepComponent { + @Input() $noiDocuments!: BehaviorSubject; + + DOCUMENT_TYPE = DOCUMENT_TYPE; + + protected fileId = ''; + + protected abstract save(): Promise; + + protected constructor( + protected noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + protected dialog: MatDialog + ) { + super(); + } + + async attachFile(file: FileHandle, documentType: DOCUMENT_TYPE | null) { + if (this.fileId) { + await this.save(); + const mappedFiles = file.file; + await this.noticeOfIntentDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); + const documents = await this.noticeOfIntentDocumentService.getByFileId(this.fileId); + if (documents) { + this.$noiDocuments.next(documents); + } + } + } + + async onDeleteFile($event: NoticeOfIntentDocumentDto) { + if (this.draftMode) { + this.dialog + .open(RemoveFileConfirmationDialogComponent) + .beforeClosed() + .subscribe(async (didConfirm) => { + if (didConfirm) { + this.deleteFile($event); + } + }); + } else { + await this.deleteFile($event); + } + } + + private async deleteFile($event: NoticeOfIntentDocumentDto) { + await this.noticeOfIntentDocumentService.deleteExternalFile($event.uuid); + if (this.fileId) { + const documents = await this.noticeOfIntentDocumentService.getByFileId(this.fileId); + if (documents) { + this.$noiDocuments.next(documents); + } + } + } + + async openFile(uuid: string) { + const res = await this.noticeOfIntentDocumentService.openFile(uuid); + if (res) { + window.open(res.url, '_blank'); + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/step.partial.ts b/portal-frontend/src/app/features/notifications/edit-submission/step.partial.ts new file mode 100644 index 0000000000..c3d7d8a6ac --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/step.partial.ts @@ -0,0 +1,33 @@ +import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import { NotificationSubmissionDto } from '../../../services/notification-submission/notification-submission.dto'; + +@Component({ + selector: 'app-step', + template: '

', + styleUrls: [], +}) +export class StepComponent implements OnDestroy { + protected $destroy = new Subject(); + + @Input() $notificationSubmission!: BehaviorSubject; + + @Input() showErrors = false; + @Input() draftMode = false; + + @Output() navigateToStep = new EventEmitter(); + @Output() exit = new EventEmitter(); + + async onSaveExit() { + this.exit.emit(); + } + + onNavigateToStep(step: number) { + this.navigateToStep.emit(step); + } + + async ngOnDestroy() { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html new file mode 100644 index 0000000000..421cdfab6d --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html @@ -0,0 +1,91 @@ + + + +
+ +
+
+ +
diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.scss b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.scss new file mode 100644 index 0000000000..2b720dbcd3 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.scss @@ -0,0 +1,183 @@ +@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; + } + } +} + +.buttons { + display: flex; + flex-direction: column-reverse; + justify-content: center; + + button { + width: 100%; + margin-bottom: rem(16) !important; + } + + @media screen and (min-width: $tabletBreakpoint) { + flex-direction: row; + + div { + margin-right: rem(16); + } + + div:last-child { + margin-right: 0 !important; + } + } +} + +.action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + width: 100%; + background-color: colors.$primary-color-light; + display: flex; + justify-content: center; + align-items: center; + height: rem(56); + z-index: 2; + + button { + width: 100%; + height: 100%; + text-align: right !important; + display: flex; + justify-content: flex-end; + padding: rem(8) rem(24); + + @media screen and (min-width: $tabletBreakpoint) { + padding: rem(12) rem(36); + } + + @media screen and (min-width: $desktopBreakpoint) { + padding: rem(18) rem(80); + } + } + + div { + display: flex; + align-items: center; + justify-content: center; + + .mat-icon { + margin-left: rem(6); + } + } +} + +.page-selector { + width: 100%; + margin-bottom: rem(24); +} + +.change-step { + width: 100%; +} + +.no-comment { + color: colors.$grey-dark; + font-style: italic; +} + +.tractor { + width: 100%; + margin-bottom: rem(16); +} + +section { + margin-bottom: rem(24); +} + +.document { + margin-bottom: rem(8); +} diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.spec.ts b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.spec.ts new file mode 100644 index 0000000000..bbbfa7a822 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.spec.ts @@ -0,0 +1,46 @@ +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 { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; + +import { ViewNotificationSubmissionComponent } from './view-notification-submission.component'; + +describe('ViewNotificationSubmissionComponent', () => { + let component: ViewNotificationSubmissionComponent; + let fixture: ComponentFixture; + + let mockNotificationSubmissionService: DeepMocked; + let mockActivatedRoute: DeepMocked; + + beforeEach(async () => { + mockNotificationSubmissionService = createMock(); + mockActivatedRoute = createMock(); + + mockActivatedRoute.paramMap = new BehaviorSubject(new Map()); + + await TestBed.configureTestingModule({ + declarations: [ViewNotificationSubmissionComponent], + providers: [ + { + provide: NotificationSubmissionService, + useValue: mockNotificationSubmissionService, + }, + { + provide: ActivatedRoute, + useValue: mockActivatedRoute, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ViewNotificationSubmissionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts new file mode 100644 index 0000000000..3bafb2e64c --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts @@ -0,0 +1,64 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NotificationSubmissionDto } from '../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; + +@Component({ + selector: 'app-view-notification-submission', + templateUrl: './view-notification-submission.component.html', + styleUrls: ['./view-notification-submission.component.scss'], +}) +export class ViewNotificationSubmissionComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + $notificationSubmission = new BehaviorSubject(undefined); + submission: NotificationSubmissionDto | undefined; + + constructor( + private notificationSubmissionService: NotificationSubmissionService, + private route: ActivatedRoute, + private router: Router + ) {} + + ngOnInit(): void { + this.route.paramMap.pipe(takeUntil(this.$destroy)).subscribe((paramMap) => { + const fileId = paramMap.get('fileId'); + if (fileId) { + this.loadSubmission(fileId); + this.loadDocuments(fileId); + } + }); + } + + async loadSubmission(fileId: string) { + const notificationSubmission = await this.notificationSubmissionService.getByFileId(fileId); + this.submission = notificationSubmission; + this.$notificationSubmission.next(notificationSubmission); + } + + async loadDocuments(fileId: string) { + // const documents = await this.noiDocumentService.getByFileId(fileId); + // if (documents) { + // this.$noiDocuments.next(documents); + // } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + async onNavigateHome() { + await this.router.navigateByUrl(`home`); + } + + async onCancel(uuid: string) { + await this.notificationSubmissionService.cancel(uuid); + await this.router.navigateByUrl(`home`); + } + + onDownloadSubmissionPdf(fileNumber: string) { + //TODO: When we add PDFs + } +} diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts new file mode 100644 index 0000000000..700cda7875 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts @@ -0,0 +1,19 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; +import { SharedModule } from '../../../shared/shared.module'; +import { ViewNotificationSubmissionComponent } from './view-notification-submission.component'; + +const routes: Routes = [ + { + path: '', + component: ViewNotificationSubmissionComponent, + }, +]; + +@NgModule({ + imports: [CommonModule, SharedModule, RouterModule.forChild(routes), NgxMaskDirective, NgxMaskPipe], + declarations: [ViewNotificationSubmissionComponent], +}) +export class ViewNotificationSubmissionModule {} diff --git a/portal-frontend/src/app/services/notification-parcel/notification-parcel.dto.ts b/portal-frontend/src/app/services/notification-parcel/notification-parcel.dto.ts new file mode 100644 index 0000000000..0e7ec19785 --- /dev/null +++ b/portal-frontend/src/app/services/notification-parcel/notification-parcel.dto.ts @@ -0,0 +1,22 @@ +import { BaseCodeDto } from '../../shared/dto/base.dto'; + +export interface NotificationParcelUpdateDto { + uuid: string; + pid?: string | null; + pin?: string | null; + civicAddress?: string | null; + legalDescription?: string | null; + mapAreaHectares?: string | null; + ownershipTypeCode?: string | null; + crownLandOwnerType?: string | null; + isConfirmedByApplicant: boolean; +} + +export interface NotificationParcelDto extends Omit { + ownershipType?: BaseCodeDto; +} + +export enum PARCEL_OWNERSHIP_TYPE { + FEE_SIMPLE = 'SMPL', + CROWN = 'CRWN', +} diff --git a/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.spec.ts b/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.spec.ts new file mode 100644 index 0000000000..b66c572ec1 --- /dev/null +++ b/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.spec.ts @@ -0,0 +1,101 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; +import { NotificationParcelUpdateDto } from './notification-parcel.dto'; +import { NotificationParcelService } from './notification-parcel.service'; + +describe('NotificationParcelService', () => { + let service: NotificationParcelService; + let mockHttpClient: DeepMocked; + let mockDocumentService: DeepMocked; + let mockToastService: DeepMocked; + + const mockUuid = 'fake_uuid'; + + beforeEach(() => { + mockHttpClient = createMock(); + mockToastService = createMock(); + + TestBed.configureTestingModule({ + imports: [], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + ], + }); + service = TestBed.inject(NotificationParcelService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for loading parcels', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.fetchBySubmissionUuid(mockUuid); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notification-parcel'); + }); + + it('should show an error toast if getting parcel fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.fetchBySubmissionUuid(mockUuid); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for create', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.create(mockUuid); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-parcel'); + }); + + it('should show an error toast if creating a parcel fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.create(mockUuid); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a put request for update', async () => { + mockHttpClient.put.mockReturnValue(of({})); + let mockUuid = 'fake'; + + await service.update([{ uuid: mockUuid }] as NotificationParcelUpdateDto[]); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockToastService.showSuccessToast).toHaveBeenCalledTimes(1); + expect(mockHttpClient.put.mock.calls[0][0]).toContain('notification-parcel'); + }); + + it('should show an error toast if updating a parcel fails', async () => { + mockHttpClient.put.mockReturnValue(throwError(() => ({}))); + + await service.update([{}] as NotificationParcelUpdateDto[]); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.ts b/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.ts new file mode 100644 index 0000000000..8ad3bcd735 --- /dev/null +++ b/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.ts @@ -0,0 +1,87 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; +import { NotificationParcelDto, NotificationParcelUpdateDto } from './notification-parcel.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationParcelService { + private serviceUrl = `${environment.apiUrl}/notification-parcel`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private documentService: DocumentService, + private overlayService: OverlaySpinnerService + ) {} + + async fetchBySubmissionUuid(submissionUuid: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/submission/${submissionUuid}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Parcel, please try again later'); + } + return undefined; + } + + async create(notificationSubmissionUuid: string, ownerUuid?: string) { + try { + return await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}`, { + noticeOfIntentSubmissionUuid: notificationSubmissionUuid, + ownerUuid, + }) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to create Parcel, please try again later'); + return undefined; + } + } + + async update(updateDtos: NotificationParcelUpdateDto[]) { + try { + this.overlayService.showSpinner(); + const formattedDtos = updateDtos.map((e) => ({ + ...e, + mapAreaHectares: e.mapAreaHectares ? parseFloat(e.mapAreaHectares) : e.mapAreaHectares, + })); + + const result = await firstValueFrom( + this.httpClient.put(`${this.serviceUrl}`, formattedDtos) + ); + + this.toastService.showSuccessToast('Parcel saved'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Parcel, please try again later'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } + + async deleteMany(parcelUuids: string[]) { + try { + this.overlayService.showSpinner(); + const result = await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}`, { body: parcelUuids })); + this.toastService.showSuccessToast('Parcel deleted'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete Parcel, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } +} diff --git a/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts b/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts new file mode 100644 index 0000000000..4cc056db23 --- /dev/null +++ b/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts @@ -0,0 +1,26 @@ +import { NotificationTransfereeDto } from '../notification-transferee/notification-transferee.dto'; + +export interface NotificationSubmissionDto { + fileNumber: string; + uuid: string; + createdAt: number; + updatedAt: number; + applicant: string; + localGovernmentUuid: string; + type: string; + typeCode: string; + lastStatusUpdate: number; + owners: NotificationTransfereeDto[]; + canEdit: boolean; + canView: boolean; +} + +export interface NotificationSubmissionDetailedDto extends NotificationSubmissionDto { + purpose: string | null; +} + +export interface NotificationSubmissionUpdateDto { + applicant?: string | null; + purpose?: string | null; + localGovernmentUuid?: string | null; +} diff --git a/portal-frontend/src/app/services/notification-submission/notification-submission.service.spec.ts b/portal-frontend/src/app/services/notification-submission/notification-submission.service.spec.ts new file mode 100644 index 0000000000..4f902eb57d --- /dev/null +++ b/portal-frontend/src/app/services/notification-submission/notification-submission.service.spec.ts @@ -0,0 +1,132 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../toast/toast.service'; + +import { NotificationSubmissionService } from './notification-submission.service'; + +describe('NotificationSubmissionService', () => { + let service: NotificationSubmissionService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NotificationSubmissionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for loading notifications', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.getNotifications(); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notification'); + }); + + it('should show an error toast if getting notifications fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.getNotifications(); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a get request for loading a single notification', async () => { + mockHttpClient.get.mockReturnValue(of({})); + let mockFileId = 'file-id'; + + await service.getByFileId(mockFileId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notification'); + expect(mockHttpClient.get.mock.calls[0][0]).toContain(mockFileId); + }); + + it('should show an error toast if getting a specific notification fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.getByFileId('file-id'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for create', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.create(); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification'); + }); + + it('should show an error toast if creating an notification fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.create(); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a put request for update', async () => { + mockHttpClient.put.mockReturnValue(of({})); + let mockFileId = 'fileId'; + + await service.updatePending('fileId', {}); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockHttpClient.put.mock.calls[0][0]).toContain('notification'); + expect(mockHttpClient.put.mock.calls[0][0]).toContain(mockFileId); + }); + + it('should show an error toast if updating an notification fails', async () => { + mockHttpClient.put.mockReturnValue(throwError(() => ({}))); + + await service.updatePending('file-id', {}); + + expect(mockHttpClient.put).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for cancelling', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.cancel('fileId'); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification'); + }); + + it('should show an error toast if cancelling a file fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.cancel('fileId'); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notification-submission/notification-submission.service.ts b/portal-frontend/src/app/services/notification-submission/notification-submission.service.ts new file mode 100644 index 0000000000..088aa3c689 --- /dev/null +++ b/portal-frontend/src/app/services/notification-submission/notification-submission.service.ts @@ -0,0 +1,117 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { ToastService } from '../toast/toast.service'; +import { + NotificationSubmissionDetailedDto, + NotificationSubmissionDto, + NotificationSubmissionUpdateDto, +} from './notification-submission.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationSubmissionService { + private serviceUrl = `${environment.apiUrl}/notification-submission`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private overlayService: OverlaySpinnerService + ) {} + + async getNotifications() { + try { + return await firstValueFrom(this.httpClient.get(`${this.serviceUrl}`)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load SRWs, please try again later'); + return []; + } + } + + async getByFileId(fileId: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/notification/${fileId}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load SRW, please try again later'); + return undefined; + } + } + + async getByUuid(uuid: string) { + try { + return await firstValueFrom(this.httpClient.get(`${this.serviceUrl}/${uuid}`)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load SRW, please try again later'); + return undefined; + } + } + + async create() { + try { + this.overlayService.showSpinner(); + return await firstValueFrom(this.httpClient.post<{ fileId: string }>(`${this.serviceUrl}`, {})); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to create SRW, please try again later'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } + + async updatePending(uuid: string, updateDto: NotificationSubmissionUpdateDto) { + try { + this.overlayService.showSpinner(); + const result = await firstValueFrom( + this.httpClient.put(`${this.serviceUrl}/${uuid}`, updateDto) + ); + this.toastService.showSuccessToast('SRW Saved'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update SRW, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + + return undefined; + } + + async cancel(uuid: string) { + try { + this.overlayService.showSpinner(); + return await firstValueFrom(this.httpClient.post<{ fileId: string }>(`${this.serviceUrl}/${uuid}/cancel`, {})); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to cancel SRW, please try again later'); + } finally { + this.overlayService.hideSpinner(); + } + return undefined; + } + + async submitToAlcs(uuid: string) { + let res; + try { + this.overlayService.showSpinner(); + res = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/alcs/submit/${uuid}`, {}) + ); + this.toastService.showSuccessToast('SRW Submitted'); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to submit SRW, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + return res; + } +} diff --git a/portal-frontend/src/app/services/notification-transferee/notification-transferee.dto.ts b/portal-frontend/src/app/services/notification-transferee/notification-transferee.dto.ts new file mode 100644 index 0000000000..2f796b2fe2 --- /dev/null +++ b/portal-frontend/src/app/services/notification-transferee/notification-transferee.dto.ts @@ -0,0 +1,50 @@ +import { BaseCodeDto } from '../../shared/dto/base.dto'; + +export enum OWNER_TYPE { + INDIVIDUAL = 'INDV', + ORGANIZATION = 'ORGZ', + AGENT = 'AGEN', + CROWN = 'CRWN', + GOVERNMENT = 'GOVR', +} + +export interface OwnerTypeDto extends BaseCodeDto { + code: OWNER_TYPE; +} + +export interface NotificationTransfereeDto { + uuid: string; + notificationSubmissionUuid: string; + displayName: string; + firstName: string | null; + lastName: string | null; + organizationName: string | null; + phoneNumber: string | null; + email: string | null; + type: OwnerTypeDto; +} + +export interface NotificationOwnerUpdateDto { + firstName?: string | null; + lastName?: string | null; + organizationName?: string | null; + phoneNumber: string; + email: string; + typeCode: string; + corporateSummaryUuid?: string | null; +} + +export interface NotificationOwnerCreateDto extends NotificationOwnerUpdateDto { + noticeOfIntentSubmissionUuid: string; +} + +export interface SetPrimaryContactDto { + firstName?: string; + lastName?: string; + organization?: string; + phoneNumber?: string; + email?: string; + type?: OWNER_TYPE; + ownerUuid?: string; + noticeOfIntentSubmissionUuid: string; +} diff --git a/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.spec.ts b/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.spec.ts new file mode 100644 index 0000000000..bb8b61c98d --- /dev/null +++ b/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.spec.ts @@ -0,0 +1,189 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; + +import { NotificationTransfereeService } from './notification-transferee.service'; + +describe('NotificationTransfereeService', () => { + let service: NotificationTransfereeService; + let mockHttpClient: DeepMocked; + let mockToastService: DeepMocked; + let mockDocumentService: DeepMocked; + + let fileId = '123'; + + beforeEach(() => { + mockHttpClient = createMock(); + mockToastService = createMock(); + mockDocumentService = createMock(); + + TestBed.configureTestingModule({ + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + ], + }); + service = TestBed.inject(NotificationTransfereeService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for loading owners', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.fetchBySubmissionId(fileId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notification-transferee'); + }); + + it('should show an error toast if getting owners fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.fetchBySubmissionId(fileId); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for create', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.create({ + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-transferee'); + }); + + it('should show an error toast if creating owner fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.create({ + noticeOfIntentSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a patch request for update', async () => { + mockHttpClient.patch.mockReturnValue(of({})); + + await service.update('', { + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch.mock.calls[0][0]).toContain('notification-transferee'); + }); + + it('should show an error toast if updating owner fails', async () => { + mockHttpClient.patch.mockReturnValue(throwError(() => ({}))); + + await service.update('', { + email: '', + phoneNumber: '', + typeCode: '', + }); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a delete request for delete', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.delete(''); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete.mock.calls[0][0]).toContain('notification-transferee'); + }); + + it('should show an error toast if delete owner fails', async () => { + mockHttpClient.delete.mockReturnValue(throwError(() => ({}))); + + await service.delete(''); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for removeFromParcel', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.removeFromParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-transferee'); + }); + + it('should show an error toast if removeFromParcel', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.removeFromParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for linkToParcel', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.linkToParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-transferee'); + }); + + it('should show an error toast if linkToParcel', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.linkToParcel('', ''); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for setPrimaryContact', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.setPrimaryContact({ noticeOfIntentSubmissionUuid: '' }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-transferee'); + }); + + it('should show an error toast if setPrimaryContact', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.setPrimaryContact({ noticeOfIntentSubmissionUuid: '' }); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.ts b/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.ts new file mode 100644 index 0000000000..f5ea226734 --- /dev/null +++ b/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.ts @@ -0,0 +1,143 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; +import { + NotificationOwnerCreateDto, + NotificationTransfereeDto, + NotificationOwnerUpdateDto, + SetPrimaryContactDto, +} from './notification-transferee.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationTransfereeService { + private serviceUrl = `${environment.apiUrl}/notification-transferee`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private documentService: DocumentService + ) {} + async fetchBySubmissionId(submissionUuid: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/submission/${submissionUuid}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Owners, please try again later'); + } + return undefined; + } + + async create(dto: NotificationOwnerCreateDto) { + try { + const res = await firstValueFrom(this.httpClient.post(`${this.serviceUrl}`, dto)); + this.toastService.showSuccessToast('Owner created'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to create Owner, please try again later'); + return undefined; + } + } + + async update(uuid: string, updateDto: NotificationOwnerUpdateDto) { + try { + const res = await firstValueFrom( + this.httpClient.patch(`${this.serviceUrl}/${uuid}`, updateDto) + ); + this.toastService.showSuccessToast('Owner saved'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Owner, please try again later'); + return undefined; + } + } + + async setPrimaryContact(updateDto: SetPrimaryContactDto) { + try { + const res = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/setPrimaryContact`, updateDto) + ); + this.toastService.showSuccessToast('Notice of Intent saved'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update Notice of Intent, please try again later'); + return undefined; + } + } + + async delete(uuid: string) { + try { + const result = await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}/${uuid}`)); + this.toastService.showSuccessToast('Owner deleted'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete Owner, please try again'); + } + return undefined; + } + + async removeFromParcel(ownerUuid: string, parcelUuid: string) { + try { + const result = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/${ownerUuid}/unlink/${parcelUuid}`, {}) + ); + this.toastService.showSuccessToast('Owner removed from parcel'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to remove Owner, please try again'); + } + return undefined; + } + + async linkToParcel(ownerUuid: any, parcelUuid: string) { + try { + const result = await firstValueFrom( + this.httpClient.post(`${this.serviceUrl}/${ownerUuid}/link/${parcelUuid}`, {}) + ); + this.toastService.showSuccessToast('Owner linked to parcel'); + return result; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to link Owner, please try again'); + } + return undefined; + } + + sortOwners(a: NotificationTransfereeDto, b: NotificationTransfereeDto) { + if (a.displayName < b.displayName) { + return -1; + } + if (a.displayName > b.displayName) { + return 1; + } + return 0; + } + + async uploadCorporateSummary(noticeOfIntentFileId: string, file: File) { + try { + return await this.documentService.uploadFile<{ uuid: string }>( + noticeOfIntentFileId, + file, + DOCUMENT_TYPE.CORPORATE_SUMMARY, + DOCUMENT_SOURCE.APPLICANT, + `${this.serviceUrl}/attachCorporateSummary` + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to attach document to Owner, please try again'); + } + return undefined; + } +} diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts index 54cffee43d..6b420e72f2 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-modification/notice-of-intent-modification.dto.ts @@ -8,7 +8,7 @@ import { IsString, } from 'class-validator'; import { BaseCodeDto } from '../../../common/dtos/base.dto'; -import { NoticeOfIntentTypeDto } from '../../code/application-code/notice-of-intent-type/notice-of-intent-type.dto'; +import { NoticeOfIntentTypeDto } from '../../notice-of-intent/notice-of-intent-type/notice-of-intent-type.dto'; import { LocalGovernmentDto } from '../../local-government/local-government.dto'; import { CardDto } from '../../card/card.dto'; import { ApplicationRegionDto } from '../../code/application-code/application-region/application-region.dto'; diff --git a/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.dto.ts similarity index 77% rename from services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.dto.ts rename to services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.dto.ts index 0f555620b9..ee97b9c460 100644 --- a/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.dto.ts @@ -1,5 +1,5 @@ import { AutoMap } from '@automapper/classes'; -import { BaseCodeDto } from '../../../../common/dtos/base.dto'; +import { BaseCodeDto } from '../../../common/dtos/base.dto'; export class NoticeOfIntentTypeDto extends BaseCodeDto { @AutoMap() diff --git a/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity.ts similarity index 86% rename from services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity.ts rename to services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity.ts index ca9959746a..13355ff1b2 100644 --- a/services/apps/alcs/src/alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity.ts @@ -1,6 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { Column, Entity } from 'typeorm'; -import { BaseCodeEntity } from '../../../../common/entities/base.code.entity'; +import { BaseCodeEntity } from '../../../common/entities/base.code.entity'; @Entity() export class NoticeOfIntentType extends BaseCodeEntity { diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts index 9b3ee13dae..e9ec34fd2d 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.dto.ts @@ -13,7 +13,7 @@ import { NoticeOfIntentOwnerDto } from '../../portal/notice-of-intent-submission import { NoticeOfIntentSubmissionDetailedDto } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.dto'; import { CardDto } from '../card/card.dto'; import { ApplicationRegionDto } from '../code/application-code/application-region/application-region.dto'; -import { NoticeOfIntentTypeDto } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.dto'; +import { NoticeOfIntentTypeDto } from './notice-of-intent-type/notice-of-intent-type.dto'; import { LocalGovernmentDto } from '../local-government/local-government.dto'; export class AlcsNoticeOfIntentSubmissionDto extends NoticeOfIntentSubmissionDetailedDto { diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts index d4001d18e1..384a1f08d1 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.entity.ts @@ -15,7 +15,7 @@ import { Base } from '../../common/entities/base.entity'; import { ColumnNumericTransformer } from '../../utils/column-numeric-transform'; import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; -import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { NoticeOfIntentType } from './notice-of-intent-type/notice-of-intent-type.entity'; import { LocalGovernment } from '../local-government/local-government.entity'; import { NoticeOfIntentDocument } from './notice-of-intent-document/notice-of-intent-document.entity'; import { NoticeOfIntentSubtype } from './notice-of-intent-subtype.entity'; diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts index 5c73a57a42..98ebf77cc2 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.module.ts @@ -9,7 +9,7 @@ import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submissi import { NoticeOfIntentSubmissionModule } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.module'; import { BoardModule } from '../board/board.module'; import { CardModule } from '../card/card.module'; -import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { NoticeOfIntentType } from './notice-of-intent-type/notice-of-intent-type.entity'; import { CodeModule } from '../code/code.module'; import { LocalGovernmentModule } from '../local-government/local-government.module'; import { NoticeOfIntentDocumentController } from './notice-of-intent-document/notice-of-intent-document.controller'; diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts index 13436a77b9..05edfe12dc 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.spec.ts @@ -15,7 +15,7 @@ import { Board } from '../board/board.entity'; import { Card } from '../card/card.entity'; import { CardService } from '../card/card.service'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; -import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { NoticeOfIntentType } from './notice-of-intent-type/notice-of-intent-type.entity'; import { CodeService } from '../code/code.service'; import { LocalGovernmentService } from '../local-government/local-government.service'; import { NOI_SUBMISSION_STATUS } from './notice-of-intent-submission-status/notice-of-intent-status.dto'; diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts index 81c0130b04..2a48a521ce 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent.service.ts @@ -26,7 +26,7 @@ import { Board } from '../board/board.entity'; import { CARD_TYPE } from '../card/card-type/card-type.entity'; import { Card } from '../card/card.entity'; import { CardService } from '../card/card.service'; -import { NoticeOfIntentType } from '../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { NoticeOfIntentType } from './notice-of-intent-type/notice-of-intent-type.entity'; import { CodeService } from '../code/code.service'; import { LocalGovernmentService } from '../local-government/local-government.service'; import { NOI_SUBMISSION_STATUS } from './notice-of-intent-submission-status/notice-of-intent-status.dto'; diff --git a/services/apps/alcs/src/alcs/notification/notification-type/notification-type.dto.ts b/services/apps/alcs/src/alcs/notification/notification-type/notification-type.dto.ts new file mode 100644 index 0000000000..cdb075dac1 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-type/notification-type.dto.ts @@ -0,0 +1,13 @@ +import { AutoMap } from '@automapper/classes'; +import { BaseCodeDto } from '../../../common/dtos/base.dto'; + +export class NotificationTypeDto extends BaseCodeDto { + @AutoMap() + shortLabel: string; + + @AutoMap() + backgroundColor: string; + + @AutoMap() + textColor: string; +} diff --git a/services/apps/alcs/src/alcs/notification/notification-type/notification-type.entity.ts b/services/apps/alcs/src/alcs/notification/notification-type/notification-type.entity.ts new file mode 100644 index 0000000000..318e214a32 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-type/notification-type.entity.ts @@ -0,0 +1,25 @@ +import { AutoMap } from '@automapper/classes'; +import { Column, Entity } from 'typeorm'; +import { BaseCodeEntity } from '../../../common/entities/base.code.entity'; + +@Entity() +export class NotificationType extends BaseCodeEntity { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @Column() + shortLabel: string; + + @AutoMap() + @Column({ type: 'text', default: '' }) + htmlDescription: string; + + @AutoMap() + @Column({ type: 'text', default: '' }) + portalLabel: string; +} diff --git a/services/apps/alcs/src/alcs/notification/notification.controller.spec.ts b/services/apps/alcs/src/alcs/notification/notification.controller.spec.ts new file mode 100644 index 0000000000..cb749cb202 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification.controller.spec.ts @@ -0,0 +1,84 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; +import { NotificationController } from './notification.controller'; +import { Notification } from './notification.entity'; +import { NotificationService } from './notification.service'; + +describe('NotificationController', () => { + let controller: NotificationController; + let mockService: DeepMocked; + + beforeEach(async () => { + mockService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NotificationController], + providers: [ + { + provide: NotificationService, + useValue: mockService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get(NotificationController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call through to service for get', async () => { + mockService.getByFileNumber.mockResolvedValue(new Notification()); + mockService.mapToDtos.mockResolvedValue([]); + + await controller.get('fileNumber'); + + expect(mockService.getByFileNumber).toHaveBeenCalledTimes(1); + expect(mockService.mapToDtos).toHaveBeenCalledTimes(1); + }); + + it('should call through to service for search', async () => { + mockService.searchByFileNumber.mockResolvedValue([new Notification()]); + mockService.mapToDtos.mockResolvedValue([]); + + await controller.search('fileNumber'); + + expect(mockService.searchByFileNumber).toHaveBeenCalledTimes(1); + expect(mockService.mapToDtos).toHaveBeenCalledTimes(1); + }); + + it('should call through to service for update', async () => { + mockService.update.mockResolvedValue(new Notification()); + mockService.mapToDtos.mockResolvedValue([]); + + await controller.update({}, 'fileNumber'); + + expect(mockService.update).toHaveBeenCalledTimes(1); + expect(mockService.mapToDtos).toHaveBeenCalledTimes(1); + }); + + it('should call through to service for get card', async () => { + mockService.getByCardUuid.mockResolvedValue(new Notification()); + mockService.mapToDtos.mockResolvedValue([]); + + await controller.getByCard('uuid'); + + expect(mockService.getByCardUuid).toHaveBeenCalledTimes(1); + expect(mockService.mapToDtos).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/notification/notification.controller.ts b/services/apps/alcs/src/alcs/notification/notification.controller.ts new file mode 100644 index 0000000000..67995f54cd --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification.controller.ts @@ -0,0 +1,61 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { + ROLES_ALLOWED_APPLICATIONS, + ROLES_ALLOWED_BOARDS, +} from '../../common/authorization/roles'; +import { UserRoles } from '../../common/authorization/roles.decorator'; +import { UpdateNotificationDto } from './notification.dto'; +import { NotificationService } from './notification.service'; + +@Controller('notification') +export class NotificationController { + constructor( + private notificationService: NotificationService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('/:fileNumber') + @UserRoles(...ROLES_ALLOWED_BOARDS) + async get(@Param('fileNumber') fileNumber: string) { + const notification = await this.notificationService.getByFileNumber( + fileNumber, + ); + const mapped = await this.notificationService.mapToDtos([notification]); + return mapped[0]; + } + + @Get('/card/:uuid') + @UserRoles(...ROLES_ALLOWED_BOARDS) + async getByCard(@Param('uuid') cardUuid: string) { + const notification = await this.notificationService.getByCardUuid(cardUuid); + const mapped = await this.notificationService.mapToDtos([notification]); + return mapped[0]; + } + + @Post('/:fileNumber') + @UserRoles(...ROLES_ALLOWED_BOARDS) + async update( + @Body() updateDto: UpdateNotificationDto, + @Param('fileNumber') fileNumber: string, + ) { + const updatedNotification = await this.notificationService.update( + fileNumber, + updateDto, + ); + const mapped = await this.notificationService.mapToDtos([ + updatedNotification, + ]); + return mapped[0]; + } + + @Get('/search/:fileNumber') + @UserRoles(...ROLES_ALLOWED_APPLICATIONS) + async search(@Param('fileNumber') fileNumber: string) { + const noticeOfIntents = await this.notificationService.searchByFileNumber( + fileNumber, + ); + return this.notificationService.mapToDtos(noticeOfIntents); + } +} diff --git a/services/apps/alcs/src/alcs/notification/notification.dto.ts b/services/apps/alcs/src/alcs/notification/notification.dto.ts new file mode 100644 index 0000000000..c1f7cae2e3 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification.dto.ts @@ -0,0 +1,76 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; +import { CardDto } from '../card/card.dto'; +import { ApplicationRegionDto } from '../code/application-code/application-region/application-region.dto'; +import { LocalGovernmentDto } from '../local-government/local-government.dto'; +import { NotificationTypeDto } from './notification-type/notification-type.dto'; + +export class NotificationDto { + @AutoMap() + uuid: string; + + @AutoMap() + fileNumber: string; + + @AutoMap() + applicant: string; + + @AutoMap() + card: CardDto; + + dateSubmittedToAlc?: number; + + @AutoMap() + localGovernment: LocalGovernmentDto; + + @AutoMap() + region: ApplicationRegionDto; + + @AutoMap(() => String) + summary?: string; + + @AutoMap(() => NotificationTypeDto) + type: NotificationTypeDto; + + @AutoMap(() => String) + staffObservations?: string; + + proposalEndDate?: number; +} + +export class UpdateNotificationDto { + @IsOptional() + @IsNumber() + dateSubmittedToAlc?: number; + + @IsOptional() + @IsUUID() + localGovernmentUuid?: string; + + @IsString() + @IsOptional() + summary?: string; + + @IsOptional() + @IsString() + staffObservations?: string; + + @IsOptional() + @IsNumber() + proposalEndDate?: number; +} + +export class CreateNotificationServiceDto { + fileNumber: string; + applicant: string; + typeCode: string; + dateSubmittedToAlc?: Date | null | undefined; + regionCode?: string; + localGovernmentUuid?: string; +} diff --git a/services/apps/alcs/src/alcs/notification/notification.entity.ts b/services/apps/alcs/src/alcs/notification/notification.entity.ts new file mode 100644 index 0000000000..c8ce3c0544 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification.entity.ts @@ -0,0 +1,82 @@ +import { AutoMap } from '@automapper/classes'; +import { Type } from 'class-transformer'; +import { + Column, + Entity, + Index, + JoinColumn, + ManyToOne, + OneToOne, +} from 'typeorm'; +import { Base } from '../../common/entities/base.entity'; +import { Card } from '../card/card.entity'; +import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; +import { LocalGovernment } from '../local-government/local-government.entity'; +import { NotificationType } from './notification-type/notification-type.entity'; + +@Entity() +export class Notification extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @Index() + @Column({ unique: true }) + fileNumber: string; + + @Column() + applicant: string; + + @Column({ type: 'uuid', nullable: true }) + cardUuid: string; + + @OneToOne(() => Card, { cascade: true }) + @JoinColumn() + @Type(() => Card) + card: Card | null; + + @ManyToOne(() => LocalGovernment, { nullable: true }) + localGovernment?: LocalGovernment; + + @Index() + @Column({ + type: 'uuid', + nullable: true, + }) + localGovernmentUuid?: string; + + @ManyToOne(() => ApplicationRegion, { nullable: true }) + region?: ApplicationRegion; + + @Column({ nullable: true }) + regionCode?: string; + + @AutoMap(() => String) + @Column({ type: 'text', nullable: true }) + summary: string | null; + + @Column({ + type: 'timestamptz', + nullable: true, + }) + dateSubmittedToAlc: Date | null; + + @AutoMap(() => String) + @Column({ + type: 'text', + comment: 'ALC Staff Observations and Comments', + nullable: true, + }) + staffObservations?: string | null; + + @ManyToOne(() => NotificationType, { + nullable: false, + }) + type: NotificationType; + + @Column() + typeCode: string; +} diff --git a/services/apps/alcs/src/alcs/notification/notification.module.ts b/services/apps/alcs/src/alcs/notification/notification.module.ts new file mode 100644 index 0000000000..4ed9401bc3 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification.module.ts @@ -0,0 +1,30 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotificationProfile } from '../../common/automapper/notification.automapper.profile'; +import { DocumentCode } from '../../document/document-code.entity'; +import { DocumentModule } from '../../document/document.module'; +import { FileNumberModule } from '../../file-number/file-number.module'; +import { BoardModule } from '../board/board.module'; +import { CardModule } from '../card/card.module'; +import { CodeModule } from '../code/code.module'; +import { LocalGovernmentModule } from '../local-government/local-government.module'; +import { NotificationType } from './notification-type/notification-type.entity'; +import { NotificationController } from './notification.controller'; +import { NotificationService } from './notification.service'; +import { Notification } from './notification.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Notification, NotificationType, DocumentCode]), + forwardRef(() => BoardModule), + CardModule, + FileNumberModule, + DocumentModule, + CodeModule, + LocalGovernmentModule, + ], + providers: [NotificationService, NotificationProfile], + controllers: [NotificationController], + exports: [NotificationService], +}) +export class NotificationModule {} diff --git a/services/apps/alcs/src/alcs/notification/notification.service.spec.ts b/services/apps/alcs/src/alcs/notification/notification.service.spec.ts new file mode 100644 index 0000000000..fce06155dc --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification.service.spec.ts @@ -0,0 +1,265 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { ServiceNotFoundException } from '../../../../../libs/common/src/exceptions/base.exception'; +import { NotificationProfile } from '../../common/automapper/notification.automapper.profile'; +import { FileNumberService } from '../../file-number/file-number.service'; +import { NotificationSubmissionService } from '../../portal/notification-submission/notification-submission.service'; +import { Board } from '../board/board.entity'; +import { Card } from '../card/card.entity'; +import { CardService } from '../card/card.service'; +import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; +import { CodeService } from '../code/code.service'; +import { LocalGovernmentService } from '../local-government/local-government.service'; +import { NotificationType } from './notification-type/notification-type.entity'; +import { Notification } from './notification.entity'; +import { NotificationService } from './notification.service'; + +describe('NotificationService', () => { + let service: NotificationService; + let mockCardService: DeepMocked; + let mockRepository: DeepMocked>; + let mockTypeRepository: DeepMocked>; + let mockFileNumberService: DeepMocked; + let mockLocalGovernmentService: DeepMocked; + let mockCodeService: DeepMocked; + let mockNotificationSubmissionService: DeepMocked; + + beforeEach(async () => { + mockCardService = createMock(); + mockRepository = createMock(); + mockFileNumberService = createMock(); + mockTypeRepository = createMock(); + mockLocalGovernmentService = createMock(); + mockCodeService = createMock(); + mockNotificationSubmissionService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + providers: [ + NotificationService, + NotificationProfile, + { + provide: getRepositoryToken(Notification), + useValue: mockRepository, + }, + { + provide: getRepositoryToken(NotificationType), + useValue: mockTypeRepository, + }, + { + provide: CardService, + useValue: mockCardService, + }, + { + provide: FileNumberService, + useValue: mockFileNumberService, + }, + { + provide: LocalGovernmentService, + useValue: mockLocalGovernmentService, + }, + { + provide: CodeService, + useValue: mockCodeService, + }, + { + provide: NotificationSubmissionService, + useValue: mockNotificationSubmissionService, + }, + ], + }).compile(); + + service = module.get(NotificationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should load the type code and call the repo to save when creating', async () => { + const mockCard = {} as Card; + const fakeBoard = {} as Board; + + mockRepository.findOne.mockResolvedValue(new Notification()); + mockRepository.save.mockResolvedValue(new Notification()); + mockCardService.create.mockResolvedValue(mockCard); + mockFileNumberService.checkValidFileNumber.mockResolvedValue(true); + mockCodeService.fetchRegion.mockResolvedValue(new ApplicationRegion()); + mockTypeRepository.findOneOrFail.mockResolvedValue(new NotificationType()); + + const res = await service.create( + { + applicant: 'fake-applicant', + fileNumber: '1512311', + localGovernmentUuid: 'fake-uuid', + regionCode: 'region-code', + typeCode: '', + dateSubmittedToAlc: new Date(0), + }, + fakeBoard, + ); + + expect(mockFileNumberService.checkValidFileNumber).toHaveBeenCalledTimes(1); + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + expect(mockCardService.create).toHaveBeenCalledTimes(1); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + expect(mockRepository.save.mock.calls[0][0].card).toBe(mockCard); + expect(mockTypeRepository.findOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should call through to the repo for get by card', async () => { + mockRepository.findOne.mockResolvedValue(new Notification()); + const cardUuid = 'fake-card-uuid'; + await service.getByCardUuid(cardUuid); + + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + }); + + it('should throw an exception when getting by card fails', async () => { + mockRepository.findOne.mockResolvedValue(null); + const cardUuid = 'fake-card-uuid'; + const promise = service.getByCardUuid(cardUuid); + + await expect(promise).rejects.toMatchObject( + new Error(`Failed to find notice of intent with card uuid ${cardUuid}`), + ); + + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + }); + + it('should call through to the repo for get cards', async () => { + mockRepository.find.mockResolvedValue([]); + await service.getByBoard('fake'); + + expect(mockRepository.find).toHaveBeenCalledTimes(1); + }); + + it('should call through to the repo for getBy', async () => { + const mockFilter = { + uuid: '5', + }; + mockRepository.find.mockResolvedValue([]); + await service.getBy(mockFilter); + + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(mockRepository.find.mock.calls[0][0]!.where).toEqual(mockFilter); + }); + + it('should call throw an exception when getOrFailByUuid fails', async () => { + mockRepository.findOne.mockResolvedValue(null); + const promise = service.getOrFailByUuid('uuid'); + + await expect(promise).rejects.toMatchObject( + new ServiceNotFoundException( + `Failed to find notice of intent with uuid uuid`, + ), + ); + + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + }); + + it('should call through to the repo for getByFileNumber', async () => { + mockRepository.findOneOrFail.mockResolvedValue(new Notification()); + await service.getByFileNumber('file'); + + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should call through to the repo for searchByFileNumber', async () => { + mockRepository.find.mockResolvedValue([new Notification()]); + const res = await service.searchByFileNumber('file'); + + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(res.length).toEqual(1); + }); + + it('should call through to the repo for getFileNumber', async () => { + mockRepository.findOneOrFail.mockResolvedValue( + new Notification({ + fileNumber: 'fileNumber', + }), + ); + const res = await service.getFileNumber('file'); + + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); + expect(res).toEqual('fileNumber'); + }); + + it('should call through to the repo for getUuid', async () => { + mockRepository.findOneOrFail.mockResolvedValue( + new Notification({ + uuid: 'uuid', + }), + ); + const res = await service.getUuid('file'); + + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); + expect(res).toEqual('uuid'); + }); + + it('should set values and call save for update', async () => { + const notice = new Notification({ + summary: 'old-summary', + }); + mockRepository.findOneOrFail.mockResolvedValue(notice); + mockRepository.save.mockResolvedValue(new Notification()); + const res = await service.update('file', { + summary: 'new-summary', + }); + + expect(res).toBeDefined(); + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(2); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + expect(notice.summary).toEqual('new-summary'); + }); + + it('should load deleted cards', async () => { + mockRepository.find.mockResolvedValue([]); + + await service.getDeletedCards('file-number'); + + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(mockRepository.find.mock.calls[0][0]!.withDeleted).toEqual(true); + }); + + it('should call the repo for get update applicant', async () => { + mockRepository.update.mockResolvedValue({} as any); + + await service.updateApplicant('file-number', 'applicant'); + + expect(mockRepository.update).toHaveBeenCalledTimes(1); + }); + + it('should create a card and save it for submit', async () => { + const mockNoi = new Notification(); + mockRepository.findOne.mockResolvedValue(mockNoi); + mockRepository.findOneOrFail.mockResolvedValue(mockNoi); + mockCodeService.fetchRegion.mockResolvedValue(new ApplicationRegion()); + mockRepository.save.mockResolvedValue({} as any); + + await service.submit({ + applicant: 'Bruce Wayne', + typeCode: 'CAT', + fileNumber: 'fileNumber', + localGovernmentUuid: 'governmentUuid', + regionCode: 'REGION', + }); + + expect(mockNoi.fileNumber).toEqual('fileNumber'); + expect(mockNoi.region).toBeDefined(); + expect(mockNoi.card).toBeDefined(); + expect(mockCodeService.fetchRegion).toHaveBeenCalledTimes(1); + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + 0; + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/notification/notification.service.ts b/services/apps/alcs/src/alcs/notification/notification.service.ts new file mode 100644 index 0000000000..018aaed3a2 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification.service.ts @@ -0,0 +1,321 @@ +import { + ServiceNotFoundException, + ServiceValidationException, +} from '@app/common/exceptions/base.exception'; +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + FindOptionsRelations, + FindOptionsWhere, + IsNull, + Like, + Not, + Repository, +} from 'typeorm'; +import { FileNumberService } from '../../file-number/file-number.service'; +import { filterUndefined } from '../../utils/undefined'; +import { ApplicationTimeData } from '../application/application-time-tracking.service'; +import { Board } from '../board/board.entity'; +import { CARD_TYPE } from '../card/card-type/card-type.entity'; +import { Card } from '../card/card.entity'; +import { CardService } from '../card/card.service'; +import { CodeService } from '../code/code.service'; +import { LocalGovernmentService } from '../local-government/local-government.service'; +import { NotificationType } from './notification-type/notification-type.entity'; +import { + CreateNotificationServiceDto, + NotificationDto, + UpdateNotificationDto, +} from './notification.dto'; +import { Notification } from './notification.entity'; + +@Injectable() +export class NotificationService { + private logger = new Logger(NotificationService.name); + + private CARD_RELATIONS = { + board: true, + type: true, + status: true, + assignee: true, + }; + + private DEFAULT_RELATIONS: FindOptionsRelations = { + card: this.CARD_RELATIONS, + localGovernment: true, + region: true, + type: true, + }; + + constructor( + private cardService: CardService, + @InjectRepository(Notification) + private repository: Repository, + @InjectRepository(NotificationType) + private typeRepository: Repository, + @InjectMapper() private mapper: Mapper, + private fileNumberService: FileNumberService, + private codeService: CodeService, + private localGovernmentService: LocalGovernmentService, + ) {} + + async create( + createDto: CreateNotificationServiceDto, + board?: Board, + persist = true, + ) { + await this.fileNumberService.checkValidFileNumber(createDto.fileNumber); + + const type = await this.typeRepository.findOneOrFail({ + where: { + code: createDto.typeCode, + }, + }); + + const notification = new Notification({ + localGovernmentUuid: createDto.localGovernmentUuid, + fileNumber: createDto.fileNumber, + regionCode: createDto.regionCode, + applicant: createDto.applicant, + dateSubmittedToAlc: createDto.dateSubmittedToAlc, + type, + }); + + if (board) { + notification.card = await this.cardService.create( + CARD_TYPE.NOI, + board, + false, + ); + } + + if (persist) { + const savedNotification = await this.repository.save(notification); + + return this.getOrFailByUuid(savedNotification.uuid); + } + return notification; + } + + async getOrFailByUuid(uuid: string) { + const notification = await this.get(uuid); + if (!notification) { + throw new ServiceNotFoundException( + `Failed to find notice of intent with uuid ${uuid}`, + ); + } + + return notification; + } + + async mapToDtos(notifications: Notification[]) { + return this.mapper.mapArray(notifications, Notification, NotificationDto); + } + + async getByCardUuid(cardUuid: string) { + const notification = await this.repository.findOne({ + where: { cardUuid }, + relations: this.DEFAULT_RELATIONS, + }); + + if (!notification) { + throw new ServiceNotFoundException( + `Failed to find notice of intent with card uuid ${cardUuid}`, + ); + } + + return notification; + } + + getBy(findOptions: FindOptionsWhere) { + return this.repository.find({ + where: findOptions, + relations: this.DEFAULT_RELATIONS, + }); + } + + getDeletedCards(fileNumber: string) { + return this.repository.find({ + where: { + fileNumber, + card: { + auditDeletedDateAt: Not(IsNull()), + }, + }, + withDeleted: true, + relations: this.DEFAULT_RELATIONS, + }); + } + + private get(uuid: string) { + return this.repository.findOne({ + where: { + uuid, + }, + relations: { + ...this.DEFAULT_RELATIONS, + card: { ...this.CARD_RELATIONS, board: false }, + }, + }); + } + + async getByBoard(boardUuid: string) { + return this.repository.find({ + where: { card: { boardUuid } }, + relations: this.DEFAULT_RELATIONS, + }); + } + + async getWithIncompleteSubtaskByType(subtaskType: string) { + return this.repository.find({ + where: { + card: { + subtasks: { + completedAt: IsNull(), + type: { + code: subtaskType, + }, + }, + }, + }, + relations: { + card: { + status: true, + board: true, + type: true, + subtasks: { type: true, assignee: true }, + }, + }, + }); + } + + async getByFileNumber(fileNumber: string) { + return this.repository.findOneOrFail({ + where: { fileNumber }, + relations: this.DEFAULT_RELATIONS, + }); + } + + async update(fileNumber: string, updateDto: UpdateNotificationDto) { + const notification = await this.getByFileNumber(fileNumber); + + notification.summary = filterUndefined( + updateDto.summary, + notification.summary, + ); + if (updateDto.localGovernmentUuid) { + notification.localGovernmentUuid = updateDto.localGovernmentUuid; + } + + notification.staffObservations = filterUndefined( + updateDto.staffObservations, + notification.staffObservations, + ); + + await this.repository.save(notification); + + return this.getByFileNumber(notification.fileNumber); + } + + async listTypes() { + return this.typeRepository.find(); + } + + async updateByUuid(uuid: string, updates: Partial) { + await this.repository.update(uuid, updates); + } + + async searchByFileNumber(fileNumber: string) { + return this.repository.find({ + where: { + fileNumber: Like(`${fileNumber}%`), + }, + order: { + fileNumber: 'ASC', + }, + relations: { + region: true, + localGovernment: true, + }, + }); + } + + async getFileNumber(uuid: string) { + const notification = await this.repository.findOneOrFail({ + where: { + uuid, + }, + select: { + fileNumber: true, + }, + }); + return notification.fileNumber; + } + + async getUuid(fileNumber: string) { + const notification = await this.repository.findOneOrFail({ + where: { + fileNumber, + }, + select: { + uuid: true, + }, + }); + return notification.uuid; + } + + async submit(createDto: CreateNotificationServiceDto) { + const existingNotification = await this.repository.findOne({ + where: { fileNumber: createDto.fileNumber }, + }); + + if (!existingNotification) { + throw new ServiceValidationException( + `Notification with file number does not exist ${createDto.fileNumber}`, + ); + } + + if (!createDto.localGovernmentUuid) { + throw new ServiceValidationException( + `Local government is not set for notification ${createDto.fileNumber}`, + ); + } + + let region = createDto.regionCode + ? await this.codeService.fetchRegion(createDto.regionCode) + : undefined; + + if (!region) { + const localGov = await this.localGovernmentService.getByUuid( + createDto.localGovernmentUuid, + ); + region = localGov?.preferredRegion; + } + + existingNotification.fileNumber = createDto.fileNumber; + existingNotification.applicant = createDto.applicant; + existingNotification.dateSubmittedToAlc = + createDto.dateSubmittedToAlc || null; + existingNotification.localGovernmentUuid = createDto.localGovernmentUuid; + existingNotification.typeCode = createDto.typeCode; + existingNotification.region = region; + existingNotification.card = new Card(); + existingNotification.card.typeCode = CARD_TYPE.NOI; + + await this.repository.save(existingNotification); + return this.getByFileNumber(createDto.fileNumber); + } + + async updateApplicant(fileNumber: string, applicant: string) { + await this.repository.update( + { + fileNumber, + }, + { + applicant, + }, + ); + } +} diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-search-view.entity.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-search-view.entity.ts index 8c1c397a93..6dfb3d0fec 100644 --- a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-search-view.entity.ts +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-search-view.entity.ts @@ -7,7 +7,7 @@ import { ViewEntity, } from 'typeorm'; import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; -import { NoticeOfIntentType } from '../../code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { NoticeOfIntentType } from '../../notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity'; import { LocalGovernment } from '../../local-government/local-government.entity'; import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; diff --git a/services/apps/alcs/src/common/automapper/application-parcel.automapper.profile.ts b/services/apps/alcs/src/common/automapper/application-parcel.automapper.profile.ts index 1b44f6e07a..e013581ec1 100644 --- a/services/apps/alcs/src/common/automapper/application-parcel.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/application-parcel.automapper.profile.ts @@ -5,12 +5,12 @@ import { ApplicationDocumentDto } from '../../alcs/application/application-docum import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; import { ApplicationOwnerDto } from '../../portal/application-submission/application-owner/application-owner.dto'; import { ApplicationOwner } from '../../portal/application-submission/application-owner/application-owner.entity'; -import { ApplicationParcelOwnershipType } from '../../portal/application-submission/application-parcel/application-parcel-ownership-type/application-parcel-ownership-type.entity'; -import { - ApplicationParcelDto, - ApplicationParcelOwnershipTypeDto, -} from '../../portal/application-submission/application-parcel/application-parcel.dto'; +import { ApplicationParcelDto } from '../../portal/application-submission/application-parcel/application-parcel.dto'; import { ApplicationParcel } from '../../portal/application-submission/application-parcel/application-parcel.entity'; +import { + ParcelOwnershipType, + ParcelOwnershipTypeDto, +} from '../entities/parcel-ownership-type/parcel-ownership-type.entity'; @Injectable() export class ApplicationParcelProfile extends AutomapperProfile { @@ -65,8 +65,8 @@ export class ApplicationParcelProfile extends AutomapperProfile { if (pd.ownershipType) { return this.mapper.map( pd.ownershipType, - ApplicationParcelOwnershipType, - ApplicationParcelOwnershipTypeDto, + ParcelOwnershipType, + ParcelOwnershipTypeDto, ); } else { return undefined; @@ -75,11 +75,7 @@ export class ApplicationParcelProfile extends AutomapperProfile { ), ); - createMap( - mapper, - ApplicationParcelOwnershipType, - ApplicationParcelOwnershipTypeDto, - ); + createMap(mapper, ParcelOwnershipType, ParcelOwnershipTypeDto); }; } } diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts index eb57ac85aa..9ae88c39bc 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-parcel.automapper.profile.ts @@ -5,12 +5,12 @@ import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; import { NoticeOfIntentOwnerDto } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; -import { NoticeOfIntentParcelOwnershipType } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity'; -import { - NoticeOfIntentParcelDto, - NoticeOfIntentParcelOwnershipTypeDto, -} from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto'; +import { NoticeOfIntentParcelDto } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto'; import { NoticeOfIntentParcel } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; +import { + ParcelOwnershipType, + ParcelOwnershipTypeDto, +} from '../entities/parcel-ownership-type/parcel-ownership-type.entity'; @Injectable() export class NoticeOfIntentParcelProfile extends AutomapperProfile { @@ -65,8 +65,8 @@ export class NoticeOfIntentParcelProfile extends AutomapperProfile { if (pd.ownershipType) { return this.mapper.map( pd.ownershipType, - NoticeOfIntentParcelOwnershipType, - NoticeOfIntentParcelOwnershipTypeDto, + ParcelOwnershipType, + ParcelOwnershipTypeDto, ); } else { return undefined; @@ -75,11 +75,7 @@ export class NoticeOfIntentParcelProfile extends AutomapperProfile { ), ); - createMap( - mapper, - NoticeOfIntentParcelOwnershipType, - NoticeOfIntentParcelOwnershipTypeDto, - ); + createMap(mapper, ParcelOwnershipType, ParcelOwnershipTypeDto); }; } } diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent.automapper.profile.ts index 54d47bc7f6..41c1976f81 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent.automapper.profile.ts @@ -1,8 +1,8 @@ import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; -import { NoticeOfIntentTypeDto } from '../../alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.dto'; -import { NoticeOfIntentType } from '../../alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { NoticeOfIntentTypeDto } from '../../alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.dto'; +import { NoticeOfIntentType } from '../../alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity'; import { NoticeOfIntentDocumentDto } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; diff --git a/services/apps/alcs/src/common/automapper/notification-parcel.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notification-parcel.automapper.profile.ts new file mode 100644 index 0000000000..e54b68e853 --- /dev/null +++ b/services/apps/alcs/src/common/automapper/notification-parcel.automapper.profile.ts @@ -0,0 +1,46 @@ +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { NotificationParcelDto } from '../../portal/notification-submission/notification-parcel/notification-parcel.dto'; +import { NotificationParcel } from '../../portal/notification-submission/notification-parcel/notification-parcel.entity'; +import { + ParcelOwnershipType, + ParcelOwnershipTypeDto, +} from '../entities/parcel-ownership-type/parcel-ownership-type.entity'; + +@Injectable() +export class NotificationParcelProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap( + mapper, + NotificationParcel, + NotificationParcelDto, + 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, ParcelOwnershipType, ParcelOwnershipTypeDto); + }; + } +} diff --git a/services/apps/alcs/src/common/automapper/notification-submission.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notification-submission.automapper.profile.ts new file mode 100644 index 0000000000..a6227876d7 --- /dev/null +++ b/services/apps/alcs/src/common/automapper/notification-submission.automapper.profile.ts @@ -0,0 +1,55 @@ +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { + NotificationSubmissionDetailedDto, + NotificationSubmissionDto, +} from '../../portal/notification-submission/notification-submission.dto'; +import { NotificationSubmission } from '../../portal/notification-submission/notification-submission.entity'; + +@Injectable() +export class NotificationSubmissionProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap( + mapper, + NotificationSubmission, + NotificationSubmissionDto, + forMember( + (a) => a.createdAt, + mapFrom((ad) => { + return ad.auditCreatedAt.getTime(); + }), + ), + forMember( + (a) => a.updatedAt, + mapFrom((ad) => { + return ad.auditUpdatedAt?.getTime(); + }), + ), + ); + + createMap( + mapper, + NotificationSubmission, + NotificationSubmissionDetailedDto, + forMember( + (a) => a.createdAt, + mapFrom((ad) => { + return ad.auditCreatedAt.getTime(); + }), + ), + forMember( + (a) => a.updatedAt, + mapFrom((ad) => { + return ad.auditUpdatedAt?.getTime(); + }), + ), + ); + }; + } +} diff --git a/services/apps/alcs/src/common/automapper/notification-transferee.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notification-transferee.automapper.profile.ts new file mode 100644 index 0000000000..542db36397 --- /dev/null +++ b/services/apps/alcs/src/common/automapper/notification-transferee.automapper.profile.ts @@ -0,0 +1,29 @@ +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { NotificationTransfereeDto } from '../../portal/notification-submission/notification-transferee/notification-transferee.dto'; +import { NotificationTransferee } from '../../portal/notification-submission/notification-transferee/notification-transferee.entity'; +import { OwnerType, OwnerTypeDto } from '../owner-type/owner-type.entity'; + +@Injectable() +export class NotificationTransfereeProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap( + mapper, + NotificationTransferee, + NotificationTransfereeDto, + forMember( + (pd) => pd.displayName, + mapFrom((p) => `${p.firstName} ${p.lastName}`), + ), + ); + + createMap(mapper, OwnerType, OwnerTypeDto); + }; + } +} diff --git a/services/apps/alcs/src/common/automapper/notification.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notification.automapper.profile.ts new file mode 100644 index 0000000000..9e5838daaa --- /dev/null +++ b/services/apps/alcs/src/common/automapper/notification.automapper.profile.ts @@ -0,0 +1,34 @@ +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { NotificationTypeDto } from '../../alcs/notification/notification-type/notification-type.dto'; +import { NotificationType } from '../../alcs/notification/notification-type/notification-type.entity'; +import { NotificationDto } from '../../alcs/notification/notification.dto'; +import { DocumentCode } from '../../document/document-code.entity'; +import { DocumentTypeDto } from '../../document/document.dto'; +import { Notification } from '../../alcs/notification/notification.entity'; + +@Injectable() +export class NotificationProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap(mapper, NotificationType, NotificationTypeDto); + + createMap( + mapper, + Notification, + NotificationDto, + forMember( + (a) => a.dateSubmittedToAlc, + mapFrom((ad) => ad.dateSubmittedToAlc?.getTime()), + ), + ); + + createMap(mapper, DocumentCode, DocumentTypeDto); + }; + } +} diff --git a/services/apps/alcs/src/common/entities/parcel-ownership-type/parcel-ownership-type.entity.ts b/services/apps/alcs/src/common/entities/parcel-ownership-type/parcel-ownership-type.entity.ts new file mode 100644 index 0000000000..6ae47f4d92 --- /dev/null +++ b/services/apps/alcs/src/common/entities/parcel-ownership-type/parcel-ownership-type.entity.ts @@ -0,0 +1,8 @@ +import { Entity } from 'typeorm'; +import { BaseCodeDto } from '../../dtos/base.dto'; +import { BaseCodeEntity } from '../base.code.entity'; + +export class ParcelOwnershipTypeDto extends BaseCodeDto {} + +@Entity() +export class ParcelOwnershipType extends BaseCodeEntity {} diff --git a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts index fc9d96e259..1ff7684867 100644 --- a/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts +++ b/services/apps/alcs/src/portal/application-submission-draft/application-submission-draft.module.ts @@ -2,9 +2,9 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ApplicationSubmissionStatusModule } from '../../alcs/application/application-submission-status/application-submission-status.module'; import { ApplicationSubmissionStatusType } from '../../alcs/application/application-submission-status/submission-status-type.entity'; +import { ParcelOwnershipType } from '../../common/entities/parcel-ownership-type/parcel-ownership-type.entity'; import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { ApplicationOwner } from '../application-submission/application-owner/application-owner.entity'; -import { ApplicationParcelOwnershipType } from '../application-submission/application-parcel/application-parcel-ownership-type/application-parcel-ownership-type.entity'; import { ApplicationParcel } from '../application-submission/application-parcel/application-parcel.entity'; import { ApplicationSubmission } from '../application-submission/application-submission.entity'; import { ApplicationSubmissionModule } from '../application-submission/application-submission.module'; @@ -18,7 +18,7 @@ import { ApplicationSubmissionDraftService } from './application-submission-draf ApplicationSubmission, ApplicationSubmissionStatusType, ApplicationParcel, - ApplicationParcelOwnershipType, + ParcelOwnershipType, ApplicationOwner, OwnerType, ]), diff --git a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel-ownership-type/application-parcel-ownership-type.entity.ts b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel-ownership-type/application-parcel-ownership-type.entity.ts deleted file mode 100644 index fc19749d91..0000000000 --- a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel-ownership-type/application-parcel-ownership-type.entity.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Entity } from 'typeorm'; -import { BaseCodeEntity } from '../../../../common/entities/base.code.entity'; - -@Entity() -export class ApplicationParcelOwnershipType extends BaseCodeEntity {} diff --git a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts index 3fd81342d6..659d40f7ac 100644 --- a/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-parcel/application-parcel.entity.ts @@ -10,10 +10,10 @@ import { import { ApplicationDocumentDto } from '../../../alcs/application/application-document/application-document.dto'; import { ApplicationDocument } from '../../../alcs/application/application-document/application-document.entity'; import { Base } from '../../../common/entities/base.entity'; +import { ParcelOwnershipType } from '../../../common/entities/parcel-ownership-type/parcel-ownership-type.entity'; import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; import { ApplicationOwner } from '../application-owner/application-owner.entity'; import { ApplicationSubmission } from '../application-submission.entity'; -import { ApplicationParcelOwnershipType } from './application-parcel-ownership-type/application-parcel-ownership-type.entity'; @Entity() export class ApplicationParcel extends Base { @@ -117,8 +117,8 @@ export class ApplicationParcel extends Base { ownershipTypeCode?: string | null; @AutoMap() - @ManyToOne(() => ApplicationParcelOwnershipType) - ownershipType: ApplicationParcelOwnershipType; + @ManyToOne(() => ParcelOwnershipType) + ownershipType: ParcelOwnershipType; @AutoMap(() => Boolean) @Column({ diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts index 8960faeeae..e21e206c59 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.module.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.module.ts @@ -8,6 +8,7 @@ import { AuthorizationModule } from '../../common/authorization/authorization.mo import { ApplicationOwnerProfile } from '../../common/automapper/application-owner.automapper.profile'; import { ApplicationParcelProfile } from '../../common/automapper/application-parcel.automapper.profile'; import { ApplicationSubmissionProfile } from '../../common/automapper/application-submission.automapper.profile'; +import { ParcelOwnershipType } from '../../common/entities/parcel-ownership-type/parcel-ownership-type.entity'; import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; import { PdfGenerationModule } from '../pdf-generation/pdf-generation.module'; @@ -15,7 +16,6 @@ import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { ApplicationOwnerController } from './application-owner/application-owner.controller'; import { ApplicationOwner } from './application-owner/application-owner.entity'; import { ApplicationOwnerService } from './application-owner/application-owner.service'; -import { ApplicationParcelOwnershipType } from './application-parcel/application-parcel-ownership-type/application-parcel-ownership-type.entity'; import { ApplicationParcelController } from './application-parcel/application-parcel.controller'; import { ApplicationParcel } from './application-parcel/application-parcel.entity'; import { ApplicationParcelService } from './application-parcel/application-parcel.service'; @@ -31,7 +31,7 @@ import { NaruSubtype } from './naru-subtype/naru-subtype.entity'; ApplicationSubmission, ApplicationSubmissionStatusType, ApplicationParcel, - ApplicationParcelOwnershipType, + ParcelOwnershipType, ApplicationOwner, OwnerType, NaruSubtype, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts deleted file mode 100644 index e6d3229bd5..0000000000 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Entity } from 'typeorm'; -import { BaseCodeEntity } from '../../../../common/entities/base.code.entity'; - -@Entity() -export class NoticeOfIntentParcelOwnershipType extends BaseCodeEntity {} diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts index 2e6732dd14..77f20736f8 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.dto.ts @@ -8,11 +8,9 @@ import { IsString, } from 'class-validator'; import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; -import { BaseCodeDto } from '../../../common/dtos/base.dto'; +import { ParcelOwnershipTypeDto } from '../../../common/entities/parcel-ownership-type/parcel-ownership-type.entity'; import { NoticeOfIntentOwnerDto } from '../notice-of-intent-owner/notice-of-intent-owner.dto'; -export class NoticeOfIntentParcelOwnershipTypeDto extends BaseCodeDto {} - export class NoticeOfIntentParcelDto { @AutoMap() uuid: string; @@ -53,7 +51,7 @@ export class NoticeOfIntentParcelDto { @AutoMap(() => String) crownLandOwnerType?: string | null; - ownershipType?: NoticeOfIntentParcelOwnershipTypeDto; + ownershipType?: ParcelOwnershipTypeDto; @AutoMap(() => String) parcelType: string; diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts index 12b817142c..3215f14c90 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity.ts @@ -10,10 +10,10 @@ import { import { NoticeOfIntentDocumentDto } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.dto'; import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; import { Base } from '../../../common/entities/base.entity'; +import { ParcelOwnershipType } from '../../../common/entities/parcel-ownership-type/parcel-ownership-type.entity'; import { ColumnNumericTransformer } from '../../../utils/column-numeric-transform'; import { NoticeOfIntentOwner } from '../notice-of-intent-owner/notice-of-intent-owner.entity'; import { NoticeOfIntentSubmission } from '../notice-of-intent-submission.entity'; -import { NoticeOfIntentParcelOwnershipType } from './notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity'; @Entity() export class NoticeOfIntentParcel extends Base { @@ -107,8 +107,8 @@ export class NoticeOfIntentParcel extends Base { ownershipTypeCode?: string | null; @AutoMap() - @ManyToOne(() => NoticeOfIntentParcelOwnershipType) - ownershipType: NoticeOfIntentParcelOwnershipType; + @ManyToOne(() => ParcelOwnershipType) + ownershipType: ParcelOwnershipType; @AutoMap(() => Boolean) @Column({ diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts index 257dce1c7a..d67b02181b 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.module.ts @@ -2,19 +2,20 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BoardModule } from '../../alcs/board/board.module'; import { LocalGovernmentModule } from '../../alcs/local-government/local-government.module'; +import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; import { NoticeOfIntentSubmissionStatusModule } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; import { AuthorizationModule } from '../../common/authorization/authorization.module'; import { NoticeOfIntentOwnerProfile } from '../../common/automapper/notice-of-intent-owner.automapper.profile'; import { NoticeOfIntentParcelProfile } from '../../common/automapper/notice-of-intent-parcel.automapper.profile'; import { NoticeOfIntentSubmissionProfile } from '../../common/automapper/notice-of-intent-submission.automapper.profile'; +import { ParcelOwnershipType } from '../../common/entities/parcel-ownership-type/parcel-ownership-type.entity'; import { OwnerType } from '../../common/owner-type/owner-type.entity'; import { DocumentModule } from '../../document/document.module'; import { FileNumberModule } from '../../file-number/file-number.module'; import { NoticeOfIntentOwnerController } from './notice-of-intent-owner/notice-of-intent-owner.controller'; import { NoticeOfIntentOwner } from './notice-of-intent-owner/notice-of-intent-owner.entity'; import { NoticeOfIntentOwnerService } from './notice-of-intent-owner/notice-of-intent-owner.service'; -import { NoticeOfIntentParcelOwnershipType } from './notice-of-intent-parcel/notice-of-intent-parcel-ownership-type/notice-of-intent-parcel-ownership-type.entity'; import { NoticeOfIntentParcelController } from './notice-of-intent-parcel/notice-of-intent-parcel.controller'; import { NoticeOfIntentParcel } from './notice-of-intent-parcel/notice-of-intent-parcel.entity'; import { NoticeOfIntentParcelService } from './notice-of-intent-parcel/notice-of-intent-parcel.service'; @@ -22,14 +23,13 @@ import { NoticeOfIntentSubmissionValidatorService } from './notice-of-intent-sub import { NoticeOfIntentSubmissionController } from './notice-of-intent-submission.controller'; import { NoticeOfIntentSubmission } from './notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from './notice-of-intent-submission.service'; -import { NoticeOfIntentSubmissionStatusType } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status-type.entity'; @Module({ imports: [ TypeOrmModule.forFeature([ NoticeOfIntentSubmission, NoticeOfIntentParcel, - NoticeOfIntentParcelOwnershipType, + ParcelOwnershipType, OwnerType, NoticeOfIntentOwner, NoticeOfIntentSubmissionStatusType, diff --git a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts index 5cb5e32961..b09fc2a10a 100644 --- a/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/notice-of-intent-submission/notice-of-intent-submission.service.spec.ts @@ -5,7 +5,7 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { NoticeOfIntentType } from '../../alcs/code/application-code/notice-of-intent-type/notice-of-intent-type.entity'; +import { NoticeOfIntentType } from '../../alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.entity'; diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts new file mode 100644 index 0000000000..9c336f99aa --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts @@ -0,0 +1,158 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentParcelProfile } from '../../../common/automapper/notice-of-intent-parcel.automapper.profile'; +import { NotificationParcelProfile } from '../../../common/automapper/notification-parcel.automapper.profile'; +import { NotificationProfile } from '../../../common/automapper/notification.automapper.profile'; +import { DocumentService } from '../../../document/document.service'; +import { User } from '../../../user/user.entity'; +import { NotificationSubmission } from '../notification-submission.entity'; +import { NotificationSubmissionService } from '../notification-submission.service'; +import { NotificationTransfereeService } from '../notification-transferee/notification-transferee.service'; +import { NotificationParcelController } from './notification-parcel.controller'; +import { NotificationParcelUpdateDto } from './notification-parcel.dto'; +import { NotificationParcel } from './notification-parcel.entity'; +import { NotificationParcelService } from './notification-parcel.service'; + +describe('NotificationParcelController', () => { + let controller: NotificationParcelController; + let mockNotificationParcelService: DeepMocked; + let mockNotificationSubmissionsService: DeepMocked; + let mockNotificationTransfereeService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockSubmission; + + beforeEach(async () => { + mockNotificationParcelService = createMock(); + mockNotificationSubmissionsService = createMock(); + mockNotificationTransfereeService = createMock(); + mockDocumentService = createMock(); + + mockSubmission = new NotificationSubmission({ + createdBy: new User(), + }); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NotificationParcelController], + providers: [ + NotificationProfile, + NotificationParcelProfile, + { + provide: NotificationParcelService, + useValue: mockNotificationParcelService, + }, + { + provide: NotificationSubmissionService, + useValue: mockNotificationSubmissionsService, + }, + { + provide: NotificationTransfereeService, + useValue: mockNotificationTransfereeService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NotificationParcelController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call out to service when fetching parcels', async () => { + mockNotificationParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue( + [], + ); + + const parcels = await controller.fetchByFileId('mockFileID'); + + expect(parcels).toBeDefined(); + expect( + mockNotificationParcelService.fetchByApplicationSubmissionUuid, + ).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when creating parcels', async () => { + mockNotificationSubmissionsService.getByUuid.mockResolvedValue( + mockSubmission, + ); + mockNotificationParcelService.create.mockResolvedValue( + {} as NotificationParcel, + ); + + const parcel = await controller.create( + { + notificationSubmissionUuid: 'fake', + }, + { + user: { + entity: new User(), + }, + }, + ); + + expect(mockNotificationSubmissionsService.getByUuid).toBeCalledTimes(1); + expect(mockNotificationParcelService.create).toBeCalledTimes(1); + expect(parcel).toBeDefined(); + }); + + it('should call out to service when updating parcel', async () => { + const mockUpdateDto: NotificationParcelUpdateDto[] = [ + { + uuid: 'fake_uuid', + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legal', + mapAreaHectares: 2, + isConfirmedByApplicant: true, + ownershipTypeCode: 'SMPL', + }, + ]; + + mockNotificationParcelService.update.mockResolvedValue([ + {}, + ] as NotificationParcel[]); + + const parcel = await controller.update(mockUpdateDto, { + user: { + entity: new User(), + }, + }); + + expect(mockNotificationParcelService.update).toBeCalledTimes(1); + expect(parcel).toBeDefined(); + }); + + it('should call out to service when deleting parcel', async () => { + const fakeUuid = 'fake_uuid'; + mockNotificationParcelService.deleteMany.mockResolvedValue([]); + + const result = await controller.delete([fakeUuid], { + user: { + entity: new User(), + }, + }); + + expect(mockNotificationParcelService.deleteMany).toBeCalledTimes(1); + expect(result).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts new file mode 100644 index 0000000000..7174dac2bb --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts @@ -0,0 +1,95 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + Body, + Controller, + Delete, + Get, + Param, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { PortalAuthGuard } from '../../../common/authorization/portal-auth-guard.service'; +import { DocumentService } from '../../../document/document.service'; +import { NotificationSubmissionService } from '../notification-submission.service'; +import { + NotificationParcelCreateDto, + NotificationParcelDto, + NotificationParcelUpdateDto, +} from './notification-parcel.dto'; +import { NotificationParcel } from './notification-parcel.entity'; +import { NotificationParcelService } from './notification-parcel.service'; + +@Controller('notification-parcel') +@UseGuards(PortalAuthGuard) +export class NotificationParcelController { + constructor( + private parcelService: NotificationParcelService, + private notificationSubmissionService: NotificationSubmissionService, + @InjectMapper() private mapper: Mapper, + private documentService: DocumentService, + ) {} + + @Get('submission/:submissionUuid') + async fetchByFileId( + @Param('submissionUuid') submissionUuid: string, + ): Promise { + const parcels = await this.parcelService.fetchByApplicationSubmissionUuid( + submissionUuid, + ); + return this.mapper.mapArrayAsync( + parcels, + NotificationParcel, + NotificationParcelDto, + ); + } + + @Post() + async create( + @Body() createDto: NotificationParcelCreateDto, + @Req() req, + ): Promise { + const user = req.user.entity; + const notificationSubmission = + await this.notificationSubmissionService.getByUuid( + createDto.notificationSubmissionUuid, + user, + ); + const parcel = await this.parcelService.create(notificationSubmission.uuid); + + return this.mapper.mapAsync( + parcel, + NotificationParcel, + NotificationParcelDto, + ); + } + + @Put('/') + async update( + @Body() updateDtos: NotificationParcelUpdateDto[], + @Req() req, + ): Promise { + const updatedParcels = await this.parcelService.update(updateDtos); + + return this.mapper.mapArrayAsync( + updatedParcels, + NotificationParcel, + NotificationParcelDto, + ); + } + + @Delete() + async delete(@Body() uuids: string[], @Req() req) { + const deletedParcels = await this.parcelService.deleteMany( + uuids, + req.user.entity, + ); + return this.mapper.mapArrayAsync( + deletedParcels, + NotificationParcel, + NotificationParcelDto, + ); + } +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.dto.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.dto.ts new file mode 100644 index 0000000000..4a2a541c8b --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.dto.ts @@ -0,0 +1,93 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsBoolean, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, +} from 'class-validator'; +import { ParcelOwnershipTypeDto } from '../../../common/entities/parcel-ownership-type/parcel-ownership-type.entity'; + +export class NotificationParcelDto { + @AutoMap() + uuid: string; + + @AutoMap() + notificationSubmissionUuid: 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(() => Boolean) + isConfirmedByApplicant?: boolean; + + @AutoMap(() => String) + parcelType: string; + + @AutoMap(() => Number) + alrArea: number | null; + + ownershipType?: ParcelOwnershipTypeDto; + + @AutoMap(() => String) + ownershipTypeCode?: string; +} + +export class NotificationParcelCreateDto { + @IsNotEmpty() + @IsString() + notificationSubmissionUuid: string; + + @IsOptional() + @IsString() + parcelType?: string; +} + +export class NotificationParcelUpdateDto { + @IsString() + uuid: string; + + @IsString() + @IsOptional() + pid?: string | null; + + @IsString() + @IsOptional() + pin?: string | null; + + @IsString() + @IsOptional() + civicAddress?: string | null; + + @IsString() + @IsOptional() + legalDescription?: string | null; + + @IsNumber() + @IsOptional() + mapAreaHectares?: number | null; + + @IsBoolean() + @IsOptional() + isConfirmedByApplicant?: boolean; + + @IsString() + @IsOptional() + ownershipTypeCode?: string | null; + + @IsString() + @IsOptional() + crownLandOwnerType?: string | null; +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.entity.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.entity.ts new file mode 100644 index 0000000000..aced2c8b95 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.entity.ts @@ -0,0 +1,90 @@ +import { AutoMap } from '@automapper/classes'; +import { IsOptional, IsString } from 'class-validator'; +import { Column, Entity, ManyToOne } from 'typeorm'; +import { Base } from '../../../common/entities/base.entity'; +import { ParcelOwnershipType } from '../../../common/entities/parcel-ownership-type/parcel-ownership-type.entity'; +import { NotificationSubmission } from '../notification-submission.entity'; + +@Entity() +export class NotificationParcel extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: + 'The Parcels pid entered by the user or populated from third-party data', + nullable: true, + }) + pid?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: + 'The Parcels pin entered by the user or populated from third-party data', + nullable: true, + }) + pin?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: + 'The Parcels legalDescription entered by the user or populated from third-party data', + nullable: true, + }) + legalDescription?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: 'The standard address for the parcel', + nullable: true, + }) + civicAddress?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'float', + comment: + 'The Parcels map are in hectares entered by the user or populated from third-party data', + nullable: true, + }) + mapAreaHectares?: number | null; + + @AutoMap(() => Boolean) + @Column({ + type: 'boolean', + comment: + 'The Parcels indication whether applicant signed off provided data including the Certificate of Title', + nullable: false, + default: false, + }) + isConfirmedByApplicant: boolean; + + @IsString() + @IsOptional() + crownLandOwnerType?: string | null; + + @AutoMap(() => String) + @Column({ nullable: true }) + ownershipTypeCode?: string | null; + + @AutoMap() + @ManyToOne(() => ParcelOwnershipType) + ownershipType: ParcelOwnershipType; + + @AutoMap() + @ManyToOne(() => NotificationSubmission) + notificationSubmission: NotificationSubmission; + + @AutoMap() + @Column() + notificationSubmissionUuid: string; +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts new file mode 100644 index 0000000000..17a7c1c22e --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts @@ -0,0 +1,199 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { User } from '../../../user/user.entity'; +import { NotificationTransfereeService } from '../notification-transferee/notification-transferee.service'; +import { NotificationParcelUpdateDto } from './notification-parcel.dto'; +import { NotificationParcel } from './notification-parcel.entity'; +import { NotificationParcelService } from './notification-parcel.service'; + +describe('NotificationParcelService', () => { + let service: NotificationParcelService; + let mockParcelRepo: DeepMocked>; + let mockOwnerService: DeepMocked; + + const mockFileNumber = 'mock_applicationFileNumber'; + const mockUuid = 'mock_uuid'; + const mockNOIParcel = new NotificationParcel({ + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isConfirmedByApplicant: true, + notificationSubmissionUuid: mockFileNumber, + ownershipTypeCode: 'mock_ownershipTypeCode', + }); + const mockError = new Error('Parcel does not exist.'); + + beforeEach(async () => { + mockParcelRepo = createMock(); + mockOwnerService = createMock(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NotificationParcelService, + { + provide: getRepositoryToken(NotificationParcel), + useValue: mockParcelRepo, + }, + { + provide: NotificationTransfereeService, + useValue: mockOwnerService, + }, + ], + }).compile(); + + service = module.get(NotificationParcelService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should fetch parcels by fileNumber', async () => { + mockParcelRepo.find.mockResolvedValue([mockNOIParcel]); + + const result = await service.fetchByFileId(mockFileNumber); + + expect(result).toEqual([mockNOIParcel]); + expect(mockParcelRepo.find).toBeCalledTimes(1); + expect(mockParcelRepo.find).toBeCalledWith({ + where: { + notificationSubmission: { + fileNumber: mockFileNumber, + }, + }, + order: { auditCreatedAt: 'ASC' }, + }); + }); + + it('should get one parcel by id', async () => { + mockParcelRepo.findOneOrFail.mockResolvedValue(mockNOIParcel); + + const result = await service.getOneOrFail(mockUuid); + + expect(result).toEqual(mockNOIParcel); + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + }); + + it('should raise error on get parcel by uuid if the parcel does not exist', async () => { + mockParcelRepo.findOneOrFail.mockRejectedValue(mockError); + + await expect(service.getOneOrFail(mockUuid)).rejects.toMatchObject( + mockError, + ); + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + }); + + it('should successfully update parcel', async () => { + const updateParcelDto = [ + { + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isConfirmedByApplicant: true, + ownershipTypeCode: 'mock_ownershipTypeCode', + }, + ] as NotificationParcelUpdateDto[]; + + mockParcelRepo.findOneOrFail.mockResolvedValue(mockNOIParcel); + mockParcelRepo.save.mockResolvedValue({} as NotificationParcel); + + await service.update(updateParcelDto); + + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + expect(mockParcelRepo.save).toBeCalledTimes(1); + }); + + it('it should fail to update a parcel if the parcel does not exist. ', async () => { + const updateParcelDto: NotificationParcelUpdateDto[] = [ + { + uuid: mockUuid, + pid: 'mock_pid', + pin: 'mock_pin', + legalDescription: 'mock_legalDescription', + mapAreaHectares: 1, + isConfirmedByApplicant: true, + ownershipTypeCode: 'mock_ownershipTypeCode', + }, + ]; + const mockError = new Error('Parcel does not exist.'); + + mockParcelRepo.findOneOrFail.mockRejectedValue(mockError); + mockParcelRepo.save.mockResolvedValue(new NotificationParcel()); + + await expect(service.update(updateParcelDto)).rejects.toMatchObject( + mockError, + ); + expect(mockParcelRepo.findOneOrFail).toBeCalledTimes(1); + expect(mockParcelRepo.findOneOrFail).toBeCalledWith({ + where: { uuid: mockUuid }, + }); + expect(mockParcelRepo.save).toBeCalledTimes(0); + }); + + it('should successfully delete a parcel and update applicant', async () => { + mockParcelRepo.find.mockResolvedValue([mockNOIParcel]); + mockParcelRepo.remove.mockResolvedValue(new NotificationParcel()); + mockOwnerService.updateSubmissionApplicant.mockResolvedValue(); + + const result = await service.deleteMany([mockUuid], new User()); + + expect(result).toBeDefined(); + expect(mockParcelRepo.find).toBeCalledTimes(1); + expect(mockParcelRepo.find).toBeCalledWith({ + where: { uuid: In([mockUuid]) }, + }); + expect(mockParcelRepo.remove).toBeCalledWith([mockNOIParcel]); + expect(mockParcelRepo.remove).toBeCalledTimes(1); + expect(mockOwnerService.updateSubmissionApplicant).toHaveBeenCalledTimes(1); + }); + + it('should not call remove if the parcel does not exist', async () => { + const exception = new ServiceValidationException( + `Unable to find parcels with provided uuids: ${mockUuid}.`, + ); + + mockParcelRepo.find.mockResolvedValue([]); + mockParcelRepo.remove.mockResolvedValue(new NotificationParcel()); + + await expect( + service.deleteMany([mockUuid], new User()), + ).rejects.toMatchObject(exception); + expect(mockParcelRepo.find).toBeCalledTimes(1); + expect(mockParcelRepo.find).toBeCalledWith({ + where: { uuid: In([mockUuid]) }, + }); + expect(mockParcelRepo.remove).toBeCalledTimes(0); + }); + + it('should successfully create a parcel', async () => { + mockParcelRepo.save.mockResolvedValue({ + uuid: mockUuid, + notificationSubmissionUuid: mockFileNumber, + } as NotificationParcel); + + const mockParcel = new NotificationParcel({ + uuid: mockUuid, + notificationSubmissionUuid: mockFileNumber, + }); + + const result = await service.create(mockFileNumber); + + expect(result).toEqual(mockParcel); + expect(mockParcelRepo.save).toBeCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts new file mode 100644 index 0000000000..8328997211 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts @@ -0,0 +1,92 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; +import { User } from '../../../user/user.entity'; +import { filterUndefined } from '../../../utils/undefined'; +import { NotificationTransfereeService } from '../notification-transferee/notification-transferee.service'; +import { NotificationParcelUpdateDto } from './notification-parcel.dto'; +import { NotificationParcel } from './notification-parcel.entity'; + +@Injectable() +export class NotificationParcelService { + constructor( + @InjectRepository(NotificationParcel) + private parcelRepository: Repository, + @Inject(forwardRef(() => NotificationTransfereeService)) + private noticeOfIntentOwnerService: NotificationTransfereeService, + ) {} + + async fetchByFileId(fileId: string) { + return this.parcelRepository.find({ + where: { + notificationSubmission: { fileNumber: fileId }, + }, + order: { auditCreatedAt: 'ASC' }, + }); + } + + async fetchByApplicationSubmissionUuid(uuid: string) { + return this.parcelRepository.find({ + where: { notificationSubmissionUuid: uuid }, + order: { auditCreatedAt: 'ASC' }, + }); + } + + async create(notificationSubmissionUuid: string) { + const parcel = new NotificationParcel({ + notificationSubmissionUuid, + }); + + return this.parcelRepository.save(parcel); + } + + async getOneOrFail(uuid: string) { + return this.parcelRepository.findOneOrFail({ + where: { uuid }, + }); + } + + async update(updateDtos: NotificationParcelUpdateDto[]) { + const updatedParcels: NotificationParcel[] = []; + + for (const updateDto of updateDtos) { + const parcel = await this.getOneOrFail(updateDto.uuid); + + parcel.pid = updateDto.pid; + parcel.pin = updateDto.pin; + parcel.legalDescription = updateDto.legalDescription; + parcel.mapAreaHectares = updateDto.mapAreaHectares; + parcel.civicAddress = updateDto.civicAddress; + parcel.isConfirmedByApplicant = filterUndefined( + updateDto.isConfirmedByApplicant, + parcel.isConfirmedByApplicant, + ); + parcel.crownLandOwnerType = updateDto.crownLandOwnerType; + + updatedParcels.push(parcel); + } + + return await this.parcelRepository.save(updatedParcels); + } + + async deleteMany(uuids: string[], user: User) { + const parcels = await this.parcelRepository.find({ + where: { uuid: In(uuids) }, + }); + + if (parcels.length === 0) { + throw new ServiceValidationException( + `Unable to find parcels with provided uuids: ${uuids}.`, + ); + } + + const result = await this.parcelRepository.remove(parcels); + await this.noticeOfIntentOwnerService.updateSubmissionApplicant( + parcels[0].notificationSubmissionUuid, + user, + ); + + return result; + } +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts new file mode 100644 index 0000000000..53eed12b80 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts @@ -0,0 +1,264 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.entity'; +import { NotificationSubmissionProfile } from '../../common/automapper/notification-submission.automapper.profile'; +import { EmailService } from '../../providers/email/email.service'; +import { User } from '../../user/user.entity'; +import { NotificationSubmissionController } from './notification-submission.controller'; +import { + NotificationSubmissionDetailedDto, + NotificationSubmissionDto, +} from './notification-submission.dto'; +import { NotificationSubmission } from './notification-submission.entity'; +import { NotificationSubmissionService } from './notification-submission.service'; +import { NotificationTransferee } from './notification-transferee/notification-transferee.entity'; + +describe('NotificationSubmissionController', () => { + let controller: NotificationSubmissionController; + let mockNoiSubmissionService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockLgService: DeepMocked; + let mockEmailService: DeepMocked; + + const primaryContactOwnerUuid = 'primary-contact'; + const localGovernmentUuid = 'local-government'; + const applicant = 'fake-applicant'; + const bceidBusinessGuid = 'business-guid'; + + beforeEach(async () => { + mockNoiSubmissionService = createMock(); + mockDocumentService = createMock(); + mockLgService = createMock(); + mockEmailService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [NotificationSubmissionController], + providers: [ + NotificationSubmissionProfile, + { + provide: NotificationSubmissionService, + useValue: mockNoiSubmissionService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockDocumentService, + }, + { + provide: LocalGovernmentService, + useValue: mockLgService, + }, + { + provide: EmailService, + useValue: mockEmailService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + }).compile(); + + controller = module.get( + NotificationSubmissionController, + ); + + mockNoiSubmissionService.update.mockResolvedValue( + new NotificationSubmission({ + applicant: applicant, + localGovernmentUuid, + }), + ); + + mockNoiSubmissionService.create.mockResolvedValue('2'); + mockNoiSubmissionService.getByFileNumber.mockResolvedValue( + new NotificationSubmission(), + ); + mockNoiSubmissionService.getByUuid.mockResolvedValue( + new NotificationSubmission(), + ); + + mockNoiSubmissionService.mapToDTOs.mockResolvedValue([]); + mockLgService.list.mockResolvedValue([ + new LocalGovernment({ + uuid: localGovernmentUuid, + bceidBusinessGuid, + name: 'fake-name', + isFirstNation: false, + }), + ]); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call out to service when fetching notice of intents', async () => { + mockNoiSubmissionService.getAllByUser.mockResolvedValue([]); + + const submissions = await controller.getSubmissions({ + user: { + entity: new User(), + }, + }); + + expect(submissions).toBeDefined(); + expect(mockNoiSubmissionService.getAllByUser).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when fetching a notice of intent', async () => { + mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as NotificationSubmissionDetailedDto, + ); + + const noticeOfIntent = controller.getSubmission( + { + user: { + entity: new User(), + }, + }, + '', + ); + + expect(noticeOfIntent).toBeDefined(); + expect(mockNoiSubmissionService.getByUuid).toHaveBeenCalledTimes(1); + }); + + it('should fetch notice of intent by bceid if user has same guid as a local government', async () => { + mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as NotificationSubmissionDetailedDto, + ); + mockNoiSubmissionService.getByUuid.mockResolvedValue( + new NotificationSubmission({ + localGovernmentUuid: '', + }), + ); + + const noiSubmission = controller.getSubmission( + { + user: { + entity: new User({ + bceidBusinessGuid: 'guid', + }), + }, + }, + '', + ); + + expect(noiSubmission).toBeDefined(); + expect(mockNoiSubmissionService.getByUuid).toHaveBeenCalledTimes(1); + }); + + it('should call out to service when creating an notice of intent', async () => { + mockNoiSubmissionService.create.mockResolvedValue(''); + mockNoiSubmissionService.mapToDTOs.mockResolvedValue([ + {} as NotificationSubmissionDto, + ]); + + const noiSubmission = await controller.create({ + user: { + entity: new User(), + }, + }); + + expect(noiSubmission).toBeDefined(); + expect(mockNoiSubmissionService.create).toHaveBeenCalledTimes(1); + }); + + it('should call out to service for update and map', async () => { + mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as NotificationSubmissionDetailedDto, + ); + mockNoiSubmissionService.getByUuid.mockResolvedValue( + new NotificationSubmission({}), + ); + + await controller.update( + 'file-id', + { + localGovernmentUuid, + applicant, + }, + { + user: { + entity: new User({ + clientRoles: [], + }), + }, + }, + ); + + expect(mockNoiSubmissionService.update).toHaveBeenCalledTimes(1); + expect(mockNoiSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(1); + }); + + // it('should throw an exception when trying to update a not in progress noi', async () => { + // mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + // {} as NotificationSubmissionDetailedDto, + // ); + // + // const promise = controller.update( + // 'file-id', + // { + // localGovernmentUuid, + // applicant, + // }, + // { + // user: { + // entity: new User({ + // clientRoles: [], + // }), + // }, + // }, + // ); + // await expect(promise).rejects.toMatchObject( + // new BadRequestException('Can only edit in progress Notice of Intents'), + // ); + // + // expect(mockNoiSubmissionService.update).toHaveBeenCalledTimes(0); + // expect(mockNoiSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(0); + // }); + + it('should call out to service on submitAlcs', async () => { + const mockFileId = 'file-id'; + const mockOwner = new NotificationTransferee({ + uuid: primaryContactOwnerUuid, + }); + const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); + const mockSubmission = new NotificationSubmission({ + fileNumber: mockFileId, + transferees: [mockOwner], + localGovernmentUuid, + }); + + mockNoiSubmissionService.submitToAlcs.mockResolvedValue( + new NoticeOfIntent(), + ); + mockNoiSubmissionService.getByUuid.mockResolvedValue(mockSubmission); + mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as NotificationSubmissionDetailedDto, + ); + + await controller.submitAsApplicant(mockFileId, { + user: { + entity: new User(), + }, + }); + + expect(mockNoiSubmissionService.getByUuid).toHaveBeenCalledTimes(2); + expect(mockNoiSubmissionService.submitToAlcs).toHaveBeenCalledTimes(1); + expect(mockNoiSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts new file mode 100644 index 0000000000..67a0b261ee --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts @@ -0,0 +1,149 @@ +import { + Body, + Controller, + Get, + Logger, + Param, + Post, + Put, + Req, + UseGuards, +} from '@nestjs/common'; +import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; +import { User } from '../../user/user.entity'; +import { NotificationSubmissionUpdateDto } from './notification-submission.dto'; +import { NotificationSubmissionService } from './notification-submission.service'; + +@Controller('notification-submission') +@UseGuards(PortalAuthGuard) +export class NotificationSubmissionController { + private logger: Logger = new Logger(NotificationSubmissionController.name); + + constructor( + private notificationSubmissionService: NotificationSubmissionService, + ) {} + + @Get() + async getSubmissions(@Req() req) { + const user = req.user.entity as User; + + const notificationSubmissions = + await this.notificationSubmissionService.getAllByUser(user); + return this.notificationSubmissionService.mapToDTOs( + notificationSubmissions, + user, + ); + } + + @Get('/notification/:fileId') + async getSubmissionByFileId(@Req() req, @Param('fileId') fileId: string) { + const user = req.user.entity as User; + + const submission = await this.notificationSubmissionService.getByFileNumber( + fileId, + user, + ); + + return await this.notificationSubmissionService.mapToDetailedDTO( + submission, + user, + ); + } + + @Get('/:uuid') + async getSubmission(@Req() req, @Param('uuid') uuid: string) { + const user = req.user.entity as User; + + const submission = await this.notificationSubmissionService.getByUuid( + uuid, + user, + ); + + return await this.notificationSubmissionService.mapToDetailedDTO( + submission, + user, + ); + } + + @Post() + async create(@Req() req) { + const user = req.user.entity as User; + const newFileNumber = await this.notificationSubmissionService.create( + 'SRW', + user, + ); + return { + fileId: newFileNumber, + }; + } + + @Put('/:uuid') + async update( + @Param('uuid') uuid: string, + @Body() updateDto: NotificationSubmissionUpdateDto, + @Req() req, + ) { + // const noticeOfIntentSubmission = await this.notificationSubmissionService.getByUuid( + // uuid, + // req.user.entity, + // ); + // + // if ( + // noticeOfIntentSubmission.status.statusTypeCode !== + // NOI_SUBMISSION_STATUS.IN_PROGRESS && + // overlappingRoles.length === 0 + // ) { + // throw new BadRequestException('Can only edit in progress SRWs'); + // } + + const updatedSubmission = await this.notificationSubmissionService.update( + uuid, + updateDto, + req.user.entity, + ); + + return await this.notificationSubmissionService.mapToDetailedDTO( + updatedSubmission, + req.user.entity, + ); + } + + @Post('/:uuid/cancel') + async cancel(@Param('uuid') uuid: string, @Req() req) { + const noticeOfIntentSubmission = + await this.notificationSubmissionService.getByUuid(uuid, req.user.entity); + + // if ( + // noticeOfIntentSubmission.status.statusTypeCode !== + // NOI_SUBMISSION_STATUS.IN_PROGRESS + // ) { + // throw new BadRequestException('Can only cancel in progress SRWs'); + // } + + await this.notificationSubmissionService.cancel(noticeOfIntentSubmission); + + return { + cancelled: true, + }; + } + + @Post('/alcs/submit/:uuid') + async submitAsApplicant(@Param('uuid') uuid: string, @Req() req) { + const notificationSubmission = + await this.notificationSubmissionService.getByUuid(uuid, req.user.entity); + + await this.notificationSubmissionService.submitToAlcs( + notificationSubmission, + ); + + const finalSubmission = await this.notificationSubmissionService.getByUuid( + uuid, + req.user.entity, + ); + + return await this.notificationSubmissionService.mapToDetailedDTO( + finalSubmission, + req.user.entity, + ); + } +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts new file mode 100644 index 0000000000..634a4b8fa7 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts @@ -0,0 +1,59 @@ +import { AutoMap } from '@automapper/classes'; +import { IsOptional, IsString, IsUUID, MaxLength } from 'class-validator'; +import { NoticeOfIntentStatusDto } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NotificationTransfereeDto } from './notification-transferee/notification-transferee.dto'; + +export const MAX_DESCRIPTION_FIELD_LENGTH = 4000; + +export class NotificationSubmissionDto { + @AutoMap() + fileNumber: string; + + @AutoMap() + uuid: string; + + @AutoMap() + createdAt: number; + + @AutoMap() + updatedAt: number; + + @AutoMap() + applicant: string; + + @AutoMap() + localGovernmentUuid: string; + + @AutoMap() + type: string; + + @AutoMap() + typeCode: string; + + status: NoticeOfIntentStatusDto; + lastStatusUpdate: number; + owners: NotificationTransfereeDto[]; + + canEdit: boolean; + canView: boolean; +} + +export class NotificationSubmissionDetailedDto extends NotificationSubmissionDto { + @AutoMap(() => String) + purpose: string | null; +} + +export class NotificationSubmissionUpdateDto { + @IsString() + @IsOptional() + applicant?: string; + + @IsString() + @IsOptional() + @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) + purpose?: string; + + @IsUUID() + @IsOptional() + localGovernmentUuid?: string; +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts new file mode 100644 index 0000000000..f74317f39c --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts @@ -0,0 +1,88 @@ +import { AutoMap } from '@automapper/classes'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { Notification } from '../../alcs/notification/notification.entity'; +import { Base } from '../../common/entities/base.entity'; +import { User } from '../../user/user.entity'; +import { NotificationParcel } from './notification-parcel/notification-parcel.entity'; +import { NotificationTransferee } from './notification-transferee/notification-transferee.entity'; + +@Entity() +export class NotificationSubmission extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @PrimaryGeneratedColumn('uuid') + uuid: string; + + @AutoMap({}) + @Column({ + comment: 'File Number of attached SRW', + }) + fileNumber: string; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: 'The Applicants name on the application', + nullable: true, + }) + applicant?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'uuid', + comment: 'UUID of the Local Government', + nullable: true, + }) + localGovernmentUuid?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: 'The purpose of the application', + nullable: true, + }) + purpose?: string | null; + + @AutoMap() + @ManyToOne(() => User) + createdBy: User; + + @AutoMap() + @Column({ + comment: 'SRW Type Code', + }) + typeCode: string; + + @AutoMap(() => Notification) + @ManyToOne(() => Notification) + @JoinColumn({ + name: 'file_number', + referencedColumnName: 'fileNumber', + }) + notification: Notification; + + @OneToMany( + () => NotificationTransferee, + (transferee) => transferee.notificationSubmission, + ) + transferees: NotificationTransferee[]; + + @OneToMany( + () => NotificationParcel, + (parcel) => parcel.notificationSubmission, + ) + parcels: NotificationParcel[]; +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts new file mode 100644 index 0000000000..83c31fd910 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts @@ -0,0 +1,58 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BoardModule } from '../../alcs/board/board.module'; +import { LocalGovernmentModule } from '../../alcs/local-government/local-government.module'; +import { NotificationModule } from '../../alcs/notification/notification.module'; +import { AuthorizationModule } from '../../common/authorization/authorization.module'; +import { NotificationParcelProfile } from '../../common/automapper/notification-parcel.automapper.profile'; +import { NotificationSubmissionProfile } from '../../common/automapper/notification-submission.automapper.profile'; +import { NotificationTransfereeProfile } from '../../common/automapper/notification-transferee.automapper.profile'; +import { OwnerType } from '../../common/owner-type/owner-type.entity'; +import { DocumentModule } from '../../document/document.module'; +import { FileNumberModule } from '../../file-number/file-number.module'; +import { NotificationParcelController } from './notification-parcel/notification-parcel.controller'; +import { NotificationParcel } from './notification-parcel/notification-parcel.entity'; +import { NotificationParcelService } from './notification-parcel/notification-parcel.service'; +import { NotificationSubmissionController } from './notification-submission.controller'; +import { NotificationSubmission } from './notification-submission.entity'; +import { NotificationSubmissionService } from './notification-submission.service'; +import { NotificationTransfereeController } from './notification-transferee/notification-transferee.controller'; +import { NotificationTransferee } from './notification-transferee/notification-transferee.entity'; +import { NotificationTransfereeService } from './notification-transferee/notification-transferee.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + NotificationSubmission, + NotificationParcel, + OwnerType, + NotificationTransferee, + ]), + forwardRef(() => NotificationModule), + forwardRef(() => AuthorizationModule), + forwardRef(() => DocumentModule), + forwardRef(() => BoardModule), + LocalGovernmentModule, + FileNumberModule, + ], + controllers: [ + NotificationSubmissionController, + NotificationParcelController, + NotificationTransfereeController, + ], + providers: [ + NotificationSubmissionService, + NotificationParcelService, + NotificationTransfereeService, + NotificationSubmissionProfile, + NotificationTransfereeProfile, + NotificationParcelProfile, + ], + exports: [ + NotificationSubmissionService, + NotificationParcelService, + NotificationParcelProfile, + NotificationTransfereeService, + ], +}) +export class NotificationSubmissionModule {} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts new file mode 100644 index 0000000000..efd7d9acbb --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts @@ -0,0 +1,248 @@ +import { BaseServiceException } 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 { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NoticeOfIntentType } from '../../alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity'; +import { Notification } from '../../alcs/notification/notification.entity'; +import { NotificationService } from '../../alcs/notification/notification.service'; +import { NotificationSubmissionProfile } from '../../common/automapper/notification-submission.automapper.profile'; +import { FileNumberService } from '../../file-number/file-number.service'; +import { User } from '../../user/user.entity'; +import { NotificationSubmission } from './notification-submission.entity'; +import { NotificationSubmissionService } from './notification-submission.service'; + +describe('NotificationSubmissionService', () => { + let service: NotificationSubmissionService; + let mockRepository: DeepMocked>; + let mockNotificationService: DeepMocked; + let mockLGService: DeepMocked; + let mockFileNumberService: DeepMocked; + let mockSubmission; + + beforeEach(async () => { + mockRepository = createMock(); + mockNotificationService = createMock(); + mockLGService = createMock(); + mockFileNumberService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + providers: [ + NotificationSubmissionService, + NotificationSubmissionProfile, + { + provide: getRepositoryToken(NotificationSubmission), + useValue: mockRepository, + }, + { + provide: NotificationService, + useValue: mockNotificationService, + }, + { + provide: LocalGovernmentService, + useValue: mockLGService, + }, + { + provide: FileNumberService, + useValue: mockFileNumberService, + }, + ], + }).compile(); + + service = module.get( + NotificationSubmissionService, + ); + + mockSubmission = new NotificationSubmission({ + fileNumber: 'file-number', + applicant: 'incognito', + typeCode: 'fake', + localGovernmentUuid: 'uuid', + createdBy: new User({ + clientRoles: [], + }), + }); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should return the fetched notification', async () => { + const notificationSubmission = new NotificationSubmission(); + mockRepository.findOneOrFail.mockResolvedValue(notificationSubmission); + + const app = await service.getByFileNumber( + '', + new User({ + clientRoles: [], + }), + ); + expect(app).toBe(notificationSubmission); + }); + + it('should return the fetched notification when fetching with user', async () => { + const noiSubmission = new NotificationSubmission(); + mockRepository.findOneOrFail.mockResolvedValue(noiSubmission); + + const app = await service.getByFileNumber( + '', + new User({ + clientRoles: [], + }), + ); + expect(app).toBe(noiSubmission); + }); + + it('save a new noi for create', async () => { + const fileId = 'file-id'; + mockRepository.findOne.mockResolvedValue(null); + mockRepository.save.mockResolvedValue(new NotificationSubmission()); + mockFileNumberService.generateNextFileNumber.mockResolvedValue(fileId); + mockNotificationService.create.mockResolvedValue(new Notification()); + + const fileNumber = await service.create( + 'type', + new User({ + clientRoles: [], + }), + ); + + expect(fileNumber).toEqual(fileId); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + expect(mockNotificationService.create).toHaveBeenCalledTimes(1); + }); + + it('should call through for get by user', async () => { + const noiSubmission = new NotificationSubmission(); + mockRepository.find.mockResolvedValue([noiSubmission]); + + const res = await service.getAllByUser( + new User({ + clientRoles: [], + }), + ); + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(res.length).toEqual(1); + expect(res[0]).toBe(noiSubmission); + }); + + it('should call through for getByFileId', async () => { + const noiSubmission = new NotificationSubmission(); + mockRepository.findOneOrFail.mockResolvedValue(noiSubmission); + + const res = await service.getByFileNumber( + '', + new User({ + clientRoles: [], + }), + ); + expect(mockRepository.findOneOrFail).toHaveBeenCalledTimes(1); + expect(res).toBe(noiSubmission); + }); + + it('should use notification type service for mapping DTOs', async () => { + const applicant = 'Bruce Wayne'; + const typeCode = 'fake-code'; + + mockNotificationService.listTypes.mockResolvedValue([ + new NoticeOfIntentType({ + code: typeCode, + portalLabel: 'portalLabel', + htmlDescription: 'htmlDescription', + label: 'label', + }), + ]); + + const noiSubmission = new NotificationSubmission({ + applicant, + typeCode: typeCode, + auditCreatedAt: new Date(), + createdBy: new User(), + }); + mockRepository.findOne.mockResolvedValue(noiSubmission); + + const res = await service.mapToDTOs( + [noiSubmission], + new User({ + clientRoles: [], + }), + ); + expect(mockNotificationService.listTypes).toHaveBeenCalledTimes(1); + expect(res[0].type).toEqual('label'); + expect(res[0].applicant).toEqual(applicant); + }); + + it('should fail on submitToAlcs if error', async () => { + const applicant = 'Bruce Wayne'; + const typeCode = 'fake-code'; + const fileNumber = 'fake'; + const localGovernmentUuid = 'fake-uuid'; + const noticeOfIntentSubmission = new NotificationSubmission({ + fileNumber, + applicant, + typeCode, + localGovernmentUuid, + }); + + mockNotificationService.submit.mockRejectedValue(new Error()); + + await expect( + service.submitToAlcs(noticeOfIntentSubmission), + ).rejects.toMatchObject( + new BaseServiceException(`Failed to submit notification: ${fileNumber}`), + ); + }); + + it('should call out to service on submitToAlcs', async () => { + const notification = new Notification({ + dateSubmittedToAlc: new Date(), + }); + + mockNotificationService.submit.mockResolvedValue(notification); + await service.submitToAlcs(mockSubmission); + + expect(mockNotificationService.submit).toBeCalledTimes(1); + }); + + it('should update fields if notification exists', async () => { + const applicant = 'Bruce Wayne'; + const fileNumber = 'fake'; + const localGovernmentUuid = 'fake-uuid'; + + mockRepository.findOneOrFail.mockResolvedValue(mockSubmission); + mockRepository.save.mockResolvedValue(mockSubmission); + mockNotificationService.update.mockResolvedValue(new Notification()); + + const result = await service.update( + fileNumber, + { + applicant, + localGovernmentUuid, + }, + new User({ + clientRoles: [], + }), + ); + + expect(mockRepository.save).toBeCalledTimes(1); + expect(mockRepository.findOneOrFail).toBeCalledTimes(2); + }); + + it('should return the fetched notification when fetching with file number', async () => { + const noiSubmission = new NotificationSubmission(); + mockRepository.findOneOrFail.mockResolvedValue(noiSubmission); + + const app = await service.getOrFailByFileNumber(''); + + expect(app).toBe(noiSubmission); + }); +}); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts new file mode 100644 index 0000000000..581f183229 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts @@ -0,0 +1,336 @@ +import { BaseServiceException } from '@app/common/exceptions/base.exception'; +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + FindOptionsRelations, + FindOptionsWhere, + IsNull, + Not, + Repository, +} from 'typeorm'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NotificationService } from '../../alcs/notification/notification.service'; +import { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; +import { FileNumberService } from '../../file-number/file-number.service'; +import { User } from '../../user/user.entity'; +import { FALLBACK_APPLICANT_NAME } from '../../utils/owner.constants'; +import { filterUndefined } from '../../utils/undefined'; +import { + NotificationSubmissionDetailedDto, + NotificationSubmissionDto, + NotificationSubmissionUpdateDto, +} from './notification-submission.dto'; +import { NotificationSubmission } from './notification-submission.entity'; + +@Injectable() +export class NotificationSubmissionService { + private logger: Logger = new Logger(NotificationSubmissionService.name); + + private DEFAULT_RELATIONS: FindOptionsRelations = { + createdBy: true, + transferees: { + type: true, + }, + parcels: true, + }; + + constructor( + @InjectRepository(NotificationSubmission) + private notificationSubmissionRepository: Repository, + private notificationService: NotificationService, + private localGovernmentService: LocalGovernmentService, + private fileNumberService: FileNumberService, + @InjectMapper() private mapper: Mapper, + ) {} + + async getOrFailByFileNumber(fileNumber: string) { + return await this.notificationSubmissionRepository.findOneOrFail({ + where: { + fileNumber, + }, + relations: this.DEFAULT_RELATIONS, + }); + } + + async create(type: string, createdBy: User) { + const fileNumber = await this.fileNumberService.generateNextFileNumber(); + + await this.notificationService.create({ + fileNumber, + applicant: FALLBACK_APPLICANT_NAME, + typeCode: type, + }); + + const noiSubmission = new NotificationSubmission({ + fileNumber, + typeCode: type, + createdBy, + }); + + const savedSubmission = await this.notificationSubmissionRepository.save( + noiSubmission, + ); + + // await this.noticeOfIntentSubmissionStatusService.setInitialStatuses( + // savedSubmission.uuid, + // ); + + return fileNumber; + } + + async update( + submissionUuid: string, + updateDto: NotificationSubmissionUpdateDto, + user: User, + ) { + const notificationSubmission = await this.getByUuid(submissionUuid, user); + + notificationSubmission.applicant = updateDto.applicant; + notificationSubmission.purpose = filterUndefined( + updateDto.purpose, + notificationSubmission.purpose, + ); + notificationSubmission.localGovernmentUuid = updateDto.localGovernmentUuid; + + await this.notificationSubmissionRepository.save(notificationSubmission); + + if (updateDto.localGovernmentUuid) { + await this.notificationService.update(notificationSubmission.fileNumber, { + localGovernmentUuid: updateDto.localGovernmentUuid, + }); + } + + return this.getByUuid(submissionUuid, user); + } + + async getFileNumber(submissionUuid: string) { + const submission = await this.notificationSubmissionRepository.findOne({ + where: { + uuid: submissionUuid, + }, + select: { + uuid: true, + fileNumber: true, + }, + }); + return submission?.fileNumber; + } + + async getAllByUser(user: User) { + const whereClauses = await this.generateWhereClauses({}, user); + + return this.notificationSubmissionRepository.find({ + where: whereClauses, + order: { + auditUpdatedAt: 'DESC', + }, + relations: this.DEFAULT_RELATIONS, + }); + } + + async getByFileNumber(fileNumber: string, user: User) { + const overlappingRoles = ROLES_ALLOWED_APPLICATIONS.filter((value) => + user.clientRoles!.includes(value), + ); + if (overlappingRoles.length > 0) { + return await this.getOrFailByFileNumber(fileNumber); + } + + const whereClauses = await this.generateWhereClauses( + { + fileNumber, + }, + user, + ); + + return this.notificationSubmissionRepository.findOneOrFail({ + where: whereClauses, + order: { + auditUpdatedAt: 'DESC', + }, + relations: this.DEFAULT_RELATIONS, + }); + } + + async getByUuid(uuid: string, user: User) { + const overlappingRoles = ROLES_ALLOWED_APPLICATIONS.filter((value) => + user.clientRoles!.includes(value), + ); + if (overlappingRoles.length > 0) { + return await this.notificationSubmissionRepository.findOneOrFail({ + where: { + uuid, + }, + relations: { + ...this.DEFAULT_RELATIONS, + }, + }); + } + + const findOptions = await this.generateWhereClauses( + { + uuid, + }, + user, + ); + + return this.notificationSubmissionRepository.findOneOrFail({ + where: findOptions, + relations: { + ...this.DEFAULT_RELATIONS, + }, + order: { + auditUpdatedAt: 'DESC', + }, + }); + } + + private async generateWhereClauses( + searchOptions: FindOptionsWhere, + user: User, + ) { + const searchQueries: FindOptionsWhere[] = []; + + searchQueries.push({ + ...searchOptions, + createdBy: { + uuid: user.uuid, + }, + }); + + if (user.bceidBusinessGuid) { + searchQueries.push({ + ...searchOptions, + createdBy: { + bceidBusinessGuid: user.bceidBusinessGuid, + }, + }); + + const matchingLocalGovernment = + await this.localGovernmentService.getByGuid(user.bceidBusinessGuid); + if (matchingLocalGovernment) { + searchQueries.push({ + ...searchOptions, + localGovernmentUuid: matchingLocalGovernment.uuid, + notification: { + dateSubmittedToAlc: Not(IsNull()), + }, + }); + } + } + + return searchQueries; + } + + async mapToDTOs(submissions: NotificationSubmission[], user: User) { + const types = await this.notificationService.listTypes(); + + return submissions.map((noiSubmission) => { + const isCreator = noiSubmission.createdBy.uuid === user.uuid; + const isSameAccount = + user.bceidBusinessGuid && + noiSubmission.createdBy.bceidBusinessGuid === user.bceidBusinessGuid; + + return { + ...this.mapper.map( + noiSubmission, + NotificationSubmission, + NotificationSubmissionDto, + ), + type: types.find((type) => type.code === noiSubmission.typeCode)!.label, + canEdit: isCreator || isSameAccount, + canView: true, + }; + }); + } + + async mapToDetailedDTO( + notificationSubmission: NotificationSubmission, + user: User, + ) { + const types = await this.notificationService.listTypes(); + const mappedApp = this.mapper.map( + notificationSubmission, + NotificationSubmission, + NotificationSubmissionDetailedDto, + ); + const isCreator = notificationSubmission.createdBy.uuid === user.uuid; + const isSameAccount = + user.bceidBusinessGuid && + notificationSubmission.createdBy.bceidBusinessGuid === + user.bceidBusinessGuid; + + return { + ...mappedApp, + type: types.find((type) => type.code === notificationSubmission.typeCode)! + .label, + canEdit: isCreator || isSameAccount, + canView: true, + }; + } + + async submitToAlcs(notificationSubmission: NotificationSubmission) { + try { + const submittedNotification = await this.notificationService.submit({ + fileNumber: notificationSubmission.fileNumber, + applicant: notificationSubmission.applicant!, + localGovernmentUuid: notificationSubmission.localGovernmentUuid!, + typeCode: notificationSubmission.typeCode, + dateSubmittedToAlc: new Date(), + }); + + // await this.noticeOfIntentSubmissionStatusService.setStatusDate( + // notificationSubmission.uuid, + // NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, + // submittedNotification.dateSubmittedToAlc, + // ); + + return submittedNotification; + } catch (ex) { + this.logger.error(ex); + throw new BaseServiceException( + `Failed to submit notification: ${notificationSubmission.fileNumber}`, + ); + } + } + + async updateStatus( + uuid: string, + statusCode: NOI_SUBMISSION_STATUS, + effectiveDate?: Date | null, + ) { + const submission = await this.loadBarebonesSubmission(uuid); + // await this.noticeOfIntentSubmissionStatusService.setStatusDate( + // submission.uuid, + // statusCode, + // effectiveDate, + // ); + } + + async getStatus(code: NOI_SUBMISSION_STATUS) { + // return await this.noticeOfIntentStatusRepository.findOneOrFail({ + // where: { + // code, + // }, + // }); + } + + async cancel(submission: NotificationSubmission) { + // return await this.noticeOfIntentSubmissionStatusService.setStatusDate( + // noticeOfIntentSubmission.uuid, + // NOI_SUBMISSION_STATUS.CANCELLED, + // ); + } + + private loadBarebonesSubmission(uuid: string) { + //Load submission without relations to prevent save from crazy cascading + return this.notificationSubmissionRepository.findOneOrFail({ + where: { + uuid, + }, + }); + } +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.spec.ts new file mode 100644 index 0000000000..87fed7ade8 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.spec.ts @@ -0,0 +1,202 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { BadRequestException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; +import { NotificationTransfereeProfile } from '../../../common/automapper/notification-transferee.automapper.profile'; +import { DocumentService } from '../../../document/document.service'; +import { NotificationSubmission } from '../notification-submission.entity'; +import { NotificationSubmissionService } from '../notification-submission.service'; +import { NotificationTransfereeController } from './notification-transferee.controller'; +import { NotificationTransferee } from './notification-transferee.entity'; +import { NotificationTransfereeService } from './notification-transferee.service'; + +describe('NotificationTransfereeController', () => { + let controller: NotificationTransfereeController; + let mockNOISubmissionService: DeepMocked; + let mockOwnerService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockNOIDocumentService: DeepMocked; + + beforeEach(async () => { + mockNOISubmissionService = createMock(); + mockOwnerService = createMock(); + mockDocumentService = createMock(); + mockNOIDocumentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NotificationTransfereeController], + providers: [ + { + provide: NotificationSubmissionService, + useValue: mockNOISubmissionService, + }, + { + provide: NotificationTransfereeService, + useValue: mockOwnerService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: NoticeOfIntentDocumentService, + useValue: mockNOIDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + NotificationTransfereeProfile, + ...mockKeyCloakProviders, + ], + }).compile(); + + controller = module.get( + NotificationTransfereeController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should verify access before fetching applications and map displayName', async () => { + const transferee = new NotificationTransferee({ + firstName: 'Bruce', + lastName: 'Wayne', + }); + mockNOISubmissionService.getByUuid.mockResolvedValue( + new NotificationSubmission({ + transferees: [transferee], + }), + ); + + const owners = await controller.fetchByFileId('', { + user: { + entity: {}, + }, + }); + + expect(owners.length).toEqual(1); + expect(owners[0].displayName).toBe('Bruce Wayne'); + expect(mockNOISubmissionService.getByUuid).toHaveBeenCalledTimes(1); + }); + + it('should verify the dto and file access then create', async () => { + const owner = new NotificationTransferee({ + firstName: 'Bruce', + lastName: 'Wayne', + }); + mockNOISubmissionService.getByUuid.mockResolvedValue( + new NotificationSubmission(), + ); + mockOwnerService.create.mockResolvedValue(owner); + + const createdOwner = await controller.create( + { + firstName: 'B', + lastName: 'W', + notificationSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: 'INDV', + }, + { + user: { + entity: {}, + }, + }, + ); + + expect(createdOwner).toBeDefined(); + expect(mockNOISubmissionService.getByUuid).toHaveBeenCalledTimes(1); + expect(mockOwnerService.create).toHaveBeenCalledTimes(1); + }); + + it('should throw an exception when creating an individual owner without first name', async () => { + const promise = controller.create( + { + lastName: 'W', + notificationSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: 'INDV', + }, + { + user: { + entity: {}, + }, + }, + ); + await expect(promise).rejects.toMatchObject( + new BadRequestException('Individuals require both first and last name'), + ); + }); + + it('should throw an exception when creating an organization an org name', async () => { + const promise = controller.create( + { + notificationSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: 'ORGZ', + }, + { + user: { + entity: {}, + }, + }, + ); + await expect(promise).rejects.toMatchObject( + new BadRequestException('Organizations must have an organizationName'), + ); + }); + + it('should call through for update', async () => { + mockOwnerService.update.mockResolvedValue(new NotificationTransferee()); + + const res = await controller.update( + '', + { + organizationName: 'orgName', + email: '', + phoneNumber: '', + typeCode: 'ORGZ', + }, + { + user: { + entity: {}, + }, + }, + ); + + expect(mockOwnerService.update).toHaveBeenCalledTimes(1); + }); + + it('should call through for delete', async () => { + mockOwnerService.delete.mockResolvedValue({} as any); + mockOwnerService.getOwner.mockResolvedValue(new NotificationTransferee()); + mockNOISubmissionService.getByUuid.mockResolvedValue( + new NotificationSubmission(), + ); + + await controller.delete('', { + user: { + entity: {}, + }, + }); + + expect(mockNOISubmissionService.getByUuid).toHaveBeenCalledTimes(1); + expect(mockOwnerService.delete).toHaveBeenCalledTimes(1); + expect(mockOwnerService.getOwner).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts new file mode 100644 index 0000000000..1262329846 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts @@ -0,0 +1,137 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + BadRequestException, + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { PortalAuthGuard } from '../../../common/authorization/portal-auth-guard.service'; +import { OWNER_TYPE } from '../../../common/owner-type/owner-type.entity'; +import { + NoticeOfIntentOwnerCreateDto, + NoticeOfIntentOwnerUpdateDto, +} from '../../notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.dto'; +import { NotificationSubmissionService } from '../notification-submission.service'; +import { + NotificationTransfereeCreateDto, + NotificationTransfereeDto, + NotificationTransfereeUpdateDto, +} from './notification-transferee.dto'; +import { NotificationTransferee } from './notification-transferee.entity'; +import { NotificationTransfereeService } from './notification-transferee.service'; + +@Controller('srw-transferee') +@UseGuards(PortalAuthGuard) +export class NotificationTransfereeController { + constructor( + private ownerService: NotificationTransfereeService, + private notificationSubmissionService: NotificationSubmissionService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('submission/:submissionUuid') + async fetchByFileId( + @Param('submissionUuid') submissionUuid: string, + @Req() req, + ): Promise { + const notificationSubmission = + await this.notificationSubmissionService.getByUuid( + submissionUuid, + req.user.entity, + ); + + return this.mapper.mapArrayAsync( + notificationSubmission.transferees, + NotificationTransferee, + NotificationTransfereeDto, + ); + } + + @Post() + async create( + @Body() createDto: NotificationTransfereeCreateDto, + @Req() req, + ): Promise { + this.verifyDto(createDto); + + const noticeOfIntentSubmission = + await this.notificationSubmissionService.getByUuid( + createDto.notificationSubmissionUuid, + req.user.entity, + ); + const owner = await this.ownerService.create( + createDto, + noticeOfIntentSubmission, + ); + + return this.mapper.mapAsync( + owner, + NotificationTransferee, + NotificationTransfereeDto, + ); + } + + @Patch('/:uuid') + async update( + @Param('uuid') uuid: string, + @Body() updateDto: NotificationTransfereeUpdateDto, + @Req() req, + ) { + this.verifyDto(updateDto); + + const newParcel = await this.ownerService.update( + uuid, + updateDto, + req.user.entity, + ); + + return this.mapper.mapAsync( + newParcel, + NotificationTransferee, + NotificationTransfereeDto, + ); + } + + @Delete('/:uuid') + async delete(@Param('uuid') uuid: string, @Req() req) { + const owner = await this.verifyAccessAndGetOwner(req, uuid); + await this.ownerService.delete(owner, req.user.entity); + return { uuid }; + } + + private verifyDto( + dto: NoticeOfIntentOwnerUpdateDto | NoticeOfIntentOwnerCreateDto, + ) { + if ( + dto.typeCode === OWNER_TYPE.INDIVIDUAL && + (!dto.firstName || !dto.lastName) + ) { + throw new BadRequestException( + 'Individuals require both first and last name', + ); + } + + if (dto.typeCode === OWNER_TYPE.ORGANIZATION && !dto.organizationName) { + throw new BadRequestException( + 'Organizations must have an organizationName', + ); + } + } + + private async verifyAccessAndGetOwner(@Req() req, ownerUuid: string) { + const owner = await this.ownerService.getOwner(ownerUuid); + await this.notificationSubmissionService.getByUuid( + owner.notificationSubmissionUuid, + req.user.entity, + ); + + return owner; + } +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.dto.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.dto.ts new file mode 100644 index 0000000000..c6441130de --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.dto.ts @@ -0,0 +1,63 @@ +import { AutoMap } from '@automapper/classes'; +import { IsOptional, IsString, Matches } from 'class-validator'; +import { OwnerTypeDto } from '../../../common/owner-type/owner-type.entity'; +import { emailRegex } from '../../../utils/email.helper'; + +export class NotificationTransfereeDto { + @AutoMap() + uuid: string; + + @AutoMap() + notificationSubmissionUuid: string; + + displayName: string; + + @AutoMap(() => String) + firstName?: string | null; + + @AutoMap(() => String) + lastName?: string | null; + + @AutoMap(() => String) + organizationName?: string | null; + + @AutoMap(() => String) + phoneNumber?: string | null; + + @AutoMap(() => String) + email?: string | null; + + @AutoMap() + type: OwnerTypeDto; +} + +export class NotificationTransfereeUpdateDto { + @IsString() + @IsOptional() + firstName?: string; + + @IsString() + @IsOptional() + lastName?: string; + + @IsString() + @IsOptional() + organizationName?: string; + + @IsString() + @IsOptional() + phoneNumber?: string; + + @Matches(emailRegex) + @IsOptional() + email?: string; + + @IsString() + @IsOptional() + typeCode?: string; +} + +export class NotificationTransfereeCreateDto extends NotificationTransfereeUpdateDto { + @IsString() + notificationSubmissionUuid: string; +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.entity.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.entity.ts new file mode 100644 index 0000000000..4913dd4981 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.entity.ts @@ -0,0 +1,61 @@ +import { AutoMap } from '@automapper/classes'; +import { Column, Entity, ManyToOne } from 'typeorm'; +import { Base } from '../../../common/entities/base.entity'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; +import { NotificationSubmission } from '../notification-submission.entity'; + +@Entity() +export class NotificationTransferee extends Base { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + firstName?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + lastName?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + organizationName?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + phoneNumber?: string | null; + + @AutoMap(() => String) + @Column({ + type: 'varchar', + nullable: true, + }) + email?: string | null; + + @AutoMap() + @ManyToOne(() => OwnerType, { nullable: false }) + type: OwnerType; + + @ManyToOne(() => NotificationSubmission, { nullable: false }) + notificationSubmission: NotificationSubmission; + + @AutoMap() + @Column() + notificationSubmissionUuid: string; +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.spec.ts new file mode 100644 index 0000000000..822dd90d5e --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.spec.ts @@ -0,0 +1,217 @@ +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { NotificationService } from '../../../alcs/notification/notification.service'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; +import { User } from '../../../user/user.entity'; +import { NotificationSubmission } from '../notification-submission.entity'; +import { NotificationSubmissionService } from '../notification-submission.service'; +import { NotificationTransferee } from './notification-transferee.entity'; +import { NotificationTransfereeService } from './notification-transferee.service'; + +describe('NotificationTransfereeService', () => { + let service: NotificationTransfereeService; + let mockRepo: DeepMocked>; + let mockTypeRepo: DeepMocked>; + let mockSubmissionService: DeepMocked; + let mockNotificationService: DeepMocked; + + beforeEach(async () => { + mockRepo = createMock(); + mockTypeRepo = createMock(); + mockSubmissionService = createMock(); + mockNotificationService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NotificationTransfereeService, + { + provide: getRepositoryToken(NotificationTransferee), + useValue: mockRepo, + }, + { + provide: getRepositoryToken(OwnerType), + useValue: mockTypeRepo, + }, + { + provide: NotificationSubmissionService, + useValue: mockSubmissionService, + }, + { + provide: NotificationService, + useValue: mockNotificationService, + }, + ], + }).compile(); + + service = module.get( + NotificationTransfereeService, + ); + mockSubmissionService.update.mockResolvedValue( + new NotificationSubmission(), + ); + mockNotificationService.updateApplicant.mockResolvedValue(); + mockSubmissionService.getFileNumber.mockResolvedValue('file-number'); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should call find for find', async () => { + mockRepo.find.mockResolvedValue([new NotificationTransferee()]); + + await service.fetchByFileId(''); + + expect(mockRepo.find).toHaveBeenCalledTimes(1); + }); + + it('should load the type and then call save for create', async () => { + mockRepo.save.mockResolvedValue(new NotificationTransferee()); + mockTypeRepo.findOneOrFail.mockResolvedValue(new OwnerType()); + + await service.create( + { + notificationSubmissionUuid: '', + email: '', + phoneNumber: '', + typeCode: '', + }, + new NotificationSubmission(), + ); + + expect(mockRepo.save).toHaveBeenCalledTimes(1); + expect(mockTypeRepo.findOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should set properties and call save for update', async () => { + const owner = new NotificationTransferee({ + firstName: 'Bruce', + lastName: 'Wayne', + }); + mockRepo.findOneOrFail.mockResolvedValue(owner); + mockRepo.save.mockResolvedValue(new NotificationTransferee()); + mockRepo.find.mockResolvedValue([new NotificationTransferee()]); + + await service.update( + '', + { + firstName: 'I Am', + lastName: 'Batman', + email: '', + phoneNumber: '', + typeCode: '', + }, + new User(), + ); + + expect(owner.firstName).toEqual('I Am'); + expect(owner.lastName).toEqual('Batman'); + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + expect(mockRepo.save).toHaveBeenCalledTimes(1); + }); + + it('should call through for delete', async () => { + mockRepo.remove.mockResolvedValue({} as any); + mockRepo.find.mockResolvedValue([new NotificationTransferee()]); + + await service.delete(new NotificationTransferee(), new User()); + + expect(mockRepo.remove).toHaveBeenCalledTimes(1); + }); + + it('should call through for verify', async () => { + mockRepo.findOneOrFail.mockResolvedValue(new NotificationTransferee()); + + await service.getOwner(''); + + expect(mockRepo.findOneOrFail).toHaveBeenCalledTimes(1); + }); + + it('should call through for getMany', async () => { + mockRepo.find.mockResolvedValue([new NotificationTransferee()]); + + await service.getMany([]); + + expect(mockRepo.find).toHaveBeenCalledTimes(1); + }); + + it('should call through for save', async () => { + mockRepo.save.mockResolvedValue(new NotificationTransferee()); + + await service.save(new NotificationTransferee()); + + expect(mockRepo.save).toHaveBeenCalledTimes(1); + }); + + it('should call update with the first transferees last name', async () => { + mockRepo.find.mockResolvedValue([ + new NotificationTransferee({ + firstName: 'B', + lastName: 'A', + }), + ]); + + await service.updateSubmissionApplicant('', new User()); + + expect(mockSubmissionService.update).toHaveBeenCalledTimes(1); + expect(mockSubmissionService.update.mock.calls[0][1].applicant).toEqual( + 'A', + ); + expect(mockSubmissionService.getFileNumber).toHaveBeenCalledTimes(1); + expect(mockNotificationService.updateApplicant).toHaveBeenCalledTimes(1); + }); + + it('should call update with the first owners last name', async () => { + const transferees = [ + new NotificationTransferee({ + firstName: 'F', + lastName: 'B', + }), + new NotificationTransferee({ + firstName: 'F', + lastName: 'A', + }), + new NotificationTransferee({ + firstName: 'F', + lastName: '1', + }), + new NotificationTransferee({ + firstName: 'F', + lastName: 'C', + }), + ]; + mockRepo.find.mockResolvedValue(transferees); + + await service.updateSubmissionApplicant('', new User()); + + expect(mockSubmissionService.update).toHaveBeenCalledTimes(1); + expect(mockSubmissionService.update.mock.calls[0][1].applicant).toEqual( + 'A et al.', + ); + expect(mockSubmissionService.getFileNumber).toHaveBeenCalledTimes(1); + expect(mockNotificationService.updateApplicant).toHaveBeenCalledTimes(1); + }); + + it('should call update with the number owners last name', async () => { + const transferees = [ + new NotificationTransferee({ + firstName: '1', + lastName: '1', + }), + new NotificationTransferee({ + firstName: '2', + lastName: '2', + }), + ]; + mockRepo.find.mockResolvedValue(transferees); + + await service.updateSubmissionApplicant('', new User()); + + expect(mockSubmissionService.update).toHaveBeenCalledTimes(1); + expect(mockSubmissionService.update.mock.calls[0][1].applicant).toEqual( + '1 et al.', + ); + }); +}); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.ts new file mode 100644 index 0000000000..9359664545 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.ts @@ -0,0 +1,196 @@ +import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Any, Repository } from 'typeorm'; +import { NotificationService } from '../../../alcs/notification/notification.service'; +import { OwnerType } from '../../../common/owner-type/owner-type.entity'; +import { User } from '../../../user/user.entity'; +import { FALLBACK_APPLICANT_NAME } from '../../../utils/owner.constants'; +import { NotificationSubmission } from '../notification-submission.entity'; +import { NotificationSubmissionService } from '../notification-submission.service'; +import { + NotificationTransfereeCreateDto, + NotificationTransfereeUpdateDto, +} from './notification-transferee.dto'; +import { NotificationTransferee } from './notification-transferee.entity'; + +@Injectable() +export class NotificationTransfereeService { + constructor( + @InjectRepository(NotificationTransferee) + private repository: Repository, + @InjectRepository(OwnerType) + private typeRepository: Repository, + @Inject(forwardRef(() => NotificationSubmissionService)) + private notificationSubmissionService: NotificationSubmissionService, + private notificationService: NotificationService, + ) {} + + async fetchByFileId(fileId: string) { + return this.repository.find({ + where: { + notificationSubmission: { + fileNumber: fileId, + }, + }, + relations: { + type: true, + }, + }); + } + + async create( + createDto: NotificationTransfereeCreateDto, + notificationSubmission: NotificationSubmission, + ) { + const type = await this.typeRepository.findOneOrFail({ + where: { + code: createDto.typeCode, + }, + }); + + const newOwner = new NotificationTransferee({ + firstName: createDto.firstName, + lastName: createDto.lastName, + organizationName: createDto.organizationName, + email: createDto.email, + phoneNumber: createDto.phoneNumber, + notificationSubmission: notificationSubmission, + type, + }); + + return await this.repository.save(newOwner); + } + + async save(owner: NotificationTransferee) { + await this.repository.save(owner); + } + + async update( + uuid: string, + updateDto: NotificationTransfereeUpdateDto, + user: User, + ) { + const existingOwner = await this.repository.findOneOrFail({ + where: { + uuid, + }, + }); + + if (updateDto.typeCode) { + existingOwner.type = await this.typeRepository.findOneOrFail({ + where: { + code: updateDto.typeCode, + }, + }); + } + existingOwner.organizationName = + updateDto.organizationName !== undefined + ? updateDto.organizationName + : existingOwner.organizationName; + + existingOwner.firstName = + updateDto.firstName !== undefined + ? updateDto.firstName + : existingOwner.firstName; + + existingOwner.lastName = + updateDto.lastName !== undefined + ? updateDto.lastName + : existingOwner.lastName; + + existingOwner.phoneNumber = + updateDto.phoneNumber !== undefined + ? updateDto.phoneNumber + : existingOwner.phoneNumber; + + existingOwner.email = + updateDto.email !== undefined ? updateDto.email : existingOwner.email; + + await this.updateSubmissionApplicant( + existingOwner.notificationSubmissionUuid, + user, + ); + + return await this.repository.save(existingOwner); + } + + async delete(owner: NotificationTransferee, user: User) { + const res = await this.repository.remove(owner); + await this.updateSubmissionApplicant( + owner.notificationSubmissionUuid, + user, + ); + return res; + } + + async getOwner(ownerUuid: string) { + return await this.repository.findOneOrFail({ + where: { + uuid: ownerUuid, + }, + relations: { + type: true, + }, + }); + } + + async getMany(ownerUuids: string[]) { + return await this.repository.find({ + where: { + uuid: Any(ownerUuids), + }, + }); + } + + async updateSubmissionApplicant(submissionUuid: string, user: User) { + const transferees = await this.repository.find({ + where: { + notificationSubmissionUuid: submissionUuid, + }, + }); + + //Filter to only alphabetic + const alphabetOwners = transferees.filter((owner) => + isNaN( + parseInt((owner.organizationName ?? owner.lastName ?? '').charAt(0)), + ), + ); + + //If no alphabetic use them all + if (alphabetOwners.length === 0) { + alphabetOwners.push(...transferees); + } + + const firstOwner = alphabetOwners.sort((a, b) => { + const mappedA = a.organizationName ?? a.lastName ?? ''; + const mappedB = b.organizationName ?? b.lastName ?? ''; + return mappedA.localeCompare(mappedB); + })[0]; + if (firstOwner) { + let applicantName = firstOwner.organizationName + ? firstOwner.organizationName + : firstOwner.lastName; + if (transferees.length > 1) { + applicantName += ' et al.'; + } + + await this.notificationSubmissionService.update( + submissionUuid, + { + applicant: applicantName || '', + }, + user, + ); + + const fileNumber = await this.notificationSubmissionService.getFileNumber( + submissionUuid, + ); + if (fileNumber) { + await this.notificationService.updateApplicant( + fileNumber, + applicantName || FALLBACK_APPLICANT_NAME, + ); + } + } + } +} diff --git a/services/apps/alcs/src/portal/portal.module.ts b/services/apps/alcs/src/portal/portal.module.ts index 1819e29a5b..89269be6d1 100644 --- a/services/apps/alcs/src/portal/portal.module.ts +++ b/services/apps/alcs/src/portal/portal.module.ts @@ -18,6 +18,7 @@ import { NoticeOfIntentSubmissionDraftModule } from './notice-of-intent-submissi import { NoticeOfIntentSubmissionModule } from './notice-of-intent-submission/notice-of-intent-submission.module'; import { ParcelModule } from './parcel/parcel.module'; import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; +import { NotificationSubmissionModule } from './notification-submission/notification-submission.module'; @Module({ imports: [ @@ -38,6 +39,7 @@ import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; PortalNoticeOfIntentDocumentModule, NoticeOfIntentSubmissionDraftModule, PortalNoticeOfIntentDecisionModule, + NotificationSubmissionModule, RouterModule.register([ { path: 'portal', module: ApplicationSubmissionModule }, { path: 'portal', module: NoticeOfIntentSubmissionModule }, @@ -51,6 +53,7 @@ import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; { path: 'portal', module: PortalNoticeOfIntentDocumentModule }, { path: 'portal', module: NoticeOfIntentSubmissionDraftModule }, { path: 'portal', module: PortalNoticeOfIntentDecisionModule }, + { path: 'portal', module: NotificationSubmissionModule }, ]), ], controllers: [CodeController], diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693608113088-add_notifications.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693608113088-add_notifications.ts new file mode 100644 index 0000000000..755ef69164 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693608113088-add_notifications.ts @@ -0,0 +1,170 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNotifications1693608113088 implements MigrationInterface { + name = 'addNotifications1693608113088'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER INDEX "alcs"."PK_b9fa421f94f7707ba109bf73b82" RENAME TO "PK_b9fa421f94fba109bf73b82"; + `); + await queryRunner.query( + `ALTER TABLE "alcs"."message" DROP CONSTRAINT "FK_fc0e9a26b0a8f7c76658ca1c6ca"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."message" DROP CONSTRAINT "FK_b776eec36c2a6b6879c14241e91"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_parcel" DROP CONSTRAINT "FK_33f7b06b2c4e0e128d670526c66"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" DROP CONSTRAINT "FK_7b23cfdc8574f66dd2f88e37f3f"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_parcel_ownership_type" RENAME TO "parcel_ownership_type"`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notification_type" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "label" character varying NOT NULL, "code" text NOT NULL, "description" text NOT NULL, "short_label" character varying NOT NULL, "html_description" text NOT NULL DEFAULT '', "portal_label" text NOT NULL DEFAULT '', CONSTRAINT "UQ_4c3787562f347d8d0ff63aa1a58" UNIQUE ("description"), CONSTRAINT "PK_585e07520c840e791dcfb7da013" PRIMARY KEY ("code"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notification" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "file_number" character varying NOT NULL, "applicant" character varying NOT NULL, "card_uuid" uuid, "local_government_uuid" uuid, "region_code" text, "summary" text, "date_submitted_to_alc" TIMESTAMP WITH TIME ZONE, "staff_observations" text, "type_code" text NOT NULL, CONSTRAINT "UQ_2b146911206953684d622397340" UNIQUE ("file_number"), CONSTRAINT "REL_ea8f1884038f559b573689d8de" UNIQUE ("card_uuid"), CONSTRAINT "PK_b9fa421f94f7707ba109bf73b82" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notification"."staff_observations" IS 'ALC Staff Observations and Comments'`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_2b146911206953684d62239734" ON "alcs"."notification" ("file_number") `, + ); + await queryRunner.query( + `CREATE INDEX "IDX_8a9fed11ce76672d806ac8b4b6" ON "alcs"."notification" ("local_government_uuid") `, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notification_parcel" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "pid" character varying, "pin" character varying, "legal_description" character varying, "civic_address" character varying, "map_area_hectares" double precision, "is_confirmed_by_applicant" boolean NOT NULL DEFAULT false, "ownership_type_code" text, "notification_submission_uuid" uuid NOT NULL, CONSTRAINT "PK_0f33b7df283efdcfdb0ceceb11b" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notification_parcel"."pid" IS 'The Parcels pid entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notification_parcel"."pin" IS 'The Parcels pin entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notification_parcel"."legal_description" IS 'The Parcels legalDescription entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notification_parcel"."civic_address" IS 'The standard address for the parcel'; COMMENT ON COLUMN "alcs"."notification_parcel"."map_area_hectares" IS 'The Parcels map are in hectares entered by the user or populated from third-party data'; COMMENT ON COLUMN "alcs"."notification_parcel"."is_confirmed_by_applicant" IS 'The Parcels indication whether applicant signed off provided data including the Certificate of Title'`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notification_transferee" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "first_name" character varying, "last_name" character varying, "organization_name" character varying, "phone_number" character varying, "email" character varying, "notification_submission_uuid" uuid NOT NULL, "type_code" text NOT NULL, CONSTRAINT "PK_77e372de24341033c6f1242135d" PRIMARY KEY ("uuid"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notification_submission" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "file_number" character varying NOT NULL, "applicant" character varying, "local_government_uuid" uuid, "purpose" character varying, "type_code" character varying NOT NULL, "created_by_uuid" uuid, CONSTRAINT "PK_31d1568857de3aeec7cf5c8ad34" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notification_submission"."file_number" IS 'File Number of attached SRW'; COMMENT ON COLUMN "alcs"."notification_submission"."applicant" IS 'The Applicants name on the application'; COMMENT ON COLUMN "alcs"."notification_submission"."local_government_uuid" IS 'UUID of the Local Government'; COMMENT ON COLUMN "alcs"."notification_submission"."purpose" IS 'The purpose of the application'; COMMENT ON COLUMN "alcs"."notification_submission"."type_code" IS 'SRW Type Code'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."message" ADD CONSTRAINT "FK_f9d295dbeac38c6b05f0994b415" FOREIGN KEY ("actor_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."message" ADD CONSTRAINT "FK_23212dcb31290e8972bd4e8aa4d" FOREIGN KEY ("receiver_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification" ADD CONSTRAINT "FK_ea8f1884038f559b573689d8de2" FOREIGN KEY ("card_uuid") REFERENCES "alcs"."card"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification" ADD CONSTRAINT "FK_8a9fed11ce76672d806ac8b4b6f" FOREIGN KEY ("local_government_uuid") REFERENCES "alcs"."local_government"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification" ADD CONSTRAINT "FK_569fdffb2b9c20c938390abf6a3" FOREIGN KEY ("region_code") REFERENCES "alcs"."application_region"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification" ADD CONSTRAINT "FK_585e07520c840e791dcfb7da013" FOREIGN KEY ("type_code") REFERENCES "alcs"."notification_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_parcel" ADD CONSTRAINT "FK_33f7b06b2c4e0e128d670526c66" FOREIGN KEY ("ownership_type_code") REFERENCES "alcs"."parcel_ownership_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" ADD CONSTRAINT "FK_7b23cfdc8574f66dd2f88e37f3f" FOREIGN KEY ("ownership_type_code") REFERENCES "alcs"."parcel_ownership_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_parcel" ADD CONSTRAINT "FK_ee3c1a069b76e3877ad5fbfa80e" FOREIGN KEY ("ownership_type_code") REFERENCES "alcs"."parcel_ownership_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_parcel" ADD CONSTRAINT "FK_3286388e9f39a4342f302d8ebc4" FOREIGN KEY ("notification_submission_uuid") REFERENCES "alcs"."notification_submission"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_transferee" ADD CONSTRAINT "FK_b7a59b609714919f65604e2ad96" FOREIGN KEY ("type_code") REFERENCES "alcs"."owner_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_transferee" ADD CONSTRAINT "FK_300863f6b4601735373ea3f6142" FOREIGN KEY ("notification_submission_uuid") REFERENCES "alcs"."notification_submission"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD CONSTRAINT "FK_99aed25adf97bc0591103e246c2" FOREIGN KEY ("created_by_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD CONSTRAINT "FK_5ef174c63d20561082cda20da3b" FOREIGN KEY ("file_number") REFERENCES "alcs"."notification"("file_number") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP CONSTRAINT "FK_5ef174c63d20561082cda20da3b"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP CONSTRAINT "FK_99aed25adf97bc0591103e246c2"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_transferee" DROP CONSTRAINT "FK_300863f6b4601735373ea3f6142"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_transferee" DROP CONSTRAINT "FK_b7a59b609714919f65604e2ad96"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_parcel" DROP CONSTRAINT "FK_3286388e9f39a4342f302d8ebc4"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_parcel" DROP CONSTRAINT "FK_ee3c1a069b76e3877ad5fbfa80e"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" DROP CONSTRAINT "FK_7b23cfdc8574f66dd2f88e37f3f"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_parcel" DROP CONSTRAINT "FK_33f7b06b2c4e0e128d670526c66"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification" DROP CONSTRAINT "FK_585e07520c840e791dcfb7da013"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification" DROP CONSTRAINT "FK_569fdffb2b9c20c938390abf6a3"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification" DROP CONSTRAINT "FK_8a9fed11ce76672d806ac8b4b6f"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification" DROP CONSTRAINT "FK_ea8f1884038f559b573689d8de2"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" DROP CONSTRAINT "FK_d4a78fa6d709aace10890017271"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."message" DROP CONSTRAINT "FK_23212dcb31290e8972bd4e8aa4d"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."message" DROP CONSTRAINT "FK_f9d295dbeac38c6b05f0994b415"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ALTER COLUMN "outcome_code" DROP NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_decision" ADD CONSTRAINT "FK_d4a78fa6d709aace10890017271" FOREIGN KEY ("outcome_code") REFERENCES "alcs"."notice_of_intent_decision_outcome"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query(`DROP TABLE "alcs"."notification_submission"`); + await queryRunner.query(`DROP TABLE "alcs"."notification_transferee"`); + await queryRunner.query(`DROP TABLE "alcs"."notification_parcel"`); + + await queryRunner.query( + `ALTER TABLE "alcs"."parcel_ownership_type" RENAME TO "application_parcel_ownership_type"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_8a9fed11ce76672d806ac8b4b6"`, + ); + await queryRunner.query( + `DROP INDEX "alcs"."IDX_2b146911206953684d62239734"`, + ); + await queryRunner.query(`DROP TABLE "alcs"."notification"`); + await queryRunner.query(`DROP TABLE "alcs"."notification_type"`); + await queryRunner.query( + `ALTER TABLE "alcs"."notice_of_intent_parcel" ADD CONSTRAINT "FK_7b23cfdc8574f66dd2f88e37f3f" FOREIGN KEY ("ownership_type_code") REFERENCES "alcs"."notice_of_intent_parcel_ownership_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_parcel" ADD CONSTRAINT "FK_33f7b06b2c4e0e128d670526c66" FOREIGN KEY ("ownership_type_code") REFERENCES "alcs"."application_parcel_ownership_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."message" ADD CONSTRAINT "FK_b776eec36c2a6b6879c14241e91" FOREIGN KEY ("receiver_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."message" ADD CONSTRAINT "FK_fc0e9a26b0a8f7c76658ca1c6ca" FOREIGN KEY ("actor_uuid") REFERENCES "alcs"."user"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693610085203-seed_notification_tables.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693610085203-seed_notification_tables.ts new file mode 100644 index 0000000000..9f27d572b3 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693610085203-seed_notification_tables.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class seedNotificationTables1693610085203 implements MigrationInterface { + name = 'seedNotificationTables1693610085203'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO "alcs"."notification_type" + ("audit_deleted_date_at", "audit_created_at", "audit_updated_at", "audit_created_by", "audit_updated_by", "label", "code", "description", "short_label", "html_description", "portal_label") VALUES + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Statutory Right of Way', 'SRW', 'Statutory Right of Way', 'SRW', 'TODO', 'Statutory Right of Way'); + `); + } + + public async down(): Promise { + //Nope + } +} From 9a16ed1cdb12d68cb21cdbb3d36df6ce18572e5b Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 5 Sep 2023 10:20:33 -0700 Subject: [PATCH 335/954] Sort statuses by weight not date for Uncancel * Should be sorted by weight to get correct result --- .../uncancel-application-dialog.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/alcs-frontend/src/app/features/application/overview/uncancel-application-dialog/uncancel-application-dialog.component.ts b/alcs-frontend/src/app/features/application/overview/uncancel-application-dialog/uncancel-application-dialog.component.ts index 7eea9e85d5..73a760218d 100644 --- a/alcs-frontend/src/app/features/application/overview/uncancel-application-dialog/uncancel-application-dialog.component.ts +++ b/alcs-frontend/src/app/features/application/overview/uncancel-application-dialog/uncancel-application-dialog.component.ts @@ -43,7 +43,7 @@ export class UncancelApplicationDialogComponent { status.statusTypeCode !== SUBMISSION_STATUS.CANCELLED && status.effectiveDate < Date.now() ) - .sort((a, b) => a.effectiveDate! - b.effectiveDate!); + .sort((a, b) => b.status.weight! - a.status.weight!); if (validStatuses && validStatuses.length > 0) { const validStatus = validStatuses[0].status; this.status = { From b84512cd3ba00c315c01ba2e8c280e57e98c8bb1 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 5 Sep 2023 11:13:17 -0700 Subject: [PATCH 336/954] Code Review Feedback --- .../create-submission-dialog.component.scss | 2 +- .../edit-submission/edit-submission.component.ts | 2 +- .../notification-submission.controller.ts | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.scss b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.scss index 6589d81fb6..0c33e790ba 100644 --- a/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.scss +++ b/portal-frontend/src/app/features/create-submission-dialog/create-submission-dialog.component.scss @@ -60,7 +60,7 @@ ul { } .srw-list { - margin-left: -24px; + margin-left: rem(-24); margin-bottom: 0; li { diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts index 42bf3c4530..2d77d82860 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts @@ -104,7 +104,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { return of(true); } - async onStepChange($event: StepperSelectionEvent) { + onStepChange($event: StepperSelectionEvent) { // scrolls to step if step selected programmatically scrollToElement({ id: `stepWrapper_${$event.selectedIndex}`, center: false }); } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts index 67a0b261ee..818b1d8989 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts @@ -83,13 +83,13 @@ export class NotificationSubmissionController { @Body() updateDto: NotificationSubmissionUpdateDto, @Req() req, ) { - // const noticeOfIntentSubmission = await this.notificationSubmissionService.getByUuid( + // const submission = await this.notificationSubmissionService.getByUuid( // uuid, // req.user.entity, // ); // // if ( - // noticeOfIntentSubmission.status.statusTypeCode !== + // submission.status.statusTypeCode !== // NOI_SUBMISSION_STATUS.IN_PROGRESS && // overlappingRoles.length === 0 // ) { @@ -110,17 +110,17 @@ export class NotificationSubmissionController { @Post('/:uuid/cancel') async cancel(@Param('uuid') uuid: string, @Req() req) { - const noticeOfIntentSubmission = + const notificationSubmission = await this.notificationSubmissionService.getByUuid(uuid, req.user.entity); // if ( - // noticeOfIntentSubmission.status.statusTypeCode !== + // notificationSubmission.status.statusTypeCode !== // NOI_SUBMISSION_STATUS.IN_PROGRESS // ) { // throw new BadRequestException('Can only cancel in progress SRWs'); // } - await this.notificationSubmissionService.cancel(noticeOfIntentSubmission); + await this.notificationSubmissionService.cancel(notificationSubmission); return { cancelled: true, From 1c6eed4e9e14ccc1749fac8b175bc3e7b2dd33dc Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 5 Sep 2023 13:24:18 -0700 Subject: [PATCH 337/954] Add Notification Parcel UI * Cleanup isDraft flag since not needed for Notifications * Merge modules for the same reason * Delete unused parcel fields --- .../edit-submission-base.module.ts | 12 - .../edit-submission.component.html | 10 +- .../edit-submission.component.ts | 17 +- .../edit-submission/edit-submission.module.ts | 16 +- .../edit-submission/files-step.partial.ts | 16 -- .../delete-parcel-dialog.component.html | 36 +++ .../delete-parcel-dialog.component.scss | 24 ++ .../delete-parcel-dialog.component.spec.ts | 47 ++++ .../delete-parcel-dialog.component.ts | 52 ++++ .../parcels/parcel-details.component.html | 57 ++++ .../parcels/parcel-details.component.scss | 0 .../parcels/parcel-details.component.spec.ts | 60 +++++ .../parcels/parcel-details.component.ts | 138 ++++++++++ ...l-entry-confirmation-dialog.component.html | 35 +++ ...l-entry-confirmation-dialog.component.scss | 24 ++ ...ntry-confirmation-dialog.component.spec.ts | 35 +++ ...cel-entry-confirmation-dialog.component.ts | 33 +++ .../parcel-entry/parcel-entry.component.html | 136 ++++++++++ .../parcel-entry/parcel-entry.component.scss | 96 +++++++ .../parcel-entry.component.spec.ts | 62 +++++ .../parcel-entry/parcel-entry.component.ts | 247 ++++++++++++++++++ .../edit-submission/step.partial.ts | 8 +- .../notification-parcel.dto.ts | 2 - .../notification-parcel.service.ts | 5 +- .../notification-parcel.entity.ts | 14 - .../notification-parcel.service.spec.ts | 1 - .../notification-parcel.service.ts | 7 +- ...940144496-clean_up_notification_parcels.ts | 19 ++ 28 files changed, 1144 insertions(+), 65 deletions(-) delete mode 100644 portal-frontend/src/app/features/notifications/edit-submission/edit-submission-base.module.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693940144496-clean_up_notification_parcels.ts diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission-base.module.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission-base.module.ts deleted file mode 100644 index 0caf1a7971..0000000000 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission-base.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; -import { SharedModule } from '../../../shared/shared.module'; -import { EditSubmissionComponent } from './edit-submission.component'; - -@NgModule({ - imports: [CommonModule, SharedModule, NgxMaskDirective, NgxMaskPipe], - declarations: [EditSubmissionComponent], - exports: [EditSubmissionComponent], -}) -export class EditSubmissionBaseModule {} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html index c16152382b..932a741a8a 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html @@ -31,7 +31,15 @@
Notice of Intent ID: {{ notificationSubmissio > -
+
+ + +
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts index 2d77d82860..bd702397f0 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts @@ -4,16 +4,13 @@ import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; -import { - NOI_SUBMISSION_STATUS, - NoticeOfIntentSubmissionDetailedDto, -} from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; import { NotificationSubmissionDetailedDto } from '../../../services/notification-submission/notification-submission.dto'; import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; import { ToastService } from '../../../services/toast/toast.service'; import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; import { scrollToElement } from '../../../shared/utils/scroll-helper'; +import { ParcelDetailsComponent } from './parcels/parcel-details.component'; export enum EditNotificationSteps { Parcel = 0, @@ -43,6 +40,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { showValidationErrors = false; @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; + @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; constructor( private notificationSubmissionService: NotificationSubmissionService, @@ -116,9 +114,20 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { await this.router.navigateByUrl(`notification/${this.fileId}/edit/${index}`); } + onParcelDetailsInitialized() { + if (this.expandedParcelUuid && this.parcelDetailsComponent) { + this.parcelDetailsComponent.openParcel(this.expandedParcelUuid); + this.expandedParcelUuid = undefined; + } + } + async saveSubmission(step: number) { switch (step) { case EditNotificationSteps.Parcel: + if (this.parcelDetailsComponent) { + await this.parcelDetailsComponent.onSave(); + } + break; case EditNotificationSteps.Transferees: case EditNotificationSteps.PrimaryContact: case EditNotificationSteps.Government: diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts index ba969b4f33..b4bdc31cde 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts @@ -3,8 +3,11 @@ import { CommonModule } from '@angular/common'; import { RouterModule, Routes } from '@angular/router'; import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; import { SharedModule } from '../../../shared/shared.module'; -import { EditSubmissionBaseModule } from './edit-submission-base.module'; import { EditSubmissionComponent } from './edit-submission.component'; +import { DeleteParcelDialogComponent } from './parcels/delete-parcel/delete-parcel-dialog.component'; +import { ParcelDetailsComponent } from './parcels/parcel-details.component'; +import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; +import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; import { StepComponent } from './step.partial'; const routes: Routes = [ @@ -20,7 +23,14 @@ const routes: Routes = [ ]; @NgModule({ - declarations: [StepComponent], - imports: [CommonModule, SharedModule, RouterModule.forChild(routes), EditSubmissionBaseModule], + declarations: [ + StepComponent, + EditSubmissionComponent, + ParcelDetailsComponent, + ParcelEntryComponent, + ParcelEntryConfirmationDialogComponent, + DeleteParcelDialogComponent, + ], + imports: [CommonModule, SharedModule, RouterModule.forChild(routes)], }) export class EditSubmissionModule {} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts index 5c5cdcf681..000753042e 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts @@ -5,7 +5,6 @@ import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-do import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; import { DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; import { FileHandle } from '../../../shared/file-drag-drop/drag-drop.directive'; -import { RemoveFileConfirmationDialogComponent } from '../../applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; import { StepComponent } from './step.partial'; @Component({ @@ -42,21 +41,6 @@ export abstract class FilesStepComponent extends StepComponent { } async onDeleteFile($event: NoticeOfIntentDocumentDto) { - if (this.draftMode) { - this.dialog - .open(RemoveFileConfirmationDialogComponent) - .beforeClosed() - .subscribe(async (didConfirm) => { - if (didConfirm) { - this.deleteFile($event); - } - }); - } else { - await this.deleteFile($event); - } - } - - private async deleteFile($event: NoticeOfIntentDocumentDto) { await this.noticeOfIntentDocumentService.deleteExternalFile($event.uuid); if (this.fileId) { const documents = await this.noticeOfIntentDocumentService.getByFileId(this.fileId); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html new file mode 100644 index 0000000000..77c2809519 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.html @@ -0,0 +1,36 @@ + + + + + + + + + + +
+

Delete Parcel #{{ parcelNumber }}

+
+ +
+
+ + Warning: All information relevant to this parcel, including information added in subsequent steps, will be + deleted. + + +
+ +
+
+ +
+
+
Are you sure you want to delete this? This action cannot be undone.
+
+ +
+ +
+
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss new file mode 100644 index 0000000000..1f78db7f1e --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.scss @@ -0,0 +1,24 @@ +@use '../../../../../../styles/functions' as *; + +.margin-bottom-1 { + margin-bottom: rem(16); +} + +.step-controls { + display: flex; + justify-content: space-between; +} + +.confirm-content { + margin: rem(24) 0; +} + +@media screen and (min-width: $desktopBreakpoint) { + .step-controls { + justify-content: flex-end; + + button { + margin-left: rem(25) !important; + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts new file mode 100644 index 0000000000..59d4818b54 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts @@ -0,0 +1,47 @@ +import { HttpClient } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { DeleteParcelDialogComponent } from './delete-parcel-dialog.component'; + +describe('DeleteParcelDialogComponent', () => { + let component: DeleteParcelDialogComponent; + let fixture: ComponentFixture; + let mockHttpClient: DeepMocked; + let mockNoiParcelService: DeepMocked; + + beforeEach(async () => { + mockHttpClient = createMock(); + mockNoiParcelService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [DeleteParcelDialogComponent], + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockNoiParcelService, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + { provide: MAT_DIALOG_DATA, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(DeleteParcelDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts new file mode 100644 index 0000000000..7445939f2f --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts @@ -0,0 +1,52 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; + +export enum NotificationParcelDeleteStepsEnum { + warning = 0, + confirmation = 1, +} + +@Component({ + selector: 'app-delete-parcel-dialog', + templateUrl: './delete-parcel-dialog.component.html', + styleUrls: ['./delete-parcel-dialog.component.scss'], +}) +export class DeleteParcelDialogComponent { + parcelUuid!: string; + parcelNumber!: string; + + stepIdx = 0; + + warningStep = NotificationParcelDeleteStepsEnum.warning; + confirmationStep = NotificationParcelDeleteStepsEnum.confirmation; + + constructor( + private noticeOfIntentParcelService: NoticeOfIntentParcelService, + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DeleteParcelDialogComponent + ) { + this.parcelUuid = data.parcelUuid; + this.parcelNumber = data.parcelNumber; + } + + async next() { + this.stepIdx += 1; + } + + async back() { + this.stepIdx -= 1; + } + + async onCancel(dialogResult: boolean = false) { + this.dialogRef.close(dialogResult); + } + + async onDelete() { + const result = await this.noticeOfIntentParcelService.deleteMany([this.parcelUuid]); + + if (result) { + this.onCancel(true); + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.html b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.html new file mode 100644 index 0000000000..6ce7b7f4b2 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.html @@ -0,0 +1,57 @@ +
+
+

Identify Parcels Under SRW

+

Provide parcel identification and registered ownership information for each parcel.

+

*All fields are required unless stated optional.

+
+ + + Parcel #{{ parcelInd + 1 }} Details & Owner Information + + + + +
+
+ + +
+
+
+ +
+ +
+ +
+ +
+
+ +
+
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.spec.ts new file mode 100644 index 0000000000..2d71c5772a --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.spec.ts @@ -0,0 +1,60 @@ +import { HttpClient } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NotificationParcelService } from '../../../../services/notification-parcel/notification-parcel.service'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { ParcelDetailsComponent } from './parcel-details.component'; + +describe('ParcelDetailsComponent', () => { + let component: ParcelDetailsComponent; + let fixture: ComponentFixture; + let mockHttpClient: DeepMocked; + let mockParcelService: DeepMocked; + let mockToastService: DeepMocked; + let mockMatDialog: DeepMocked; + let notificationPipe = new BehaviorSubject(undefined); + + beforeEach(async () => { + mockHttpClient = createMock(); + mockParcelService = createMock(); + mockToastService = createMock(); + mockMatDialog = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ParcelDetailsComponent], + providers: [ + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: NotificationParcelService, + useValue: mockParcelService, + }, + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: MatDialog, + useValue: mockMatDialog, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelDetailsComponent); + component = fixture.componentInstance; + component.$notificationSubmission = notificationPipe; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.ts new file mode 100644 index 0000000000..d057afd748 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-details.component.ts @@ -0,0 +1,138 @@ +import { AfterViewInit, Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { takeUntil } from 'rxjs'; +import { + NotificationParcelDto, + NotificationParcelUpdateDto, +} from '../../../../services/notification-parcel/notification-parcel.dto'; +import { NotificationParcelService } from '../../../../services/notification-parcel/notification-parcel.service'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { EditNotificationSteps } from '../edit-submission.component'; +import { StepComponent } from '../step.partial'; +import { DeleteParcelDialogComponent } from './delete-parcel/delete-parcel-dialog.component'; +import { ParcelEntryFormData } from './parcel-entry/parcel-entry.component'; + +@Component({ + selector: 'app-notification-parcel-details', + templateUrl: './parcel-details.component.html', + styleUrls: ['./parcel-details.component.scss'], +}) +export class ParcelDetailsComponent extends StepComponent implements OnInit, AfterViewInit { + @Output() componentInitialized = new EventEmitter(); + + currentStep = EditNotificationSteps.Parcel; + fileId = ''; + submissionUuid = ''; + parcels: NotificationParcelDto[] = []; + newParcelAdded = false; + isDirty = false; + expandedParcel: string = ''; + + constructor( + private router: Router, + private notificationParcelService: NotificationParcelService, + private toastService: ToastService, + private dialog: MatDialog + ) { + super(); + } + + ngOnInit(): void { + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.fileId = noiSubmission.fileNumber; + this.submissionUuid = noiSubmission.uuid; + this.loadParcels(); + } + }); + + this.newParcelAdded = false; + } + + ngAfterViewInit(): void { + setTimeout((_) => this.componentInitialized.emit(true)); + } + + openParcel(index: string) { + this.expandedParcel = index; + } + + async loadParcels() { + this.parcels = (await this.notificationParcelService.fetchBySubmissionUuid(this.submissionUuid)) || []; + if (!this.parcels || this.parcels.length === 0) { + await this.onAddParcel(); + } + } + + async onAddParcel() { + const parcel = await this.notificationParcelService.create(this.submissionUuid); + + if (parcel) { + this.parcels.push({ + uuid: parcel!.uuid, + }); + this.newParcelAdded = true; + } else { + this.toastService.showErrorToast('Error adding new parcel. Please refresh page and try again.'); + } + } + + async onParcelFormChange(formData: Partial) { + const parcel = this.parcels.find((e) => e.uuid === formData.uuid); + if (!parcel) { + this.toastService.showErrorToast('Error updating the parcel. Please refresh page and try again.'); + return; + } + + this.isDirty = true; + parcel.pid = formData.pid !== undefined ? formData.pid : parcel.pid; + parcel.pin = formData.pid !== undefined ? formData.pin : parcel.pin; + parcel.civicAddress = formData.civicAddress !== undefined ? formData.civicAddress : parcel.civicAddress; + parcel.legalDescription = + formData.legalDescription !== undefined ? formData.legalDescription : parcel.legalDescription; + + parcel.mapAreaHectares = formData.mapArea !== undefined ? formData.mapArea : parcel.mapAreaHectares; + parcel.ownershipTypeCode = formData.parcelType !== undefined ? formData.parcelType : parcel.ownershipTypeCode; + } + + private async saveProgress() { + if (this.isDirty || this.newParcelAdded) { + const parcelsToUpdate: NotificationParcelUpdateDto[] = []; + for (const parcel of this.parcels) { + parcelsToUpdate.push({ + uuid: parcel.uuid, + pid: parcel.pid?.toString() || null, + pin: parcel.pin?.toString() || null, + civicAddress: parcel.civicAddress ?? null, + legalDescription: parcel.legalDescription, + mapAreaHectares: parcel.mapAreaHectares, + ownershipTypeCode: parcel.ownershipTypeCode, + }); + } + await this.notificationParcelService.update(parcelsToUpdate); + } + } + + async onSave() { + await this.saveProgress(); + } + + async onDelete(parcelUuid: string, parcelNumber: number) { + this.dialog + .open(DeleteParcelDialogComponent, { + panelClass: 'no-padding', + disableClose: true, + data: { + parcelUuid, + parcelNumber, + }, + }) + .beforeClosed() + .subscribe((result) => { + if (result) { + this.loadParcels(); + } + }); + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html new file mode 100644 index 0000000000..e7e96cb4c5 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.html @@ -0,0 +1,35 @@ + + + + + + + + + + +
+

Change Parcel Ownership Type

+
+ +
+
+ + Warning: Changing parcel ownership type will remove some inputs relevant to the current parcel. + + +
+ +
+
+ +
+
+
Are you sure you want to change parcel ownership type? This action cannot be undone.
+
+ +
+ +
+
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss new file mode 100644 index 0000000000..c4e03e5c59 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.scss @@ -0,0 +1,24 @@ +@use '../../../../../../../styles/functions' as *; + +.margin-bottom-1 { + margin-bottom: rem(16); +} + +.step-controls { + display: flex; + justify-content: space-between; +} + +.confirm-content { + margin: rem(24) 0; +} + +@media screen and (min-width: $desktopBreakpoint) { + .step-controls { + justify-content: flex-end; + + button { + margin-left: rem(25) !important; + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts new file mode 100644 index 0000000000..705f0c459e --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog.component'; + +describe('ParcelEntryConfirmationDialogComponent', () => { + let component: ParcelEntryConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ParcelEntryConfirmationDialogComponent], + providers: [ + { + provide: MatDialog, + useValue: {}, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelEntryConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts new file mode 100644 index 0000000000..1a40b4b055 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { NotificationParcelDeleteStepsEnum } from '../../delete-parcel/delete-parcel-dialog.component'; + +@Component({ + selector: 'app-parcel-entry-confirmation-dialog', + templateUrl: './parcel-entry-confirmation-dialog.component.html', + styleUrls: ['./parcel-entry-confirmation-dialog.component.scss'], +}) +export class ParcelEntryConfirmationDialogComponent { + stepIdx = 0; + + warningStep = NotificationParcelDeleteStepsEnum.warning; + confirmationStep = NotificationParcelDeleteStepsEnum.confirmation; + + constructor(private dialogRef: MatDialogRef) {} + + async next() { + this.stepIdx += 1; + } + + async back() { + this.stepIdx -= 1; + } + + async onCancel(dialogResult: boolean = false) { + this.dialogRef.close(dialogResult); + } + + async onDelete() { + this.onCancel(true); + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.html new file mode 100644 index 0000000000..a10babbf69 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.html @@ -0,0 +1,136 @@ +
+
+ +
+ The answer to the following question will change the rest of the notice of intent form. Do not change this answer + once selected. +
+ + Fee Simple + Crown + +
+ warning +
This field is required
+
+
+ +
Parcel Lookup
+
+ + +
+ +
Can be found on the parcel's Certificate of Title
+ + + +
+ warning +
This field is required
+
+
+ +
+ +
The area of the entire parcel in hectares, not just the area under application.
+ + + +
+ warning +
This field is required
+
+
+ +
+ +
A unique nine-digit number found on the parcel's Certificate of Title
+ + + +
+ warning +
This field is required
+
Invalid format
+
+
+ +
+ +
Unique numeric identifier for Crown land parcels
+ + + +
+ +
+ + + + +
+ warning +
This field is required
+
+
+
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.scss new file mode 100644 index 0000000000..223b120fa2 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.scss @@ -0,0 +1,96 @@ +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; + +.owner-option { + display: flex; + justify-content: space-between; + align-items: center; +} + +.new-owner { + margin: rem(8) 0 !important; +} + +.link-text { + color: colors.$link-color; + text-decoration: underline; +} + +.lookup-pid-fields { + display: flex; + align-items: center; + flex-direction: column; + + @media screen and (min-width: $desktopBreakpoint) { + flex-direction: row; + } +} + +.lookup-search-by { + margin-top: rem(8); + + @media screen and (min-width: $desktopBreakpoint) { + width: unset !important; + flex-grow: 1; + } +} + +.lookup-input { + margin-top: rem(8); + + @media screen and (min-width: $desktopBreakpoint) { + width: unset !important; + flex-grow: 5; + } +} + +.lookup-search-button { + width: 100%; + margin-top: rem(8) !important; + + @media screen and (min-width: $desktopBreakpoint) { + height: rem(55); + margin-top: rem(7) !important; + width: rem(150); + } +} + +.lookup-bottom-row { + display: flex; + align-items: center; + flex-direction: column; + margin-top: rem(24); + + .reset-button { + width: 100%; + margin-bottom: rem(8); + } + + @media screen and (min-width: $desktopBreakpoint) { + flex-direction: row; + justify-content: space-between; + + .reset-button { + width: unset; + } + } +} + +.crown-owner-type { + display: block; + margin-bottom: rem(24); + + .mat-mdc-radio-button ~ .mat-mdc-radio-button { + margin-left: rem(16); + } +} + +mat-button-toggle-group#isFarm { + height: rem(55); + + .mat-button-toggle { + display: flex; + align-items: center; + height: 100%; + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts new file mode 100644 index 0000000000..304dfc245d --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.spec.ts @@ -0,0 +1,62 @@ +import { HttpClient } from '@angular/common/http'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NotificationParcelDto } from '../../../../../services/notification-parcel/notification-parcel.dto'; +import { NotificationParcelService } from '../../../../../services/notification-parcel/notification-parcel.service'; +import { ParcelService } from '../../../../../services/parcel/parcel.service'; +import { ParcelEntryComponent } from './parcel-entry.component'; + +describe('ParcelEntryComponent', () => { + let component: ParcelEntryComponent; + let fixture: ComponentFixture; + let mockParcelService: DeepMocked; + let mockHttpClient: DeepMocked; + let mockNotificationParcelService: DeepMocked; + + let mockParcel: NotificationParcelDto = { + uuid: '', + }; + + beforeEach(async () => { + mockParcelService = createMock(); + mockHttpClient = createMock(); + mockNotificationParcelService = createMock(); + + await TestBed.configureTestingModule({ + imports: [MatAutocompleteModule], + declarations: [ParcelEntryComponent], + providers: [ + { + provide: ParcelService, + useValue: mockParcelService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + { + provide: NotificationParcelService, + useValue: mockNotificationParcelService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelEntryComponent); + component = fixture.componentInstance; + component.parcel = mockParcel; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.ts new file mode 100644 index 0000000000..8107b14e6a --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/parcel-entry/parcel-entry.component.ts @@ -0,0 +1,247 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { MatDialog } from '@angular/material/dialog'; +import { PARCEL_OWNERSHIP_TYPE } from '../../../../../services/application-parcel/application-parcel.dto'; +import { NotificationParcelDto } from '../../../../../services/notification-parcel/notification-parcel.dto'; +import { NotificationParcelService } from '../../../../../services/notification-parcel/notification-parcel.service'; +import { ParcelService } from '../../../../../services/parcel/parcel.service'; +import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; + +export interface ParcelEntryFormData { + uuid: string; + legalDescription: string | undefined | null; + mapArea: string | undefined | null; + pin: string | undefined | null; + pid: string | undefined | null; + civicAddress: string | undefined | null; + parcelType: string | undefined | null; + isFarm: string | undefined | null; + purchaseDate?: Date | null; + crownLandOwnerType?: string | null; + isConfirmedByApplicant: boolean; +} + +@Component({ + selector: 'app-notification-parcel-entry[parcel][fileId][submissionUuid]', + templateUrl: './parcel-entry.component.html', + styleUrls: ['./parcel-entry.component.scss'], +}) +export class ParcelEntryComponent implements OnInit { + @Input() parcel!: NotificationParcelDto; + @Input() fileId!: string; + @Input() submissionUuid!: string; + + @Input() showErrors = false; + @Input() _disabled = false; + @Input() isDraft = false; + + @Input() + public set disabled(disabled: boolean) { + this._disabled = disabled; + this.onFormDisabled(); + } + + @Output() private onFormGroupChange = new EventEmitter>(); + @Output() private onSaveProgress = new EventEmitter(); + + searchBy = new FormControl(null); + isCrownLand: boolean | null = null; + + pidPin = new FormControl(''); + legalDescription = new FormControl(null, [Validators.required]); + mapArea = new FormControl(null, [Validators.required]); + pid = new FormControl(null, [Validators.required]); + pin = new FormControl(null); + civicAddress = new FormControl(null, [Validators.required]); + parcelType = new FormControl(null, [Validators.required]); + parcelForm = new FormGroup({ + pidPin: this.pidPin, + legalDescription: this.legalDescription, + mapArea: this.mapArea, + pin: this.pin, + pid: this.pid, + civicAddress: this.civicAddress, + parcelType: this.parcelType, + searchBy: this.searchBy, + }); + pidPinPlaceholder = ''; + + ownerInput = new FormControl(null); + + PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; + maxPurchasedDate = new Date(); + + constructor( + private parcelService: ParcelService, + private notificationParcelService: NotificationParcelService, + private dialog: MatDialog + ) {} + + ngOnInit(): void { + this.setupForm(); + } + + async onSearch() { + let result; + if (this.searchBy.getRawValue() === 'pin') { + result = await this.parcelService.getByPin(this.pidPin.getRawValue()!); + } else { + result = await this.parcelService.getByPid(this.pidPin.getRawValue()!); + } + + this.onReset(); + if (result) { + this.legalDescription.setValue(result.legalDescription); + this.mapArea.setValue(result.mapArea); + + if (result.pin) { + this.pin.setValue(result.pin); + } + + if (result.pid) { + this.pid.setValue(result.pid); + } + + this.emitFormChangeOnSearchActions(); + } + } + + onReset() { + this.parcelForm.controls.pidPin.reset(); + this.parcelForm.controls.pid.reset(); + this.parcelForm.controls.pin.reset(); + this.parcelForm.controls.legalDescription.reset(); + this.parcelForm.controls.mapArea.reset(); + this.parcelForm.controls.civicAddress.reset(); + + this.emitFormChangeOnSearchActions(); + + if (this.showErrors) { + this.parcelForm.markAllAsTouched(); + } + } + + private emitFormChangeOnSearchActions() { + this.onFormGroupChange.emit({ + uuid: this.parcel.uuid, + legalDescription: this.legalDescription.getRawValue(), + mapArea: this.mapArea.getRawValue(), + pin: this.pin.getRawValue(), + pid: this.pid.getRawValue(), + }); + } + + onChangeParcelType($event: MatButtonToggleChange) { + const dirtyForm = + this.legalDescription.value || this.mapArea.value || this.pid.value || this.pin.value || this.civicAddress.value; + + const changeParcelType = () => { + if ($event.value === this.PARCEL_OWNERSHIP_TYPES.CROWN) { + this.searchBy.setValue(null); + this.pidPinPlaceholder = ''; + this.isCrownLand = true; + this.pid.setValidators([]); + } else { + this.searchBy.setValue('pid'); + this.pidPinPlaceholder = 'Type 9 digit PID'; + this.isCrownLand = false; + this.pid.setValidators([Validators.required]); + } + + this.pid.updateValueAndValidity(); + }; + + if (dirtyForm && this.isCrownLand !== null) { + this.dialog + .open(ParcelEntryConfirmationDialogComponent, { + panelClass: 'no-padding', + disableClose: true, + }) + .beforeClosed() + .subscribe(async (result) => { + if (result) { + this.onReset(); + return changeParcelType(); + } else { + const newParcelType = this.parcelType.getRawValue(); + + const prevParcelType = + newParcelType === this.PARCEL_OWNERSHIP_TYPES.CROWN + ? this.PARCEL_OWNERSHIP_TYPES.FEE_SIMPLE + : this.PARCEL_OWNERSHIP_TYPES.CROWN; + + this.parcelType.setValue(prevParcelType); + } + }); + } else { + return changeParcelType(); + } + } + + private setupForm() { + this.parcelForm.patchValue({ + legalDescription: this.parcel.legalDescription, + mapArea: this.parcel.mapAreaHectares, + pid: this.parcel.pid, + pin: this.parcel.pin, + civicAddress: this.parcel.civicAddress, + parcelType: this.parcel.ownershipTypeCode, + }); + + this.isCrownLand = this.parcelType.value + ? this.parcelType.getRawValue() === this.PARCEL_OWNERSHIP_TYPES.CROWN + : null; + + if (this.isCrownLand) { + this.pidPin.disable(); + this.pid.setValidators([]); + this.pidPinPlaceholder = ''; + } else { + this.pidPinPlaceholder = 'Type 9 digit PID'; + } + + if (this.showErrors) { + this.parcelForm.markAllAsTouched(); + } + + this.parcelForm.valueChanges.subscribe((formData) => { + if (!this.parcelForm.dirty) { + return; + } + + if ((this.isCrownLand && !this.searchBy.getRawValue()) || this.disabled) { + this.pidPin.disable({ + emitEvent: false, + }); + } else { + this.pidPin.enable({ + emitEvent: false, + }); + } + + return this.onFormGroupChange.emit({ + ...formData, + uuid: this.parcel.uuid, + }); + }); + } + + private onFormDisabled() { + if (this._disabled) { + this.parcelForm.disable(); + this.ownerInput.disable(); + } else { + this.parcelForm.enable(); + this.ownerInput.enable(); + } + } + + onChangeSearchBy(value: string) { + if (value === 'pid') { + this.pidPinPlaceholder = 'Type 9 digit PID'; + } else { + this.pidPinPlaceholder = 'Type PIN'; + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/step.partial.ts b/portal-frontend/src/app/features/notifications/edit-submission/step.partial.ts index c3d7d8a6ac..519bde91dd 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/step.partial.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/step.partial.ts @@ -1,6 +1,9 @@ import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; import { BehaviorSubject, Subject } from 'rxjs'; -import { NotificationSubmissionDto } from '../../../services/notification-submission/notification-submission.dto'; +import { + NotificationSubmissionDetailedDto, + NotificationSubmissionDto, +} from '../../../services/notification-submission/notification-submission.dto'; @Component({ selector: 'app-step', @@ -10,10 +13,9 @@ import { NotificationSubmissionDto } from '../../../services/notification-submis export class StepComponent implements OnDestroy { protected $destroy = new Subject(); - @Input() $notificationSubmission!: BehaviorSubject; + @Input() $notificationSubmission!: BehaviorSubject; @Input() showErrors = false; - @Input() draftMode = false; @Output() navigateToStep = new EventEmitter(); @Output() exit = new EventEmitter(); diff --git a/portal-frontend/src/app/services/notification-parcel/notification-parcel.dto.ts b/portal-frontend/src/app/services/notification-parcel/notification-parcel.dto.ts index 0e7ec19785..922b4a3f8e 100644 --- a/portal-frontend/src/app/services/notification-parcel/notification-parcel.dto.ts +++ b/portal-frontend/src/app/services/notification-parcel/notification-parcel.dto.ts @@ -8,8 +8,6 @@ export interface NotificationParcelUpdateDto { legalDescription?: string | null; mapAreaHectares?: string | null; ownershipTypeCode?: string | null; - crownLandOwnerType?: string | null; - isConfirmedByApplicant: boolean; } export interface NotificationParcelDto extends Omit { diff --git a/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.ts b/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.ts index 8ad3bcd735..1c35d65a57 100644 --- a/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.ts +++ b/portal-frontend/src/app/services/notification-parcel/notification-parcel.service.ts @@ -32,12 +32,11 @@ export class NotificationParcelService { return undefined; } - async create(notificationSubmissionUuid: string, ownerUuid?: string) { + async create(notificationSubmissionUuid: string) { try { return await firstValueFrom( this.httpClient.post(`${this.serviceUrl}`, { - noticeOfIntentSubmissionUuid: notificationSubmissionUuid, - ownerUuid, + notificationSubmissionUuid: notificationSubmissionUuid, }) ); } catch (e) { diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.entity.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.entity.ts index aced2c8b95..db3e0522f0 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.entity.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.entity.ts @@ -58,20 +58,6 @@ export class NotificationParcel extends Base { }) mapAreaHectares?: number | null; - @AutoMap(() => Boolean) - @Column({ - type: 'boolean', - comment: - 'The Parcels indication whether applicant signed off provided data including the Certificate of Title', - nullable: false, - default: false, - }) - isConfirmedByApplicant: boolean; - - @IsString() - @IsOptional() - crownLandOwnerType?: string | null; - @AutoMap(() => String) @Column({ nullable: true }) ownershipTypeCode?: string | null; diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts index 17a7c1c22e..0a031358a3 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts @@ -22,7 +22,6 @@ describe('NotificationParcelService', () => { pin: 'mock_pin', legalDescription: 'mock_legalDescription', mapAreaHectares: 1, - isConfirmedByApplicant: true, notificationSubmissionUuid: mockFileNumber, ownershipTypeCode: 'mock_ownershipTypeCode', }); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts index 8328997211..28f3dacc19 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts @@ -3,7 +3,6 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { User } from '../../../user/user.entity'; -import { filterUndefined } from '../../../utils/undefined'; import { NotificationTransfereeService } from '../notification-transferee/notification-transferee.service'; import { NotificationParcelUpdateDto } from './notification-parcel.dto'; import { NotificationParcel } from './notification-parcel.entity'; @@ -58,11 +57,7 @@ export class NotificationParcelService { parcel.legalDescription = updateDto.legalDescription; parcel.mapAreaHectares = updateDto.mapAreaHectares; parcel.civicAddress = updateDto.civicAddress; - parcel.isConfirmedByApplicant = filterUndefined( - updateDto.isConfirmedByApplicant, - parcel.isConfirmedByApplicant, - ); - parcel.crownLandOwnerType = updateDto.crownLandOwnerType; + parcel.ownershipTypeCode = updateDto.ownershipTypeCode; updatedParcels.push(parcel); } diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693940144496-clean_up_notification_parcels.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693940144496-clean_up_notification_parcels.ts new file mode 100644 index 0000000000..d2f098a09c --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693940144496-clean_up_notification_parcels.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class cleanUpNotificationParcels1693940144496 + implements MigrationInterface +{ + name = 'cleanUpNotificationParcels1693940144496'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_parcel" DROP COLUMN "is_confirmed_by_applicant"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_parcel" ADD "is_confirmed_by_applicant" boolean NOT NULL DEFAULT false`, + ); + } +} From 6de352c78c76a09823bc4aee6104edee20a38239 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Tue, 5 Sep 2023 13:45:55 -0700 Subject: [PATCH 338/954] add migration --- .../1693945647800-rename_nfu_placement.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts new file mode 100644 index 0000000000..4da33dd170 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class renameNfuPlacement1693945647800 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" ADD "nfu_total_fill_area" numeric(12,2)`, + ); + await queryRunner.query( + `UPDATE "alcs"."application_submission" SET "nfu_total_fill_area" = "nfu_total_fill_placement"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "nfu_total_fill_placement"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_submission"."nfu_total_fill_area" IS 'Area for nfu placement of fill'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_submission"."nfu_total_fill_area" IS 'Area for nfu placement of fill'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" DROP COLUMN "nfu_total_fill_area"`, + ); + } + +} From 6c2a02ca49b788e7a53cf0091141ade0b9b8bf85 Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:09:24 -0700 Subject: [PATCH 339/954] Add NOI timeline and status to detailed page (#939) * Fetch NOI submission status from overview page * Bold noi timeline statuses and update unit tests * Retrieve current noi status and show badge in details header * Refactor and pass unit test * Pass status service as detail header input --- .../application/application.component.html | 1 + .../application/application.component.spec.ts | 7 ++ .../application/application.component.ts | 4 +- .../notice-of-intent.component.html | 2 + .../notice-of-intent.component.spec.ts | 8 +- .../notice-of-intent.component.ts | 4 +- .../overview/overview.component.spec.ts | 14 ++-- .../overview/overview.component.ts | 28 ++++++- .../application-timeline.service.ts | 2 +- .../details-header.component.spec.ts | 12 +-- .../details-header.component.ts | 21 +++--- .../application-timeline.service.spec.ts | 3 - .../notice-of-intent-timeline.module.ts | 2 + .../notice-of-intent-timeline.service.spec.ts | 54 ++++++++++++- .../notice-of-intent-timeline.service.ts | 75 +++++++++++-------- 15 files changed, 167 insertions(+), 70 deletions(-) diff --git a/alcs-frontend/src/app/features/application/application.component.html b/alcs-frontend/src/app/features/application/application.component.html index 88d26342b3..5e2933a43d 100644 --- a/alcs-frontend/src/app/features/application/application.component.html +++ b/alcs-frontend/src/app/features/application/application.component.html @@ -7,6 +7,7 @@ days="Business Days" heading="Application" [showStatus]="true" + [submissionStatusService]="applicationStatusService" >
@@ -24,7 +24,7 @@

SRW ID: {{ submission.fileNumber }}

SRW Status
- TODO + {{ submission.status.label }}
diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts index 3bafb2e64c..b14c06927f 100644 --- a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts @@ -1,7 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; import { NotificationSubmissionDto } from '../../../services/notification-submission/notification-submission.dto'; import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; diff --git a/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts b/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts index 4cc056db23..70dcc62f46 100644 --- a/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts +++ b/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts @@ -1,5 +1,27 @@ +import { BaseCodeDto } from '../../shared/dto/base.dto'; +import { NOI_SUBMISSION_STATUS } from '../notice-of-intent-submission/notice-of-intent-submission.dto'; import { NotificationTransfereeDto } from '../notification-transferee/notification-transferee.dto'; +export enum NOTIFICATION_STATUS { + IN_PROGRESS = 'PROG', + SUBMITTED_TO_ALC = 'SUBM', //Submitted to ALC + ALC_RESPONSE = 'ALCR', // Response sent + CANCELLED = 'CANC', +} + +export interface NotificationSubmissionStatusDto extends BaseCodeDto { + code: NOI_SUBMISSION_STATUS; + portalBackgroundColor: string; + portalColor: string; +} + +export interface NotificationSubmissionToSubmissionStatusDto { + submissionUuid: string; + effectiveDate: number | null; + statusTypeCode: string; + status: NotificationSubmissionStatusDto; +} + export interface NotificationSubmissionDto { fileNumber: string; uuid: string; @@ -9,6 +31,7 @@ export interface NotificationSubmissionDto { localGovernmentUuid: string; type: string; typeCode: string; + status: NotificationSubmissionStatusDto; lastStatusUpdate: number; owners: NotificationTransfereeDto[]; canEdit: boolean; diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-status-type.entity.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-status-type.entity.ts new file mode 100644 index 0000000000..dea2524aa9 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-status-type.entity.ts @@ -0,0 +1,40 @@ +import { AutoMap } from '@automapper/classes'; +import { Column, Entity, OneToMany } from 'typeorm'; +import { BaseCodeEntity } from '../../../common/entities/base.code.entity'; +import { NotificationSubmissionToSubmissionStatus } from './notification-status.entity'; + +@Entity() +export class NotificationSubmissionStatusType extends BaseCodeEntity { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => Number) + @Column({ type: 'smallint', default: 0 }) + weight: number; + + @OneToMany( + () => NotificationSubmissionToSubmissionStatus, + (s) => s.statusType, + ) + public submissionStatuses: NotificationSubmissionToSubmissionStatus[]; + + @AutoMap() + @Column() + alcsBackgroundColor: string; + + @AutoMap() + @Column() + alcsColor: string; + + @AutoMap() + @Column() + portalBackgroundColor: string; + + @AutoMap() + @Column() + portalColor: string; +} diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-status.dto.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-status.dto.ts new file mode 100644 index 0000000000..79a2b17821 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-status.dto.ts @@ -0,0 +1,40 @@ +import { AutoMap } from '@automapper/classes'; +import { BaseCodeDto } from '../../../common/dtos/base.dto'; + +export enum NOTIFICATION_STATUS { + IN_PROGRESS = 'PROG', + SUBMITTED_TO_ALC = 'SUBM', //Submitted to ALC + ALC_RESPONSE_SENT = 'ALCR', //Response sent to applicant + CANCELLED = 'CANC', +} + +export class NotificationStatusDto extends BaseCodeDto { + @AutoMap() + alcsBackgroundColor: string; + + @AutoMap() + alcsColor: string; + + @AutoMap() + portalBackgroundColor: string; + + @AutoMap() + portalColor: string; + + @AutoMap() + weight: number; +} + +export class NotificationSubmissionToSubmissionStatusDto { + @AutoMap() + submissionUuid: string; + + @AutoMap() + effectiveDate: number; + + @AutoMap() + statusTypeCode: string; + + @AutoMap(() => NotificationStatusDto) + status: NotificationStatusDto; +} diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-status.entity.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-status.entity.ts new file mode 100644 index 0000000000..b21e291e41 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-status.entity.ts @@ -0,0 +1,50 @@ +import { AutoMap } from '@automapper/classes'; +import { + BaseEntity, + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryColumn, +} from 'typeorm'; +import { NotificationSubmission } from '../../../portal/notification-submission/notification-submission.entity'; +import { NotificationSubmissionStatusType } from './notification-status-type.entity'; + +@Entity() +export class NotificationSubmissionToSubmissionStatus extends BaseEntity { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap(() => Date) + @Column({ type: 'timestamptz', nullable: true }) + effectiveDate: Date | null; + + @AutoMap() + @PrimaryColumn({ type: 'uuid' }) + submissionUuid: string; + + @AutoMap() + @ManyToOne( + () => NotificationSubmission, + (submission) => submission.submissionStatuses, + ) + @JoinColumn({ name: 'submission_uuid' }) + submission: NotificationSubmission; + + @AutoMap() + @PrimaryColumn() + statusTypeCode: string; + + @AutoMap() + @ManyToOne( + () => NotificationSubmissionStatusType, + (status) => status.submissionStatuses, + { eager: true }, + ) + @JoinColumn({ name: 'status_type_code' }) + statusType: NotificationSubmissionStatusType; +} diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.controller.spec.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.controller.spec.ts new file mode 100644 index 0000000000..c066f98dfa --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.controller.spec.ts @@ -0,0 +1,87 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NotificationSubmissionProfile } from '../../../common/automapper/notification-submission.automapper.profile'; +import { NotificationSubmissionToSubmissionStatus } from './notification-status.entity'; +import { NotificationSubmissionStatusController } from './notification-submission-status.controller'; +import { NotificationSubmissionStatusService } from './notification-submission-status.service'; + +describe('NotificationSubmissionStatusController', () => { + let controller: NotificationSubmissionStatusController; + let mockNoticeOfIntentSubmissionStatusService: DeepMocked; + + beforeEach(async () => { + mockNoticeOfIntentSubmissionStatusService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [NotificationSubmissionStatusController], + providers: [ + NotificationSubmissionProfile, + { + provide: NotificationSubmissionStatusService, + useValue: mockNoticeOfIntentSubmissionStatusService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + }).compile(); + + controller = module.get( + NotificationSubmissionStatusController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call service to get statuses by file number', async () => { + const fakeFileNumber = 'fake'; + + mockNoticeOfIntentSubmissionStatusService.getCurrentStatusesByFileNumber.mockResolvedValue( + [new NotificationSubmissionToSubmissionStatus()], + ); + + const result = await controller.getStatusesByFileNumber(fakeFileNumber); + + expect( + mockNoticeOfIntentSubmissionStatusService.getCurrentStatusesByFileNumber, + ).toBeCalledTimes(1); + expect( + mockNoticeOfIntentSubmissionStatusService.getCurrentStatusesByFileNumber, + ).toBeCalledWith(fakeFileNumber); + expect(result.length).toEqual(1); + expect(result).toBeDefined(); + }); + + it('should call service to get current submission status by file number', async () => { + const fakeFileNumber = 'fake'; + + mockNoticeOfIntentSubmissionStatusService.getCurrentStatusByFileNumber.mockResolvedValue( + new NotificationSubmissionToSubmissionStatus(), + ); + + const result = await controller.getCurrentStatusByFileNumber( + fakeFileNumber, + ); + + expect( + mockNoticeOfIntentSubmissionStatusService.getCurrentStatusByFileNumber, + ).toBeCalledTimes(1); + expect( + mockNoticeOfIntentSubmissionStatusService.getCurrentStatusByFileNumber, + ).toBeCalledWith(fakeFileNumber); + expect(result).toBeDefined(); + }); +}); diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.controller.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.controller.ts new file mode 100644 index 0000000000..501c6d8246 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.controller.ts @@ -0,0 +1,45 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Controller, Get, Param } from '@nestjs/common'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { NotificationSubmissionToSubmissionStatusDto } from './notification-status.dto'; +import { NotificationSubmissionToSubmissionStatus } from './notification-status.entity'; +import { NotificationSubmissionStatusService } from './notification-submission-status.service'; + +@Controller('notification-submission-status') +@UserRoles(...ANY_AUTH_ROLE) +export class NotificationSubmissionStatusController { + constructor( + private notificationSubmissionStatusService: NotificationSubmissionStatusService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('/:fileNumber') + async getStatusesByFileNumber(@Param('fileNumber') fileNumber) { + const statuses = + await this.notificationSubmissionStatusService.getCurrentStatusesByFileNumber( + fileNumber, + ); + + return this.mapper.mapArrayAsync( + statuses, + NotificationSubmissionToSubmissionStatus, + NotificationSubmissionToSubmissionStatusDto, + ); + } + + @Get('/current-status/:fileNumber') + async getCurrentStatusByFileNumber(@Param('fileNumber') fileNumber) { + const status = + await this.notificationSubmissionStatusService.getCurrentStatusByFileNumber( + fileNumber, + ); + + return this.mapper.mapAsync( + status, + NotificationSubmissionToSubmissionStatus, + NotificationSubmissionToSubmissionStatusDto, + ); + } +} diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.module.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.module.ts new file mode 100644 index 0000000000..d90a36722a --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { NotificationSubmission } from '../../../portal/notification-submission/notification-submission.entity'; +import { NotificationSubmissionStatusType } from './notification-status-type.entity'; +import { NotificationSubmissionToSubmissionStatus } from './notification-status.entity'; +import { NotificationSubmissionStatusController } from './notification-submission-status.controller'; +import { NotificationSubmissionStatusService } from './notification-submission-status.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([ + NotificationSubmissionToSubmissionStatus, + NotificationSubmissionStatusType, + NotificationSubmission, + ]), + ], + providers: [NotificationSubmissionStatusService], + exports: [NotificationSubmissionStatusService], + controllers: [NotificationSubmissionStatusController], +}) +export class NotificationSubmissionStatusModule {} diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.spec.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.spec.ts new file mode 100644 index 0000000000..8bd6592946 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.spec.ts @@ -0,0 +1,395 @@ +import { ServiceNotFoundException } from '@app/common/exceptions/base.exception'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import * as dayjs from 'dayjs'; +import * as timezone from 'dayjs/plugin/timezone'; +import * as utc from 'dayjs/plugin/utc'; +import { Repository } from 'typeorm'; +import { NotificationSubmission } from '../../../portal/notification-submission/notification-submission.entity'; +import { NotificationSubmissionStatusType } from './notification-status-type.entity'; +import { NOTIFICATION_STATUS } from './notification-status.dto'; +import { NotificationSubmissionToSubmissionStatus } from './notification-status.entity'; +import { NotificationSubmissionStatusService } from './notification-submission-status.service'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +describe('NotificationSubmissionStatusService', () => { + let service: NotificationSubmissionStatusService; + let mockSubmissionToSubmissionStatusRepository: DeepMocked< + Repository + >; + let mockSubmissionStatusTypeRepository: DeepMocked< + Repository + >; + let mockNotificationSubmissionRepository: DeepMocked< + Repository + >; + + beforeEach(async () => { + jest.useFakeTimers().setSystemTime(new Date('2022-01-01')); + + mockSubmissionToSubmissionStatusRepository = createMock(); + mockSubmissionStatusTypeRepository = createMock(); + mockNotificationSubmissionRepository = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NotificationSubmissionStatusService, + { + provide: getRepositoryToken(NotificationSubmissionToSubmissionStatus), + useValue: mockSubmissionToSubmissionStatusRepository, + }, + { + provide: getRepositoryToken(NotificationSubmissionStatusType), + useValue: mockSubmissionStatusTypeRepository, + }, + { + provide: getRepositoryToken(NotificationSubmission), + useValue: mockNotificationSubmissionRepository, + }, + ], + }).compile(); + + service = module.get( + NotificationSubmissionStatusService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should successfully set initial statuses', async () => { + mockSubmissionStatusTypeRepository.find.mockResolvedValue([ + new NotificationSubmissionStatusType({ + weight: 0, + code: NOTIFICATION_STATUS.IN_PROGRESS, + }), + new NotificationSubmissionStatusType({ + weight: 1, + code: NOTIFICATION_STATUS.ALC_RESPONSE_SENT, + }), + ]); + + const fakeSubmissionUuid = 'fake'; + + const savedStatuses: NotificationSubmissionToSubmissionStatus[] = [ + new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.ALC_RESPONSE_SENT, + }), + new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }), + ]; + + mockSubmissionToSubmissionStatusRepository.save.mockResolvedValue( + savedStatuses as any, + ); + + const result = await service.setInitialStatuses(fakeSubmissionUuid); + + expect(mockSubmissionStatusTypeRepository.find).toBeCalledTimes(1); + expect(mockSubmissionStatusTypeRepository.find).toBeCalledWith(); + + expect(mockSubmissionToSubmissionStatusRepository.save).toBeCalledTimes(1); + expect(result).toMatchObject(savedStatuses); + }); + + it('Should return current statuses by submission uuid', async () => { + const fakeSubmissionUuid = 'fake'; + const mockStatuses = [ + new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }), + ]; + + mockSubmissionToSubmissionStatusRepository.findBy.mockResolvedValue( + mockStatuses, + ); + + const statuses = await service.getCurrentStatusesBy(fakeSubmissionUuid); + + expect( + mockSubmissionToSubmissionStatusRepository.findBy, + ).toHaveBeenCalledTimes(1); + expect( + mockSubmissionToSubmissionStatusRepository.findBy, + ).toHaveBeenCalledWith({ + submissionUuid: fakeSubmissionUuid, + }); + expect(statuses).toEqual(mockStatuses); + }); + + it('Should return current statuses by fileNumber', async () => { + const fakeSubmissionUuid = 'fake'; + const fakeFileNumber = 'fake-number'; + const mockStatuses = [ + new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }), + ]; + + mockSubmissionToSubmissionStatusRepository.findBy.mockResolvedValue( + mockStatuses, + ); + mockNotificationSubmissionRepository.findOneBy.mockResolvedValue( + new NotificationSubmission({ + uuid: fakeSubmissionUuid, + fileNumber: fakeFileNumber, + }), + ); + + const statuses = await service.getCurrentStatusesByFileNumber( + fakeFileNumber, + ); + + expect( + mockSubmissionToSubmissionStatusRepository.findBy, + ).toHaveBeenCalledTimes(1); + expect( + mockSubmissionToSubmissionStatusRepository.findBy, + ).toHaveBeenCalledWith({ + submissionUuid: fakeSubmissionUuid, + }); + expect(statuses).toEqual(mockStatuses); + expect( + mockNotificationSubmissionRepository.findOneBy, + ).toHaveBeenCalledTimes(1); + expect(mockNotificationSubmissionRepository.findOneBy).toHaveBeenCalledWith( + { + fileNumber: fakeFileNumber, + }, + ); + }); + + it('Should fail return current statuses by fileNumber if submission not found', async () => { + const fakeSubmissionUuid = 'fake'; + const fakeFileNumber = 'fake-number'; + const mockStatuses = [ + new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }), + ]; + + mockSubmissionToSubmissionStatusRepository.findBy.mockResolvedValue( + mockStatuses, + ); + mockNotificationSubmissionRepository.findOneBy.mockResolvedValue(null); + + await expect( + service.getCurrentStatusesByFileNumber(fakeFileNumber), + ).rejects.toMatchObject( + new ServiceNotFoundException( + `Submission does not exist for provided notice of intent ${fakeFileNumber}. Only notice of intents originated in portal have statuses.`, + ), + ); + + expect( + mockSubmissionToSubmissionStatusRepository.findBy, + ).toHaveBeenCalledTimes(0); + expect( + mockNotificationSubmissionRepository.findOneBy, + ).toHaveBeenCalledTimes(1); + }); + + it('Should set status effective date to now if no effective date passed', async () => { + const fakeSubmissionUuid = 'fake'; + const mockStatus = new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }); + + mockSubmissionToSubmissionStatusRepository.findOneOrFail.mockResolvedValue( + mockStatus, + ); + mockSubmissionToSubmissionStatusRepository.save.mockResolvedValue( + mockStatus, + ); + + const result = await service.setStatusDate( + fakeSubmissionUuid, + NOTIFICATION_STATUS.IN_PROGRESS, + ); + + expect( + mockSubmissionToSubmissionStatusRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect( + mockSubmissionToSubmissionStatusRepository.findOneOrFail, + ).toBeCalledWith({ + where: { + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + }, + }); + expect(result).toMatchObject(mockStatus); + }); + + it('Should set status effective date', async () => { + const fakeSubmissionUuid = 'fake'; + const mockStatus = new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }); + + mockSubmissionToSubmissionStatusRepository.findOneOrFail.mockResolvedValue( + mockStatus, + ); + mockSubmissionToSubmissionStatusRepository.save.mockResolvedValue( + new NotificationSubmissionToSubmissionStatus({ + ...mockStatus, + effectiveDate: new Date(1, 1, 1), + }), + ); + + const result = await service.setStatusDate( + fakeSubmissionUuid, + NOTIFICATION_STATUS.IN_PROGRESS, + ); + + expect( + mockSubmissionToSubmissionStatusRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect( + mockSubmissionToSubmissionStatusRepository.findOneOrFail, + ).toBeCalledWith({ + where: { + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + }, + }); + expect(result).toMatchObject( + new NotificationSubmissionToSubmissionStatus({ + ...mockStatus, + effectiveDate: new Date(1, 1, 1), + }), + ); + }); + + it('Should set status effective date by fileNumber', async () => { + const fakeSubmissionUuid = 'fake'; + const fakeFileNumber = 'fake-number'; + const mockStatus = new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }); + + mockSubmissionToSubmissionStatusRepository.findOneOrFail.mockResolvedValue( + mockStatus, + ); + mockSubmissionToSubmissionStatusRepository.save.mockResolvedValue( + mockStatus, + ); + mockNotificationSubmissionRepository.findOneBy.mockResolvedValue( + new NotificationSubmission({ + uuid: fakeSubmissionUuid, + fileNumber: fakeFileNumber, + }), + ); + + const result = await service.setStatusDateByFileNumber( + fakeFileNumber, + NOTIFICATION_STATUS.IN_PROGRESS, + ); + + expect( + mockSubmissionToSubmissionStatusRepository.findOneOrFail, + ).toBeCalledTimes(1); + expect( + mockSubmissionToSubmissionStatusRepository.findOneOrFail, + ).toBeCalledWith({ + where: { + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + }, + }); + expect(result).toMatchObject(mockStatus); + expect( + mockNotificationSubmissionRepository.findOneBy, + ).toHaveBeenCalledTimes(1); + expect(mockNotificationSubmissionRepository.findOneBy).toHaveBeenCalledWith( + { + fileNumber: fakeFileNumber, + }, + ); + }); + + it('Should return current status by fileNumber', async () => { + const fakeSubmissionUuid = 'fake'; + const fakeFileNumber = 'fake-number'; + const mockStatus = new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }); + + mockNotificationSubmissionRepository.findOneBy.mockResolvedValue( + new NotificationSubmission({ + uuid: fakeSubmissionUuid, + fileNumber: fakeFileNumber, + status: mockStatus, + }), + ); + + const status = await service.getCurrentStatusByFileNumber(fakeFileNumber); + + expect(status).toEqual(mockStatus); + expect( + mockNotificationSubmissionRepository.findOneBy, + ).toHaveBeenCalledTimes(1); + expect(mockNotificationSubmissionRepository.findOneBy).toHaveBeenCalledWith( + { + fileNumber: fakeFileNumber, + }, + ); + }); + + it('Should remove statuses', async () => { + const fakeSubmissionUuid = 'fake'; + const mockStatuses = [ + new NotificationSubmissionToSubmissionStatus({ + submissionUuid: fakeSubmissionUuid, + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + effectiveDate: new Date(), + }), + ]; + + mockSubmissionToSubmissionStatusRepository.findBy.mockResolvedValue( + mockStatuses, + ); + mockSubmissionToSubmissionStatusRepository.remove.mockResolvedValue( + {} as any, + ); + + await service.removeStatuses(fakeSubmissionUuid); + + expect( + mockSubmissionToSubmissionStatusRepository.findBy, + ).toHaveBeenCalledTimes(1); + expect( + mockSubmissionToSubmissionStatusRepository.findBy, + ).toHaveBeenCalledWith({ + submissionUuid: fakeSubmissionUuid, + }); + expect(mockSubmissionToSubmissionStatusRepository.remove).toBeCalledTimes( + 1, + ); + expect(mockSubmissionToSubmissionStatusRepository.remove).toBeCalledWith( + mockStatuses, + ); + }); +}); diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.ts new file mode 100644 index 0000000000..cffbdef72f --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.ts @@ -0,0 +1,126 @@ +import { ServiceNotFoundException } from '@app/common/exceptions/base.exception'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import * as dayjs from 'dayjs'; +import * as timezone from 'dayjs/plugin/timezone'; +import * as utc from 'dayjs/plugin/utc'; +import { Repository } from 'typeorm'; +import { NotificationSubmission } from '../../../portal/notification-submission/notification-submission.entity'; +import { NotificationSubmissionStatusType } from './notification-status-type.entity'; +import { NOTIFICATION_STATUS } from './notification-status.dto'; +import { NotificationSubmissionToSubmissionStatus } from './notification-status.entity'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +@Injectable() +export class NotificationSubmissionStatusService { + constructor( + @InjectRepository(NotificationSubmissionToSubmissionStatus) + private statusesRepository: Repository, + @InjectRepository(NotificationSubmissionStatusType) + private submissionStatusTypeRepository: Repository, + @InjectRepository(NotificationSubmission) + private notificationSubmissionRepository: Repository, + ) {} + + async setInitialStatuses(submissionUuid: string, persist = true) { + const statuses = await this.submissionStatusTypeRepository.find(); + const newStatuses: NotificationSubmissionToSubmissionStatus[] = []; + + for (const status of statuses) { + const newStatus = new NotificationSubmissionToSubmissionStatus({ + submissionUuid: submissionUuid, + statusTypeCode: status.code, + }); + + if (newStatus.statusTypeCode === NOTIFICATION_STATUS.IN_PROGRESS) { + newStatus.effectiveDate = dayjs() + .tz('Canada/Pacific') + .startOf('day') + .toDate(); + } + + newStatuses.push(newStatus); + } + + if (persist) { + return await this.statusesRepository.save(newStatuses); + } + + return newStatuses; + } + + async setStatusDate( + submissionUuid: string, + statusTypeCode: string, + effectiveDate?: Date | null, + ) { + const status = await this.statusesRepository.findOneOrFail({ + where: { + submissionUuid, + statusTypeCode, + }, + }); + + let date = new Date(); + if (effectiveDate) { + date = effectiveDate; + } + date = dayjs(date).tz('Canada/Pacific').startOf('day').toDate(); + status.effectiveDate = effectiveDate !== null ? date : effectiveDate; + + return this.statusesRepository.save(status); + } + + async setStatusDateByFileNumber( + fileNumber: string, + statusTypeCode: string, + effectiveDate?: Date | null, + ) { + const submission = await this.getSubmission(fileNumber); + return await this.setStatusDate( + submission.uuid, + statusTypeCode, + effectiveDate, + ); + } + + async getCurrentStatusesBy(submissionUuid: string) { + return await this.statusesRepository.findBy({ submissionUuid }); + } + + async getCurrentStatusesByFileNumber(fileNumber: string) { + const submission = await this.getSubmission(fileNumber); + + return await this.statusesRepository.findBy({ + submissionUuid: submission?.uuid, + }); + } + + async getCurrentStatusByFileNumber(fileNumber: string) { + const submission = await this.getSubmission(fileNumber); + + return submission.status; + } + + private async getSubmission(fileNumber: string) { + const submission = await this.notificationSubmissionRepository.findOneBy({ + fileNumber, + }); + + if (!submission) { + throw new ServiceNotFoundException( + `Submission does not exist for provided notice of intent ${fileNumber}. Only notice of intents originated in portal have statuses.`, + ); + } + + return submission; + } + + async removeStatuses(submissionUuid: string) { + const statusesToRemove = await this.getCurrentStatusesBy(submissionUuid); + + return await this.statusesRepository.remove(statusesToRemove); + } +} diff --git a/services/apps/alcs/src/alcs/notification/notification.module.ts b/services/apps/alcs/src/alcs/notification/notification.module.ts index 4ed9401bc3..626e18522d 100644 --- a/services/apps/alcs/src/alcs/notification/notification.module.ts +++ b/services/apps/alcs/src/alcs/notification/notification.module.ts @@ -8,6 +8,7 @@ import { BoardModule } from '../board/board.module'; import { CardModule } from '../card/card.module'; import { CodeModule } from '../code/code.module'; import { LocalGovernmentModule } from '../local-government/local-government.module'; +import { NotificationSubmissionStatusModule } from './notification-submission-status/notification-submission-status.module'; import { NotificationType } from './notification-type/notification-type.entity'; import { NotificationController } from './notification.controller'; import { NotificationService } from './notification.service'; @@ -22,6 +23,7 @@ import { Notification } from './notification.entity'; DocumentModule, CodeModule, LocalGovernmentModule, + NotificationSubmissionStatusModule, ], providers: [NotificationService, NotificationProfile], controllers: [NotificationController], diff --git a/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts index eb6bb96886..e6d5527d23 100644 --- a/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notice-of-intent-submission.automapper.profile.ts @@ -127,13 +127,12 @@ export class NoticeOfIntentSubmissionProfile extends AutomapperProfile { mapper, NoticeOfIntentSubmission, AlcsNoticeOfIntentSubmissionDto, - // TODO uncomment when working on statuses - // forMember( - // (a) => a.lastStatusUpdate, - // mapFrom((ad) => { - // return ad.status?.effectiveDate?.getTime(); - // }), - // ), + forMember( + (a) => a.lastStatusUpdate, + mapFrom((ad) => { + return ad.status?.effectiveDate?.getTime(); + }), + ), forMember( (a) => a.status, mapFrom((ad) => { diff --git a/services/apps/alcs/src/common/automapper/notification-submission.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notification-submission.automapper.profile.ts index a6227876d7..3920e50f9d 100644 --- a/services/apps/alcs/src/common/automapper/notification-submission.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notification-submission.automapper.profile.ts @@ -1,6 +1,12 @@ import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; +import { NotificationSubmissionStatusType } from '../../alcs/notification/notification-submission-status/notification-status-type.entity'; +import { + NotificationStatusDto, + NotificationSubmissionToSubmissionStatusDto, +} from '../../alcs/notification/notification-submission-status/notification-status.dto'; +import { NotificationSubmissionToSubmissionStatus } from '../../alcs/notification/notification-submission-status/notification-status.entity'; import { NotificationSubmissionDetailedDto, NotificationSubmissionDto, @@ -31,6 +37,18 @@ export class NotificationSubmissionProfile extends AutomapperProfile { return ad.auditUpdatedAt?.getTime(); }), ), + forMember( + (a) => a.status, + mapFrom((ad) => { + return ad.status.statusType; + }), + ), + forMember( + (a) => a.lastStatusUpdate, + mapFrom((ad) => { + return ad.status?.effectiveDate?.getTime(); + }), + ), ); createMap( @@ -49,6 +67,40 @@ export class NotificationSubmissionProfile extends AutomapperProfile { return ad.auditUpdatedAt?.getTime(); }), ), + forMember( + (a) => a.status, + mapFrom((ad) => { + return ad.status.statusType; + }), + ), + forMember( + (a) => a.lastStatusUpdate, + mapFrom((ad) => { + return ad.status?.effectiveDate?.getTime(); + }), + ), + ); + + createMap( + mapper, + NotificationSubmissionToSubmissionStatus, + NotificationSubmissionToSubmissionStatusDto, + forMember( + (a) => a.effectiveDate, + mapFrom((ad) => { + return ad.effectiveDate?.getTime(); + }), + ), + forMember( + (a) => a.status, + mapFrom((ad) => { + return this.mapper.map( + ad.statusType, + NotificationSubmissionStatusType, + NotificationStatusDto, + ); + }), + ), ); }; } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts index 53eed12b80..1735fe4aae 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts @@ -1,6 +1,7 @@ import { classes } from '@automapper/classes'; import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { BadRequestException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; @@ -8,6 +9,8 @@ import { LocalGovernment } from '../../alcs/local-government/local-government.en import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.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 { NotificationSubmissionProfile } from '../../common/automapper/notification-submission.automapper.profile'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; @@ -22,7 +25,7 @@ import { NotificationTransferee } from './notification-transferee/notification-t describe('NotificationSubmissionController', () => { let controller: NotificationSubmissionController; - let mockNoiSubmissionService: DeepMocked; + let mockNotificationSubmissionService: DeepMocked; let mockDocumentService: DeepMocked; let mockLgService: DeepMocked; let mockEmailService: DeepMocked; @@ -33,7 +36,7 @@ describe('NotificationSubmissionController', () => { const bceidBusinessGuid = 'business-guid'; beforeEach(async () => { - mockNoiSubmissionService = createMock(); + mockNotificationSubmissionService = createMock(); mockDocumentService = createMock(); mockLgService = createMock(); mockEmailService = createMock(); @@ -44,7 +47,7 @@ describe('NotificationSubmissionController', () => { NotificationSubmissionProfile, { provide: NotificationSubmissionService, - useValue: mockNoiSubmissionService, + useValue: mockNotificationSubmissionService, }, { provide: NoticeOfIntentDocumentService, @@ -75,22 +78,22 @@ describe('NotificationSubmissionController', () => { NotificationSubmissionController, ); - mockNoiSubmissionService.update.mockResolvedValue( + mockNotificationSubmissionService.update.mockResolvedValue( new NotificationSubmission({ applicant: applicant, localGovernmentUuid, }), ); - mockNoiSubmissionService.create.mockResolvedValue('2'); - mockNoiSubmissionService.getByFileNumber.mockResolvedValue( + mockNotificationSubmissionService.create.mockResolvedValue('2'); + mockNotificationSubmissionService.getByFileNumber.mockResolvedValue( new NotificationSubmission(), ); - mockNoiSubmissionService.getByUuid.mockResolvedValue( + mockNotificationSubmissionService.getByUuid.mockResolvedValue( new NotificationSubmission(), ); - mockNoiSubmissionService.mapToDTOs.mockResolvedValue([]); + mockNotificationSubmissionService.mapToDTOs.mockResolvedValue([]); mockLgService.list.mockResolvedValue([ new LocalGovernment({ uuid: localGovernmentUuid, @@ -106,7 +109,7 @@ describe('NotificationSubmissionController', () => { }); it('should call out to service when fetching notice of intents', async () => { - mockNoiSubmissionService.getAllByUser.mockResolvedValue([]); + mockNotificationSubmissionService.getAllByUser.mockResolvedValue([]); const submissions = await controller.getSubmissions({ user: { @@ -115,11 +118,13 @@ describe('NotificationSubmissionController', () => { }); expect(submissions).toBeDefined(); - expect(mockNoiSubmissionService.getAllByUser).toHaveBeenCalledTimes(1); + expect( + mockNotificationSubmissionService.getAllByUser, + ).toHaveBeenCalledTimes(1); }); it('should call out to service when fetching a notice of intent', async () => { - mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + mockNotificationSubmissionService.mapToDetailedDTO.mockResolvedValue( {} as NotificationSubmissionDetailedDto, ); @@ -133,14 +138,16 @@ describe('NotificationSubmissionController', () => { ); expect(noticeOfIntent).toBeDefined(); - expect(mockNoiSubmissionService.getByUuid).toHaveBeenCalledTimes(1); + expect(mockNotificationSubmissionService.getByUuid).toHaveBeenCalledTimes( + 1, + ); }); it('should fetch notice of intent by bceid if user has same guid as a local government', async () => { - mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + mockNotificationSubmissionService.mapToDetailedDTO.mockResolvedValue( {} as NotificationSubmissionDetailedDto, ); - mockNoiSubmissionService.getByUuid.mockResolvedValue( + mockNotificationSubmissionService.getByUuid.mockResolvedValue( new NotificationSubmission({ localGovernmentUuid: '', }), @@ -158,12 +165,14 @@ describe('NotificationSubmissionController', () => { ); expect(noiSubmission).toBeDefined(); - expect(mockNoiSubmissionService.getByUuid).toHaveBeenCalledTimes(1); + expect(mockNotificationSubmissionService.getByUuid).toHaveBeenCalledTimes( + 1, + ); }); it('should call out to service when creating an notice of intent', async () => { - mockNoiSubmissionService.create.mockResolvedValue(''); - mockNoiSubmissionService.mapToDTOs.mockResolvedValue([ + mockNotificationSubmissionService.create.mockResolvedValue(''); + mockNotificationSubmissionService.mapToDTOs.mockResolvedValue([ {} as NotificationSubmissionDto, ]); @@ -174,15 +183,19 @@ describe('NotificationSubmissionController', () => { }); expect(noiSubmission).toBeDefined(); - expect(mockNoiSubmissionService.create).toHaveBeenCalledTimes(1); + expect(mockNotificationSubmissionService.create).toHaveBeenCalledTimes(1); }); it('should call out to service for update and map', async () => { - mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + mockNotificationSubmissionService.mapToDetailedDTO.mockResolvedValue( {} as NotificationSubmissionDetailedDto, ); - mockNoiSubmissionService.getByUuid.mockResolvedValue( - new NotificationSubmission({}), + mockNotificationSubmissionService.getByUuid.mockResolvedValue( + new NotificationSubmission({ + status: new NotificationSubmissionToSubmissionStatus({ + statusTypeCode: NOTIFICATION_STATUS.IN_PROGRESS, + }), + }), ); await controller.update( @@ -200,54 +213,66 @@ describe('NotificationSubmissionController', () => { }, ); - expect(mockNoiSubmissionService.update).toHaveBeenCalledTimes(1); - expect(mockNoiSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(1); + expect(mockNotificationSubmissionService.update).toHaveBeenCalledTimes(1); + expect( + mockNotificationSubmissionService.mapToDetailedDTO, + ).toHaveBeenCalledTimes(1); }); - // it('should throw an exception when trying to update a not in progress noi', async () => { - // mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( - // {} as NotificationSubmissionDetailedDto, - // ); - // - // const promise = controller.update( - // 'file-id', - // { - // localGovernmentUuid, - // applicant, - // }, - // { - // user: { - // entity: new User({ - // clientRoles: [], - // }), - // }, - // }, - // ); - // await expect(promise).rejects.toMatchObject( - // new BadRequestException('Can only edit in progress Notice of Intents'), - // ); - // - // expect(mockNoiSubmissionService.update).toHaveBeenCalledTimes(0); - // expect(mockNoiSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(0); - // }); + it('should throw an exception when trying to update a not in progress notification', async () => { + mockNotificationSubmissionService.mapToDetailedDTO.mockResolvedValue( + {} as NotificationSubmissionDetailedDto, + ); + mockNotificationSubmissionService.getByUuid.mockResolvedValue( + new NotificationSubmission({ + status: new NotificationSubmissionToSubmissionStatus({ + statusTypeCode: NOTIFICATION_STATUS.CANCELLED, + }), + }), + ); + + const promise = controller.update( + 'file-id', + { + localGovernmentUuid, + applicant, + }, + { + user: { + entity: new User({ + clientRoles: [], + }), + }, + }, + ); + await expect(promise).rejects.toMatchObject( + new BadRequestException('Can only edit in progress SRWs'), + ); + + expect(mockNotificationSubmissionService.update).toHaveBeenCalledTimes(0); + expect( + mockNotificationSubmissionService.mapToDetailedDTO, + ).toHaveBeenCalledTimes(0); + }); it('should call out to service on submitAlcs', async () => { const mockFileId = 'file-id'; const mockOwner = new NotificationTransferee({ uuid: primaryContactOwnerUuid, }); - const mockGovernment = new LocalGovernment({ uuid: localGovernmentUuid }); const mockSubmission = new NotificationSubmission({ fileNumber: mockFileId, transferees: [mockOwner], localGovernmentUuid, }); - mockNoiSubmissionService.submitToAlcs.mockResolvedValue( + mockNotificationSubmissionService.submitToAlcs.mockResolvedValue( new NoticeOfIntent(), ); - mockNoiSubmissionService.getByUuid.mockResolvedValue(mockSubmission); - mockNoiSubmissionService.mapToDetailedDTO.mockResolvedValue( + mockNotificationSubmissionService.getByUuid.mockResolvedValue( + mockSubmission, + ); + mockNotificationSubmissionService.mapToDetailedDTO.mockResolvedValue( {} as NotificationSubmissionDetailedDto, ); @@ -257,8 +282,14 @@ describe('NotificationSubmissionController', () => { }, }); - expect(mockNoiSubmissionService.getByUuid).toHaveBeenCalledTimes(2); - expect(mockNoiSubmissionService.submitToAlcs).toHaveBeenCalledTimes(1); - expect(mockNoiSubmissionService.mapToDetailedDTO).toHaveBeenCalledTimes(1); + expect(mockNotificationSubmissionService.getByUuid).toHaveBeenCalledTimes( + 2, + ); + expect( + mockNotificationSubmissionService.submitToAlcs, + ).toHaveBeenCalledTimes(1); + expect( + mockNotificationSubmissionService.mapToDetailedDTO, + ).toHaveBeenCalledTimes(1); }); }); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts index 818b1d8989..6be4e1d581 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts @@ -1,4 +1,5 @@ import { + BadRequestException, Body, Controller, Get, @@ -9,6 +10,7 @@ import { Req, UseGuards, } from '@nestjs/common'; +import { NOTIFICATION_STATUS } from '../../alcs/notification/notification-submission-status/notification-status.dto'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; import { User } from '../../user/user.entity'; import { NotificationSubmissionUpdateDto } from './notification-submission.dto'; @@ -83,18 +85,14 @@ export class NotificationSubmissionController { @Body() updateDto: NotificationSubmissionUpdateDto, @Req() req, ) { - // const submission = await this.notificationSubmissionService.getByUuid( - // uuid, - // req.user.entity, - // ); - // - // if ( - // submission.status.statusTypeCode !== - // NOI_SUBMISSION_STATUS.IN_PROGRESS && - // overlappingRoles.length === 0 - // ) { - // throw new BadRequestException('Can only edit in progress SRWs'); - // } + const submission = await this.notificationSubmissionService.getByUuid( + uuid, + req.user.entity, + ); + + if (submission.status.statusTypeCode !== NOTIFICATION_STATUS.IN_PROGRESS) { + throw new BadRequestException('Can only edit in progress SRWs'); + } const updatedSubmission = await this.notificationSubmissionService.update( uuid, @@ -113,12 +111,12 @@ export class NotificationSubmissionController { const notificationSubmission = await this.notificationSubmissionService.getByUuid(uuid, req.user.entity); - // if ( - // notificationSubmission.status.statusTypeCode !== - // NOI_SUBMISSION_STATUS.IN_PROGRESS - // ) { - // throw new BadRequestException('Can only cancel in progress SRWs'); - // } + if ( + notificationSubmission.status.statusTypeCode !== + NOTIFICATION_STATUS.IN_PROGRESS + ) { + throw new BadRequestException('Can only cancel in progress SRWs'); + } await this.notificationSubmissionService.cancel(notificationSubmission); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts index f74317f39c..cbfec5a6f2 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts @@ -1,5 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { + AfterLoad, Column, Entity, JoinColumn, @@ -7,6 +8,8 @@ import { OneToMany, PrimaryGeneratedColumn, } from 'typeorm'; +import { NoticeOfIntentSubmissionToSubmissionStatus } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.entity'; +import { NotificationSubmissionToSubmissionStatus } from '../../alcs/notification/notification-submission-status/notification-status.entity'; import { Notification } from '../../alcs/notification/notification.entity'; import { Base } from '../../common/entities/base.entity'; import { User } from '../../user/user.entity'; @@ -85,4 +88,46 @@ export class NotificationSubmission extends Base { (parcel) => parcel.notificationSubmission, ) parcels: NotificationParcel[]; + + @OneToMany( + () => NotificationSubmissionToSubmissionStatus, + (status) => status.submission, + { + eager: true, + persistence: false, + }, + ) + submissionStatuses: NotificationSubmissionToSubmissionStatus[] = []; + + private _status: NotificationSubmissionToSubmissionStatus; + + get status(): NotificationSubmissionToSubmissionStatus { + return this._status; + } + + private set status(value: NotificationSubmissionToSubmissionStatus) { + this._status = value; + } + + @AfterLoad() + populateCurrentStatus() { + // using JS date object is intentional for performance reasons + const now = Date.now(); + + for (const status of this.submissionStatuses) { + const effectiveDate = status.effectiveDate?.getTime(); + const currentEffectiveDate = this.status?.effectiveDate?.getTime(); + + if ( + effectiveDate && + effectiveDate <= now && + (!currentEffectiveDate || + effectiveDate > currentEffectiveDate || + (effectiveDate === currentEffectiveDate && + status.statusType.weight > this.status.statusType.weight)) + ) { + this.status = status; + } + } + } } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts index 83c31fd910..62d5b44899 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts @@ -2,6 +2,7 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BoardModule } from '../../alcs/board/board.module'; import { LocalGovernmentModule } from '../../alcs/local-government/local-government.module'; +import { NotificationSubmissionStatusModule } from '../../alcs/notification/notification-submission-status/notification-submission-status.module'; import { NotificationModule } from '../../alcs/notification/notification.module'; import { AuthorizationModule } from '../../common/authorization/authorization.module'; import { NotificationParcelProfile } from '../../common/automapper/notification-parcel.automapper.profile'; @@ -34,6 +35,7 @@ import { NotificationTransfereeService } from './notification-transferee/notific forwardRef(() => BoardModule), LocalGovernmentModule, FileNumberModule, + NotificationSubmissionStatusModule, ], controllers: [ NotificationSubmissionController, diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts index efd7d9acbb..c98e98df0d 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts @@ -7,6 +7,8 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { NoticeOfIntentType } from '../../alcs/notice-of-intent/notice-of-intent-type/notice-of-intent-type.entity'; +import { NotificationSubmissionToSubmissionStatus } from '../../alcs/notification/notification-submission-status/notification-status.entity'; +import { NotificationSubmissionStatusService } from '../../alcs/notification/notification-submission-status/notification-submission-status.service'; import { Notification } from '../../alcs/notification/notification.entity'; import { NotificationService } from '../../alcs/notification/notification.service'; import { NotificationSubmissionProfile } from '../../common/automapper/notification-submission.automapper.profile'; @@ -21,6 +23,7 @@ describe('NotificationSubmissionService', () => { let mockNotificationService: DeepMocked; let mockLGService: DeepMocked; let mockFileNumberService: DeepMocked; + let mockStatusService: DeepMocked; let mockSubmission; beforeEach(async () => { @@ -28,6 +31,7 @@ describe('NotificationSubmissionService', () => { mockNotificationService = createMock(); mockLGService = createMock(); mockFileNumberService = createMock(); + mockStatusService = createMock(); const module: TestingModule = await Test.createTestingModule({ imports: [ @@ -54,6 +58,10 @@ describe('NotificationSubmissionService', () => { provide: FileNumberService, useValue: mockFileNumberService, }, + { + provide: NotificationSubmissionStatusService, + useValue: mockStatusService, + }, ], }).compile(); @@ -108,6 +116,7 @@ describe('NotificationSubmissionService', () => { mockRepository.save.mockResolvedValue(new NotificationSubmission()); mockFileNumberService.generateNextFileNumber.mockResolvedValue(fileId); mockNotificationService.create.mockResolvedValue(new Notification()); + mockStatusService.setInitialStatuses.mockResolvedValue([]); const fileNumber = await service.create( 'type', @@ -119,6 +128,7 @@ describe('NotificationSubmissionService', () => { expect(fileNumber).toEqual(fileId); expect(mockRepository.save).toHaveBeenCalledTimes(1); expect(mockNotificationService.create).toHaveBeenCalledTimes(1); + expect(mockStatusService.setInitialStatuses).toHaveBeenCalledTimes(1); }); it('should call through for get by user', async () => { @@ -167,6 +177,8 @@ describe('NotificationSubmissionService', () => { typeCode: typeCode, auditCreatedAt: new Date(), createdBy: new User(), + submissionStatuses: [], + status: new NotificationSubmissionToSubmissionStatus(), }); mockRepository.findOne.mockResolvedValue(noiSubmission); @@ -206,11 +218,15 @@ describe('NotificationSubmissionService', () => { const notification = new Notification({ dateSubmittedToAlc: new Date(), }); + mockStatusService.setStatusDate.mockResolvedValue( + new NotificationSubmissionToSubmissionStatus(), + ); mockNotificationService.submit.mockResolvedValue(notification); await service.submitToAlcs(mockSubmission); expect(mockNotificationService.submit).toBeCalledTimes(1); + expect(mockStatusService.setStatusDate).toHaveBeenCalledTimes(1); }); it('should update fields if notification exists', async () => { diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts index 581f183229..75e7979baf 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts @@ -11,7 +11,8 @@ import { Repository, } from 'typeorm'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; -import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NOTIFICATION_STATUS } from '../../alcs/notification/notification-submission-status/notification-status.dto'; +import { NotificationSubmissionStatusService } from '../../alcs/notification/notification-submission-status/notification-submission-status.service'; import { NotificationService } from '../../alcs/notification/notification.service'; import { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; import { FileNumberService } from '../../file-number/file-number.service'; @@ -43,6 +44,7 @@ export class NotificationSubmissionService { private notificationService: NotificationService, private localGovernmentService: LocalGovernmentService, private fileNumberService: FileNumberService, + private notificationSubmissionStatusService: NotificationSubmissionStatusService, @InjectMapper() private mapper: Mapper, ) {} @@ -74,9 +76,9 @@ export class NotificationSubmissionService { noiSubmission, ); - // await this.noticeOfIntentSubmissionStatusService.setInitialStatuses( - // savedSubmission.uuid, - // ); + await this.notificationSubmissionStatusService.setInitialStatuses( + savedSubmission.uuid, + ); return fileNumber; } @@ -282,11 +284,11 @@ export class NotificationSubmissionService { dateSubmittedToAlc: new Date(), }); - // await this.noticeOfIntentSubmissionStatusService.setStatusDate( - // notificationSubmission.uuid, - // NOI_SUBMISSION_STATUS.SUBMITTED_TO_ALC, - // submittedNotification.dateSubmittedToAlc, - // ); + await this.notificationSubmissionStatusService.setStatusDate( + notificationSubmission.uuid, + NOTIFICATION_STATUS.SUBMITTED_TO_ALC, + submittedNotification.dateSubmittedToAlc, + ); return submittedNotification; } catch (ex) { @@ -299,30 +301,22 @@ export class NotificationSubmissionService { async updateStatus( uuid: string, - statusCode: NOI_SUBMISSION_STATUS, + statusCode: NOTIFICATION_STATUS, effectiveDate?: Date | null, ) { const submission = await this.loadBarebonesSubmission(uuid); - // await this.noticeOfIntentSubmissionStatusService.setStatusDate( - // submission.uuid, - // statusCode, - // effectiveDate, - // ); - } - - async getStatus(code: NOI_SUBMISSION_STATUS) { - // return await this.noticeOfIntentStatusRepository.findOneOrFail({ - // where: { - // code, - // }, - // }); + await this.notificationSubmissionStatusService.setStatusDate( + submission.uuid, + statusCode, + effectiveDate, + ); } async cancel(submission: NotificationSubmission) { - // return await this.noticeOfIntentSubmissionStatusService.setStatusDate( - // noticeOfIntentSubmission.uuid, - // NOI_SUBMISSION_STATUS.CANCELLED, - // ); + return await this.notificationSubmissionStatusService.setStatusDate( + submission.uuid, + NOTIFICATION_STATUS.CANCELLED, + ); } private loadBarebonesSubmission(uuid: string) { diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693952313867-add_notification_statuses.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693952313867-add_notification_statuses.ts new file mode 100644 index 0000000000..0e5d15c5fb --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693952313867-add_notification_statuses.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNotificationStatuses1693952313867 + implements MigrationInterface +{ + name = 'addNotificationStatuses1693952313867'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "alcs"."notification_submission_status_type" ("audit_deleted_date_at" TIMESTAMP WITH TIME ZONE, "audit_created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "audit_updated_at" TIMESTAMP WITH TIME ZONE DEFAULT now(), "audit_created_by" character varying NOT NULL, "audit_updated_by" character varying, "label" character varying NOT NULL, "code" text NOT NULL, "description" text NOT NULL, "weight" smallint NOT NULL DEFAULT '0', "alcs_background_color" character varying NOT NULL, "alcs_color" character varying NOT NULL, "portal_background_color" character varying NOT NULL, "portal_color" character varying NOT NULL, CONSTRAINT "UQ_7fb8eeb106bb4bb0b0738bd1106" UNIQUE ("description"), CONSTRAINT "PK_9a28eacfd5a7b00f06bdf234bcd" PRIMARY KEY ("code"))`, + ); + await queryRunner.query( + `CREATE TABLE "alcs"."notification_submission_to_submission_status" ("effective_date" TIMESTAMP WITH TIME ZONE, "submission_uuid" uuid NOT NULL, "status_type_code" text NOT NULL, CONSTRAINT "PK_913960c8fe5b20339701b6be841" PRIMARY KEY ("submission_uuid", "status_type_code"))`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission_to_submission_status" ADD CONSTRAINT "FK_279dfb38cef709b730605d296a1" FOREIGN KEY ("submission_uuid") REFERENCES "alcs"."notification_submission"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission_to_submission_status" ADD CONSTRAINT "FK_a38303f5bfdee8a260a48320b24" FOREIGN KEY ("status_type_code") REFERENCES "alcs"."notification_submission_status_type"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission_to_submission_status" DROP CONSTRAINT "FK_a38303f5bfdee8a260a48320b24"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission_to_submission_status" DROP CONSTRAINT "FK_279dfb38cef709b730605d296a1"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."notification_submission_to_submission_status"`, + ); + await queryRunner.query( + `DROP TABLE "alcs"."notification_submission_status_type"`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693952400742-seed_notification_statuses.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693952400742-seed_notification_statuses.ts new file mode 100644 index 0000000000..a6e8486caa --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693952400742-seed_notification_statuses.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class seedNotificationStatuses1693952400742 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO "alcs"."notification_submission_status_type" + ("audit_deleted_date_at", "audit_created_at", "audit_updated_at", "audit_created_by", "audit_updated_by", "label", "code", "description", "weight", "alcs_background_color", "alcs_color", "portal_background_color", "portal_color") VALUES + (NULL, NOW(), NULL, 'migration_seed', NULL, 'ALC Response Sent', 'ALCR', 'Response sent to applicant', 3, '#94c6ac', '#002f17', '#94c6ac', '#002f17'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Cancelled', 'CANC', 'Notification has been cancelled', 2, '#efefef', '#565656', '#efefef', '#565656'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'In Progress', 'PROG', 'Notification is in progress and has not been submitted', 0, '#fee9b5', '#313132', '#acd2ed', '#0c2e46'), + (NULL, NOW(), NULL, 'migration_seed', NULL, 'Submitted to ALC', 'SUBM', 'Notification has been submitted', 1, '#94c6ac', '#002f17', '#94c6ac', '#002f17'); + `); + } + + public async down(): Promise { + //No + } +} From 8f37d1c4c986c845355284772fc7833d0fd8fdf4 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 6 Sep 2023 09:15:36 -0700 Subject: [PATCH 341/954] Code Review Feedback --- .../notification-submission-status.service.spec.ts | 2 +- .../notification-submission-status.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.spec.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.spec.ts index 8bd6592946..b0dd51a9be 100644 --- a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.spec.ts +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.spec.ts @@ -191,7 +191,7 @@ describe('NotificationSubmissionStatusService', () => { service.getCurrentStatusesByFileNumber(fakeFileNumber), ).rejects.toMatchObject( new ServiceNotFoundException( - `Submission does not exist for provided notice of intent ${fakeFileNumber}. Only notice of intents originated in portal have statuses.`, + `Submission does not exist for provided notification ${fakeFileNumber}. Only notifications originated in portal have statuses.`, ), ); diff --git a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.ts b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.ts index cffbdef72f..b650be65e8 100644 --- a/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.ts +++ b/services/apps/alcs/src/alcs/notification/notification-submission-status/notification-submission-status.service.ts @@ -111,7 +111,7 @@ export class NotificationSubmissionStatusService { if (!submission) { throw new ServiceNotFoundException( - `Submission does not exist for provided notice of intent ${fileNumber}. Only notice of intents originated in portal have statuses.`, + `Submission does not exist for provided notification ${fileNumber}. Only notifications originated in portal have statuses.`, ); } From ff74bed809782b800f04582915bc7f6f9ef04f2a Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 6 Sep 2023 10:12:36 -0700 Subject: [PATCH 342/954] Add Transferees UI * Add Transferee to Step 2 --- .../edit-submission.component.html | 28 ++--- .../edit-submission/edit-submission.module.ts | 18 ++- .../transferee-dialog.component.html | 84 +++++++++++++ .../transferee-dialog.component.scss | 19 +++ .../transferee-dialog.component.spec.ts | 48 ++++++++ .../transferee-dialog.component.ts | 110 ++++++++++++++++++ .../transferees/transferees.component.html | 89 ++++++++++++++ .../transferees/transferees.component.scss | 38 ++++++ .../transferees/transferees.component.spec.ts | 43 +++++++ .../transferees/transferees.component.ts | 91 +++++++++++++++ .../notification-transferee.dto.ts | 21 +--- .../notification-transferee.service.spec.ts | 68 ++--------- .../notification-transferee.service.ts | 90 ++------------ portal-frontend/src/styles.scss | 7 ++ .../notification-transferee.controller.ts | 2 +- 15 files changed, 584 insertions(+), 172 deletions(-) create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.ts diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html index 932a741a8a..32d6dae0f4 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html @@ -1,6 +1,8 @@
-
Notice of Intent ID: {{ notificationSubmission.fileNumber }} | {{ notificationSubmission.type }}
+
+ Notification ID: {{ notificationSubmission.fileNumber }} | {{ notificationSubmission.type }} +
-
- - - help_outline - -
@@ -33,17 +26,26 @@
Notice of Intent ID: {{ notificationSubmissio
+ (exit)="onExit()" + >
-
+
+ + +
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts index b4bdc31cde..9653d0b94b 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts @@ -1,6 +1,10 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTableModule } from '@angular/material/table'; import { RouterModule, Routes } from '@angular/router'; +import { NgxMaskPipe } from 'ngx-mask'; import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; import { SharedModule } from '../../../shared/shared.module'; import { EditSubmissionComponent } from './edit-submission.component'; @@ -9,6 +13,8 @@ import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; import { StepComponent } from './step.partial'; +import { TransfereeDialogComponent } from './transferees/transferee-dialog/transferee-dialog.component'; +import { TransfereesComponent } from './transferees/transferees.component'; const routes: Routes = [ { @@ -30,7 +36,17 @@ const routes: Routes = [ ParcelEntryComponent, ParcelEntryConfirmationDialogComponent, DeleteParcelDialogComponent, + TransfereesComponent, + TransfereeDialogComponent, + ], + imports: [ + CommonModule, + SharedModule, + RouterModule.forChild(routes), + MatButtonModule, + MatIconModule, + MatTableModule, + NgxMaskPipe, ], - imports: [CommonModule, SharedModule, RouterModule.forChild(routes)], }) export class EditSubmissionModule {} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.html b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.html new file mode 100644 index 0000000000..74498dc2e2 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.html @@ -0,0 +1,84 @@ +
+

Add New Transferee

+

Edit Transferee

+
+
+
+
+
+ + + Individual + Organization + +
+
+ + + + +
+ warning +
This field is required
+
+
+

Organization Contact Info

+
+ + + + +
+ warning +
This field is required
+
+
+
+ + + + +
+ warning +
This field is required
+
+
+
+ + + + +
+ warning +
This field is required
+
Invalid format
+
+
+
+ + + + +
+ warning +
This field is required
+
Invalid format
+
+
+
+
+
+
+ + + +
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.scss new file mode 100644 index 0000000000..2af88008a7 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.scss @@ -0,0 +1,19 @@ +@use '../../../../../../styles/functions' as *; +@use '../../../../../../styles/colors'; + +.actions { + button:not(:last-child) { + margin-right: rem(8) !important; + } +} + +:host::ng-deep { + .field-error { + color: colors.$error-color; + font-size: rem(15); + font-weight: 700; + display: flex; + align-items: center; + margin-top: rem(4); + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.spec.ts new file mode 100644 index 0000000000..dacebfb382 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.spec.ts @@ -0,0 +1,48 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NotificationTransfereeService } from '../../../../../services/notification-transferee/notification-transferee.service'; + +import { TransfereeDialogComponent } from './transferee-dialog.component'; + +describe('TransfereeDialogComponent', () => { + let component: TransfereeDialogComponent; + let fixture: ComponentFixture; + let mockTransfereeService: DeepMocked; + + beforeEach(async () => { + mockTransfereeService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: NotificationTransfereeService, + useValue: mockTransfereeService, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + { + provide: MAT_DIALOG_DATA, + useValue: {}, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + declarations: [TransfereeDialogComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TransfereeDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.ts new file mode 100644 index 0000000000..2b9dca88b8 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferee-dialog/transferee-dialog.component.ts @@ -0,0 +1,110 @@ +import { Component, Inject } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatButtonToggleChange } from '@angular/material/button-toggle'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + NotificationTransfereeCreateDto, + NotificationTransfereeDto, + NotificationTransfereeUpdateDto, +} from '../../../../../services/notification-transferee/notification-transferee.dto'; +import { NotificationTransfereeService } from '../../../../../services/notification-transferee/notification-transferee.service'; +import { OWNER_TYPE } from '../../../../../shared/dto/owner.dto'; + +@Component({ + selector: 'app-transferee-dialog', + templateUrl: './transferee-dialog.component.html', + styleUrls: ['./transferee-dialog.component.scss'], +}) +export class TransfereeDialogComponent { + OWNER_TYPE = OWNER_TYPE; + type = new FormControl(OWNER_TYPE.INDIVIDUAL); + firstName = new FormControl('', [Validators.required]); + lastName = new FormControl('', [Validators.required]); + organizationName = new FormControl(''); + phoneNumber = new FormControl('', [Validators.required]); + email = new FormControl('', [Validators.required, Validators.email]); + + isEdit = false; + existingUuid: string | undefined; + + form = new FormGroup({ + type: this.type, + firstName: this.firstName, + lastName: this.lastName, + organizationName: this.organizationName, + phoneNumber: this.phoneNumber, + email: this.email, + }); + + constructor( + private dialogRef: MatDialogRef, + private transfereeService: NotificationTransfereeService, + @Inject(MAT_DIALOG_DATA) + public data: { + submissionUuid: string; + existingTransferee?: NotificationTransfereeDto; + } + ) { + if (data && data.existingTransferee) { + this.onChangeType({ + value: data.existingTransferee.type.code, + } as any); + this.isEdit = true; + this.type.setValue(data.existingTransferee.type.code); + this.firstName.setValue(data.existingTransferee.firstName); + this.lastName.setValue(data.existingTransferee.lastName); + this.organizationName.setValue(data.existingTransferee.organizationName); + this.phoneNumber.setValue(data.existingTransferee.phoneNumber); + this.email.setValue(data.existingTransferee.email); + this.existingUuid = data.existingTransferee.uuid; + } + } + + onChangeType($event: MatButtonToggleChange) { + if ($event.value === OWNER_TYPE.ORGANIZATION) { + this.organizationName.setValidators([Validators.required]); + } else { + this.organizationName.setValidators([]); + this.organizationName.reset(); + } + } + + async onCreate() { + if (!this.data.submissionUuid) { + console.error('TransfereeDialogComponent misconfigured, needs submissionUuid for create'); + return; + } + + const createDto: NotificationTransfereeCreateDto = { + organizationName: this.organizationName.getRawValue() || undefined, + firstName: this.firstName.getRawValue() || undefined, + lastName: this.lastName.getRawValue() || undefined, + email: this.email.getRawValue()!, + phoneNumber: this.phoneNumber.getRawValue()!, + typeCode: this.type.getRawValue()!, + notificationSubmissionUuid: this.data.submissionUuid, + }; + + await this.transfereeService.create(createDto); + this.dialogRef.close(true); + } + + async onClose() { + this.dialogRef.close(false); + } + + async onSave() { + const updateDto: NotificationTransfereeUpdateDto = { + organizationName: this.organizationName.getRawValue(), + firstName: this.firstName.getRawValue(), + lastName: this.lastName.getRawValue(), + email: this.email.getRawValue()!, + phoneNumber: this.phoneNumber.getRawValue()!, + typeCode: this.type.getRawValue()!, + }; + if (this.existingUuid) { + await this.transfereeService.update(this.existingUuid, updateDto); + this.dialogRef.close(true); + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.html b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.html new file mode 100644 index 0000000000..318f8f0a10 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.html @@ -0,0 +1,89 @@ +
+

Identify Transferee(s)

+

Provide the name and contact information for all transferees who will be registered owners of the SRW.

+

*All fields are required unless stated optional.

+
+
+
+

All Transferees

+
+ + + At least one transferee is required + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type{{ element.type.code === 'INVD' ? 'Individual' : 'Organization' }}Full Name{{ element.displayName }}Organization Name + + {{ element.organizationName }} + + No Data + Phone + {{ element.phoneNumber | mask : '(000) 000-0000' }} + Email + {{ element.email }} + Actions + + +
+ No Transferee added. Use ‘Add New Transferee’ button to the right to add your first transferee. +
+
+
+
+ +
+ + +
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.scss new file mode 100644 index 0000000000..b0180f9d5f --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.scss @@ -0,0 +1,38 @@ +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; + +.no-data { + color: colors.$grey; + text-align: center; + padding: rem(8); +} + +.actions-cell { + button:not(:last-child) { + margin-right: rem(12) !important; + } +} + +.split { + margin-bottom: rem(16); +} + +@media screen and (max-width: $desktopBreakpoint) { + .table { + overflow-x: auto; + max-width: 100vw; + + table { + white-space: nowrap; + } + } + + .split { + flex-direction: column; + align-items: start; + + button { + width: 100%; + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.spec.ts new file mode 100644 index 0000000000..4e99960555 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.spec.ts @@ -0,0 +1,43 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { NotificationTransfereeService } from '../../../../services/notification-transferee/notification-transferee.service'; + +import { TransfereesComponent } from './transferees.component'; + +describe('TransfereesComponent', () => { + let component: TransfereesComponent; + let fixture: ComponentFixture; + let mockTransfereeService: DeepMocked; + + beforeEach(async () => { + mockTransfereeService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: NotificationTransfereeService, + useValue: mockTransfereeService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + declarations: [TransfereesComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(TransfereesComponent); + component = fixture.componentInstance; + component.$notificationSubmission = new BehaviorSubject(undefined); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.ts new file mode 100644 index 0000000000..7988c8735a --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.ts @@ -0,0 +1,91 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { takeUntil } from 'rxjs'; +import { NotificationTransfereeDto } from '../../../../services/notification-transferee/notification-transferee.dto'; +import { NotificationTransfereeService } from '../../../../services/notification-transferee/notification-transferee.service'; +import { EditNotificationSteps } from '../edit-submission.component'; +import { StepComponent } from '../step.partial'; +import { TransfereeDialogComponent } from './transferee-dialog/transferee-dialog.component'; + +@Component({ + selector: 'app-transferees', + templateUrl: './transferees.component.html', + styleUrls: ['./transferees.component.scss'], +}) +export class TransfereesComponent extends StepComponent implements OnInit, OnDestroy { + currentStep = EditNotificationSteps.Transferees; + + transferees: NotificationTransfereeDto[] = []; + isDirty = false; + displayedColumns: string[] = ['type', 'fullName', 'organizationName', 'phone', 'email', 'actions']; + + private submissionUuid = ''; + + constructor( + private router: Router, + private notificationTransfereeService: NotificationTransfereeService, + private dialog: MatDialog + ) { + super(); + } + + ngOnInit(): void { + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + if (submission) { + this.submissionUuid = submission.uuid; + this.loadTransferees(submission.uuid); + } + }); + } + + protected async save() { + //Do Nothing + } + + private async loadTransferees(submissionUuid: string, primaryContactOwnerUuid?: string | null) { + const transferees = await this.notificationTransfereeService.fetchBySubmissionId(submissionUuid); + if (transferees) { + this.transferees = transferees; + } + } + + onAdd() { + this.dialog + .open(TransfereeDialogComponent, { + data: { + submissionUuid: this.submissionUuid, + }, + }) + .beforeClosed() + .subscribe((didCreate) => { + if (didCreate) { + this.loadTransferees(this.submissionUuid); + } + }); + } + + onEdit(uuid: string) { + const selectedTransferee = this.transferees.find((transferee) => transferee.uuid === uuid); + this.dialog + .open(TransfereeDialogComponent, { + data: { + submissionUuid: this.submissionUuid, + existingTransferee: selectedTransferee, + }, + }) + .beforeClosed() + .subscribe((didSave) => { + if (didSave) { + this.loadTransferees(this.submissionUuid); + } + }); + } + + async onDelete(uuid: string) { + await this.notificationTransfereeService.delete(uuid); + await this.loadTransferees(this.submissionUuid); + } + + protected readonly undefined = undefined; +} diff --git a/portal-frontend/src/app/services/notification-transferee/notification-transferee.dto.ts b/portal-frontend/src/app/services/notification-transferee/notification-transferee.dto.ts index 2f796b2fe2..a6e60023b3 100644 --- a/portal-frontend/src/app/services/notification-transferee/notification-transferee.dto.ts +++ b/portal-frontend/src/app/services/notification-transferee/notification-transferee.dto.ts @@ -3,9 +3,6 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; export enum OWNER_TYPE { INDIVIDUAL = 'INDV', ORGANIZATION = 'ORGZ', - AGENT = 'AGEN', - CROWN = 'CRWN', - GOVERNMENT = 'GOVR', } export interface OwnerTypeDto extends BaseCodeDto { @@ -24,27 +21,15 @@ export interface NotificationTransfereeDto { type: OwnerTypeDto; } -export interface NotificationOwnerUpdateDto { +export interface NotificationTransfereeUpdateDto { firstName?: string | null; lastName?: string | null; organizationName?: string | null; phoneNumber: string; email: string; typeCode: string; - corporateSummaryUuid?: string | null; } -export interface NotificationOwnerCreateDto extends NotificationOwnerUpdateDto { - noticeOfIntentSubmissionUuid: string; -} - -export interface SetPrimaryContactDto { - firstName?: string; - lastName?: string; - organization?: string; - phoneNumber?: string; - email?: string; - type?: OWNER_TYPE; - ownerUuid?: string; - noticeOfIntentSubmissionUuid: string; +export interface NotificationTransfereeCreateDto extends NotificationTransfereeUpdateDto { + notificationSubmissionUuid: string; } diff --git a/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.spec.ts b/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.spec.ts index bb8b61c98d..986e30a836 100644 --- a/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.spec.ts +++ b/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.spec.ts @@ -43,7 +43,7 @@ describe('NotificationTransfereeService', () => { expect(service).toBeTruthy(); }); - it('should make a get request for loading owners', async () => { + it('should make a get request for loading transferees', async () => { mockHttpClient.get.mockReturnValue(of({})); await service.fetchBySubmissionId(fileId); @@ -52,7 +52,7 @@ describe('NotificationTransfereeService', () => { expect(mockHttpClient.get.mock.calls[0][0]).toContain('notification-transferee'); }); - it('should show an error toast if getting owners fails', async () => { + it('should show an error toast if getting transferees fails', async () => { mockHttpClient.get.mockReturnValue(throwError(() => ({}))); await service.fetchBySubmissionId(fileId); @@ -65,7 +65,7 @@ describe('NotificationTransfereeService', () => { mockHttpClient.post.mockReturnValue(of({})); await service.create({ - noticeOfIntentSubmissionUuid: '', + notificationSubmissionUuid: '', email: '', phoneNumber: '', typeCode: '', @@ -75,11 +75,11 @@ describe('NotificationTransfereeService', () => { expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-transferee'); }); - it('should show an error toast if creating owner fails', async () => { + it('should show an error toast if creating transferee fails', async () => { mockHttpClient.post.mockReturnValue(throwError(() => ({}))); await service.create({ - noticeOfIntentSubmissionUuid: '', + notificationSubmissionUuid: '', email: '', phoneNumber: '', typeCode: '', @@ -102,7 +102,7 @@ describe('NotificationTransfereeService', () => { expect(mockHttpClient.patch.mock.calls[0][0]).toContain('notification-transferee'); }); - it('should show an error toast if updating owner fails', async () => { + it('should show an error toast if updating transferee fails', async () => { mockHttpClient.patch.mockReturnValue(throwError(() => ({}))); await service.update('', { @@ -124,7 +124,7 @@ describe('NotificationTransfereeService', () => { expect(mockHttpClient.delete.mock.calls[0][0]).toContain('notification-transferee'); }); - it('should show an error toast if delete owner fails', async () => { + it('should show an error toast if delete transferee fails', async () => { mockHttpClient.delete.mockReturnValue(throwError(() => ({}))); await service.delete(''); @@ -132,58 +132,4 @@ describe('NotificationTransfereeService', () => { expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); }); - - it('should make a post request for removeFromParcel', async () => { - mockHttpClient.post.mockReturnValue(of({})); - - await service.removeFromParcel('', ''); - - expect(mockHttpClient.post).toHaveBeenCalledTimes(1); - expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-transferee'); - }); - - it('should show an error toast if removeFromParcel', async () => { - mockHttpClient.post.mockReturnValue(throwError(() => ({}))); - - await service.removeFromParcel('', ''); - - expect(mockHttpClient.post).toHaveBeenCalledTimes(1); - expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); - }); - - it('should make a post request for linkToParcel', async () => { - mockHttpClient.post.mockReturnValue(of({})); - - await service.linkToParcel('', ''); - - expect(mockHttpClient.post).toHaveBeenCalledTimes(1); - expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-transferee'); - }); - - it('should show an error toast if linkToParcel', async () => { - mockHttpClient.post.mockReturnValue(throwError(() => ({}))); - - await service.linkToParcel('', ''); - - expect(mockHttpClient.post).toHaveBeenCalledTimes(1); - expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); - }); - - it('should make a post request for setPrimaryContact', async () => { - mockHttpClient.post.mockReturnValue(of({})); - - await service.setPrimaryContact({ noticeOfIntentSubmissionUuid: '' }); - - expect(mockHttpClient.post).toHaveBeenCalledTimes(1); - expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-transferee'); - }); - - it('should show an error toast if setPrimaryContact', async () => { - mockHttpClient.post.mockReturnValue(throwError(() => ({}))); - - await service.setPrimaryContact({ noticeOfIntentSubmissionUuid: '' }); - - expect(mockHttpClient.post).toHaveBeenCalledTimes(1); - expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); - }); }); diff --git a/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.ts b/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.ts index f5ea226734..6493cf3d0c 100644 --- a/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.ts +++ b/portal-frontend/src/app/services/notification-transferee/notification-transferee.service.ts @@ -2,14 +2,11 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; -import { DocumentService } from '../document/document.service'; import { ToastService } from '../toast/toast.service'; import { - NotificationOwnerCreateDto, + NotificationTransfereeCreateDto, NotificationTransfereeDto, - NotificationOwnerUpdateDto, - SetPrimaryContactDto, + NotificationTransfereeUpdateDto, } from './notification-transferee.dto'; @Injectable({ @@ -18,11 +15,7 @@ import { export class NotificationTransfereeService { private serviceUrl = `${environment.apiUrl}/notification-transferee`; - constructor( - private httpClient: HttpClient, - private toastService: ToastService, - private documentService: DocumentService - ) {} + constructor(private httpClient: HttpClient, private toastService: ToastService) {} async fetchBySubmissionId(submissionUuid: string) { try { return await firstValueFrom( @@ -30,87 +23,44 @@ export class NotificationTransfereeService { ); } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to load Owners, please try again later'); + this.toastService.showErrorToast('Failed to load Transferees, please try again later'); } return undefined; } - async create(dto: NotificationOwnerCreateDto) { + async create(dto: NotificationTransfereeCreateDto) { try { const res = await firstValueFrom(this.httpClient.post(`${this.serviceUrl}`, dto)); - this.toastService.showSuccessToast('Owner created'); + this.toastService.showSuccessToast('Transferee created'); return res; } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to create Owner, please try again later'); + this.toastService.showErrorToast('Failed to create Transferee, please try again later'); return undefined; } } - async update(uuid: string, updateDto: NotificationOwnerUpdateDto) { + async update(uuid: string, updateDto: NotificationTransfereeUpdateDto) { try { const res = await firstValueFrom( this.httpClient.patch(`${this.serviceUrl}/${uuid}`, updateDto) ); - this.toastService.showSuccessToast('Owner saved'); + this.toastService.showSuccessToast('Transferee saved'); return res; } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to update Owner, please try again later'); + this.toastService.showErrorToast('Failed to update Transferee, please try again later'); return undefined; } } - - async setPrimaryContact(updateDto: SetPrimaryContactDto) { - try { - const res = await firstValueFrom( - this.httpClient.post(`${this.serviceUrl}/setPrimaryContact`, updateDto) - ); - this.toastService.showSuccessToast('Notice of Intent saved'); - return res; - } catch (e) { - console.error(e); - this.toastService.showErrorToast('Failed to update Notice of Intent, please try again later'); - return undefined; - } - } - async delete(uuid: string) { try { const result = await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}/${uuid}`)); - this.toastService.showSuccessToast('Owner deleted'); + this.toastService.showSuccessToast('Transferees deleted'); return result; } catch (e) { console.error(e); - this.toastService.showErrorToast('Failed to delete Owner, please try again'); - } - return undefined; - } - - async removeFromParcel(ownerUuid: string, parcelUuid: string) { - try { - const result = await firstValueFrom( - this.httpClient.post(`${this.serviceUrl}/${ownerUuid}/unlink/${parcelUuid}`, {}) - ); - this.toastService.showSuccessToast('Owner removed from parcel'); - return result; - } catch (e) { - console.error(e); - this.toastService.showErrorToast('Failed to remove Owner, please try again'); - } - return undefined; - } - - async linkToParcel(ownerUuid: any, parcelUuid: string) { - try { - const result = await firstValueFrom( - this.httpClient.post(`${this.serviceUrl}/${ownerUuid}/link/${parcelUuid}`, {}) - ); - this.toastService.showSuccessToast('Owner linked to parcel'); - return result; - } catch (e) { - console.error(e); - this.toastService.showErrorToast('Failed to link Owner, please try again'); + this.toastService.showErrorToast('Failed to delete Transferee, please try again'); } return undefined; } @@ -124,20 +74,4 @@ export class NotificationTransfereeService { } return 0; } - - async uploadCorporateSummary(noticeOfIntentFileId: string, file: File) { - try { - return await this.documentService.uploadFile<{ uuid: string }>( - noticeOfIntentFileId, - file, - DOCUMENT_TYPE.CORPORATE_SUMMARY, - DOCUMENT_SOURCE.APPLICANT, - `${this.serviceUrl}/attachCorporateSummary` - ); - } catch (e) { - console.error(e); - this.toastService.showErrorToast('Failed to attach document to Owner, please try again'); - } - return undefined; - } } diff --git a/portal-frontend/src/styles.scss b/portal-frontend/src/styles.scss index 2ead013f21..4dfc2c171f 100644 --- a/portal-frontend/src/styles.scss +++ b/portal-frontend/src/styles.scss @@ -48,6 +48,13 @@ a { align-items: center; } +.split { + display: flex; + align-content: center; + justify-content: space-between; + align-items: center; +} + .no-padding .mat-dialog-container { padding: 0; } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts index 1262329846..bbac88444a 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts @@ -27,7 +27,7 @@ import { import { NotificationTransferee } from './notification-transferee.entity'; import { NotificationTransfereeService } from './notification-transferee.service'; -@Controller('srw-transferee') +@Controller('notification-transferee') @UseGuards(PortalAuthGuard) export class NotificationTransfereeController { constructor( From ce4a6303ad8b08b1b7c799c0f5ef7f16f67408e1 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 6 Sep 2023 10:22:04 -0700 Subject: [PATCH 343/954] duration in months --- bin/migrate-oats-data/submissions/submap/soil_elements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/migrate-oats-data/submissions/submap/soil_elements.py b/bin/migrate-oats-data/submissions/submap/soil_elements.py index 6a72f36721..055aaed3b4 100644 --- a/bin/migrate-oats-data/submissions/submap/soil_elements.py +++ b/bin/migrate-oats-data/submissions/submap/soil_elements.py @@ -99,8 +99,8 @@ def map_soil_data(data, soil_data): data['fill_duration'] = soil_data.get(data[app_component_id], {}).get('fill_duration', None) data['fill_area'] = soil_data.get(data[app_component_id], {}).get('fill_area', None) data['import_fill'] = soil_data.get(data[app_component_id], {}).get('import_fill', None) - data['fill_duration_unit'] = 'duration_unit' - data['remove_duration_unit'] = 'duration_unit' + data['fill_duration_unit'] = 'months' + data['remove_duration_unit'] = 'months' data['remove_type'] = soil_data.get(data[app_component_id], {}).get('remove_type', None) data['remove_origin'] = soil_data.get(data[app_component_id], {}).get('remove_origin', None) data['total_remove'] = soil_data.get(data[app_component_id], {}).get('total_remove', None) From b7ab6b7bb5628374c7650431e2a015255a33f000 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 6 Sep 2023 10:25:16 -0700 Subject: [PATCH 344/954] Fixed type code issue --- bin/migrate-oats-data/noi/noi.py | 5 +++-- bin/migrate-oats-data/noi/sql/insert_noi.sql | 23 ++++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/bin/migrate-oats-data/noi/noi.py b/bin/migrate-oats-data/noi/noi.py index c7ce704e89..82a6094c2f 100644 --- a/bin/migrate-oats-data/noi/noi.py +++ b/bin/migrate-oats-data/noi/noi.py @@ -4,7 +4,7 @@ def noi_insert_query(number_of_rows_to_insert): nois_to_insert = ",".join(["%s"] * number_of_rows_to_insert) return f""" INSERT INTO alcs.notice_of_intent (file_number, - applicant, region_code, local_government_uuid, audit_created_by) + applicant, region_code, local_government_uuid, audit_created_by, type_code) VALUES{nois_to_insert} ON CONFLICT (file_number) DO UPDATE SET @@ -12,7 +12,8 @@ def noi_insert_query(number_of_rows_to_insert): applicant = EXCLUDED.applicant, region_code = EXCLUDED.region_code, local_government_uuid = EXCLUDED.local_government_uuid, - audit_created_by = EXCLUDED.audit_created_by + audit_created_by = EXCLUDED.audit_created_by, + type_code = EXCLUDED.type_code """ @inject_conn_pool diff --git a/bin/migrate-oats-data/noi/sql/insert_noi.sql b/bin/migrate-oats-data/noi/sql/insert_noi.sql index 205f68e5b6..48bba19aae 100644 --- a/bin/migrate-oats-data/noi/sql/insert_noi.sql +++ b/bin/migrate-oats-data/noi/sql/insert_noi.sql @@ -129,6 +129,18 @@ WITH WHERE oo2.organization_type_cd = 'PANEL' OR oo3.organization_type_cd = 'PANEL' + ), + application_type_lookup AS ( + SELECT + oaac.alr_application_id AS application_id, + oacc."description" AS "description", + oaac.alr_change_code AS code + FROM + oats.oats_alr_appl_components AS oaac + JOIN oats.oats_alr_change_codes oacc ON oaac.alr_change_code = oacc.alr_change_code + LEFT JOIN oats.alcs_etl_application_exclude aee ON oaac.alr_appl_component_id = aee.component_id + WHERE + aee.component_id IS NULL ) SELECT ng.noi_application_id :: text AS file_number, @@ -142,10 +154,17 @@ SELECT WHEN alcs_gov.gov_uuid IS NOT NULL THEN alcs_gov.gov_uuid ELSE '001cfdad-bc6e-4d25-9294-1550603da980' --Peace River if unable to find uuid END AS local_government_uuid, - 'oats_etl' AS audit_created_by + 'oats_etl' AS audit_created_by, + CASE + WHEN atl.code = 'SCH' THEN 'PFRS' + WHEN atl.code = 'EXT' THEN 'ROSO' + WHEN atl.code = 'FILL' THEN 'POFO' + ELSE 'POFO' -- POFO if value is null + END AS type_code FROM noi_grouped AS ng LEFT JOIN applicant_lookup ON ng.noi_application_id = applicant_lookup.application_id LEFT JOIN panel_lookup ON ng.noi_application_id = panel_lookup.application_id LEFT JOIN alcs.application_region ar ON panel_lookup.panel_region = ar."label" - LEFT JOIN alcs_gov ON ng.noi_application_id = alcs_gov.application_id \ No newline at end of file + LEFT JOIN alcs_gov ON ng.noi_application_id = alcs_gov.application_id + LEFT JOIN application_type_lookup AS atl ON ng.noi_application_id = atl.application_id \ No newline at end of file From 2c66de8b8c625d079c117b5a327b044fbb32fbdd Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Wed, 6 Sep 2023 10:26:43 -0700 Subject: [PATCH 345/954] Feature/alcs 957 part 3 file type filter (#942) file type search dropdown --- .../file-type-filter-drop-down.component.html | 40 ++++ .../file-type-filter-drop-down.component.scss | 3 + ...le-type-filter-drop-down.component.spec.ts | 26 +++ .../file-type-filter-drop-down.component.ts | 185 ++++++++++++++++++ .../app/features/search/search.component.html | 13 +- .../app/features/search/search.component.scss | 184 ++++++++--------- .../app/features/search/search.component.ts | 15 +- .../src/app/features/search/search.module.ts | 23 ++- .../file-type-data-source.service.spec.ts | 34 ++++ .../file-type-data-source.service.ts | 118 +++++++++++ ...-of-intent-advanced-search.service.spec.ts | 2 +- ...otice-of-intent-advanced-search.service.ts | 39 ---- .../src/alcs/search/search.controller.spec.ts | 74 +++++++ .../alcs/src/alcs/search/search.controller.ts | 68 ++++++- .../generate-submission-document.service.ts | 6 + 15 files changed, 676 insertions(+), 154 deletions(-) create mode 100644 alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.html create mode 100644 alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.scss create mode 100644 alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.spec.ts create mode 100644 alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts create mode 100644 alcs-frontend/src/app/services/search/file-type/file-type-data-source.service.spec.ts create mode 100644 alcs-frontend/src/app/services/search/file-type/file-type-data-source.service.ts diff --git a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.html b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.html new file mode 100644 index 0000000000..ae960ea80f --- /dev/null +++ b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.html @@ -0,0 +1,40 @@ + + + + Please select an item from below + + + + {{ node.item?.label ?? node.item }} + + + + + + {{ node.item?.label ?? node.item }} + + + + diff --git a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.scss b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.scss new file mode 100644 index 0000000000..39864d8e44 --- /dev/null +++ b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.scss @@ -0,0 +1,3 @@ +.file-type-input { + width: 100%; +} \ No newline at end of file diff --git a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.spec.ts b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.spec.ts new file mode 100644 index 0000000000..98cd31ef11 --- /dev/null +++ b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.spec.ts @@ -0,0 +1,26 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; + +import { FileTypeFilterDropDownComponent } from './file-type-filter-drop-down.component'; + +describe('FileTypeFilterDropDownComponent', () => { + let component: FileTypeFilterDropDownComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [FileTypeFilterDropDownComponent], + schemas: [NO_ERRORS_SCHEMA], + imports: [MatAutocompleteModule], + }).compileComponents(); + + fixture = TestBed.createComponent(FileTypeFilterDropDownComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts new file mode 100644 index 0000000000..f077c50fa0 --- /dev/null +++ b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.ts @@ -0,0 +1,185 @@ +import { SelectionModel } from '@angular/cdk/collections'; +import { FlatTreeControl } from '@angular/cdk/tree'; +import { AfterViewInit, Component, EventEmitter, Output } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; +import { Observable } from 'rxjs'; +import { FileTypeDataSourceService, FlatTreeNode, TreeNode } from '../../../services/search/file-type/file-type-data-source.service'; + +@Component({ + selector: 'app-file-type-filter-drop-down', + templateUrl: './file-type-filter-drop-down.component.html', + styleUrls: ['./file-type-filter-drop-down.component.scss'], +}) +export class FileTypeFilterDropDownComponent implements AfterViewInit { + /** Map from flat node to nested node. This helps us finding the nested node to be modified */ + flatNodeMap = new Map(); + + /** Map from nested node to flattened node. This helps us to keep the same object for selection */ + nestedNodeMap = new Map(); + + treeControl: FlatTreeControl; + + treeFlattener: MatTreeFlattener; + + dataSource: MatTreeFlatDataSource; + + /** The selection for checklist */ + checklistSelection = new SelectionModel(true); + + filteredOptions = new Observable(); + componentTypeControl = new FormControl(undefined); + @Output() fileTypeChange = new EventEmitter(); + + constructor(private fileTypeData: FileTypeDataSourceService) { + this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren); + this.treeControl = new FlatTreeControl(this.getLevel, this.isExpandable); + this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); + + fileTypeData.dataChange.subscribe((data) => { + this.dataSource.data = data; + }); + } + + getLevel = (node: FlatTreeNode) => node.level; + + isExpandable = (node: FlatTreeNode) => node.expandable; + + getChildren = (node: TreeNode): TreeNode[] | undefined => node.children; + + hasChild = (_: number, _nodeData: FlatTreeNode) => _nodeData.expandable; + + hasNoContent = (_: number, _nodeData: FlatTreeNode) => _nodeData.item.value === ''; + + /** + * Transformer to convert nested node to flat node. Record the nodes in maps for later use. + */ + transformer = (node: TreeNode, level: number) => { + const existingNode = this.nestedNodeMap.get(node); + const flatNode = existingNode && existingNode.item.value === node.item.value ? existingNode : ({} as FlatTreeNode); + flatNode.item = node.item; + flatNode.level = level; + flatNode.expandable = !!node.children; + + this.flatNodeMap.set(flatNode, node); + this.nestedNodeMap.set(node, flatNode); + + return flatNode; + }; + + /** Whether all the descendants of the node are selected. */ + descendantsAllSelected(node: FlatTreeNode): boolean { + const descendants = this.treeControl.getDescendants(node); + const descAllSelected = descendants.every((child) => this.checklistSelection.isSelected(child)); + return descAllSelected; + } + + /** Whether part of the descendants are selected */ + descendantsPartiallySelected(node: FlatTreeNode): boolean { + const descendants = this.treeControl.getDescendants(node); + const result = descendants.some((child) => this.checklistSelection.isSelected(child)); + return result && !this.descendantsAllSelected(node); + } + + /** Toggle the to-do item selection. Select/deselect all the descendants node */ + todoItemSelectionToggle(node: FlatTreeNode): void { + this.checklistSelection.toggle(node); + const descendants = this.treeControl.getDescendants(node); + this.checklistSelection.isSelected(node) + ? this.checklistSelection.select(...descendants) + : this.checklistSelection.deselect(...descendants); + + // Force update for the parent + descendants.every((child) => this.checklistSelection.isSelected(child)); + this.checkAllParentsSelection(node); + } + + /** Toggle a leaf to-do item selection. Check all the parents to see if they changed */ + todoLeafItemSelectionToggle(node: FlatTreeNode): void { + this.checklistSelection.toggle(node); + this.checkAllParentsSelection(node); + } + + /* Checks all the parents when a leaf node is selected/unselected */ + checkAllParentsSelection(node: FlatTreeNode): void { + let parent: FlatTreeNode | null = this.getParentNodeAndChange(node); + while (parent) { + this.checkRootNodeSelection(parent); + parent = this.getParentNodeAndChange(parent); + } + } + + /** Check root node checked state and change it accordingly */ + checkRootNodeSelection(node: FlatTreeNode): void { + const nodeSelected = this.checklistSelection.isSelected(node); + const descendants = this.treeControl.getDescendants(node); + const descAllSelected = descendants.every((child) => this.checklistSelection.isSelected(child)); + if (nodeSelected && !descAllSelected) { + this.checklistSelection.deselect(node); + } else if (!nodeSelected && descAllSelected) { + this.checklistSelection.select(node); + } + } + + /* Get the parent node of a node and emit onChange*/ + getParentNodeAndChange(node: FlatTreeNode): FlatTreeNode | null { + this.onChange(); + + const currentLevel = this.getLevel(node); + + if (currentLevel < 1) { + return null; + } + + const startIndex = this.treeControl.dataNodes.indexOf(node) - 1; + + for (let i = startIndex; i >= 0; i--) { + const currentNode = this.treeControl.dataNodes[i]; + + if (this.getLevel(currentNode) < currentLevel) { + return currentNode; + } + } + return null; + } + + getSelectedItems(): string { + if (!this.checklistSelection.selected.length) { + return ''; + } + + return this.checklistSelection.selected + .filter((selectedItem) => selectedItem.item.value !== null) + .map((selectedItem) => selectedItem.item?.label) + .join(', '); + } + + filterChanged($event: any) { + const filterText = $event.target.value; + // FileTypeTreeComponent.filter method which actually filters the tree and gives back a tree structure + + this.fileTypeData.filter(filterText); + if (filterText) { + this.treeControl.expandAll(); + } else { + this.treeControl.collapseAll(); + } + } + + onChange() { + this.fileTypeChange.emit( + this.checklistSelection.selected + .filter((selectedItem) => selectedItem.item.value) + .map((selectedItem) => selectedItem.item.value!) + ); + } + + reset() { + this.componentTypeControl.reset(); + this.checklistSelection.selected.forEach((selectedItem) => this.checklistSelection.deselect(selectedItem)); + } + + ngAfterViewInit(): void { + this.treeControl.expandAll(); + } +} diff --git a/alcs-frontend/src/app/features/search/search.component.html b/alcs-frontend/src/app/features/search/search.component.html index 9ab3f405f2..5004e5b341 100644 --- a/alcs-frontend/src/app/features/search/search.component.html +++ b/alcs-frontend/src/app/features/search/search.component.html @@ -115,10 +115,7 @@
Provide one or more of the following criteria:
- - File Type (To be implemented) - - +
Note: This field searches both proposal and decision component type
@@ -224,11 +221,11 @@
Provide one or more of the following criteria:
+
+ + +
-
- - -

Search Results:

diff --git a/alcs-frontend/src/app/features/search/search.component.scss b/alcs-frontend/src/app/features/search/search.component.scss index f2b6428f48..a57495575d 100644 --- a/alcs-frontend/src/app/features/search/search.component.scss +++ b/alcs-frontend/src/app/features/search/search.component.scss @@ -1,24 +1,24 @@ @use '../../../styles/colors'; -h3, -div, -span { - color: colors.$black; -} +:host::ng-deep { + h3, + div, + span { + color: colors.$black; + } -.search-title { - width: 100%; - margin-top: 42px; - margin-bottom: 24px; - display: flex; - justify-content: left; - align-items: center; + .search-title { + width: 100%; + margin-top: 42px; + margin-bottom: 24px; + display: flex; + justify-content: left; + align-items: center; - h4 { - margin-right: 12px !important; + h4 { + margin-right: 12px !important; + } } -} -:host::ng-deep { .table { width: 100%; margin-top: 12px; @@ -49,106 +49,106 @@ span { height: 32px; } } -} -.search-fields-wrapper { - padding: 0 80px; -} + .search-fields-wrapper { + padding: 0 80px; + } -.row { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 24px; - width: 100%; - margin: 24px 0; + .row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 24px; + width: 100%; + margin: 24px 0; - &.date-row { - margin: 0 0 12px 0; - } + &.date-row { + margin: 0 0 12px 0; + } - &.date-row-label { - margin: 0 0 4px 0; - gap: 4px; - } + &.date-row-label { + margin: 0 0 4px 0; + gap: 4px; + } - .column { - display: flex; - flex-direction: column; - flex-basis: 100%; - flex: 1; - } + .column { + display: flex; + flex-direction: column; + flex-basis: 100%; + flex: 1; + } - .resolution-wrapper { - gap: 12px; - margin: 0; + .resolution-wrapper { + gap: 12px; + margin: 0; + } } -} -.address-search-fields-wrapper { - background-color: #fee9b580; - padding: 12px 80px; + .address-search-fields-wrapper { + background-color: #fee9b580; + padding: 12px 80px; - .row { - margin: 4px 0; - } - .column { - background-color: white; + .row { + margin: 4px 0; + } + .column { + background-color: white; + } } -} - -.expand-search { - display: flex; - align-items: center; - justify-content: right; - .expand-search-btn { + .expand-search { display: flex; align-items: center; - cursor: pointer; - color: #1a5a96; + justify-content: right; - span { + .expand-search-btn { + display: flex; + align-items: center; + cursor: pointer; color: #1a5a96; + + span { + color: #1a5a96; + } } } -} - -.btn-controls { - padding: 12px 80px; - display: flex; - justify-content: space-between; -} - -.title { - margin: 36px 80px; -} -.subtitle { - margin: 18px 80px; -} + .btn-controls { + padding: 12px 80px; + display: flex; + justify-content: space-between; + } -.info-banner { - display: flex; - align-items: center; + .title { + margin: 36px 80px; + } - .info-description { - min-width: 0; - word-break: break-word; + .subtitle { + margin: 18px 80px; } - .icon { + .info-banner { display: flex; - justify-content: center; - mat-icon { - font-size: 18px; - height: 18px; - width: 18px; + align-items: center; + + .info-description { + min-width: 0; + word-break: break-word; + } + + .icon { + display: flex; + justify-content: center; + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } } } -} -.search-result-wrapper { - margin-top: 60px; - margin-bottom: 60px; + .search-result-wrapper { + margin-top: 60px; + margin-bottom: 60px; + } } diff --git a/alcs-frontend/src/app/features/search/search.component.ts b/alcs-frontend/src/app/features/search/search.component.ts index c227623a2f..7615895dd5 100644 --- a/alcs-frontend/src/app/features/search/search.component.ts +++ b/alcs-frontend/src/app/features/search/search.component.ts @@ -22,6 +22,7 @@ import { import { SearchService } from '../../services/search/search.service'; import { ToastService } from '../../services/toast/toast.service'; import { formatDateForApi } from '../../shared/utils/api-date-formatter'; +import { FileTypeFilterDropDownComponent } from './file-type-filter-drop-down/file-type-filter-drop-down.component'; import { TableChange } from './search.interface'; export const defaultStatusBackgroundColour = '#ffffff'; @@ -42,6 +43,7 @@ export class SearchComponent implements OnInit, OnDestroy { @ViewChild(MatPaginator) paginator!: MatPaginator; @ViewChild(MatSort) sort?: MatSort; @ViewChild('searchResultTabs') tabGroup!: MatTabGroup; + @ViewChild('fileTypeDropDown') fileTypeFilterDropDownComponent!: FileTypeFilterDropDownComponent; applications: ApplicationSearchResultDto[] = []; applicationTotal = 0; @@ -60,6 +62,7 @@ export class SearchComponent implements OnInit, OnDestroy { localGovernmentControl = new FormControl(undefined); portalStatusControl = new FormControl(undefined); + componentTypeControl = new FormControl(undefined); searchForm = new FormGroup({ fileNumber: new FormControl(undefined), name: new FormControl(undefined), @@ -70,7 +73,7 @@ export class SearchComponent implements OnInit, OnDestroy { resolutionYear: new FormControl(undefined), legacyId: new FormControl(undefined), portalStatus: this.portalStatusControl, - componentType: new FormControl(undefined), + componentType: this.componentTypeControl, government: this.localGovernmentControl, region: new FormControl(undefined), dateSubmittedFrom: new FormControl(undefined), @@ -163,7 +166,7 @@ export class SearchComponent implements OnInit, OnDestroy { } onBlur() { - //Blur will fire before onChange above, so use setTimeout to delay it + //Blur will fire before onGovernmentChange above, so use setTimeout to delay it setTimeout(() => { const localGovernmentName = this.localGovernmentControl.getRawValue(); if (localGovernmentName) { @@ -178,6 +181,7 @@ export class SearchComponent implements OnInit, OnDestroy { onReset() { this.searchForm.reset(); + this.fileTypeFilterDropDownComponent.reset(); } async onSearch() { @@ -221,9 +225,8 @@ export class SearchComponent implements OnInit, OnDestroy { dateDecidedTo: this.searchForm.controls.dateDecidedTo.value ? formatDateForApi(this.searchForm.controls.dateDecidedTo.value) : undefined, - // TODO this will be reworked in later tickets applicationFileTypes: this.searchForm.controls.componentType.value - ? this.searchForm.controls.componentType.value.split(',') + ? this.searchForm.controls.componentType.value : [], }; } @@ -273,6 +276,10 @@ export class SearchComponent implements OnInit, OnDestroy { } } + onFileTypeChange(fileTypes: string[]) { + this.componentTypeControl.setValue(fileTypes); + } + private async loadGovernments() { const governments = await this.localGovernmentService.list(); this.localGovernments = governments.sort((a, b) => (a.name > b.name ? 1 : -1)); diff --git a/alcs-frontend/src/app/features/search/search.module.ts b/alcs-frontend/src/app/features/search/search.module.ts index a7aac6457d..5923c66331 100644 --- a/alcs-frontend/src/app/features/search/search.module.ts +++ b/alcs-frontend/src/app/features/search/search.module.ts @@ -2,12 +2,14 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { MatPaginatorModule } from '@angular/material/paginator'; import { MatTabsModule } from '@angular/material/tabs'; +import { MatTreeModule } from '@angular/material/tree'; import { RouterModule, Routes } from '@angular/router'; import { SharedModule } from '../../shared/shared.module'; -import { SearchComponent } from './search.component'; import { ApplicationSearchTableComponent } from './application-search-table/application-search-table.component'; -import { NoticeOfIntentSearchTableComponent } from './notice-of-intent-search-table/notice-of-intent-search-table.component'; +import { FileTypeFilterDropDownComponent } from './file-type-filter-drop-down/file-type-filter-drop-down.component'; import { NonApplicationSearchTableComponent } from './non-application-search-table/non-application-search-table.component'; +import { NoticeOfIntentSearchTableComponent } from './notice-of-intent-search-table/notice-of-intent-search-table.component'; +import { SearchComponent } from './search.component'; const routes: Routes = [ { @@ -17,7 +19,20 @@ const routes: Routes = [ ]; @NgModule({ - declarations: [SearchComponent, ApplicationSearchTableComponent, NoticeOfIntentSearchTableComponent, NonApplicationSearchTableComponent], - imports: [CommonModule, SharedModule.forRoot(), RouterModule.forChild(routes), MatTabsModule, MatPaginatorModule], + declarations: [ + SearchComponent, + ApplicationSearchTableComponent, + NoticeOfIntentSearchTableComponent, + NonApplicationSearchTableComponent, + FileTypeFilterDropDownComponent, + ], + imports: [ + CommonModule, + SharedModule.forRoot(), + RouterModule.forChild(routes), + MatTabsModule, + MatPaginatorModule, + MatTreeModule, + ], }) export class SearchModule {} diff --git a/alcs-frontend/src/app/services/search/file-type/file-type-data-source.service.spec.ts b/alcs-frontend/src/app/services/search/file-type/file-type-data-source.service.spec.ts new file mode 100644 index 0000000000..dc0a50f658 --- /dev/null +++ b/alcs-frontend/src/app/services/search/file-type/file-type-data-source.service.spec.ts @@ -0,0 +1,34 @@ +import { TestBed } from '@angular/core/testing'; +import { FileTypeDataSourceService, TreeNode } from './file-type-data-source.service'; + +describe('FileTypeDataSourceService', () => { + let service: FileTypeDataSourceService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FileTypeDataSourceService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should load initial data', () => { + expect(service.data).toBeTruthy(); + expect(service.data.length).toBeGreaterThan(0); + }); + + it('should filter data', () => { + service.filter('Covenants'); + expect(service.data.length).toBe(1); + + const node: TreeNode = service.data[0]; + console.log(service.dataChange.getValue()); + expect(node.item.label).toEqual('Non-Application'); + }); + + it('should reset data when filtering with empty text', () => { + service.filter(''); + expect(service.data.length).toBeGreaterThan(0); + }); +}); diff --git a/alcs-frontend/src/app/services/search/file-type/file-type-data-source.service.ts b/alcs-frontend/src/app/services/search/file-type/file-type-data-source.service.ts new file mode 100644 index 0000000000..3b84bbe2be --- /dev/null +++ b/alcs-frontend/src/app/services/search/file-type/file-type-data-source.service.ts @@ -0,0 +1,118 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +export interface TreeNodeItem { + label: string; + value: string | null; +} +/** + * Node for to-do item + */ +export interface TreeNode { + children?: TreeNode[]; + item: TreeNodeItem; +} + +/** Flat to-do item node with expandable and level information */ +export interface FlatTreeNode { + item: TreeNodeItem; + level: number; + expandable: boolean; +} + +const TREE_DATA: TreeNode[] = [ + { + item: { label: 'Application', value: null }, + children: [ + { item: { label: 'Exclusion', value: 'EXCL' } }, + { item: { label: 'Inclusion', value: 'INCL' } }, + { + item: { label: 'Non-Adhering Residential Use', value: 'NARU' }, + }, + { + item: { label: 'Non-Farm Use', value: 'NFUP' }, + }, + { + item: { label: 'Placement of Fill', value: 'POFO' }, + }, + { + item: { label: 'Removal of Soil and Placement of Fill', value: 'PFRS' }, + }, + { + item: { label: 'Removal of Soil Only', value: 'ROSO' }, + }, + { + item: { label: 'Subdivision', value: 'SUBD' }, + }, + { + item: { label: 'Transportation, Utility, Trail Permits', value: 'TURP' }, + }, + ], + }, + { + item: { label: 'Notice of Intent', value: 'NOI' }, + }, + { + item: { label: 'Non-Application', value: null }, + + children: [ + { + item: { label: 'Covenants', value: 'COV' }, + }, + { + item: { label: 'Notification of SRW', value: 'SRW' }, + }, + { + item: { label: 'Planning Review', value: 'PLAN' }, + }, + ], + }, +]; + +@Injectable({ providedIn: 'root' }) +export class FileTypeDataSourceService { + dataChange = new BehaviorSubject([]); + treeData: TreeNode[] = []; + + get data(): TreeNode[] { + return this.dataChange.value; + } + + constructor() { + this.initialize(); + } + + initialize() { + this.treeData = TREE_DATA; + this.dataChange.next(TREE_DATA); + } + + public filter(filterText: string) { + let filteredTreeData; + if (filterText) { + // Filter the tree + const filter = (array: TreeNode[], text: string) => { + const getChildren = (result: any, object: any) => { + console.log(text, object.item.label, object.item.label.toLowerCase().includes(text.toLowerCase())); + if (object.item.label.toLowerCase().includes(text.toLowerCase())) { + result.push(object); + return result; + } + if (Array.isArray(object.children)) { + const children = object.children.reduce(getChildren, []); + if (children.length) result.push({ ...object, children }); + } + return result; + }; + + return array.reduce(getChildren, []); + }; + + filteredTreeData = filter(this.treeData, filterText); + } else { + // Return the initial tree + filteredTreeData = this.treeData; + } + this.dataChange.next(filteredTreeData); + } +} diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts index ce29c4b358..2ac66de0c5 100644 --- a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.spec.ts @@ -93,7 +93,7 @@ describe('NoticeOfIntentService', () => { expect( mockNoticeOfIntentSubmissionSearchViewRepository.createQueryBuilder, ).toBeCalledTimes(1); - expect(mockQuery.andWhere).toBeCalledTimes(14); + expect(mockQuery.andWhere).toBeCalledTimes(13); expect(mockQuery.where).toBeCalledTimes(1); }); diff --git a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts index 4ba8d3fbdf..312a0f8dbe 100644 --- a/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts +++ b/services/apps/alcs/src/alcs/search/notice-of-intent/notice-of-intent-advanced-search.service.ts @@ -149,8 +149,6 @@ export class NoticeOfIntentAdvancedSearchService { query = this.compileDecisionSearchQuery(searchDto, query); - query = this.compileFileTypeSearchQuery(searchDto, query); - query = this.compileDateRangeSearchQuery(searchDto, query); return query; @@ -296,41 +294,4 @@ export class NoticeOfIntentAdvancedSearchService { } return query; } - - private compileFileTypeSearchQuery(searchDto: SearchRequestDto, query) { - query = query; - - if (searchDto.applicationFileTypes.length > 0) { - // if decision is not joined yet -> join it. The join of decision happens in compileApplicationDecisionSearchQuery - if ( - searchDto.resolutionNumber === undefined && - searchDto.resolutionYear === undefined - ) { - query = this.joinDecision(query); - } - - query = query.leftJoin( - NoticeOfIntentDecisionComponent, - 'decisionComponent', - 'decisionComponent.notice_of_intent_decision_uuid = decision.uuid', - ); - - query = query.andWhere( - new Brackets((qb) => - qb - .where('noiSearch.notice_of_intent_type_code IN (:...typeCodes)', { - typeCodes: searchDto.applicationFileTypes, - }) - .orWhere( - 'decisionComponent.notice_of_intent_decision_component_type_code IN (:...typeCodes)', - { - typeCodes: searchDto.applicationFileTypes, - }, - ), - ), - ); - } - - return query; - } } diff --git a/services/apps/alcs/src/alcs/search/search.controller.spec.ts b/services/apps/alcs/src/alcs/search/search.controller.spec.ts index 3e51a64d9a..ddac32eec0 100644 --- a/services/apps/alcs/src/alcs/search/search.controller.spec.ts +++ b/services/apps/alcs/src/alcs/search/search.controller.spec.ts @@ -251,4 +251,78 @@ describe('SearchController', () => { expect(result.data).toBeDefined(); expect(result.total).toBe(0); }); + + it('should call advanced search to retrieve Applications only when application file type selected', async () => { + const mockSearchRequestDto = { + pageSize: 1, + page: 1, + sortField: '1', + sortDirection: 'ASC', + isIncludeOtherParcels: false, + applicationFileTypes: ['NFUP'], + }; + + const result = await controller.advancedSearch( + mockSearchRequestDto as SearchRequestDto, + ); + + expect( + mockApplicationAdvancedSearchService.searchApplications, + ).toBeCalledTimes(1); + expect( + mockApplicationAdvancedSearchService.searchApplications, + ).toBeCalledWith(mockSearchRequestDto); + expect(result.applications).toBeDefined(); + expect(result.totalApplications).toBe(0); + }); + + it('should call advanced search to retrieve NOIs only when NOI file type selected', async () => { + const mockSearchRequestDto = { + pageSize: 1, + page: 1, + sortField: '1', + sortDirection: 'ASC', + isIncludeOtherParcels: false, + applicationFileTypes: ['NOI'], + }; + + const result = await controller.advancedSearch( + mockSearchRequestDto as SearchRequestDto, + ); + + expect( + mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents, + ).toBeCalledTimes(1); + expect( + mockNoticeOfIntentAdvancedSearchService.searchNoticeOfIntents, + ).toBeCalledWith(mockSearchRequestDto); + expect(result.noticeOfIntents).toBeDefined(); + expect(result.totalNoticeOfIntents).toBe(0); + }); + + it('should call advanced search to retrieve Non Applications only when non application file type selected', async () => { + const mockSearchRequestDto = { + pageSize: 1, + page: 1, + sortField: '1', + sortDirection: 'ASC', + isIncludeOtherParcels: false, + applicationFileTypes: ['COV'], + }; + + const result = await controller.advancedSearch( + mockSearchRequestDto as SearchRequestDto, + ); + + expect(result.totalNoticeOfIntents).toBe(0); + + expect( + mockNonApplicationsAdvancedSearchService.searchNonApplications, + ).toBeCalledTimes(1); + expect( + mockNonApplicationsAdvancedSearchService.searchNonApplications, + ).toBeCalledWith(mockSearchRequestDto); + expect(result.nonApplications).toBeDefined(); + expect(result.totalNonApplications).toBe(0); + }); }); diff --git a/services/apps/alcs/src/alcs/search/search.controller.ts b/services/apps/alcs/src/alcs/search/search.controller.ts index b9141722a4..6221021f88 100644 --- a/services/apps/alcs/src/alcs/search/search.controller.ts +++ b/services/apps/alcs/src/alcs/search/search.controller.ts @@ -6,6 +6,7 @@ import * as config from 'config'; import { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; import { RolesGuard } from '../../common/authorization/roles-guard.service'; import { UserRoles } from '../../common/authorization/roles.decorator'; +import { APPLICATION_SUBMISSION_TYPES } from '../../portal/pdf-generation/generate-submission-document.service'; import { Application } from '../application/application.entity'; import { CARD_TYPE } from '../card/card-type/card-type.entity'; import { ApplicationTypeDto } from '../code/application-code/application-type/application-type.dto'; @@ -144,14 +145,43 @@ export class SearchController { @Post('/advanced') @UserRoles(...ROLES_ALLOWED_APPLICATIONS) async advancedSearch(@Body() searchDto: SearchRequestDto) { - const applicationSearchResult = - await this.applicationSearchService.searchApplications(searchDto); + let searchApplications = true; + let searchNoi = true; + let searchNonApplications = true; + + ({ searchApplications, searchNoi, searchNonApplications } = + this.getEntitiesTypeToSearch( + searchDto, + searchApplications, + searchNoi, + searchNonApplications, + )); + + let applicationSearchResult: AdvancedSearchResultDto< + ApplicationSubmissionSearchView[] + > | null = null; + if (searchApplications) { + applicationSearchResult = + await this.applicationSearchService.searchApplications(searchDto); + } - const noticeOfIntentSearchService = - await this.noticeOfIntentSearchService.searchNoticeOfIntents(searchDto); + let noticeOfIntentSearchService: AdvancedSearchResultDto< + NoticeOfIntentSubmissionSearchView[] + > | null = null; + if (searchNoi) { + noticeOfIntentSearchService = + await this.noticeOfIntentSearchService.searchNoticeOfIntents(searchDto); + } - const nonApplications = - await this.nonApplicationsSearchService.searchNonApplications(searchDto); + let nonApplications: AdvancedSearchResultDto< + NonApplicationSearchView[] + > | null = null; + if (searchNonApplications) { + nonApplications = + await this.nonApplicationsSearchService.searchNonApplications( + searchDto, + ); + } const mappedSearchResult = this.mapAdvancedSearchResults( applicationSearchResult, @@ -162,6 +192,32 @@ export class SearchController { return mappedSearchResult; } + private getEntitiesTypeToSearch( + searchDto: SearchRequestDto, + searchApplications: boolean, + searchNoi: boolean, + searchNonApplications: boolean, + ) { + if (searchDto.applicationFileTypes.length > 0) { + searchApplications = + searchDto.applicationFileTypes.filter((searchType) => + Object.values(APPLICATION_SUBMISSION_TYPES).includes( + APPLICATION_SUBMISSION_TYPES[ + searchType as keyof typeof APPLICATION_SUBMISSION_TYPES + ], + ), + ).length > 0; + + searchNoi = searchDto.applicationFileTypes.includes('NOI'); + + searchNonApplications = + searchDto.applicationFileTypes.filter((searchType) => + ['COV', 'PLAN', 'SRW'].includes(searchType), + ).length > 0; + } + return { searchApplications, searchNoi, searchNonApplications }; + } + @Post('/advanced/application') @UserRoles(...ROLES_ALLOWED_APPLICATIONS) async advancedSearchApplications( diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts index 6d087bfb97..9a1cc5e55a 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts @@ -31,7 +31,13 @@ import { ApplicationSubmissionService } from '../application-submission/applicat export enum APPLICATION_SUBMISSION_TYPES { NFUP = 'NFUP', TURP = 'TURP', + POFO = 'POFO', + ROSO = 'ROSO', + PFRS = 'PFRS', + NARU = 'NARU', SUBD = 'SUBD', + INCL = 'INCL', + EXCL = 'EXCL', } class PdfTemplate { From b1a227cfb3c125920b732fefde24d520a8e5072e Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 6 Sep 2023 10:57:44 -0700 Subject: [PATCH 346/954] ready for MR --- bin/migrate-oats-data/submissions/app_submissions.py | 6 ++++-- .../migrations/1693945647800-rename_nfu_placement.ts | 8 +------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index 8cbb51ba06..d6f315908b 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -237,7 +237,8 @@ def get_insert_query_for_nfu(): nfu_project_duration_amount, nfu_fill_type_description, nfu_fill_origin_description, - nfu_project_duration_unit + nfu_project_duration_unit, + nfu_total_fill_area """ unique_values = """, %(alr_area)s, %(import_fill)s, @@ -246,7 +247,8 @@ def get_insert_query_for_nfu(): %(fill_duration)s, %(fill_type)s, %(fill_origin)s, - %(fill_duration_unit)s + %(fill_duration_unit)s, + %(fill_area)s """ return get_insert_query(unique_fields,unique_values) diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts index 4da33dd170..c21373ab93 100644 --- a/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts +++ b/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts @@ -4,13 +4,7 @@ export class renameNfuPlacement1693945647800 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query( - `ALTER TABLE "alcs"."application_submission" ADD "nfu_total_fill_area" numeric(12,2)`, - ); - await queryRunner.query( - `UPDATE "alcs"."application_submission" SET "nfu_total_fill_area" = "nfu_total_fill_placement"`, - ); - await queryRunner.query( - `ALTER TABLE "alcs"."application_submission" DROP COLUMN "nfu_total_fill_placement"`, + `ALTER TABLE "alcs"."application_submission" RENAME COLUMN "nfu_total_fill_placement" TO "nfu_total_fill_area"`, ); await queryRunner.query( `COMMENT ON COLUMN "alcs"."application_submission"."nfu_total_fill_area" IS 'Area for nfu placement of fill'`, From 0b997cad6f6b31dbff2ee9aa541c9113aafabbb6 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 6 Sep 2023 11:45:08 -0700 Subject: [PATCH 347/954] Add Primary Contact and L/FNG Selection for Notifications * Also fix small bug with Parcels using the wrong service --- .../edit-submission.component.html | 20 ++- .../edit-submission.component.ts | 27 +++- .../edit-submission/edit-submission.module.ts | 4 + .../delete-parcel-dialog.component.spec.ts | 10 +- .../delete-parcel-dialog.component.ts | 6 +- .../primary-contact.component.html | 112 ++++++++++++++ .../primary-contact.component.scss | 26 ++++ .../primary-contact.component.spec.ts | 52 +++++++ .../primary-contact.component.ts | 87 +++++++++++ .../select-government.component.html | 67 ++++++++ .../select-government.component.scss | 0 .../select-government.component.spec.ts | 43 ++++++ .../select-government.component.ts | 144 ++++++++++++++++++ .../notification-submission.dto.ts | 13 +- .../notification-parcel.controller.spec.ts | 6 +- .../notification-parcel.controller.ts | 7 +- .../notification-parcel.service.spec.ts | 18 +-- .../notification-parcel.service.ts | 16 +- .../notification-submission.dto.ts | 36 +++++ .../notification-submission.entity.ts | 40 +++++ .../notification-submission.service.ts | 21 +++ ...4021090-add_notification_contact_fields.ts | 58 +++++++ 22 files changed, 759 insertions(+), 54 deletions(-) create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1694024021090-add_notification_contact_fields.ts diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html index 32d6dae0f4..b906c29f4c 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html @@ -49,11 +49,27 @@
-
+
+ + +
-
+
+ + +
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts index bd702397f0..2d718d73e8 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts @@ -4,13 +4,18 @@ import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; -import { NotificationSubmissionDetailedDto } from '../../../services/notification-submission/notification-submission.dto'; +import { + NOTIFICATION_STATUS, + NotificationSubmissionDetailedDto, +} from '../../../services/notification-submission/notification-submission.dto'; import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; import { ToastService } from '../../../services/toast/toast.service'; import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; import { scrollToElement } from '../../../shared/utils/scroll-helper'; import { ParcelDetailsComponent } from './parcels/parcel-details.component'; +import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { SelectGovernmentComponent } from './select-government/select-government.component'; export enum EditNotificationSteps { Parcel = 0, @@ -41,6 +46,8 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { @ViewChild('cdkStepper') public customStepper!: CustomStepperComponent; @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; + @ViewChild(PrimaryContactComponent) primaryContactComponent!: PrimaryContactComponent; + @ViewChild(SelectGovernmentComponent) selectGovernmentComponent!: SelectGovernmentComponent; constructor( private notificationSubmissionService: NotificationSubmissionService, @@ -129,8 +136,18 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { } break; case EditNotificationSteps.Transferees: + //DO NOTHING + break; case EditNotificationSteps.PrimaryContact: + if (this.primaryContactComponent) { + await this.primaryContactComponent.onSave(); + } + break; case EditNotificationSteps.Government: + if (this.selectGovernmentComponent) { + await this.selectGovernmentComponent.onSave(); + } + break; case EditNotificationSteps.Proposal: case EditNotificationSteps.Attachments: case EditNotificationSteps.ReviewAndSubmit: @@ -171,10 +188,10 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { this.notificationSubmission = await this.notificationSubmissionService.getByFileId(fileId); this.fileId = fileId; - // if (this.notificationSubmission?.status.code !== NOI_SUBMISSION_STATUS.IN_PROGRESS) { - // this.toastService.showErrorToast('Unable to edit Notice of Intent'); - // await this.router.navigateByUrl(`/home`); - // } + if (this.notificationSubmission?.status.code !== NOTIFICATION_STATUS.IN_PROGRESS) { + this.toastService.showErrorToast('Unable to edit Notification'); + await this.router.navigateByUrl(`/home`); + } const documents: NoticeOfIntentDocumentDto[] = []; //TODO await this.noticeOfIntentDocumentService.getByFileId(fileId); if (documents) { diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts index 9653d0b94b..6124c74f91 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts @@ -12,6 +12,8 @@ import { DeleteParcelDialogComponent } from './parcels/delete-parcel/delete-parc import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; +import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { SelectGovernmentComponent } from './select-government/select-government.component'; import { StepComponent } from './step.partial'; import { TransfereeDialogComponent } from './transferees/transferee-dialog/transferee-dialog.component'; import { TransfereesComponent } from './transferees/transferees.component'; @@ -38,6 +40,8 @@ const routes: Routes = [ DeleteParcelDialogComponent, TransfereesComponent, TransfereeDialogComponent, + PrimaryContactComponent, + SelectGovernmentComponent, ], imports: [ CommonModule, diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts index 59d4818b54..adb0afffb3 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.spec.ts @@ -3,18 +3,18 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NotificationParcelService } from '../../../../../services/notification-parcel/notification-parcel.service'; import { DeleteParcelDialogComponent } from './delete-parcel-dialog.component'; describe('DeleteParcelDialogComponent', () => { let component: DeleteParcelDialogComponent; let fixture: ComponentFixture; let mockHttpClient: DeepMocked; - let mockNoiParcelService: DeepMocked; + let mockNotificationParcelService: DeepMocked; beforeEach(async () => { mockHttpClient = createMock(); - mockNoiParcelService = createMock(); + mockNotificationParcelService = createMock(); await TestBed.configureTestingModule({ declarations: [DeleteParcelDialogComponent], @@ -24,8 +24,8 @@ describe('DeleteParcelDialogComponent', () => { useValue: mockHttpClient, }, { - provide: NoticeOfIntentParcelService, - useValue: mockNoiParcelService, + provide: NotificationParcelService, + useValue: mockNotificationParcelService, }, { provide: MatDialogRef, diff --git a/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts index 7445939f2f..684354f6ad 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/parcels/delete-parcel/delete-parcel-dialog.component.ts @@ -1,6 +1,6 @@ import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { NoticeOfIntentParcelService } from '../../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NotificationParcelService } from '../../../../../services/notification-parcel/notification-parcel.service'; export enum NotificationParcelDeleteStepsEnum { warning = 0, @@ -22,7 +22,7 @@ export class DeleteParcelDialogComponent { confirmationStep = NotificationParcelDeleteStepsEnum.confirmation; constructor( - private noticeOfIntentParcelService: NoticeOfIntentParcelService, + private notificationParcelService: NotificationParcelService, private dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: DeleteParcelDialogComponent ) { @@ -43,7 +43,7 @@ export class DeleteParcelDialogComponent { } async onDelete() { - const result = await this.noticeOfIntentParcelService.deleteMany([this.parcelUuid]); + const result = await this.notificationParcelService.deleteMany([this.parcelUuid]); if (result) { this.onCancel(true); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.html b/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.html new file mode 100644 index 0000000000..c91737b379 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.html @@ -0,0 +1,112 @@ +
+

Primary Contact

+

+ Enter the contact information for the person who is submitting the SRW package to the Land Title Survey Authority. +

+

*All fields are required unless stated optional.

+
+
+
+

Primary Contact Information

+
+
+
+ + + + +
+ warning +
This field is required
+
+
+
+ + + + +
+ warning +
This field is required
+
+
+
+ + + + +
+ warning +
This field is required
+
+
+
+ + + + +
+ warning +
This field is required
+
Invalid format
+
+
+
+ + + + +
+ warning +
This field is required
+
Invalid format
+
+
+
+ + + + +
+ warning +
This field is required
+
Invalid format
+
Emails do not match
+
+
+
+
+ + The ALC's automatically generated notification response, needed to complete the LTSA SRW package, will be sent to + the Primary Contact email address provided above. + +
+
+
+ +
+ + +
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.scss new file mode 100644 index 0000000000..37f0ad1476 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.scss @@ -0,0 +1,26 @@ +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; + +section { + margin-bottom: rem(32); +} + +.form { + .form-row { + grid-template-columns: 1fr; + } + + .form-row .full-row { + grid-column: 1/2; + } + + @media screen and (min-width: $tabletBreakpoint) { + .form-row { + grid-template-columns: 1fr 1fr; + } + + .form-row .full-row { + grid-column: 1/3; + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.spec.ts new file mode 100644 index 0000000000..011af9e170 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.spec.ts @@ -0,0 +1,52 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { ApplicationDocumentDto } from '../../../../services/application-document/application-document.dto'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { ApplicationOwnerService } from '../../../../services/application-owner/application-owner.service'; +import { ApplicationSubmissionDetailedDto } from '../../../../services/application-submission/application-submission.dto'; +import { ApplicationSubmissionService } from '../../../../services/application-submission/application-submission.service'; +import { UserDto } from '../../../../services/authentication/authentication.dto'; +import { AuthenticationService } from '../../../../services/authentication/authentication.service'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; + +import { PrimaryContactComponent } from './primary-contact.component'; + +describe('PrimaryContactComponent', () => { + let component: PrimaryContactComponent; + let fixture: ComponentFixture; + let mockNotificationSubmissionService: DeepMocked; + + let applicationDocumentPipe = new BehaviorSubject([]); + + beforeEach(async () => { + mockNotificationSubmissionService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: NotificationSubmissionService, + useValue: mockNotificationSubmissionService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + declarations: [PrimaryContactComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PrimaryContactComponent); + component = fixture.componentInstance; + component.$notificationSubmission = new BehaviorSubject(undefined); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.ts new file mode 100644 index 0000000000..751d7250de --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/primary-contact/primary-contact.component.ts @@ -0,0 +1,87 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { takeUntil } from 'rxjs'; +import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; +import { EditNotificationSteps } from '../edit-submission.component'; +import { StepComponent } from '../step.partial'; + +@Component({ + selector: 'app-primary-contact', + templateUrl: './primary-contact.component.html', + styleUrls: ['./primary-contact.component.scss'], +}) +export class PrimaryContactComponent extends StepComponent implements OnInit, OnDestroy { + currentStep = EditNotificationSteps.PrimaryContact; + + firstName = new FormControl('', [Validators.required]); + lastName = new FormControl('', [Validators.required]); + organizationName = new FormControl(''); + phoneNumber = new FormControl('', [Validators.required]); + email = new FormControl('', [Validators.required, Validators.email]); + confirmEmail = new FormControl('', [Validators.required, Validators.email]); + + form = new FormGroup({ + firstName: this.firstName, + lastName: this.lastName, + organizationName: this.organizationName, + phoneNumber: this.phoneNumber, + email: this.email, + confirmEmail: this.confirmEmail, + }); + + private submissionUuid = ''; + + constructor(private notificationSubmissionService: NotificationSubmissionService) { + super(); + } + + ngOnInit(): void { + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + if (submission) { + this.submissionUuid = submission.uuid; + + this.form.patchValue({ + firstName: submission.contactFirstName, + lastName: submission.contactLastName, + organizationName: submission.contactOrganization, + phoneNumber: submission.contactPhone, + email: submission.contactEmail, + confirmEmail: submission.contactEmail, + }); + } + }); + } + + async onSave() { + await this.save(); + } + + protected async save() { + if (this.form.dirty) { + const confirmEmail = this.confirmEmail.value; + const email = this.email.value; + + const updated = await this.notificationSubmissionService.updatePending(this.submissionUuid, { + contactFirstName: this.firstName.value, + contactLastName: this.lastName.value, + contactOrganization: this.organizationName.value, + contactPhone: this.phoneNumber.value, + contactEmail: confirmEmail === email ? email : null, + }); + this.$notificationSubmission.next(updated); + } + } + + onChangeConfirmationEmail() { + const confirmEmail = this.confirmEmail.value; + const email = this.email.value; + + if (confirmEmail !== email) { + this.confirmEmail.setErrors({ + notMatch: 'true', + }); + } else { + this.confirmEmail.updateValueAndValidity(); + } + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.html b/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.html new file mode 100644 index 0000000000..f0d962adb0 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.html @@ -0,0 +1,67 @@ +
+

Government

+

+ Please indicate the local government in which the parcel(s) under Notification of SRW is located. If the property is + located within the Islands Trust, please select 'Islands Trust' and not a specific island. +

+

*All fields are required unless stated optional.

+
+
+
+
+ + + + + + {{ option.name }} + + + +
+ warning +
This field is required
+
+
+
+
+ + This Local/First Nation Government has not yet been set up with the ALC Portal to receive applications. To submit, you + will need to contact the ALC directly:  ALC.Portal@gov.bc.ca / 236-468-3342 + + + You're logged in with a Business BCeID that is associated with the government selected above. You will have the + opportunity to complete the local or first nation government review form immediately after this application is + submitted. + +

+ Please Note: If your Local or First Nation Government is not listed, please contact the ALC directly. + ALC.Portal@gov.bc.ca / 236-468-3342 +

+
+ +
+ + +
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.spec.ts new file mode 100644 index 0000000000..29b4e16495 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.spec.ts @@ -0,0 +1,43 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatAutocomplete } from '@angular/material/autocomplete'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { CodeService } from '../../../../services/code/code.service'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; + +import { SelectGovernmentComponent } from './select-government.component'; + +describe('SelectGovernmentComponent', () => { + let component: SelectGovernmentComponent; + let fixture: ComponentFixture; + let mockCodeService: DeepMocked; + let mockNotificationSubmissionService: DeepMocked; + + beforeEach(async () => { + mockCodeService = createMock(); + mockNotificationSubmissionService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [SelectGovernmentComponent, MatAutocomplete], + providers: [ + { + provide: CodeService, + useValue: mockCodeService, + }, + { provide: NotificationSubmissionService, useValue: mockNotificationSubmissionService }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SelectGovernmentComponent); + component = fixture.componentInstance; + component.$notificationSubmission = new BehaviorSubject(undefined); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.ts new file mode 100644 index 0000000000..57e9d676e9 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/select-government/select-government.component.ts @@ -0,0 +1,144 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { map, Observable, startWith, takeUntil } from 'rxjs'; +import { LocalGovernmentDto } from '../../../../services/code/code.dto'; +import { CodeService } from '../../../../services/code/code.service'; +import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; +import { EditNotificationSteps } from '../edit-submission.component'; +import { StepComponent } from '../step.partial'; + +@Component({ + selector: 'app-select-government', + templateUrl: './select-government.component.html', + styleUrls: ['./select-government.component.scss'], +}) +export class SelectGovernmentComponent extends StepComponent implements OnInit, OnDestroy { + currentStep = EditNotificationSteps.Government; + + private fileId = ''; + private submissionUuid = ''; + + localGovernment = new FormControl('', [Validators.required]); + showWarning = false; + selectedOwnGovernment = false; + selectGovernmentUuid = ''; + localGovernments: LocalGovernmentDto[] = []; + filteredLocalGovernments!: Observable; + isDirty = false; + + form = new FormGroup({ + localGovernment: this.localGovernment, + }); + + constructor(private codeService: CodeService, private notificationSubmissionService: NotificationSubmissionService) { + super(); + } + + ngOnInit(): void { + this.loadGovernments(); + + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + if (submission) { + this.selectGovernmentUuid = submission.localGovernmentUuid; + this.fileId = submission.fileNumber; + this.submissionUuid = submission.uuid; + this.populateLocalGovernment(submission.localGovernmentUuid); + } + }); + + this.filteredLocalGovernments = this.localGovernment.valueChanges.pipe( + startWith(''), + map((value) => this.filter(value || '')) + ); + + if (this.showErrors) { + this.form.markAllAsTouched(); + } + } + + onChange($event: MatAutocompleteSelectedEvent) { + this.isDirty = true; + const localGovernmentName = $event.option.value; + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + if (localGovernment) { + this.showWarning = !localGovernment.hasGuid; + + this.localGovernment.setValue(localGovernment.name); + if (localGovernment.hasGuid) { + this.localGovernment.setErrors(null); + } else { + this.localGovernment.setErrors({ invalid: localGovernment.hasGuid }); + } + + this.selectedOwnGovernment = localGovernment.matchesUserGuid; + } + } + } + + onBlur() { + //Blur will fire before onChange above, so use setTimeout to delay it + setTimeout(() => { + const localGovernmentName = this.localGovernment.getRawValue(); + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + if (!localGovernment) { + this.localGovernment.setValue(null); + console.log('Clearing Local Government field'); + } + } + }, 500); + } + + async onSave() { + await this.save(); + } + + private async save() { + if (this.isDirty) { + const localGovernmentName = this.localGovernment.getRawValue(); + if (localGovernmentName) { + const localGovernment = this.localGovernments.find((lg) => lg.name == localGovernmentName); + + if (localGovernment) { + const res = await this.notificationSubmissionService.updatePending(this.submissionUuid, { + localGovernmentUuid: localGovernment.uuid, + }); + this.$notificationSubmission.next(res); + } + } + this.isDirty = false; + } + } + + private filter(value: string): LocalGovernmentDto[] { + if (this.localGovernments) { + const filterValue = value.toLowerCase(); + return this.localGovernments.filter((localGovernment) => + localGovernment.name.toLowerCase().includes(filterValue) + ); + } + return []; + } + + private async loadGovernments() { + const codes = await this.codeService.loadCodes(); + this.localGovernments = codes.localGovernments.sort((a, b) => (a.name > b.name ? 1 : -1)); + if (this.selectGovernmentUuid) { + this.populateLocalGovernment(this.selectGovernmentUuid); + } + } + + private populateLocalGovernment(governmentUuid: string) { + const lg = this.localGovernments.find((lg) => lg.uuid === governmentUuid); + if (lg) { + this.localGovernment.patchValue(lg.name); + this.showWarning = !lg.hasGuid; + if (!lg.hasGuid) { + this.localGovernment.setErrors({ invalid: true }); + } + this.selectedOwnGovernment = lg.matchesUserGuid; + } + } +} diff --git a/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts b/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts index 70dcc62f46..07ed0ad669 100644 --- a/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts +++ b/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts @@ -1,5 +1,4 @@ import { BaseCodeDto } from '../../shared/dto/base.dto'; -import { NOI_SUBMISSION_STATUS } from '../notice-of-intent-submission/notice-of-intent-submission.dto'; import { NotificationTransfereeDto } from '../notification-transferee/notification-transferee.dto'; export enum NOTIFICATION_STATUS { @@ -10,7 +9,7 @@ export enum NOTIFICATION_STATUS { } export interface NotificationSubmissionStatusDto extends BaseCodeDto { - code: NOI_SUBMISSION_STATUS; + code: NOTIFICATION_STATUS; portalBackgroundColor: string; portalColor: string; } @@ -36,6 +35,11 @@ export interface NotificationSubmissionDto { owners: NotificationTransfereeDto[]; canEdit: boolean; canView: boolean; + contactFirstName: string | null; + contactLastName: string | null; + contactOrganization: string | null; + contactPhone: string | null; + contactEmail: string | null; } export interface NotificationSubmissionDetailedDto extends NotificationSubmissionDto { @@ -46,4 +50,9 @@ export interface NotificationSubmissionUpdateDto { applicant?: string | null; purpose?: string | null; localGovernmentUuid?: string | null; + contactFirstName?: string | null; + contactLastName?: string | null; + contactOrganization?: string | null; + contactPhone?: string | null; + contactEmail?: string | null; } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts index 9c336f99aa..5415df50ea 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts @@ -146,11 +146,7 @@ describe('NotificationParcelController', () => { const fakeUuid = 'fake_uuid'; mockNotificationParcelService.deleteMany.mockResolvedValue([]); - const result = await controller.delete([fakeUuid], { - user: { - entity: new User(), - }, - }); + const result = await controller.delete([fakeUuid]); expect(mockNotificationParcelService.deleteMany).toBeCalledTimes(1); expect(result).toBeDefined(); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts index 7174dac2bb..b71ae3cedd 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts @@ -81,11 +81,8 @@ export class NotificationParcelController { } @Delete() - async delete(@Body() uuids: string[], @Req() req) { - const deletedParcels = await this.parcelService.deleteMany( - uuids, - req.user.entity, - ); + async delete(@Body() uuids: string[]) { + const deletedParcels = await this.parcelService.deleteMany(uuids); return this.mapper.mapArrayAsync( deletedParcels, NotificationParcel, diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts index 0a031358a3..4eafffd096 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.spec.ts @@ -3,8 +3,6 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; -import { User } from '../../../user/user.entity'; -import { NotificationTransfereeService } from '../notification-transferee/notification-transferee.service'; import { NotificationParcelUpdateDto } from './notification-parcel.dto'; import { NotificationParcel } from './notification-parcel.entity'; import { NotificationParcelService } from './notification-parcel.service'; @@ -12,7 +10,6 @@ import { NotificationParcelService } from './notification-parcel.service'; describe('NotificationParcelService', () => { let service: NotificationParcelService; let mockParcelRepo: DeepMocked>; - let mockOwnerService: DeepMocked; const mockFileNumber = 'mock_applicationFileNumber'; const mockUuid = 'mock_uuid'; @@ -29,7 +26,6 @@ describe('NotificationParcelService', () => { beforeEach(async () => { mockParcelRepo = createMock(); - mockOwnerService = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ NotificationParcelService, @@ -37,10 +33,6 @@ describe('NotificationParcelService', () => { provide: getRepositoryToken(NotificationParcel), useValue: mockParcelRepo, }, - { - provide: NotificationTransfereeService, - useValue: mockOwnerService, - }, ], }).compile(); @@ -147,9 +139,8 @@ describe('NotificationParcelService', () => { it('should successfully delete a parcel and update applicant', async () => { mockParcelRepo.find.mockResolvedValue([mockNOIParcel]); mockParcelRepo.remove.mockResolvedValue(new NotificationParcel()); - mockOwnerService.updateSubmissionApplicant.mockResolvedValue(); - const result = await service.deleteMany([mockUuid], new User()); + const result = await service.deleteMany([mockUuid]); expect(result).toBeDefined(); expect(mockParcelRepo.find).toBeCalledTimes(1); @@ -158,7 +149,6 @@ describe('NotificationParcelService', () => { }); expect(mockParcelRepo.remove).toBeCalledWith([mockNOIParcel]); expect(mockParcelRepo.remove).toBeCalledTimes(1); - expect(mockOwnerService.updateSubmissionApplicant).toHaveBeenCalledTimes(1); }); it('should not call remove if the parcel does not exist', async () => { @@ -169,9 +159,9 @@ describe('NotificationParcelService', () => { mockParcelRepo.find.mockResolvedValue([]); mockParcelRepo.remove.mockResolvedValue(new NotificationParcel()); - await expect( - service.deleteMany([mockUuid], new User()), - ).rejects.toMatchObject(exception); + await expect(service.deleteMany([mockUuid])).rejects.toMatchObject( + exception, + ); expect(mockParcelRepo.find).toBeCalledTimes(1); expect(mockParcelRepo.find).toBeCalledWith({ where: { uuid: In([mockUuid]) }, diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts index 28f3dacc19..9dacb9fa06 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts @@ -1,9 +1,7 @@ import { ServiceValidationException } from '@app/common/exceptions/base.exception'; -import { forwardRef, Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; -import { User } from '../../../user/user.entity'; -import { NotificationTransfereeService } from '../notification-transferee/notification-transferee.service'; import { NotificationParcelUpdateDto } from './notification-parcel.dto'; import { NotificationParcel } from './notification-parcel.entity'; @@ -12,8 +10,6 @@ export class NotificationParcelService { constructor( @InjectRepository(NotificationParcel) private parcelRepository: Repository, - @Inject(forwardRef(() => NotificationTransfereeService)) - private noticeOfIntentOwnerService: NotificationTransfereeService, ) {} async fetchByFileId(fileId: string) { @@ -65,7 +61,7 @@ export class NotificationParcelService { return await this.parcelRepository.save(updatedParcels); } - async deleteMany(uuids: string[], user: User) { + async deleteMany(uuids: string[]) { const parcels = await this.parcelRepository.find({ where: { uuid: In(uuids) }, }); @@ -76,12 +72,6 @@ export class NotificationParcelService { ); } - const result = await this.parcelRepository.remove(parcels); - await this.noticeOfIntentOwnerService.updateSubmissionApplicant( - parcels[0].notificationSubmissionUuid, - user, - ); - - return result; + return await this.parcelRepository.remove(parcels); } } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts index 634a4b8fa7..6444eb733b 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts @@ -1,5 +1,6 @@ import { AutoMap } from '@automapper/classes'; import { IsOptional, IsString, IsUUID, MaxLength } from 'class-validator'; +import { Column } from 'typeorm'; import { NoticeOfIntentStatusDto } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NotificationTransfereeDto } from './notification-transferee/notification-transferee.dto'; @@ -21,6 +22,21 @@ export class NotificationSubmissionDto { @AutoMap() applicant: string; + @AutoMap(() => String) + contactFirstName: string | null; + + @AutoMap(() => String) + contactLastName: string | null; + + @AutoMap(() => String) + contactOrganization: string | null; + + @AutoMap(() => String) + contactPhone: string | null; + + @AutoMap(() => String) + contactEmail: string | null; + @AutoMap() localGovernmentUuid: string; @@ -56,4 +72,24 @@ export class NotificationSubmissionUpdateDto { @IsUUID() @IsOptional() localGovernmentUuid?: string; + + @IsString() + @IsOptional() + contactFirstName?: string | null; + + @IsString() + @IsOptional() + contactLastName?: string | null; + + @IsString() + @IsOptional() + contactOrganization?: string | null; + + @IsString() + @IsOptional() + contactPhone?: string | null; + + @IsString() + @IsOptional() + contactEmail?: string | null; } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts index cbfec5a6f2..7f249ea887 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts @@ -69,6 +69,46 @@ export class NotificationSubmission extends Base { }) typeCode: string; + @AutoMap(() => String) + @Column({ + nullable: true, + type: 'varchar', + comment: 'Primary Contacts First Name', + }) + contactFirstName: string | null; + + @AutoMap(() => String) + @Column({ + nullable: true, + type: 'varchar', + comment: 'Primary Contacts Last Name', + }) + contactLastName: string | null; + + @AutoMap(() => String) + @Column({ + nullable: true, + type: 'varchar', + comment: 'Primary Contacts Organization Name', + }) + contactOrganization: string | null; + + @AutoMap(() => String) + @Column({ + nullable: true, + type: 'varchar', + comment: 'Primary Contacts Phone', + }) + contactPhone: string | null; + + @AutoMap(() => String) + @Column({ + nullable: true, + type: 'varchar', + comment: 'Primary Contacts Email', + }) + contactEmail: string | null; + @AutoMap(() => Notification) @ManyToOne(() => Notification) @JoinColumn({ diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts index 75e7979baf..afc142adbd 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts @@ -2,6 +2,7 @@ import { BaseServiceException } from '@app/common/exceptions/base.exception'; import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { Injectable, Logger } from '@nestjs/common'; +import { filterMiddleware } from '@nestjs/core/middleware/utils'; import { InjectRepository } from '@nestjs/typeorm'; import { FindOptionsRelations, @@ -96,6 +97,26 @@ export class NotificationSubmissionService { notificationSubmission.purpose, ); notificationSubmission.localGovernmentUuid = updateDto.localGovernmentUuid; + notificationSubmission.contactFirstName = filterUndefined( + updateDto.contactFirstName, + notificationSubmission.contactFirstName, + ); + notificationSubmission.contactLastName = filterUndefined( + updateDto.contactLastName, + notificationSubmission.contactLastName, + ); + notificationSubmission.contactOrganization = filterUndefined( + updateDto.contactOrganization, + notificationSubmission.contactOrganization, + ); + notificationSubmission.contactPhone = filterUndefined( + updateDto.contactPhone, + notificationSubmission.contactPhone, + ); + notificationSubmission.contactEmail = filterUndefined( + updateDto.contactEmail, + notificationSubmission.contactEmail, + ); await this.notificationSubmissionRepository.save(notificationSubmission); diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1694024021090-add_notification_contact_fields.ts b/services/apps/alcs/src/providers/typeorm/migrations/1694024021090-add_notification_contact_fields.ts new file mode 100644 index 0000000000..493c636748 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1694024021090-add_notification_contact_fields.ts @@ -0,0 +1,58 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNotificationContactFields1694024021090 + implements MigrationInterface +{ + name = 'addNotificationContactFields1694024021090'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD "contact_first_name" character varying`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notification_submission"."contact_first_name" IS 'Primary Contacts First Name'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD "contact_last_name" character varying`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notification_submission"."contact_last_name" IS 'Primary Contacts Last Name'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD "contact_organization" character varying`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notification_submission"."contact_organization" IS 'Primary Contacts Organization Name'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD "contact_phone" character varying`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notification_submission"."contact_phone" IS 'Primary Contacts Phone'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD "contact_email" character varying`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notification_submission"."contact_email" IS 'Primary Contacts Email'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP COLUMN "contact_email"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP COLUMN "contact_phone"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP COLUMN "contact_organization"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP COLUMN "contact_last_name"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP COLUMN "contact_first_name"`, + ); + } +} From 37af5d5866b9511a82c1e8fdb4d4fac4411ecdf0 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:55:26 -0700 Subject: [PATCH 348/954] add search rules, ui adjustment (#946) --- .../application-search-table.component.html | 2 +- .../file-type-filter-drop-down.component.html | 2 +- .../file-type-filter-drop-down.component.scss | 8 ++- ...on-application-search-table.component.html | 2 +- ...tice-of-intent-search-table.component.html | 2 +- .../app/features/search/search.component.html | 27 ++++++-- .../app/features/search/search.component.scss | 35 ++++++++--- .../app/features/search/search.component.ts | 34 +++++++---- alcs-frontend/src/styles.scss | 3 + alcs-frontend/src/styles/autocomplete.scss | 3 + .../alcs/src/alcs/search/search.controller.ts | 61 +++++++++++-------- .../apps/alcs/src/utils/string-helper.spec.ts | 24 ++++++++ services/apps/alcs/src/utils/string-helper.ts | 3 + 13 files changed, 151 insertions(+), 55 deletions(-) create mode 100644 alcs-frontend/src/styles/autocomplete.scss create mode 100644 services/apps/alcs/src/utils/string-helper.spec.ts create mode 100644 services/apps/alcs/src/utils/string-helper.ts diff --git a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.html b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.html index bd7ef5c617..cefaa4aebd 100644 --- a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.html +++ b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.html @@ -59,7 +59,7 @@ -
No Search results.
+
No applications found.
Please adjust criteria and try again.
diff --git a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.html b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.html index ae960ea80f..882625db80 100644 --- a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.html +++ b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.html @@ -8,7 +8,7 @@ [matAutocomplete]="auto" [formControl]="componentTypeControl" /> - + Please select an item from below diff --git a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.scss b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.scss index 39864d8e44..cd3259d384 100644 --- a/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.scss +++ b/alcs-frontend/src/app/features/search/file-type-filter-drop-down/file-type-filter-drop-down.component.scss @@ -1,3 +1,7 @@ .file-type-input { - width: 100%; -} \ No newline at end of file + width: 100%; +} + +.tall-autocomplete { + max-height: 600px !important; +} diff --git a/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.html b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.html index db05dcf09b..8e702f9959 100644 --- a/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.html +++ b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.html @@ -42,7 +42,7 @@ -
No Search results.
+
No non-applications found.
Please adjust criteria and try again.
diff --git a/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.html b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.html index bd7ef5c617..18bb07fbd3 100644 --- a/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.html +++ b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.html @@ -59,7 +59,7 @@ -
No Search results.
+
No notice of intent found.
Please adjust criteria and try again.
diff --git a/alcs-frontend/src/app/features/search/search.component.html b/alcs-frontend/src/app/features/search/search.component.html index 5004e5b341..f4078570f9 100644 --- a/alcs-frontend/src/app/features/search/search.component.html +++ b/alcs-frontend/src/app/features/search/search.component.html @@ -18,24 +18,35 @@
Provide one or more of the following criteria:
Name - +
Search by Primary Contact, Parcel Owner, Organization, Ministry or Department
+
+ warning +
Enter 3 or more characters to search by name
+
- + PID - + + Civic Address
+
+
+ warning +
PID must be 9 digits including leading zeroes
+
+
@@ -223,7 +234,15 @@
Provide one or more of the following criteria:
- +
diff --git a/alcs-frontend/src/app/features/search/search.component.scss b/alcs-frontend/src/app/features/search/search.component.scss index a57495575d..bf1a900ba6 100644 --- a/alcs-frontend/src/app/features/search/search.component.scss +++ b/alcs-frontend/src/app/features/search/search.component.scss @@ -39,14 +39,12 @@ transform: scale(1); } - tr.no-data { - border: none; - background-color: colors.$grey-light; - align-items: center; - justify-content: center; - cursor: auto; - box-shadow: none !important; - height: 32px; + tr.no-data td { + text-align: start; + + > div:first-child { + margin-top: 28px; + } } } @@ -151,4 +149,25 @@ margin-top: 60px; margin-bottom: 60px; } + + .field-error { + color: colors.$error-color; + font-size: 15px; + font-weight: 700; + display: flex; + align-items: center; + margin-top: 4px; + + div { + color: colors.$error-color; + } + } + + .btn-controls .search-btn span { + color: #fff !important; + } + + .mat-mdc-select-placeholder { + color: rgba(0, 0, 0, 0.6); + } } diff --git a/alcs-frontend/src/app/features/search/search.component.ts b/alcs-frontend/src/app/features/search/search.component.ts index 7615895dd5..0d904a694b 100644 --- a/alcs-frontend/src/app/features/search/search.component.ts +++ b/alcs-frontend/src/app/features/search/search.component.ts @@ -63,10 +63,12 @@ export class SearchComponent implements OnInit, OnDestroy { localGovernmentControl = new FormControl(undefined); portalStatusControl = new FormControl(undefined); componentTypeControl = new FormControl(undefined); + pidControl = new FormControl(undefined); + nameControl = new FormControl(undefined); searchForm = new FormGroup({ fileNumber: new FormControl(undefined), - name: new FormControl(undefined), - pid: new FormControl(undefined), + name: this.nameControl, + pid: this.pidControl, civicAddress: new FormControl(undefined), isIncludeOtherParcels: new FormControl(false), resolutionNumber: new FormControl(undefined), @@ -87,6 +89,8 @@ export class SearchComponent implements OnInit, OnDestroy { regions: ApplicationRegionDto[] = []; statuses: ApplicationStatusDto[] = []; + formEmpty = true; + constructor( private searchService: SearchService, private activatedRoute: ActivatedRoute, @@ -123,6 +127,18 @@ export class SearchComponent implements OnInit, OnDestroy { .then((result) => this.mapSearchResults(result)); } }); + + this.searchForm.valueChanges.subscribe(() => { + let isEmpty = true; + for (let key in this.searchForm.controls) { + let value = this.searchForm.controls[key as keyof typeof this.searchForm.controls].value; + if (value && !(Array.isArray(value) && value.length === 0)) { + isEmpty = false; + break; + } + } + this.formEmpty = isEmpty; + }); } private setup() { @@ -148,7 +164,11 @@ export class SearchComponent implements OnInit, OnDestroy { } async onSubmit() { - await this.onSearch(); + const searchParams = this.getSearchParams(); + const result = await this.searchService.advancedSearchFetch(searchParams); + this.mapSearchResults(result); + + this.setActiveTab(); } expandSearchClicked() { @@ -184,14 +204,6 @@ export class SearchComponent implements OnInit, OnDestroy { this.fileTypeFilterDropDownComponent.reset(); } - async onSearch() { - const searchParams = this.getSearchParams(); - const result = await this.searchService.advancedSearchFetch(searchParams); - this.mapSearchResults(result); - - this.setActiveTab(); - } - getSearchParams(): SearchRequestDto { const resolutionNumberString = this.formatStringSearchParam(this.searchForm.controls.resolutionNumber.value); return { diff --git a/alcs-frontend/src/styles.scss b/alcs-frontend/src/styles.scss index a7059bdbbf..1f64919524 100644 --- a/alcs-frontend/src/styles.scss +++ b/alcs-frontend/src/styles.scss @@ -10,6 +10,9 @@ @use 'styles/buttons'; @use 'styles/ngselect'; +// mat-autocomplete +@use 'styles/autocomplete'; + // ng-select @use '@ng-select/ng-select/themes/material.theme.css'; diff --git a/alcs-frontend/src/styles/autocomplete.scss b/alcs-frontend/src/styles/autocomplete.scss new file mode 100644 index 0000000000..e52fc2e876 --- /dev/null +++ b/alcs-frontend/src/styles/autocomplete.scss @@ -0,0 +1,3 @@ +.tall-autocomplete { + max-height: 600px !important; +} diff --git a/services/apps/alcs/src/alcs/search/search.controller.ts b/services/apps/alcs/src/alcs/search/search.controller.ts index 6221021f88..f6af69df8b 100644 --- a/services/apps/alcs/src/alcs/search/search.controller.ts +++ b/services/apps/alcs/src/alcs/search/search.controller.ts @@ -7,6 +7,7 @@ import { ROLES_ALLOWED_APPLICATIONS } from '../../common/authorization/roles'; import { RolesGuard } from '../../common/authorization/roles-guard.service'; import { UserRoles } from '../../common/authorization/roles.decorator'; import { APPLICATION_SUBMISSION_TYPES } from '../../portal/pdf-generation/generate-submission-document.service'; +import { isStringSetAndNotEmpty } from '../../utils/string-helper'; import { Application } from '../application/application.entity'; import { CARD_TYPE } from '../card/card-type/card-type.entity'; import { ApplicationTypeDto } from '../code/application-code/application-type/application-type.dto'; @@ -192,32 +193,6 @@ export class SearchController { return mappedSearchResult; } - private getEntitiesTypeToSearch( - searchDto: SearchRequestDto, - searchApplications: boolean, - searchNoi: boolean, - searchNonApplications: boolean, - ) { - if (searchDto.applicationFileTypes.length > 0) { - searchApplications = - searchDto.applicationFileTypes.filter((searchType) => - Object.values(APPLICATION_SUBMISSION_TYPES).includes( - APPLICATION_SUBMISSION_TYPES[ - searchType as keyof typeof APPLICATION_SUBMISSION_TYPES - ], - ), - ).length > 0; - - searchNoi = searchDto.applicationFileTypes.includes('NOI'); - - searchNonApplications = - searchDto.applicationFileTypes.filter((searchType) => - ['COV', 'PLAN', 'SRW'].includes(searchType), - ).length > 0; - } - return { searchApplications, searchNoi, searchNonApplications }; - } - @Post('/advanced/application') @UserRoles(...ROLES_ALLOWED_APPLICATIONS) async advancedSearchApplications( @@ -279,6 +254,40 @@ export class SearchController { }; } + private getEntitiesTypeToSearch( + searchDto: SearchRequestDto, + searchApplications: boolean, + searchNoi: boolean, + searchNonApplications: boolean, + ) { + if (searchDto.applicationFileTypes.length > 0) { + searchApplications = + searchDto.applicationFileTypes.filter((searchType) => + Object.values(APPLICATION_SUBMISSION_TYPES).includes( + APPLICATION_SUBMISSION_TYPES[ + searchType as keyof typeof APPLICATION_SUBMISSION_TYPES + ], + ), + ).length > 0; + + searchNoi = searchDto.applicationFileTypes.includes('NOI'); + + searchNonApplications = + searchDto.applicationFileTypes.filter((searchType) => + ['COV', 'PLAN', 'SRW'].includes(searchType), + ).length > 0; + } + + searchNonApplications = + searchNonApplications || + isStringSetAndNotEmpty(searchDto.fileNumber) || + isStringSetAndNotEmpty(searchDto.governmentName) || + isStringSetAndNotEmpty(searchDto.regionCode) || + isStringSetAndNotEmpty(searchDto.name); + + return { searchApplications, searchNoi, searchNonApplications }; + } + private mapAdvancedSearchResults( applications: AdvancedSearchResultDto< ApplicationSubmissionSearchView[] diff --git a/services/apps/alcs/src/utils/string-helper.spec.ts b/services/apps/alcs/src/utils/string-helper.spec.ts new file mode 100644 index 0000000000..1585bd17df --- /dev/null +++ b/services/apps/alcs/src/utils/string-helper.spec.ts @@ -0,0 +1,24 @@ +import { expect, test } from '@jest/globals'; +import { isStringSetAndNotEmpty } from './string-helper'; + +describe('isStringSetAndNotEmpty', () => { + test('should return false if value is undefined', () => { + expect(isStringSetAndNotEmpty(undefined)).toBeFalsy(); + }); + + test('should return false if value is null', () => { + expect(isStringSetAndNotEmpty(null)).toBeFalsy(); + }); + + test('should return false if value is empty string', () => { + expect(isStringSetAndNotEmpty('')).toBeFalsy(); + }); + + test('should return false if value is string of all spaces', () => { + expect(isStringSetAndNotEmpty(' ')).toBeFalsy(); + }); + + test('should return true if value is non-empty string', () => { + expect(isStringSetAndNotEmpty('Hello')).toBeTruthy(); + }); +}); diff --git a/services/apps/alcs/src/utils/string-helper.ts b/services/apps/alcs/src/utils/string-helper.ts new file mode 100644 index 0000000000..46a8555afb --- /dev/null +++ b/services/apps/alcs/src/utils/string-helper.ts @@ -0,0 +1,3 @@ +export const isStringSetAndNotEmpty = (value: string | undefined | null) => { + return value !== undefined && value !== null && value.trim() !== ''; +}; From b6e43b36e0aa0c40e5b567ba663d17dca1ee22cf Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 6 Sep 2023 13:25:56 -0700 Subject: [PATCH 349/954] removed unused code & added comments --- bin/migrate-oats-data/submissions/app_submissions.py | 6 +++++- bin/migrate-oats-data/submissions/submap/soil_elements.py | 2 -- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/migrate-oats-data/submissions/app_submissions.py b/bin/migrate-oats-data/submissions/app_submissions.py index d6f315908b..585206a424 100644 --- a/bin/migrate-oats-data/submissions/app_submissions.py +++ b/bin/migrate-oats-data/submissions/app_submissions.py @@ -110,6 +110,7 @@ def insert_app_sub_records(conn, batch_size, cursor, rows, direction_data, subdi rows (list): Rows of data to insert in the database. direction_data (dict): Dictionary of adjacent parcel data subdiv_data: dictionary of subdivision data lists + soil_data: dictonary of soil element data. Returns: None: Commits the changes to the database. @@ -152,13 +153,16 @@ def prepare_app_sub_data(app_sub_raw_data_list, direction_data, subdiv_data, soi :param app_sub_raw_data_list: A list of raw data dictionaries. :param direction_data: A dictionary of adjacent parcel data. - :param subdiv_data: dictionary of subdivision data lists + :param subdiv_data: dictionary of subdivision data lists. + :param soil_data: dictonary of soil element data. :return: Five lists, each containing dictionaries from 'app_sub_raw_data_list' and 'direction_data' grouped based on the 'alr_change_code' field Detailed Workflow: - Initializes empty lists - Iterates over 'app_sub_raw_data_list' - Maps adjacent parcel data based on alr_application_id + - Maps subdivision data on appl_component_id + _ Maps soil data based on appl_component_id - Maps the basic fields of the data dictionary based on the alr_change_code - Returns the mapped lists """ diff --git a/bin/migrate-oats-data/submissions/submap/soil_elements.py b/bin/migrate-oats-data/submissions/submap/soil_elements.py index 055aaed3b4..0e7ed81ae8 100644 --- a/bin/migrate-oats-data/submissions/submap/soil_elements.py +++ b/bin/migrate-oats-data/submissions/submap/soil_elements.py @@ -27,7 +27,6 @@ def create_soil_dict(soil_rows): if app_component_id in soil_dict: if row[code] == 'REMOVE': - # if soil_dict[app_component_id]['RMV'] == True: if 'RMV' in soil_dict.get(app_component_id, {}): print('ignored element_id:',row['soil_change_element_id']) else: @@ -44,7 +43,6 @@ def create_soil_dict(soil_rows): elif row[code] == 'ADD': - # if soil_dict[app_component_id]['ADD'] == True: if 'ADD' in soil_dict.get(app_component_id, {}): print('ignored element_id:',row['soil_change_element_id']) else: From f65d4c905596a67f59f3cc787b7b114c04b37f34 Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Wed, 6 Sep 2023 14:49:37 -0700 Subject: [PATCH 350/954] open search result in new tab (#948) * open search result in new tab --- .../application-search-table.component.ts | 4 +++- .../non-application-search-table.component.ts | 17 +++++++++++++---- .../notice-of-intent-search-table.component.ts | 4 +++- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.ts b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.ts index 25f980db36..d02dbe34ab 100644 --- a/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.ts +++ b/alcs-frontend/src/app/features/search/application-search-table/application-search-table.component.ts @@ -92,7 +92,9 @@ export class ApplicationSearchTableComponent implements AfterViewInit, OnDestroy } async onSelectRecord(record: SearchResult) { - await this.router.navigateByUrl(`/application/${record.referenceId}`); + const url = this.router.serializeUrl(this.router.createUrlTree([`/application/${record.referenceId}`])); + + window.open(url, '_blank'); } private mapApplications(applications: ApplicationSearchResultDto[]): SearchResult[] { diff --git a/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.ts b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.ts index db05df9501..7413db2792 100644 --- a/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.ts +++ b/alcs-frontend/src/app/features/search/non-application-search-table/non-application-search-table.component.ts @@ -4,7 +4,10 @@ import { MatSort } from '@angular/material/sort'; import { Router } from '@angular/router'; import { Subject, takeUntil } from 'rxjs'; import { NonApplicationSearchResultDto } from '../../../services/search/search.dto'; -import { COVENANT_TYPE_LABEL, PLANNING_TYPE_LABEL } from '../../../shared/application-type-pill/application-type-pill.constants'; +import { + COVENANT_TYPE_LABEL, + PLANNING_TYPE_LABEL, +} from '../../../shared/application-type-pill/application-type-pill.constants'; import { TableChange } from '../search.interface'; interface SearchResult { @@ -44,8 +47,8 @@ export class NonApplicationSearchTableComponent implements AfterViewInit, OnDest sortDirection = 'DESC'; sortField = 'fileId'; - COVENANT_TYPE_LABEL = COVENANT_TYPE_LABEL - PLANNING_TYPE_LABEL = PLANNING_TYPE_LABEL + COVENANT_TYPE_LABEL = COVENANT_TYPE_LABEL; + PLANNING_TYPE_LABEL = PLANNING_TYPE_LABEL; constructor(private router: Router) {} @@ -85,7 +88,13 @@ export class NonApplicationSearchTableComponent implements AfterViewInit, OnDest } async onSelectRecord(record: SearchResult) { - await this.router.navigateByUrl(`/board/${record.board}?card=${record.referenceId}&type=${record.class}`); + const url = this.router.serializeUrl( + this.router.createUrlTree([`/board/${record.board}`], { + queryParams: { card: record.referenceId, type: record.class }, + }) + ); + + window.open(url, '_blank'); } private mapNonApplications(nonApplications: NonApplicationSearchResultDto[]): NonApplicationSearchResultDto[] { diff --git a/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.ts b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.ts index 536701d449..b3868dcdce 100644 --- a/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.ts +++ b/alcs-frontend/src/app/features/search/notice-of-intent-search-table/notice-of-intent-search-table.component.ts @@ -92,7 +92,9 @@ export class NoticeOfIntentSearchTableComponent implements AfterViewInit, OnDest } async onSelectRecord(record: SearchResult) { - await this.router.navigateByUrl(`/notice-of-intent/${record.referenceId}`); + const url = this.router.serializeUrl(this.router.createUrlTree([`/notice-of-intent/${record.referenceId}`])); + + window.open(url, '_blank'); } private mapNoticeOfIntent(applications: NoticeOfIntentSearchResultDto[]) { From 2a61b21dc0a8ffc1db4c6ee2015bc2b0b45a8bf6 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 6 Sep 2023 14:53:53 -0700 Subject: [PATCH 351/954] verified front-end changes --- ...ation-submission-validator.service.spec.ts | 4 ++-- ...pplication-submission-validator.service.ts | 2 +- .../application-submission.dto.ts | 4 ++-- .../application-submission.entity.ts | 2 +- .../application-submission.service.ts | 6 ++--- .../generate-submission-document.service.ts | 2 +- .../1693945647800-rename_nfu_placement.ts | 23 ------------------- .../1694033215304-rename_nfu_placement.ts | 23 +++++++++++++++++++ 8 files changed, 33 insertions(+), 33 deletions(-) delete mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1694033215304-rename_nfu_placement.ts diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts index 4c11f99ea3..0d32a6b4f2 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.spec.ts @@ -549,7 +549,7 @@ describe('ApplicationSubmissionValidatorService', () => { nfuWillImportFill: true, nfuFillTypeDescription: 'VALID', nfuFillOriginDescription: 'VALID', - nfuTotalFillPlacement: 0.0, + nfuTotalFillArea: 0.0, nfuMaxFillDepth: 1.5125, nfuAverageFillDepth: 1261.21, nfuFillVolume: 742.1, @@ -598,7 +598,7 @@ describe('ApplicationSubmissionValidatorService', () => { nfuWillImportFill: true, nfuFillTypeDescription: 'VALID', nfuFillOriginDescription: null, - nfuTotalFillPlacement: 0.0, + nfuTotalFillArea: 0.0, nfuMaxFillDepth: 1.5125, nfuAverageFillDepth: 121, nfuFillVolume: 742.1, diff --git a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts index bcf00d8681..e7864c88b2 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission-validator.service.ts @@ -413,7 +413,7 @@ export class ApplicationSubmissionValidatorService { if ( !applicationSubmission.nfuFillTypeDescription || !applicationSubmission.nfuFillOriginDescription || - applicationSubmission.nfuTotalFillPlacement === null || + applicationSubmission.nfuTotalFillArea === null || applicationSubmission.nfuMaxFillDepth === null || applicationSubmission.nfuAverageFillDepth === null || applicationSubmission.nfuFillVolume === null || diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts index 7072eb34ea..a5f2092a88 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.dto.ts @@ -102,7 +102,7 @@ export class ApplicationSubmissionDetailedDto extends ApplicationSubmissionDto { nfuWillImportFill?: boolean | null; @AutoMap(() => Number) - nfuTotalFillPlacement?: number | null; + nfuTotalFillArea?: number | null; @AutoMap(() => Number) nfuMaxFillDepth?: number | null; @@ -415,7 +415,7 @@ export class ApplicationSubmissionUpdateDto { @IsNumber() @IsOptional() - nfuTotalFillPlacement?: number | null; + nfuTotalFillArea?: number | null; @IsNumber() @IsOptional() diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts index 099b291103..1eda7691ca 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.entity.ts @@ -238,7 +238,7 @@ export class ApplicationSubmission extends Base { scale: 2, transformer: new ColumnNumericTransformer(), }) - nfuTotalFillPlacement: number | null; + nfuTotalFillArea: number | null; @AutoMap(() => Number) @Column({ diff --git a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts index 0b9f8c0df4..2e25515371 100644 --- a/services/apps/alcs/src/portal/application-submission/application-submission.service.ts +++ b/services/apps/alcs/src/portal/application-submission/application-submission.service.ts @@ -690,9 +690,9 @@ export class ApplicationSubmissionService { updateDto.nfuWillImportFill, application.nfuWillImportFill, ); - application.nfuTotalFillPlacement = filterUndefined( - updateDto.nfuTotalFillPlacement, - application.nfuTotalFillPlacement, + application.nfuTotalFillArea = filterUndefined( + updateDto.nfuTotalFillArea, + application.nfuTotalFillArea, ); application.nfuMaxFillDepth = filterUndefined( updateDto.nfuMaxFillDepth, diff --git a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts index 6d087bfb97..b78e6b4d1f 100644 --- a/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts +++ b/services/apps/alcs/src/portal/pdf-generation/generate-submission-document.service.ts @@ -303,7 +303,7 @@ export class GenerateSubmissionDocumentService { // NFU Proposal => Soil and Fill nfuFillTypeDescription: submission.nfuFillTypeDescription, nfuFillOriginDescription: submission.nfuFillOriginDescription, - nfuTotalFillPlacement: submission.nfuTotalFillPlacement, + nfuTotalFillArea: submission.nfuTotalFillArea, nfuMaxFillDepth: submission.nfuMaxFillDepth, nfuAverageFillDepth: submission.nfuAverageFillDepth, nfuFillVolume: submission.nfuFillVolume, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts b/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts deleted file mode 100644 index c21373ab93..0000000000 --- a/services/apps/alcs/src/providers/typeorm/migrations/1693945647800-rename_nfu_placement.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm" - -export class renameNfuPlacement1693945647800 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "alcs"."application_submission" RENAME COLUMN "nfu_total_fill_placement" TO "nfu_total_fill_area"`, - ); - await queryRunner.query( - `COMMENT ON COLUMN "alcs"."application_submission"."nfu_total_fill_area" IS 'Area for nfu placement of fill'`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `COMMENT ON COLUMN "alcs"."application_submission"."nfu_total_fill_area" IS 'Area for nfu placement of fill'`, - ); - await queryRunner.query( - `ALTER TABLE "alcs"."application_submission" DROP COLUMN "nfu_total_fill_area"`, - ); - } - -} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1694033215304-rename_nfu_placement.ts b/services/apps/alcs/src/providers/typeorm/migrations/1694033215304-rename_nfu_placement.ts new file mode 100644 index 0000000000..b86f8e931c --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1694033215304-rename_nfu_placement.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class renameNfuPlacement1694033215304 implements MigrationInterface { + name = 'renameNfuPlacement1694033215304'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" RENAME COLUMN "nfu_total_fill_placement" TO "nfu_total_fill_area"`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_submission"."nfu_total_fill_area" IS NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."application_submission"."nfu_total_fill_area" IS 'Area for nfu placement of fill'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."application_submission" RENAME COLUMN "nfu_total_fill_area" TO "nfu_total_fill_placement"`, + ); + } +} From c2f1d1bccaeeb59c3d10e864adc4569747199763 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 6 Sep 2023 15:15:17 -0700 Subject: [PATCH 352/954] added frontend file changes --- .../application-details.component.spec.ts | 2 +- .../nfu-details/nfu-details.component.html | 2 +- .../application-submission.service.spec.ts | 2 +- .../src/app/services/application/application.dto.ts | 2 +- .../nfu-details/nfu-details.component.html | 6 +++--- .../proposal/nfu-proposal/nfu-proposal.component.ts | 4 ++-- .../application-submission/application-submission.dto.ts | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts index 9edda8f531..0f2f203f54 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/application-details.component.spec.ts @@ -82,7 +82,7 @@ describe('ApplicationDetailsComponent', () => { nfuOutsideLands: null, nfuProjectDurationAmount: null, nfuProjectDurationUnit: null, - nfuTotalFillPlacement: null, + nfuTotalFillArea: null, nfuWillImportFill: null, soilAlreadyPlacedArea: null, soilAlreadyPlacedAverageDepth: null, diff --git a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html index 2e25f03054..ed166225e5 100644 --- a/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html +++ b/alcs-frontend/src/app/features/application/applicant-info/application-details/nfu-details/nfu-details.component.html @@ -56,7 +56,7 @@

Soil and Fill Components

Volume
{{ applicationSubmission.nfuFillVolume }} m3
Area
-
{{ applicationSubmission.nfuTotalFillPlacement }} ha
+
{{ applicationSubmission.nfuTotalFillArea }} ha
Maximum Depth
{{ applicationSubmission.nfuMaxFillDepth }} m
Average Depth
diff --git a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts index db44990ab8..e891624796 100644 --- a/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts +++ b/alcs-frontend/src/app/services/application/application-submission/application-submission.service.spec.ts @@ -75,7 +75,7 @@ describe('ApplicationSubmissionService', () => { nfuProjectDurationAmount: null, nfuProjectDurationUnit: null, purpose: '', - nfuTotalFillPlacement: null, + nfuTotalFillArea: null, nfuWillImportFill: null, soilAlreadyPlacedArea: null, soilAlreadyPlacedAverageDepth: null, diff --git a/alcs-frontend/src/app/services/application/application.dto.ts b/alcs-frontend/src/app/services/application/application.dto.ts index 58f073bb2e..0e3b34dae0 100644 --- a/alcs-frontend/src/app/services/application/application.dto.ts +++ b/alcs-frontend/src/app/services/application/application.dto.ts @@ -137,7 +137,7 @@ export interface ApplicationSubmissionDto { nfuOutsideLands: string | null; nfuAgricultureSupport: string | null; nfuWillImportFill: boolean | null; - nfuTotalFillPlacement: number | null; + nfuTotalFillArea: number | null; nfuMaxFillDepth: number | null; nfuAverageFillDepth: number | null; nfuFillVolume: number | null; diff --git a/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html index 5dcc330bc5..0e4fbedb2d 100644 --- a/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html +++ b/portal-frontend/src/app/features/applications/application-details/nfu-details/nfu-details.component.html @@ -85,11 +85,11 @@

Soil and Fill Components

Area
- {{ applicationSubmission.nfuTotalFillPlacement }} - ha + {{ applicationSubmission.nfuTotalFillArea }} + ha
Maximum Depth
diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts index 61620fafe9..dbc27c67ab 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts @@ -75,7 +75,7 @@ export class NfuProposalComponent extends FilesStepComponent implements OnInit, this.fillTableData = { volume: applicationSubmission.nfuFillVolume ?? undefined, - area: applicationSubmission.nfuTotalFillPlacement ?? undefined, + area: applicationSubmission.nfuTotalFillArea ?? undefined, maximumDepth: applicationSubmission.nfuMaxFillDepth ?? undefined, averageDepth: applicationSubmission.nfuAverageFillDepth ?? undefined, }; @@ -118,7 +118,7 @@ export class NfuProposalComponent extends FilesStepComponent implements OnInit, nfuOutsideLands, nfuAgricultureSupport, nfuWillImportFill: parseStringToBoolean(nfuWillImportFill), - nfuTotalFillPlacement: this.fillTableData.area ?? null, + nfuTotalFillArea: this.fillTableData.area ?? null, nfuMaxFillDepth: this.fillTableData.maximumDepth ?? null, nfuAverageFillDepth: this.fillTableData.averageDepth ?? null, nfuFillVolume: this.fillTableData.volume ?? null, diff --git a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts index 318892f119..ca710bbd4f 100644 --- a/portal-frontend/src/app/services/application-submission/application-submission.dto.ts +++ b/portal-frontend/src/app/services/application-submission/application-submission.dto.ts @@ -77,7 +77,7 @@ export interface ApplicationSubmissionDetailedDto extends ApplicationSubmissionD nfuOutsideLands: string | null; nfuAgricultureSupport: string | null; nfuWillImportFill: boolean | null; - nfuTotalFillPlacement: number | null; + nfuTotalFillArea: number | null; nfuMaxFillDepth: number | null; nfuAverageFillDepth: number | null; nfuFillVolume: number | null; @@ -182,7 +182,7 @@ export interface ApplicationSubmissionUpdateDto { nfuOutsideLands?: string | null; nfuAgricultureSupport?: string | null; nfuWillImportFill?: boolean | null; - nfuTotalFillPlacement?: number | null; + nfuTotalFillArea?: number | null; nfuMaxFillDepth?: number | null; nfuAverageFillDepth?: number | null; nfuFillVolume?: number | null; From 31d23dc4c775732b0f30d5d5b36cd6f3eada0952 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Wed, 6 Sep 2023 15:50:42 -0700 Subject: [PATCH 353/954] Add Notification Documents * Add Notification Documents and Other Attachments Page --- .../edit-submission.component.html | 11 +- .../edit-submission.component.spec.ts | 5 + .../edit-submission.component.ts | 18 +- .../edit-submission/edit-submission.module.ts | 2 + .../edit-submission/files-step.partial.ts | 24 +- .../other-attachments.component.html | 104 ++++++ .../other-attachments.component.scss | 38 ++ .../other-attachments.component.spec.ts | 68 ++++ .../other-attachments.component.ts | 121 +++++++ .../notification-document.dto.ts | 18 + .../notification-document.service.spec.ts | 109 ++++++ .../notification-document.service.ts | 103 ++++++ services/apps/alcs/src/alcs/alcs.module.ts | 3 + .../notification-document.controller.spec.ts | 189 ++++++++++ .../notification-document.controller.ts | 211 +++++++++++ .../notification-document.dto.ts | 29 ++ .../notification-document.entity.ts | 67 ++++ .../notification-document.service.spec.ts | 342 ++++++++++++++++++ .../notification-document.service.ts | 305 ++++++++++++++++ .../alcs/notification/notification.entity.ts | 6 + .../alcs/notification/notification.module.ts | 20 +- .../notification.automapper.profile.ts | 41 ++- .../notification-document.controller.spec.ts | 197 ++++++++++ .../notification-document.controller.ts | 172 +++++++++ .../notification-document.dto.ts | 30 ++ .../notification-document.module.ts | 13 + ...notification-submission.controller.spec.ts | 4 +- .../apps/alcs/src/portal/portal.module.ts | 3 + ...694037493007-add_notification_documents.ts | 35 ++ 29 files changed, 2262 insertions(+), 26 deletions(-) create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.ts create mode 100644 portal-frontend/src/app/services/notification-document/notification-document.dto.ts create mode 100644 portal-frontend/src/app/services/notification-document/notification-document.service.spec.ts create mode 100644 portal-frontend/src/app/services/notification-document/notification-document.service.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification-document/notification-document.controller.spec.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification-document/notification-document.controller.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification-document/notification-document.dto.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification-document/notification-document.entity.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.spec.ts create mode 100644 services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.ts create mode 100644 services/apps/alcs/src/portal/notification-document/notification-document.controller.spec.ts create mode 100644 services/apps/alcs/src/portal/notification-document/notification-document.controller.ts create mode 100644 services/apps/alcs/src/portal/notification-document/notification-document.dto.ts create mode 100644 services/apps/alcs/src/portal/notification-document/notification-document.module.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1694037493007-add_notification_documents.ts diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html index b906c29f4c..6e94936da9 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html @@ -77,7 +77,16 @@
-
+
+ + +
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.spec.ts index cc48c1cd01..72e2b1f4e7 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.spec.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.spec.ts @@ -2,6 +2,7 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute } from '@angular/router'; +import { NotificationDocumentService } from '../../../services/notification-document/notification-document.service'; import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; import { ToastService } from '../../../services/toast/toast.service'; @@ -19,6 +20,10 @@ describe('EditSubmissionComponent', () => { provide: NotificationSubmissionService, useValue: {}, }, + { + provide: NotificationDocumentService, + useValue: {}, + }, { provide: ToastService, useValue: {}, diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts index 2d718d73e8..695ab88ad4 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts @@ -4,6 +4,8 @@ import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NotificationDocumentDto } from '../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../services/notification-document/notification-document.service'; import { NOTIFICATION_STATUS, NotificationSubmissionDetailedDto, @@ -13,6 +15,7 @@ import { ToastService } from '../../../services/toast/toast.service'; import { CustomStepperComponent } from '../../../shared/custom-stepper/custom-stepper.component'; import { OverlaySpinnerService } from '../../../shared/overlay-spinner/overlay-spinner.service'; import { scrollToElement } from '../../../shared/utils/scroll-helper'; +import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; @@ -37,7 +40,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { $destroy = new Subject(); $notificationSubmission = new BehaviorSubject(undefined); - $notificationDocuments = new BehaviorSubject([]); + $notificationDocuments = new BehaviorSubject([]); notificationSubmission: NotificationSubmissionDetailedDto | undefined; steps = EditNotificationSteps; @@ -48,9 +51,11 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; @ViewChild(PrimaryContactComponent) primaryContactComponent!: PrimaryContactComponent; @ViewChild(SelectGovernmentComponent) selectGovernmentComponent!: SelectGovernmentComponent; + @ViewChild(OtherAttachmentsComponent) otherAttachmentsComponent!: OtherAttachmentsComponent; constructor( private notificationSubmissionService: NotificationSubmissionService, + private notificationDocumentService: NotificationDocumentService, private activatedRoute: ActivatedRoute, private dialog: MatDialog, private toastService: ToastService, @@ -149,7 +154,12 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { } break; case EditNotificationSteps.Proposal: + break; case EditNotificationSteps.Attachments: + if (this.otherAttachmentsComponent) { + await this.otherAttachmentsComponent.onSave(); + } + break; case EditNotificationSteps.ReviewAndSubmit: //DO NOTHING break; @@ -164,10 +174,6 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { } } - onChangeSubmissionType() { - //TODO - } - async onSubmit() { //TODO } @@ -193,7 +199,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { await this.router.navigateByUrl(`/home`); } - const documents: NoticeOfIntentDocumentDto[] = []; //TODO await this.noticeOfIntentDocumentService.getByFileId(fileId); + const documents = await this.notificationDocumentService.getByFileId(fileId); if (documents) { this.$notificationDocuments.next(documents); } diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts index 6124c74f91..08d1f751b4 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts @@ -8,6 +8,7 @@ import { NgxMaskPipe } from 'ngx-mask'; import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; import { SharedModule } from '../../../shared/shared.module'; import { EditSubmissionComponent } from './edit-submission.component'; +import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { DeleteParcelDialogComponent } from './parcels/delete-parcel/delete-parcel-dialog.component'; import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; @@ -42,6 +43,7 @@ const routes: Routes = [ TransfereeDialogComponent, PrimaryContactComponent, SelectGovernmentComponent, + OtherAttachmentsComponent, ], imports: [ CommonModule, diff --git a/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts index 000753042e..152f173c0a 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts @@ -1,8 +1,8 @@ import { Component, Input } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs'; -import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; -import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NotificationDocumentDto } from '../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../services/notification-document/notification-document.service'; import { DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; import { FileHandle } from '../../../shared/file-drag-drop/drag-drop.directive'; import { StepComponent } from './step.partial'; @@ -13,7 +13,7 @@ import { StepComponent } from './step.partial'; styleUrls: [], }) export abstract class FilesStepComponent extends StepComponent { - @Input() $noiDocuments!: BehaviorSubject; + @Input() $notificationDocuments!: BehaviorSubject; DOCUMENT_TYPE = DOCUMENT_TYPE; @@ -22,7 +22,7 @@ export abstract class FilesStepComponent extends StepComponent { protected abstract save(): Promise; protected constructor( - protected noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + protected notificationDocumentService: NotificationDocumentService, protected dialog: MatDialog ) { super(); @@ -32,26 +32,26 @@ export abstract class FilesStepComponent extends StepComponent { if (this.fileId) { await this.save(); const mappedFiles = file.file; - await this.noticeOfIntentDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); - const documents = await this.noticeOfIntentDocumentService.getByFileId(this.fileId); + await this.notificationDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); + const documents = await this.notificationDocumentService.getByFileId(this.fileId); if (documents) { - this.$noiDocuments.next(documents); + this.$notificationDocuments.next(documents); } } } - async onDeleteFile($event: NoticeOfIntentDocumentDto) { - await this.noticeOfIntentDocumentService.deleteExternalFile($event.uuid); + async onDeleteFile($event: NotificationDocumentDto) { + await this.notificationDocumentService.deleteExternalFile($event.uuid); if (this.fileId) { - const documents = await this.noticeOfIntentDocumentService.getByFileId(this.fileId); + const documents = await this.notificationDocumentService.getByFileId(this.fileId); if (documents) { - this.$noiDocuments.next(documents); + this.$notificationDocuments.next(documents); } } } async openFile(uuid: string) { - const res = await this.noticeOfIntentDocumentService.openFile(uuid); + const res = await this.notificationDocumentService.openFile(uuid); if (res) { window.open(res.url, '_blank'); } diff --git a/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.html b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.html new file mode 100644 index 0000000000..ccd065dabd --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.html @@ -0,0 +1,104 @@ +
+

Optional Attachments

+

+ Please upload any optional supporting documents. Where possible, provide KML/KMZ Google Earth files or GIS + shapefiles and geodatabases. +

+
+
+

Upload Optional Attachments (Max. 100 MB per attachment)

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type + + + {{ type.label }} + + +
+ warning +
This field is required
+
+
Description + + + +
+ warning +
+ This field is required +
+
+
File Name + {{ element.fileName }} + Action + +
No attachments
+
+
+
+ +
+
+
+ +
+ + +
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.scss new file mode 100644 index 0000000000..7a223440c8 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.scss @@ -0,0 +1,38 @@ +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; + +section { + margin-top: rem(32); +} + +.uploader { + margin-top: rem(24); +} + +h4 { + margin-bottom: rem(8) !important; +} + +.scrollable { + overflow-x: auto; +} + +.mat-mdc-table .mdc-data-table__row { + height: rem(75); +} + +.mat-mdc-form-field { + width: 100%; +} + +:host::ng-deep { + .mdc-text-field--invalid { + margin-top: rem(8); + } +} + +.no-data-text { + text-align: center; + color: colors.$grey; + padding-top: rem(12); +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.spec.ts new file mode 100644 index 0000000000..333944d2cc --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.spec.ts @@ -0,0 +1,68 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { CodeService } from '../../../../services/code/code.service'; +import { NotificationDocumentDto } from '../../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../../services/notification-document/notification-document.service'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; + +import { OtherAttachmentsComponent } from './other-attachments.component'; + +describe('OtherAttachmentsComponent', () => { + let component: OtherAttachmentsComponent; + let fixture: ComponentFixture; + let mockNotificationSubmissionService: DeepMocked; + let mockNotificationDocumentService: DeepMocked; + let mockRouter: DeepMocked; + let mockCodeService: DeepMocked; + + let documentPipe = new BehaviorSubject([]); + + beforeEach(async () => { + mockNotificationSubmissionService = createMock(); + mockNotificationDocumentService = createMock(); + mockRouter = createMock(); + mockCodeService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: NotificationSubmissionService, + useValue: mockNotificationSubmissionService, + }, + { + provide: NotificationDocumentService, + useValue: mockNotificationDocumentService, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: CodeService, + useValue: mockCodeService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + declarations: [OtherAttachmentsComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(OtherAttachmentsComponent); + component = fixture.componentInstance; + component.$notificationSubmission = new BehaviorSubject(undefined); + component.$notificationDocuments = documentPipe; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.ts new file mode 100644 index 0000000000..920bc18586 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.ts @@ -0,0 +1,121 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { takeUntil } from 'rxjs'; +import { CodeService } from '../../../../services/code/code.service'; +import { + NotificationDocumentDto, + NotificationDocumentUpdateDto, +} from '../../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../../services/notification-document/notification-document.service'; +import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../shared/dto/document.dto'; +import { EditNotificationSteps } from '../edit-submission.component'; +import { FilesStepComponent } from '../files-step.partial'; + +const USER_CONTROLLED_TYPES = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; + +@Component({ + selector: 'app-other-attachments', + templateUrl: './other-attachments.component.html', + styleUrls: ['./other-attachments.component.scss'], +}) +export class OtherAttachmentsComponent extends FilesStepComponent implements OnInit, OnDestroy { + currentStep = EditNotificationSteps.Attachments; + + displayedColumns = ['type', 'description', 'fileName', 'actions']; + selectableTypes: DocumentTypeDto[] = []; + otherFiles: NotificationDocumentDto[] = []; + + private isDirty = false; + + form = new FormGroup({} as any); + private documentCodes: DocumentTypeDto[] = []; + + constructor( + private router: Router, + private applicationService: NotificationSubmissionService, + private codeService: CodeService, + notificationDocumentService: NotificationDocumentService, + dialog: MatDialog + ) { + super(notificationDocumentService, dialog); + } + + ngOnInit(): void { + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + if (submission) { + this.fileId = submission.fileNumber; + } + }); + + this.loadDocumentCodes(); + + this.$notificationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.otherFiles = documents + .filter((file) => (file.type ? USER_CONTROLLED_TYPES.includes(file.type.code) : true)) + .filter((file) => file.source === DOCUMENT_SOURCE.APPLICANT) + .sort((a, b) => { + return a.uploadedAt - b.uploadedAt; + }); + const newForm = new FormGroup({}); + for (const file of this.otherFiles) { + newForm.addControl(`${file.uuid}-type`, new FormControl(file.type?.code, [Validators.required])); + newForm.addControl(`${file.uuid}-description`, new FormControl(file.description, [Validators.required])); + } + this.form = newForm; + if (this.showErrors) { + this.form.markAllAsTouched(); + } + }); + } + + async onSave() { + await this.save(); + } + + protected async save() { + if (this.isDirty) { + const updateDtos: NotificationDocumentUpdateDto[] = this.otherFiles.map((file) => ({ + uuid: file.uuid, + description: file.description, + type: file.type?.code ?? null, + })); + await this.notificationDocumentService.update(this.fileId, updateDtos); + } + } + + onChangeDescription(uuid: string, event: Event) { + this.isDirty = true; + const input = event.target as HTMLInputElement; + const description = input.value; + this.otherFiles = this.otherFiles.map((file) => { + if (uuid === file.uuid) { + file.description = description; + } + return file; + }); + } + + onChangeType(uuid: string, selectedValue: DOCUMENT_TYPE) { + this.isDirty = true; + this.otherFiles = this.otherFiles.map((file) => { + if (uuid === file.uuid) { + const newType = this.documentCodes.find((code) => code.code === selectedValue); + if (newType) { + file.type = newType; + } else { + console.error('Failed to find matching document type'); + } + } + return file; + }); + } + + private async loadDocumentCodes() { + const codes = await this.codeService.loadCodes(); + this.documentCodes = codes.documentTypes; + this.selectableTypes = this.documentCodes.filter((code) => USER_CONTROLLED_TYPES.includes(code.code)); + } +} diff --git a/portal-frontend/src/app/services/notification-document/notification-document.dto.ts b/portal-frontend/src/app/services/notification-document/notification-document.dto.ts new file mode 100644 index 0000000000..9e345f8edf --- /dev/null +++ b/portal-frontend/src/app/services/notification-document/notification-document.dto.ts @@ -0,0 +1,18 @@ +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../shared/dto/document.dto'; + +export interface NotificationDocumentDto { + type: DocumentTypeDto | null; + description?: string | null; + uuid: string; + fileName: string; + fileSize: number; + uploadedBy: string; + uploadedAt: number; + source: DOCUMENT_SOURCE; +} + +export interface NotificationDocumentUpdateDto { + uuid: string; + type: DOCUMENT_TYPE | null; + description?: string | null; +} diff --git a/portal-frontend/src/app/services/notification-document/notification-document.service.spec.ts b/portal-frontend/src/app/services/notification-document/notification-document.service.spec.ts new file mode 100644 index 0000000000..b5d044ac77 --- /dev/null +++ b/portal-frontend/src/app/services/notification-document/notification-document.service.spec.ts @@ -0,0 +1,109 @@ +import { HttpClient } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { of, throwError } from 'rxjs'; +import { ToastService } from '../toast/toast.service'; +import { NotificationDocumentService } from './notification-document.service'; + +describe('NotificationDocumentService', () => { + let service: NotificationDocumentService; + let mockToastService: DeepMocked; + let mockHttpClient: DeepMocked; + + beforeEach(() => { + mockToastService = createMock(); + mockHttpClient = createMock(); + + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: HttpClient, + useValue: mockHttpClient, + }, + ], + }); + service = TestBed.inject(NotificationDocumentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should make a get request for open file', async () => { + mockHttpClient.get.mockReturnValue(of({})); + + await service.openFile('fileId'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockHttpClient.get.mock.calls[0][0]).toContain('notification-document'); + }); + + it('should show an error toast if opening a file fails', async () => { + mockHttpClient.get.mockReturnValue(throwError(() => ({}))); + + await service.openFile('fileId'); + + expect(mockHttpClient.get).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a delete request for delete file', async () => { + mockHttpClient.delete.mockReturnValue(of({})); + + await service.deleteExternalFile('fileId'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockHttpClient.delete.mock.calls[0][0]).toContain('notification-document'); + }); + + it('should show an error toast if deleting a file fails', async () => { + mockHttpClient.delete.mockReturnValue(throwError(() => ({}))); + + await service.deleteExternalFile('fileId'); + + expect(mockHttpClient.delete).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a patch request for update file', async () => { + mockHttpClient.patch.mockReturnValue(of({})); + + await service.update('fileId', []); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockHttpClient.patch.mock.calls[0][0]).toContain('notification-document'); + }); + + it('should show an error toast if updating a file fails', async () => { + mockHttpClient.patch.mockReturnValue(throwError(() => ({}))); + + await service.update('fileId', []); + + expect(mockHttpClient.patch).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); + + it('should make a post request for deleting multiple files', async () => { + mockHttpClient.post.mockReturnValue(of({})); + + await service.deleteExternalFiles(['fileId']); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockHttpClient.post.mock.calls[0][0]).toContain('notification-document'); + }); + + it('should show an error toast if deleting a file fails', async () => { + mockHttpClient.post.mockReturnValue(throwError(() => ({}))); + + await service.deleteExternalFiles(['fileId']); + + expect(mockHttpClient.post).toHaveBeenCalledTimes(1); + expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); + }); +}); diff --git a/portal-frontend/src/app/services/notification-document/notification-document.service.ts b/portal-frontend/src/app/services/notification-document/notification-document.service.ts new file mode 100644 index 0000000000..60eff39d81 --- /dev/null +++ b/portal-frontend/src/app/services/notification-document/notification-document.service.ts @@ -0,0 +1,103 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { environment } from '../../../environments/environment'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../shared/dto/document.dto'; +import { OverlaySpinnerService } from '../../shared/overlay-spinner/overlay-spinner.service'; +import { DocumentService } from '../document/document.service'; +import { ToastService } from '../toast/toast.service'; +import { NotificationDocumentDto, NotificationDocumentUpdateDto } from './notification-document.dto'; + +@Injectable({ + providedIn: 'root', +}) +export class NotificationDocumentService { + private serviceUrl = `${environment.apiUrl}/notification-document`; + + constructor( + private httpClient: HttpClient, + private toastService: ToastService, + private documentService: DocumentService, + private overlayService: OverlaySpinnerService + ) {} + + async attachExternalFile( + fileNumber: string, + file: File, + documentType: DOCUMENT_TYPE | null, + source = DOCUMENT_SOURCE.APPLICANT + ) { + try { + const res = await this.documentService.uploadFile( + fileNumber, + file, + documentType, + source, + `${this.serviceUrl}/notification/${fileNumber}/attachExternal` + ); + this.toastService.showSuccessToast('Document uploaded'); + return res; + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to attach document, please try again'); + } + return undefined; + } + + async openFile(fileUuid: string) { + try { + return await firstValueFrom(this.httpClient.get<{ url: string }>(`${this.serviceUrl}/${fileUuid}/open`)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to open the document, please try again'); + } + return undefined; + } + + async deleteExternalFile(fileUuid: string) { + try { + this.overlayService.showSpinner(); + await firstValueFrom(this.httpClient.delete(`${this.serviceUrl}/${fileUuid}`)); + this.toastService.showSuccessToast('Document deleted'); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete document, please try again'); + } finally { + this.overlayService.hideSpinner(); + } + } + + async deleteExternalFiles(fileUuids: string[]) { + try { + this.overlayService.showSpinner(); + await firstValueFrom(this.httpClient.post(`${this.serviceUrl}/delete-files`, fileUuids)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to delete documents'); + } finally { + this.overlayService.hideSpinner(); + } + } + + async update(fileNumber: string | undefined, updateDtos: NotificationDocumentUpdateDto[]) { + try { + await firstValueFrom(this.httpClient.patch(`${this.serviceUrl}/notification/${fileNumber}`, updateDtos)); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to update documents, please try again'); + } + return undefined; + } + + async getByFileId(fileNumber: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/notification/${fileNumber}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to fetch documents, please try again'); + } + return undefined; + } +} diff --git a/services/apps/alcs/src/alcs/alcs.module.ts b/services/apps/alcs/src/alcs/alcs.module.ts index 8a49f98dd7..29bbc475a5 100644 --- a/services/apps/alcs/src/alcs/alcs.module.ts +++ b/services/apps/alcs/src/alcs/alcs.module.ts @@ -19,6 +19,7 @@ import { NoticeOfIntentTimelineModule } from './notice-of-intent/notice-of-inten import { NoticeOfIntentSubmissionStatusModule } from './notice-of-intent/notice-of-intent-submission-status/notice-of-intent-submission-status.module'; import { NoticeOfIntentModule } from './notice-of-intent/notice-of-intent.module'; import { MessageModule } from './message/message.module'; +import { NotificationModule } from './notification/notification.module'; import { PlanningReviewModule } from './planning-review/planning-review.module'; import { SearchModule } from './search/search.module'; import { StaffJournalModule } from './staff-journal/staff-journal.module'; @@ -45,6 +46,7 @@ import { StaffJournalModule } from './staff-journal/staff-journal.module'; NoticeOfIntentTimelineModule, SearchModule, LocalGovernmentModule, + NotificationModule, RouterModule.register([ { path: 'alcs', module: ApplicationModule }, { path: 'alcs', module: CommentModule }, @@ -67,6 +69,7 @@ import { StaffJournalModule } from './staff-journal/staff-journal.module'; { path: 'alcs', module: LocalGovernmentModule }, { path: 'alcs', module: ApplicationTimelineModule }, { path: 'alcs', module: NoticeOfIntentTimelineModule }, + { path: 'alcs', module: NotificationModule }, ]), ], controllers: [], diff --git a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.controller.spec.ts b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.controller.spec.ts new file mode 100644 index 0000000000..99172858f8 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.controller.spec.ts @@ -0,0 +1,189 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { BadRequestException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; +import { NotificationProfile } from '../../../common/automapper/notification.automapper.profile'; +import { DOCUMENT_TYPE } from '../../../document/document-code.entity'; +import { DOCUMENT_SOURCE } from '../../../document/document.dto'; +import { Document } from '../../../document/document.entity'; +import { User } from '../../../user/user.entity'; +import { CodeService } from '../../code/code.service'; +import { NotificationDocumentController } from './notification-document.controller'; +import { NotificationDocument } from './notification-document.entity'; +import { NotificationDocumentService } from './notification-document.service'; + +describe('NotificationDocumentController', () => { + let controller: NotificationDocumentController; + let notificationDocumentService: DeepMocked; + + const mockDocument = new NotificationDocument({ + document: new Document({ + mimeType: 'mimeType', + uploadedBy: new User(), + uploadedAt: new Date(), + }), + }); + + beforeEach(async () => { + notificationDocumentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NotificationDocumentController], + providers: [ + { + provide: CodeService, + useValue: {}, + }, + NotificationProfile, + { + provide: NotificationDocumentService, + useValue: notificationDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + controller = module.get( + NotificationDocumentController, + ); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should return the attached document', async () => { + const mockFile = {}; + const mockUser = {}; + + notificationDocumentService.attachDocument.mockResolvedValue(mockDocument); + + const res = await controller.attachDocument('fileNumber', { + isMultipart: () => true, + body: { + documentType: { + value: DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + }, + fileName: { + value: 'file', + }, + source: { + value: DOCUMENT_SOURCE.APPLICANT, + }, + visibilityFlags: { + value: '', + }, + file: mockFile, + }, + user: { + entity: mockUser, + }, + }); + + expect(res.mimeType).toEqual(mockDocument.document.mimeType); + + expect(notificationDocumentService.attachDocument).toHaveBeenCalledTimes(1); + const callData = + notificationDocumentService.attachDocument.mock.calls[0][0]; + expect(callData.fileName).toEqual('file'); + expect(callData.file).toEqual(mockFile); + expect(callData.user).toEqual(mockUser); + }); + + it('should throw an exception if request is not the right type', async () => { + const mockFile = {}; + const mockUser = {}; + + notificationDocumentService.attachDocument.mockResolvedValue(mockDocument); + + await expect( + controller.attachDocument('fileNumber', { + isMultipart: () => false, + file: () => mockFile, + user: { + entity: mockUser, + }, + }), + ).rejects.toMatchObject( + new BadRequestException('Request is not multipart'), + ); + }); + + it('should list documents', async () => { + notificationDocumentService.list.mockResolvedValue([mockDocument]); + + const res = await controller.listDocuments( + 'fake-number', + DOCUMENT_TYPE.DECISION_DOCUMENT, + ); + + expect(res[0].mimeType).toEqual(mockDocument.document.mimeType); + }); + + it('should call through to delete documents', async () => { + notificationDocumentService.delete.mockResolvedValue(mockDocument); + notificationDocumentService.get.mockResolvedValue(mockDocument); + + await controller.delete('fake-uuid'); + + expect(notificationDocumentService.get).toHaveBeenCalledTimes(1); + expect(notificationDocumentService.delete).toHaveBeenCalledTimes(1); + }); + + it('should call through for open', async () => { + const fakeUrl = 'fake-url'; + notificationDocumentService.getInlineUrl.mockResolvedValue(fakeUrl); + notificationDocumentService.get.mockResolvedValue(mockDocument); + + const res = await controller.open('fake-uuid'); + + expect(res.url).toEqual(fakeUrl); + }); + + it('should call through for download', async () => { + const fakeUrl = 'fake-url'; + notificationDocumentService.getDownloadUrl.mockResolvedValue(fakeUrl); + notificationDocumentService.get.mockResolvedValue(mockDocument); + + const res = await controller.download('fake-uuid'); + + expect(res.url).toEqual(fakeUrl); + }); + + it('should call through for list types', async () => { + notificationDocumentService.fetchTypes.mockResolvedValue([]); + + const res = await controller.listTypes(); + + expect(notificationDocumentService.fetchTypes).toHaveBeenCalledTimes(1); + }); + + it('should call through for list app documents', async () => { + notificationDocumentService.getApplicantDocuments.mockResolvedValue([]); + + const res = await controller.listApplicantDocuments(''); + + expect( + notificationDocumentService.getApplicantDocuments, + ).toHaveBeenCalledTimes(1); + }); + + it('should call through for list review documents', async () => { + notificationDocumentService.list.mockResolvedValue([]); + + const res = await controller.listReviewDocuments(''); + + expect(notificationDocumentService.list).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.controller.ts b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.controller.ts new file mode 100644 index 0000000000..c7633e4fcd --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.controller.ts @@ -0,0 +1,211 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + BadRequestException, + Controller, + Delete, + Get, + Param, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { ANY_AUTH_ROLE } from '../../../common/authorization/roles'; +import { RolesGuard } from '../../../common/authorization/roles-guard.service'; +import { UserRoles } from '../../../common/authorization/roles.decorator'; +import { + DOCUMENT_TYPE, + DocumentCode, +} from '../../../document/document-code.entity'; +import { + DOCUMENT_SOURCE, + DOCUMENT_SYSTEM, + DocumentTypeDto, +} from '../../../document/document.dto'; +import { NotificationDocumentDto } from './notification-document.dto'; +import { + NotificationDocument, + VISIBILITY_FLAG, +} from './notification-document.entity'; +import { NotificationDocumentService } from './notification-document.service'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(RolesGuard) +@Controller('notification-document') +export class NotificationDocumentController { + constructor( + private notificationDocumentService: NotificationDocumentService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('/notification/:fileNumber') + @UserRoles(...ANY_AUTH_ROLE) + async listAll( + @Param('fileNumber') fileNumber: string, + ): Promise { + const documents = await this.notificationDocumentService.list(fileNumber); + return this.mapper.mapArray( + documents, + NotificationDocument, + NotificationDocumentDto, + ); + } + + @Post('/notification/:fileNumber') + @UserRoles(...ANY_AUTH_ROLE) + async attachDocument( + @Param('fileNumber') fileNumber: string, + @Req() req, + ): Promise { + if (!req.isMultipart()) { + throw new BadRequestException('Request is not multipart'); + } + + const savedDocument = await this.saveUploadedFile(req, fileNumber); + + return this.mapper.map( + savedDocument, + NotificationDocument, + NotificationDocumentDto, + ); + } + + @Post('/:uuid') + @UserRoles(...ANY_AUTH_ROLE) + async updateDocument( + @Param('uuid') documentUuid: string, + @Req() req, + ): Promise { + if (!req.isMultipart()) { + throw new BadRequestException('Request is not multipart'); + } + + const documentType = req.body.documentType.value as DOCUMENT_TYPE; + const file = req.body.file; + const fileName = req.body.fileName.value as string; + const documentSource = req.body.source.value as DOCUMENT_SOURCE; + const visibilityFlags = req.body.visibilityFlags.value.split(', '); + + const savedDocument = await this.notificationDocumentService.update({ + uuid: documentUuid, + fileName, + file, + documentType: documentType as DOCUMENT_TYPE, + source: documentSource, + visibilityFlags, + user: req.user.entity, + }); + + return this.mapper.map( + savedDocument, + NotificationDocument, + NotificationDocumentDto, + ); + } + + @Get('/notification/:fileNumber/reviewDocuments') + @UserRoles(...ANY_AUTH_ROLE) + async listReviewDocuments( + @Param('fileNumber') fileNumber: string, + ): Promise { + const documents = await this.notificationDocumentService.list(fileNumber); + const reviewDocuments = documents.filter( + (doc) => doc.document.source === DOCUMENT_SOURCE.LFNG, + ); + + return this.mapper.mapArray( + reviewDocuments, + NotificationDocument, + NotificationDocumentDto, + ); + } + + @Get('/notification/:fileNumber/applicantDocuments') + @UserRoles(...ANY_AUTH_ROLE) + async listApplicantDocuments( + @Param('fileNumber') fileNumber: string, + ): Promise { + const documents = + await this.notificationDocumentService.getApplicantDocuments(fileNumber); + + return this.mapper.mapArray( + documents, + NotificationDocument, + NotificationDocumentDto, + ); + } + + @Get('/notification/:fileNumber/:visibilityFlags') + @UserRoles(...ANY_AUTH_ROLE) + async listDocuments( + @Param('fileNumber') fileNumber: string, + @Param('visibilityFlags') visibilityFlags: string, + ): Promise { + const mappedFlags = visibilityFlags.split('') as VISIBILITY_FLAG[]; + const documents = await this.notificationDocumentService.list( + fileNumber, + mappedFlags, + ); + return this.mapper.mapArray( + documents, + NotificationDocument, + NotificationDocumentDto, + ); + } + + @Get('/types') + @UserRoles(...ANY_AUTH_ROLE) + async listTypes() { + const types = await this.notificationDocumentService.fetchTypes(); + return this.mapper.mapArray(types, DocumentCode, DocumentTypeDto); + } + + @Get('/:uuid/open') + @UserRoles(...ANY_AUTH_ROLE) + async open(@Param('uuid') fileUuid: string) { + const document = await this.notificationDocumentService.get(fileUuid); + const url = await this.notificationDocumentService.getInlineUrl(document); + return { + url, + }; + } + + @Get('/:uuid/download') + @UserRoles(...ANY_AUTH_ROLE) + async download(@Param('uuid') fileUuid: string) { + const document = await this.notificationDocumentService.get(fileUuid); + const url = await this.notificationDocumentService.getDownloadUrl(document); + return { + url, + }; + } + + @Delete('/:uuid') + @UserRoles(...ANY_AUTH_ROLE) + async delete(@Param('uuid') fileUuid: string) { + const document = await this.notificationDocumentService.get(fileUuid); + await this.notificationDocumentService.delete(document); + return {}; + } + + private async saveUploadedFile(req, fileNumber: string) { + const documentType = req.body.documentType.value as DOCUMENT_TYPE; + const file = req.body.file; + const fileName = req.body.fileName.value as string; + const documentSource = req.body.source.value as DOCUMENT_SOURCE; + const visibilityFlags = req.body.visibilityFlags.value.split(', '); + + return await this.notificationDocumentService.attachDocument({ + fileNumber, + fileName, + file, + user: req.user.entity, + documentType: documentType as DOCUMENT_TYPE, + source: documentSource, + visibilityFlags, + system: DOCUMENT_SYSTEM.ALCS, + }); + } +} diff --git a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.dto.ts b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.dto.ts new file mode 100644 index 0000000000..4a909c1efc --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.dto.ts @@ -0,0 +1,29 @@ +import { AutoMap } from '@automapper/classes'; +import { DocumentTypeDto } from '../../../document/document.dto'; + +export class NotificationDocumentDto { + @AutoMap(() => String) + description?: string; + + @AutoMap() + uuid: string; + + @AutoMap(() => DocumentTypeDto) + type?: DocumentTypeDto; + + @AutoMap(() => [String]) + visibilityFlags: string[]; + + @AutoMap(() => [Number]) + evidentiaryRecordSorting?: number; + + //Document Fields + documentUuid: string; + fileName: string; + fileSize?: number; + source: string; + system: string; + mimeType: string; + uploadedBy: string; + uploadedAt: number; +} diff --git a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.entity.ts b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.entity.ts new file mode 100644 index 0000000000..c8c2fe60d0 --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.entity.ts @@ -0,0 +1,67 @@ +import { AutoMap } from '@automapper/classes'; +import { + BaseEntity, + Column, + Entity, + JoinColumn, + ManyToOne, + OneToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; +import { DocumentCode } from '../../../document/document-code.entity'; +import { Document } from '../../../document/document.entity'; +import { Notification } from '../notification.entity'; + +export enum VISIBILITY_FLAG { + APPLICANT = 'A', + COMMISSIONER = 'C', + PUBLIC = 'P', + GOVERNMENT = 'G', +} + +@Entity() +export class NotificationDocument extends BaseEntity { + constructor(data?: Partial) { + super(); + if (data) { + Object.assign(this, data); + } + } + + @AutoMap() + @PrimaryGeneratedColumn('uuid') + uuid: string; + + @ManyToOne(() => DocumentCode) + type?: DocumentCode; + + @Column({ nullable: true }) + typeCode?: string | null; + + @Column({ type: 'text', nullable: true }) + description?: string | null; + + @ManyToOne(() => Notification, { nullable: false }) + notification: Notification; + + @Column() + notificationUuid: string; + + @Column({ nullable: true, type: 'uuid' }) + documentUuid?: string | null; + + @AutoMap(() => [String]) + @Column({ default: [], array: true, type: 'text' }) + visibilityFlags: VISIBILITY_FLAG[]; + + @OneToOne(() => Document) + @JoinColumn() + document: Document; + + @Column({ + nullable: true, + type: 'text', + comment: 'used only for oats etl process', + }) + auditCreatedBy?: string | null; +} diff --git a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.spec.ts b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.spec.ts new file mode 100644 index 0000000000..cc44d8acfd --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.spec.ts @@ -0,0 +1,342 @@ +import { ServiceNotFoundException } from '@app/common/exceptions/base.exception'; +import { MultipartFile } from '@fastify/multipart'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { + DOCUMENT_TYPE, + DocumentCode, +} from '../../../document/document-code.entity'; +import { + DOCUMENT_SOURCE, + DOCUMENT_SYSTEM, +} from '../../../document/document.dto'; +import { Document } from '../../../document/document.entity'; +import { DocumentService } from '../../../document/document.service'; +import { User } from '../../../user/user.entity'; +import { UserService } from '../../../user/user.service'; +import { Notification } from '../notification.entity'; +import { NotificationService } from '../notification.service'; +import { NotificationDocument } from './notification-document.entity'; +import { NotificationDocumentService } from './notification-document.service'; + +describe('NotificationDocumentService', () => { + let service: NotificationDocumentService; + let mockDocumentService: DeepMocked; + let mockNotificationService: DeepMocked; + let mockRepository: DeepMocked>; + let mockTypeRepository: DeepMocked>; + + let mockNotification; + const fileNumber = '12345'; + + beforeEach(async () => { + mockDocumentService = createMock(); + mockNotificationService = createMock(); + mockRepository = createMock(); + mockTypeRepository = createMock(); + + mockNotification = new Notification(); + mockNotificationService.getByFileNumber.mockResolvedValue(mockNotification); + mockDocumentService.create.mockResolvedValue({} as Document); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NotificationDocumentService, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: NotificationService, + useValue: mockNotificationService, + }, + { + provide: getRepositoryToken(DocumentCode), + useValue: mockTypeRepository, + }, + { + provide: getRepositoryToken(NotificationDocument), + useValue: mockRepository, + }, + { + provide: UserService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get( + NotificationDocumentService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should create a document in the happy path', async () => { + const mockUser = new User(); + const mockFile = {}; + const mockSavedDocument = {}; + + mockRepository.save.mockResolvedValue( + mockSavedDocument as NotificationDocument, + ); + + const res = await service.attachDocument({ + fileNumber, + file: mockFile as MultipartFile, + user: mockUser, + documentType: DOCUMENT_TYPE.DECISION_DOCUMENT, + fileName: '', + source: DOCUMENT_SOURCE.APPLICANT, + system: DOCUMENT_SYSTEM.PORTAL, + visibilityFlags: [], + }); + + expect(mockNotificationService.getByFileNumber).toHaveBeenCalledTimes(1); + expect(mockDocumentService.create).toHaveBeenCalledTimes(1); + expect(mockDocumentService.create.mock.calls[0][0]).toBe( + 'notification/12345', + ); + expect(mockDocumentService.create.mock.calls[0][2]).toBe(mockFile); + expect(mockDocumentService.create.mock.calls[0][3]).toBe(mockUser); + + expect(mockRepository.save).toHaveBeenCalledTimes(1); + expect(mockRepository.save.mock.calls[0][0].notification).toBe( + mockNotification, + ); + + expect(res).toBe(mockSavedDocument); + }); + + it('should delete document and application document when deleting', async () => { + const mockDocument = {}; + const mockAppDocument = { + uuid: '1', + document: mockDocument, + } as NotificationDocument; + + mockDocumentService.softRemove.mockResolvedValue(); + mockRepository.remove.mockResolvedValue({} as any); + + await service.delete(mockAppDocument); + + expect(mockDocumentService.softRemove).toHaveBeenCalledTimes(1); + expect(mockDocumentService.softRemove.mock.calls[0][0]).toBe(mockDocument); + + expect(mockRepository.remove).toHaveBeenCalledTimes(1); + expect(mockRepository.remove.mock.calls[0][0]).toBe(mockAppDocument); + }); + + it('should call through for get', async () => { + const mockDocument = {}; + const mockAppDocument = { + uuid: '1', + document: mockDocument, + } as NotificationDocument; + + mockDocumentService.softRemove.mockResolvedValue(); + mockRepository.findOne.mockResolvedValue(mockAppDocument); + + const res = await service.get('fake-uuid'); + expect(res).toBe(mockAppDocument); + }); + + it("should throw an exception when getting a document that doesn't exist", async () => { + const mockDocument = {}; + const mockAppDocument = { + uuid: '1', + document: mockDocument, + } as NotificationDocument; + + mockDocumentService.softRemove.mockResolvedValue(); + mockRepository.findOne.mockResolvedValue(null); + + await expect(service.get(mockAppDocument.uuid)).rejects.toMatchObject( + new ServiceNotFoundException( + `Failed to find document ${mockAppDocument.uuid}`, + ), + ); + }); + + it('should call through for list', async () => { + const mockDocument = {}; + const mockAppDocument = { + uuid: '1', + document: mockDocument, + } as NotificationDocument; + mockRepository.find.mockResolvedValue([mockAppDocument]); + + const res = await service.list(fileNumber); + + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(res[0]).toBe(mockAppDocument); + }); + + it('should call through for download', async () => { + const mockDocument = {}; + const mockAppDocument = { + uuid: '1', + document: mockDocument, + } as NotificationDocument; + + const fakeUrl = 'mock-url'; + mockDocumentService.getDownloadUrl.mockResolvedValue(fakeUrl); + + const res = await service.getInlineUrl(mockAppDocument); + + expect(mockDocumentService.getDownloadUrl).toHaveBeenCalledTimes(1); + expect(res).toEqual(fakeUrl); + }); + + it('should load all applicant sourced documents correctly', async () => { + const mockAppDocument = new NotificationDocument({ + uuid: '1', + document: new Document({ + source: DOCUMENT_SOURCE.APPLICANT, + }), + }); + const mockLgDocument = new NotificationDocument({ + uuid: '2', + document: new Document({ + source: DOCUMENT_SOURCE.LFNG, + }), + }); + + mockRepository.find.mockResolvedValue([mockAppDocument, mockLgDocument]); + + const res = await service.getApplicantDocuments('1'); + + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(res.length).toEqual(1); + expect(res[0]).toBe(mockAppDocument); + }); + + it('should call delete for each document loaded', async () => { + const mockAppDocument = new NotificationDocument({ + uuid: '1', + document: new Document({ + source: DOCUMENT_SOURCE.APPLICANT, + }), + }); + const mockLgDocument = new NotificationDocument({ + uuid: '2', + document: new Document({ + source: DOCUMENT_SOURCE.LFNG, + }), + }); + + mockRepository.find.mockResolvedValue([mockAppDocument, mockLgDocument]); + mockDocumentService.softRemove.mockResolvedValue(); + mockRepository.remove.mockResolvedValue({} as any); + + const res = await service.deleteByType(DOCUMENT_TYPE.STAFF_REPORT, ''); + + expect(mockRepository.find).toHaveBeenCalledTimes(1); + expect(mockDocumentService.softRemove).toHaveBeenCalledTimes(2); + }); + + it('should call through for fetchTypes', async () => { + mockTypeRepository.find.mockResolvedValue([]); + + const res = await service.fetchTypes(); + + expect(mockTypeRepository.find).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + }); + + it('should set the type and description for multiple files', async () => { + const mockDocument1 = new NotificationDocument({ + typeCode: DOCUMENT_TYPE.DECISION_DOCUMENT, + description: undefined, + }); + const mockDocument2 = new NotificationDocument({ + typeCode: DOCUMENT_TYPE.DECISION_DOCUMENT, + description: undefined, + }); + mockRepository.findOne + .mockResolvedValueOnce(mockDocument1) + .mockResolvedValueOnce(mockDocument2); + mockRepository.save.mockResolvedValue(new NotificationDocument()); + const mockUpdates = [ + { + uuid: '1', + type: DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + description: 'Secret Documents', + }, + { + uuid: '2', + type: DOCUMENT_TYPE.RESOLUTION_DOCUMENT, + description: 'New Description', + }, + ]; + + const res = await service.updateDescriptionAndType(mockUpdates, ''); + + expect(mockRepository.findOne).toHaveBeenCalledTimes(2); + expect(mockRepository.save).toHaveBeenCalledTimes(2); + expect(res).toBeDefined(); + expect(res.length).toEqual(2); + expect(mockDocument1.typeCode).toEqual(DOCUMENT_TYPE.CERTIFICATE_OF_TITLE); + expect(mockDocument1.description).toEqual('Secret Documents'); + expect(mockDocument2.typeCode).toEqual(DOCUMENT_TYPE.RESOLUTION_DOCUMENT); + expect(mockDocument2.description).toEqual('New Description'); + }); + + it('should create a record for external documents', async () => { + mockRepository.save.mockResolvedValue(new NotificationDocument()); + mockNotificationService.getUuid.mockResolvedValueOnce('app-uuid'); + mockRepository.findOne.mockResolvedValue(new NotificationDocument()); + + const res = await service.attachExternalDocument( + '', + { + type: DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + description: '', + documentUuid: 'fake-uuid', + }, + [], + ); + + expect(mockNotificationService.getUuid).toHaveBeenCalledTimes(1); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + expect(mockRepository.save.mock.calls[0][0].notificationUuid).toEqual( + 'app-uuid', + ); + expect(mockRepository.save.mock.calls[0][0].typeCode).toEqual( + DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + ); + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + expect(res).toBeDefined(); + }); + + it('should delete the existing file and create a new when updating', async () => { + mockRepository.findOne.mockResolvedValue( + new NotificationDocument({ + document: new Document(), + }), + ); + mockNotificationService.getFileNumber.mockResolvedValue('app-uuid'); + mockRepository.save.mockResolvedValue(new NotificationDocument()); + mockDocumentService.create.mockResolvedValue(new Document()); + mockDocumentService.softRemove.mockResolvedValue(); + + const res = await service.update({ + source: DOCUMENT_SOURCE.APPLICANT, + fileName: 'fileName', + user: new User(), + file: {} as File, + uuid: '', + documentType: DOCUMENT_TYPE.DECISION_DOCUMENT, + visibilityFlags: [], + }); + + expect(mockRepository.findOne).toHaveBeenCalledTimes(1); + expect(mockNotificationService.getFileNumber).toHaveBeenCalledTimes(1); + expect(mockDocumentService.create).toHaveBeenCalledTimes(1); + expect(mockRepository.save).toHaveBeenCalledTimes(1); + }); +}); diff --git a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.ts b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.ts new file mode 100644 index 0000000000..fbca313a7a --- /dev/null +++ b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.ts @@ -0,0 +1,305 @@ +import { MultipartFile } from '@fastify/multipart'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { + ArrayOverlap, + FindOptionsRelations, + FindOptionsWhere, + Repository, +} from 'typeorm'; +import { + DOCUMENT_TYPE, + DocumentCode, +} from '../../../document/document-code.entity'; +import { + DOCUMENT_SOURCE, + DOCUMENT_SYSTEM, +} from '../../../document/document.dto'; +import { DocumentService } from '../../../document/document.service'; +import { PortalNotificationDocumentUpdateDto } from '../../../portal/notification-document/notification-document.dto'; +import { User } from '../../../user/user.entity'; +import { NotificationService } from '../notification.service'; +import { + NotificationDocument, + VISIBILITY_FLAG, +} from './notification-document.entity'; + +@Injectable() +export class NotificationDocumentService { + private DEFAULT_RELATIONS: FindOptionsRelations = { + document: true, + type: true, + }; + + constructor( + private documentService: DocumentService, + private notificationService: NotificationService, + @InjectRepository(NotificationDocument) + private notificationDocumentRepository: Repository, + @InjectRepository(DocumentCode) + private documentCodeRepository: Repository, + ) {} + + async attachDocument({ + fileNumber, + fileName, + file, + documentType, + user, + system, + source = DOCUMENT_SOURCE.ALC, + visibilityFlags, + }: { + fileNumber: string; + fileName: string; + file: MultipartFile; + user: User; + documentType: DOCUMENT_TYPE; + source?: DOCUMENT_SOURCE; + system: DOCUMENT_SYSTEM; + visibilityFlags: VISIBILITY_FLAG[]; + }) { + const notification = await this.notificationService.getByFileNumber( + fileNumber, + ); + const document = await this.documentService.create( + `notification/${fileNumber}`, + fileName, + file, + user, + source, + system, + ); + const appDocument = new NotificationDocument({ + typeCode: documentType, + notification, + document, + visibilityFlags, + }); + + return this.notificationDocumentRepository.save(appDocument); + } + + async attachDocumentAsBuffer({ + fileNumber, + fileName, + file, + mimeType, + fileSize, + documentType, + user, + system, + source = DOCUMENT_SOURCE.ALC, + visibilityFlags, + }: { + fileNumber: string; + fileName: string; + file: Buffer; + mimeType: string; + fileSize: number; + user: User; + documentType: DOCUMENT_TYPE; + source?: DOCUMENT_SOURCE; + system: DOCUMENT_SYSTEM; + visibilityFlags: VISIBILITY_FLAG[]; + }) { + const notification = await this.notificationService.getByFileNumber( + fileNumber, + ); + const document = await this.documentService.createFromBuffer( + `notification/${fileNumber}`, + fileName, + file, + mimeType, + fileSize, + user, + source, + system, + ); + const appDocument = new NotificationDocument({ + typeCode: documentType, + notification, + document, + visibilityFlags, + }); + + return this.notificationDocumentRepository.save(appDocument); + } + + async get(uuid: string) { + const document = await this.notificationDocumentRepository.findOne({ + where: { + uuid: uuid, + }, + relations: this.DEFAULT_RELATIONS, + }); + if (!document) { + throw new NotFoundException(`Failed to find document ${uuid}`); + } + return document; + } + + async delete(document: NotificationDocument) { + await this.notificationDocumentRepository.remove(document); + await this.documentService.softRemove(document.document); + return document; + } + + async list(fileNumber: string, visibilityFlags?: VISIBILITY_FLAG[]) { + const where: FindOptionsWhere = { + notification: { + fileNumber, + }, + }; + if (visibilityFlags) { + where.visibilityFlags = ArrayOverlap(visibilityFlags); + } + return this.notificationDocumentRepository.find({ + where, + order: { + document: { + uploadedAt: 'DESC', + }, + }, + relations: this.DEFAULT_RELATIONS, + }); + } + + async getInlineUrl(document: NotificationDocument) { + return this.documentService.getDownloadUrl(document.document, true); + } + + async getDownloadUrl(document: NotificationDocument) { + return this.documentService.getDownloadUrl(document.document); + } + + async attachExternalDocument( + fileNumber: string, + data: { + type?: DOCUMENT_TYPE; + documentUuid: string; + description?: string; + }, + visibilityFlags: VISIBILITY_FLAG[], + ) { + const notificationUuid = await this.notificationService.getUuid(fileNumber); + const document = new NotificationDocument({ + notificationUuid, + typeCode: data.type, + documentUuid: data.documentUuid, + description: data.description, + visibilityFlags, + }); + + const savedDocument = await this.notificationDocumentRepository.save( + document, + ); + return this.get(savedDocument.uuid); + } + + async updateDescriptionAndType( + updates: PortalNotificationDocumentUpdateDto[], + notificationUuid: string, + ) { + const results: NotificationDocument[] = []; + for (const update of updates) { + const file = await this.notificationDocumentRepository.findOne({ + where: { + uuid: update.uuid, + notificationUuid, + }, + relations: { + document: true, + }, + }); + if (!file) { + throw new BadRequestException( + 'Failed to find file linked to provided notification', + ); + } + + file.typeCode = update.type; + file.description = update.description; + const updatedFile = await this.notificationDocumentRepository.save(file); + results.push(updatedFile); + } + return results; + } + + async deleteByType(documentType: DOCUMENT_TYPE, notificationUuid: string) { + const documents = await this.notificationDocumentRepository.find({ + where: { + notificationUuid, + typeCode: documentType, + }, + relations: { + document: true, + }, + }); + for (const document of documents) { + await this.documentService.softRemove(document.document); + await this.notificationDocumentRepository.remove(document); + } + + return; + } + + async getApplicantDocuments(fileNumber: string) { + const documents = await this.list(fileNumber); + return documents.filter( + (doc) => doc.document.source === DOCUMENT_SOURCE.APPLICANT, + ); + } + + async fetchTypes() { + return await this.documentCodeRepository.find(); + } + + async update({ + uuid, + documentType, + file, + fileName, + source, + visibilityFlags, + user, + }: { + uuid: string; + file?: any; + fileName: string; + documentType: DOCUMENT_TYPE; + visibilityFlags: VISIBILITY_FLAG[]; + source: DOCUMENT_SOURCE; + user: User; + }) { + const notificationDocument = await this.get(uuid); + + if (file) { + const fileNumber = await this.notificationService.getFileNumber( + notificationDocument.notificationUuid, + ); + await this.documentService.softRemove(notificationDocument.document); + notificationDocument.document = await this.documentService.create( + `notification/${fileNumber}`, + fileName, + file, + user, + source, + notificationDocument.document.system as DOCUMENT_SYSTEM, + ); + } else { + await this.documentService.update(notificationDocument.document, { + fileName, + source, + }); + } + notificationDocument.type = undefined; + notificationDocument.typeCode = documentType; + notificationDocument.visibilityFlags = visibilityFlags; + return await this.notificationDocumentRepository.save(notificationDocument); + } +} diff --git a/services/apps/alcs/src/alcs/notification/notification.entity.ts b/services/apps/alcs/src/alcs/notification/notification.entity.ts index c8ce3c0544..f594854341 100644 --- a/services/apps/alcs/src/alcs/notification/notification.entity.ts +++ b/services/apps/alcs/src/alcs/notification/notification.entity.ts @@ -6,12 +6,14 @@ import { Index, JoinColumn, ManyToOne, + OneToMany, OneToOne, } from 'typeorm'; import { Base } from '../../common/entities/base.entity'; import { Card } from '../card/card.entity'; import { ApplicationRegion } from '../code/application-code/application-region/application-region.entity'; import { LocalGovernment } from '../local-government/local-government.entity'; +import { NotificationDocument } from './notification-document/notification-document.entity'; import { NotificationType } from './notification-type/notification-type.entity'; @Entity() @@ -79,4 +81,8 @@ export class Notification extends Base { @Column() typeCode: string; + + @AutoMap() + @OneToMany(() => NotificationDocument, (document) => document.notification) + documents: NotificationDocument[]; } diff --git a/services/apps/alcs/src/alcs/notification/notification.module.ts b/services/apps/alcs/src/alcs/notification/notification.module.ts index 626e18522d..ee4d21c5e8 100644 --- a/services/apps/alcs/src/alcs/notification/notification.module.ts +++ b/services/apps/alcs/src/alcs/notification/notification.module.ts @@ -8,6 +8,9 @@ import { BoardModule } from '../board/board.module'; import { CardModule } from '../card/card.module'; import { CodeModule } from '../code/code.module'; import { LocalGovernmentModule } from '../local-government/local-government.module'; +import { NotificationDocumentController } from './notification-document/notification-document.controller'; +import { NotificationDocument } from './notification-document/notification-document.entity'; +import { NotificationDocumentService } from './notification-document/notification-document.service'; import { NotificationSubmissionStatusModule } from './notification-submission-status/notification-submission-status.module'; import { NotificationType } from './notification-type/notification-type.entity'; import { NotificationController } from './notification.controller'; @@ -16,7 +19,12 @@ import { Notification } from './notification.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([Notification, NotificationType, DocumentCode]), + TypeOrmModule.forFeature([ + Notification, + NotificationType, + NotificationDocument, + DocumentCode, + ]), forwardRef(() => BoardModule), CardModule, FileNumberModule, @@ -25,8 +33,12 @@ import { Notification } from './notification.entity'; LocalGovernmentModule, NotificationSubmissionStatusModule, ], - providers: [NotificationService, NotificationProfile], - controllers: [NotificationController], - exports: [NotificationService], + providers: [ + NotificationService, + NotificationProfile, + NotificationDocumentService, + ], + controllers: [NotificationController, NotificationDocumentController], + exports: [NotificationService, NotificationDocumentService], }) export class NotificationModule {} diff --git a/services/apps/alcs/src/common/automapper/notification.automapper.profile.ts b/services/apps/alcs/src/common/automapper/notification.automapper.profile.ts index 9e5838daaa..b9bce4509a 100644 --- a/services/apps/alcs/src/common/automapper/notification.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/notification.automapper.profile.ts @@ -1,12 +1,14 @@ import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; +import { NotificationDocumentDto } from '../../alcs/notification/notification-document/notification-document.dto'; +import { NotificationDocument } from '../../alcs/notification/notification-document/notification-document.entity'; import { NotificationTypeDto } from '../../alcs/notification/notification-type/notification-type.dto'; import { NotificationType } from '../../alcs/notification/notification-type/notification-type.entity'; import { NotificationDto } from '../../alcs/notification/notification.dto'; +import { Notification } from '../../alcs/notification/notification.entity'; import { DocumentCode } from '../../document/document-code.entity'; import { DocumentTypeDto } from '../../document/document.dto'; -import { Notification } from '../../alcs/notification/notification.entity'; @Injectable() export class NotificationProfile extends AutomapperProfile { @@ -28,6 +30,43 @@ export class NotificationProfile extends AutomapperProfile { ), ); + createMap( + mapper, + NotificationDocument, + NotificationDocumentDto, + 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.uploadedBy, + mapFrom((ad) => ad.document.uploadedBy?.name), + ), + 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), + ), + forMember( + (a) => a.system, + mapFrom((ad) => ad.document.system), + ), + ); createMap(mapper, DocumentCode, DocumentTypeDto); }; } diff --git a/services/apps/alcs/src/portal/notification-document/notification-document.controller.spec.ts b/services/apps/alcs/src/portal/notification-document/notification-document.controller.spec.ts new file mode 100644 index 0000000000..6073fc54db --- /dev/null +++ b/services/apps/alcs/src/portal/notification-document/notification-document.controller.spec.ts @@ -0,0 +1,197 @@ +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ClsService } from 'nestjs-cls'; +import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; +import { NotificationDocument } from '../../alcs/notification/notification-document/notification-document.entity'; +import { NotificationDocumentService } from '../../alcs/notification/notification-document/notification-document.service'; +import { NotificationService } from '../../alcs/notification/notification.service'; +import { NotificationProfile } from '../../common/automapper/notification.automapper.profile'; +import { DocumentCode } from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM } from '../../document/document.dto'; +import { Document } from '../../document/document.entity'; +import { DocumentService } from '../../document/document.service'; +import { User } from '../../user/user.entity'; +import { NotificationSubmission } from '../notification-submission/notification-submission.entity'; +import { NotificationSubmissionService } from '../notification-submission/notification-submission.service'; +import { NotificationDocumentController } from './notification-document.controller'; +import { AttachExternalDocumentDto } from './notification-document.dto'; + +describe('NotificationDocumentController', () => { + let controller: NotificationDocumentController; + let mockNotificationDocumentService: DeepMocked; + let mockNotificationSubmissionService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockNotificationService: DeepMocked; + + const mockDocument = new NotificationDocument({ + document: new Document({ + fileName: 'fileName', + uploadedAt: new Date(), + uploadedBy: new User(), + }), + }); + + beforeEach(async () => { + mockNotificationDocumentService = createMock(); + mockDocumentService = createMock(); + mockNotificationSubmissionService = createMock(); + mockNotificationService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + controllers: [NotificationDocumentController], + providers: [ + NotificationProfile, + { + provide: NotificationDocumentService, + useValue: mockNotificationDocumentService, + }, + { + provide: ClsService, + useValue: {}, + }, + { + provide: NotificationSubmissionService, + useValue: mockNotificationSubmissionService, + }, + { + provide: DocumentService, + useValue: mockDocumentService, + }, + { + provide: NotificationService, + useValue: mockNotificationService, + }, + ...mockKeyCloakProviders, + ], + }).compile(); + controller = module.get( + NotificationDocumentController, + ); + + mockNotificationSubmissionService.getByFileNumber.mockResolvedValue( + new NotificationSubmission(), + ); + mockNotificationService.getUuid.mockResolvedValue('uuid'); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should call through to delete documents', async () => { + mockNotificationDocumentService.delete.mockResolvedValue(mockDocument); + mockNotificationDocumentService.get.mockResolvedValue(mockDocument); + + await controller.delete('fake-uuid', { + user: { + entity: {}, + }, + }); + + expect(mockNotificationDocumentService.get).toHaveBeenCalledTimes(1); + expect(mockNotificationDocumentService.delete).toHaveBeenCalledTimes(1); + }); + + it('should call through to update documents', async () => { + mockNotificationDocumentService.updateDescriptionAndType.mockResolvedValue( + [], + ); + + await controller.update( + 'file-number', + { + user: { + entity: {}, + }, + }, + [], + ); + + expect( + mockNotificationDocumentService.updateDescriptionAndType, + ).toHaveBeenCalledTimes(1); + }); + + it('should call through for open', async () => { + const fakeUrl = 'fake-url'; + mockNotificationDocumentService.getInlineUrl.mockResolvedValue(fakeUrl); + mockNotificationDocumentService.get.mockResolvedValue(mockDocument); + + const res = await controller.open('fake-uuid', { + user: { + entity: {}, + }, + }); + + expect(res.url).toEqual(fakeUrl); + }); + + it('should call through for download', async () => { + const fakeUrl = 'fake-url'; + mockNotificationDocumentService.getDownloadUrl.mockResolvedValue(fakeUrl); + mockNotificationDocumentService.get.mockResolvedValue(mockDocument); + + const res = await controller.download('fake-uuid', { + user: { + entity: {}, + }, + }); + + expect(res.url).toEqual(fakeUrl); + }); + + it('should call out to service to attach external document', async () => { + const user = { user: { entity: 'Bruce' } }; + const fakeUuid = 'fakeUuid'; + const docObj = new Document({ uuid: 'fake-uuid' }); + const userEntity = new User({ + name: user.user.entity, + }); + + const docDto: AttachExternalDocumentDto = { + fileSize: 0, + mimeType: 'mimeType', + fileName: 'fileName', + fileKey: 'fileKey', + source: DOCUMENT_SOURCE.APPLICANT, + }; + + mockDocumentService.createDocumentRecord.mockResolvedValue(docObj); + + mockNotificationDocumentService.attachExternalDocument.mockResolvedValue( + new NotificationDocument({ + notification: undefined, + type: new DocumentCode(), + uuid: fakeUuid, + document: new Document({ + uploadedAt: new Date(), + uploadedBy: userEntity, + }), + }), + ); + + const res = await controller.attachExternalDocument( + 'fake-number', + docDto, + user, + ); + + expect(mockDocumentService.createDocumentRecord).toBeCalledTimes(1); + expect( + mockNotificationDocumentService.attachExternalDocument, + ).toBeCalledTimes(1); + expect(mockDocumentService.createDocumentRecord).toBeCalledWith({ + ...docDto, + system: DOCUMENT_SYSTEM.PORTAL, + }); + expect(res.uploadedBy).toEqual(user.user.entity); + expect(res.uuid).toEqual(fakeUuid); + }); +}); diff --git a/services/apps/alcs/src/portal/notification-document/notification-document.controller.ts b/services/apps/alcs/src/portal/notification-document/notification-document.controller.ts new file mode 100644 index 0000000000..cfe6e67121 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-document/notification-document.controller.ts @@ -0,0 +1,172 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Req, + UseGuards, +} from '@nestjs/common'; +import { ApiOAuth2 } from '@nestjs/swagger'; +import * as config from 'config'; +import { NotificationDocumentDto } from '../../alcs/notification/notification-document/notification-document.dto'; +import { + NotificationDocument, + VISIBILITY_FLAG, +} from '../../alcs/notification/notification-document/notification-document.entity'; +import { NotificationDocumentService } from '../../alcs/notification/notification-document/notification-document.service'; +import { NotificationService } from '../../alcs/notification/notification.service'; +import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { DOCUMENT_SYSTEM } from '../../document/document.dto'; +import { DocumentService } from '../../document/document.service'; +import { NotificationSubmissionService } from '../notification-submission/notification-submission.service'; +import { + AttachExternalDocumentDto, + PortalNotificationDocumentUpdateDto, +} from './notification-document.dto'; + +@ApiOAuth2(config.get('KEYCLOAK.SCOPES')) +@UseGuards(PortalAuthGuard) +@Controller('notification-document') +export class NotificationDocumentController { + constructor( + private notificationDocumentService: NotificationDocumentService, + private notificationSubmissionService: NotificationSubmissionService, + private notificationService: NotificationService, + private documentService: DocumentService, + @InjectMapper() private mapper: Mapper, + ) {} + + @Get('/notification/:fileNumber') + async listApplicantDocuments( + @Param('fileNumber') fileNumber: string, + @Param('documentType') documentType: DOCUMENT_TYPE | null, + @Req() req, + ): Promise { + await this.notificationSubmissionService.getByFileNumber( + fileNumber, + req.user.entity, + ); + + const documents = await this.notificationDocumentService.list(fileNumber, [ + VISIBILITY_FLAG.APPLICANT, + ]); + return this.mapPortalDocuments(documents); + } + + @Get('/:uuid/open') + async open(@Param('uuid') fileUuid: string, @Req() req) { + const document = await this.notificationDocumentService.get(fileUuid); + + //TODO: How do we know which documents applicant can access? + // await this.notificationSubmissionService.verifyAccess( + // document.applicationUuid, + // req.user.entity, + // ); + + const url = await this.notificationDocumentService.getInlineUrl(document); + return { url }; + } + + @Get('/:uuid/download') + async download(@Param('uuid') fileUuid: string, @Req() req) { + const document = await this.notificationDocumentService.get(fileUuid); + + //TODO: How do we know which documents applicant can access? + // await this.notificationSubmissionService.verifyAccess( + // document.applicationUuid, + // req.user.entity, + // ); + + const url = await this.notificationDocumentService.getDownloadUrl(document); + return { url }; + } + + @Patch('/notification/:fileNumber') + async update( + @Param('fileNumber') fileNumber: string, + @Req() req, + @Body() body: PortalNotificationDocumentUpdateDto[], + ) { + await this.notificationSubmissionService.getByFileNumber( + fileNumber, + req.user.entity, + ); + + //Map from file number to uuid + const notificationUuid = await this.notificationService.getUuid(fileNumber); + + const res = await this.notificationDocumentService.updateDescriptionAndType( + body, + notificationUuid, + ); + return this.mapPortalDocuments(res); + } + + @Delete('/:uuid') + async delete(@Param('uuid') fileUuid: string, @Req() req) { + const document = await this.notificationDocumentService.get(fileUuid); + + //TODO: How do we know which documents applicant can delete? + // await this.notificationSubmissionService.verifyAccess( + // document.applicationUuid, + // req.user.entity, + // ); + + await this.notificationDocumentService.delete(document); + return {}; + } + + @Post('/notification/:uuid/attachExternal') + async attachExternalDocument( + @Param('uuid') fileNumber: string, + @Body() data: AttachExternalDocumentDto, + @Req() req, + ): Promise { + const submission = await this.notificationSubmissionService.getByFileNumber( + fileNumber, + req.user.entity, + ); + + const document = await this.documentService.createDocumentRecord({ + ...data, + system: DOCUMENT_SYSTEM.PORTAL, + }); + + const savedDocument = + await this.notificationDocumentService.attachExternalDocument( + submission.fileNumber, + { + documentUuid: document.uuid, + type: data.documentType, + }, + [ + VISIBILITY_FLAG.APPLICANT, + VISIBILITY_FLAG.GOVERNMENT, + VISIBILITY_FLAG.COMMISSIONER, + ], + ); + + const mappedDocs = this.mapPortalDocuments([savedDocument]); + return mappedDocs[0]; + } + + private mapPortalDocuments(documents: NotificationDocument[]) { + const labeledDocuments = documents.map((document) => { + if (document.type?.portalLabel) { + document.type.label = document.type.portalLabel; + } + return document; + }); + return this.mapper.mapArray( + labeledDocuments, + NotificationDocument, + NotificationDocumentDto, + ); + } +} diff --git a/services/apps/alcs/src/portal/notification-document/notification-document.dto.ts b/services/apps/alcs/src/portal/notification-document/notification-document.dto.ts new file mode 100644 index 0000000000..d377534e57 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-document/notification-document.dto.ts @@ -0,0 +1,30 @@ +import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE } from '../../document/document.dto'; + +export class AttachExternalDocumentDto { + @IsString() + mimeType: string; + + @IsString() + fileName: string; + + @IsNumber() + fileSize: number; + + @IsString() + fileKey: string; + + @IsString() + source: DOCUMENT_SOURCE.APPLICANT; + + @IsString() + @IsOptional() + documentType?: DOCUMENT_TYPE; +} + +export class PortalNotificationDocumentUpdateDto { + uuid: string; + type: DOCUMENT_TYPE | null; + description: string | null; +} diff --git a/services/apps/alcs/src/portal/notification-document/notification-document.module.ts b/services/apps/alcs/src/portal/notification-document/notification-document.module.ts new file mode 100644 index 0000000000..fb544d8268 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-document/notification-document.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { NotificationModule } from '../../alcs/notification/notification.module'; +import { DocumentModule } from '../../document/document.module'; +import { NotificationSubmissionModule } from '../notification-submission/notification-submission.module'; +import { NotificationDocumentController } from './notification-document.controller'; + +@Module({ + imports: [DocumentModule, NotificationModule, NotificationSubmissionModule], + controllers: [NotificationDocumentController], + providers: [], + exports: [], +}) +export class PortalNotificationDocumentModule {} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts index 1735fe4aae..73f24fdeac 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts @@ -8,9 +8,9 @@ import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; import { NoticeOfIntentDocumentService } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; -import { NoticeOfIntent } from '../../alcs/notice-of-intent/notice-of-intent.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 { Notification } from '../../alcs/notification/notification.entity'; import { NotificationSubmissionProfile } from '../../common/automapper/notification-submission.automapper.profile'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; @@ -267,7 +267,7 @@ describe('NotificationSubmissionController', () => { }); mockNotificationSubmissionService.submitToAlcs.mockResolvedValue( - new NoticeOfIntent(), + new Notification(), ); mockNotificationSubmissionService.getByUuid.mockResolvedValue( mockSubmission, diff --git a/services/apps/alcs/src/portal/portal.module.ts b/services/apps/alcs/src/portal/portal.module.ts index 89269be6d1..f7b1ebb88a 100644 --- a/services/apps/alcs/src/portal/portal.module.ts +++ b/services/apps/alcs/src/portal/portal.module.ts @@ -16,6 +16,7 @@ import { PortalNoticeOfIntentDecisionModule } from './notice-of-intent-decision/ import { PortalNoticeOfIntentDocumentModule } from './notice-of-intent-document/notice-of-intent-document.module'; import { NoticeOfIntentSubmissionDraftModule } from './notice-of-intent-submission-draft/notice-of-intent-submission-draft.module'; import { NoticeOfIntentSubmissionModule } from './notice-of-intent-submission/notice-of-intent-submission.module'; +import { PortalNotificationDocumentModule } from './notification-document/notification-document.module'; import { ParcelModule } from './parcel/parcel.module'; import { PdfGenerationModule } from './pdf-generation/pdf-generation.module'; import { NotificationSubmissionModule } from './notification-submission/notification-submission.module'; @@ -40,6 +41,7 @@ import { NotificationSubmissionModule } from './notification-submission/notifica NoticeOfIntentSubmissionDraftModule, PortalNoticeOfIntentDecisionModule, NotificationSubmissionModule, + PortalNotificationDocumentModule, RouterModule.register([ { path: 'portal', module: ApplicationSubmissionModule }, { path: 'portal', module: NoticeOfIntentSubmissionModule }, @@ -54,6 +56,7 @@ import { NotificationSubmissionModule } from './notification-submission/notifica { path: 'portal', module: NoticeOfIntentSubmissionDraftModule }, { path: 'portal', module: PortalNoticeOfIntentDecisionModule }, { path: 'portal', module: NotificationSubmissionModule }, + { path: 'portal', module: PortalNotificationDocumentModule }, ]), ], controllers: [CodeController], diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1694037493007-add_notification_documents.ts b/services/apps/alcs/src/providers/typeorm/migrations/1694037493007-add_notification_documents.ts new file mode 100644 index 0000000000..9e80077254 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1694037493007-add_notification_documents.ts @@ -0,0 +1,35 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNotificationDocuments1694037493007 + implements MigrationInterface +{ + name = 'addNotificationDocuments1694037493007'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "alcs"."notification_document" ("uuid" uuid NOT NULL DEFAULT gen_random_uuid(), "type_code" text, "description" text, "notification_uuid" uuid NOT NULL, "document_uuid" uuid, "visibility_flags" text array NOT NULL DEFAULT '{}', "audit_created_by" text, CONSTRAINT "REL_754c65b2ab78e39c64c31f2f9f" UNIQUE ("document_uuid"), CONSTRAINT "PK_cb4155e1f9d5b5ebd27c8de6381" PRIMARY KEY ("uuid")); COMMENT ON COLUMN "alcs"."notification_document"."audit_created_by" IS 'used only for oats etl process'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" ADD CONSTRAINT "FK_dc6a7789a73ec2e0eac2b2307d3" FOREIGN KEY ("type_code") REFERENCES "alcs"."document_code"("code") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" ADD CONSTRAINT "FK_fdb3697b2dfc6ee1e72e85b01e2" FOREIGN KEY ("notification_uuid") REFERENCES "alcs"."notification"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" ADD CONSTRAINT "FK_754c65b2ab78e39c64c31f2f9f9" FOREIGN KEY ("document_uuid") REFERENCES "alcs"."document"("uuid") ON DELETE NO ACTION ON UPDATE NO ACTION`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" DROP CONSTRAINT "FK_754c65b2ab78e39c64c31f2f9f9"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" DROP CONSTRAINT "FK_fdb3697b2dfc6ee1e72e85b01e2"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" DROP CONSTRAINT "FK_dc6a7789a73ec2e0eac2b2307d3"`, + ); + await queryRunner.query(`DROP TABLE "alcs"."notification_document"`); + } +} From a70a4da33338ea17060bc78e121a9bb8d3797476 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Wed, 6 Sep 2023 17:22:18 -0700 Subject: [PATCH 354/954] added enum --- .../submissions/submap/soil_elements.py | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/bin/migrate-oats-data/submissions/submap/soil_elements.py b/bin/migrate-oats-data/submissions/submap/soil_elements.py index 0e7ed81ae8..1fab172e06 100644 --- a/bin/migrate-oats-data/submissions/submap/soil_elements.py +++ b/bin/migrate-oats-data/submissions/submap/soil_elements.py @@ -1,3 +1,4 @@ +from enum import Enum def get_soil_rows(rows, cursor): # fetches adjacent land use data, specifically direction, description and type code component_ids = [dict(item)["alr_appl_component_id"] for item in rows] @@ -12,6 +13,10 @@ def get_soil_rows(rows, cursor): def create_soil_dict(soil_rows): # creates dict contailing fill and remove data + + class SoilAction(Enum): + RMV = 'REMOVE' + ADD = 'ADD' alr_id = 'alr_appl_component_id' area = 'project_area' desc = 'material_desc' @@ -26,9 +31,9 @@ def create_soil_dict(soil_rows): app_component_id = row[alr_id] if app_component_id in soil_dict: - if row[code] == 'REMOVE': + if row[code] == SoilAction.RMV.value: if 'RMV' in soil_dict.get(app_component_id, {}): - print('ignored element_id:',row['soil_change_element_id']) + print('ignored element_id:',row['soil_change_element_id']) else: soil_dict[app_component_id]['remove_type'] = row[desc] soil_dict[app_component_id]['remove_origin'] = row[origin_desc] @@ -39,10 +44,10 @@ def create_soil_dict(soil_rows): if 'import_fill' not in soil_dict.get(app_component_id, {}): soil_dict[app_component_id]['import_fill'] = False - soil_dict[app_component_id]['RMV'] = True + soil_dict[app_component_id][SoilAction.RMV.name] = True - elif row[code] == 'ADD': + elif row[code] == SoilAction.ADD.value: if 'ADD' in soil_dict.get(app_component_id, {}): print('ignored element_id:',row['soil_change_element_id']) else: @@ -54,25 +59,25 @@ def create_soil_dict(soil_rows): soil_dict[app_component_id]['fill_area'] = row[area] soil_dict[app_component_id]['import_fill'] = True - soil_dict[app_component_id]['ADD'] = True + soil_dict[app_component_id][SoilAction.ADD.name] = True else: print('unknown soil action') else: soil_dict[app_component_id] = {} soil_dict[app_component_id][alr_id] = row[alr_id] - if row[code] == 'REMOVE': + if row[code] == SoilAction.RMV.value: soil_dict[app_component_id]['remove_type'] = row[desc] soil_dict[app_component_id]['remove_origin'] = row[origin_desc] soil_dict[app_component_id]['max_remove_depth'] = row[depth] soil_dict[app_component_id]['total_remove'] = row[volume] soil_dict[app_component_id]['remove_duration'] = row[duration] soil_dict[app_component_id]['remove_area'] = row[area] - soil_dict[app_component_id]['RMV'] = True + soil_dict[app_component_id][SoilAction.RMV.name] = True soil_dict[app_component_id]['import_fill'] = False - elif row[code] == 'ADD': + elif row[code] == SoilAction.ADD.value: soil_dict[app_component_id]['fill_type'] = row[desc] soil_dict[app_component_id]['fill_origin'] = row[origin_desc] soil_dict[app_component_id]['total_fill'] = row[volume] @@ -80,7 +85,7 @@ def create_soil_dict(soil_rows): soil_dict[app_component_id]['fill_duration'] = row[duration] soil_dict[app_component_id]['fill_area'] = row[area] soil_dict[app_component_id]['import_fill'] = True - soil_dict[app_component_id]['ADD'] = True + soil_dict[app_component_id][SoilAction.ADD.name] = True else: print('unknown soil action') From c52570a27a4c9b708636b553631a3bdf813d58c9 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 7 Sep 2023 10:16:52 -0700 Subject: [PATCH 355/954] Add Proposal Step for Notifications * Add new file specific fields for Survey Plans * Add new fields * Add table comments for Notification Tables --- .../edit-submission.component.html | 11 +- .../edit-submission.component.ts | 5 + .../edit-submission/edit-submission.module.ts | 2 + .../edit-submission/files-step.partial.ts | 4 +- .../proposal/proposal.component.html | 222 ++++++++++++++++++ .../proposal/proposal.component.scss | 5 + .../proposal/proposal.component.spec.ts | 61 +++++ .../proposal/proposal.component.ts | 150 ++++++++++++ .../notification-document.dto.ts | 6 +- .../notification-submission.dto.ts | 6 + .../src/app/shared/dto/document.dto.ts | 4 + .../notification-document.dto.ts | 8 +- .../notification-document.entity.ts | 6 + .../notification-document.service.ts | 2 + .../alcs/src/document/document-code.entity.ts | 2 + .../notification-document.dto.ts | 22 +- .../notification-submission.dto.ts | 30 ++- .../notification-submission.entity.ts | 23 ++ .../notification-submission.service.ts | 13 + ...857-add_plans_to_notification_documents.ts | 25 ++ ...94044003667-add_notification_srw_fields.ts | 34 +++ ...6690840-add_notification_table_comments.ts | 33 +++ 22 files changed, 666 insertions(+), 8 deletions(-) create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1694042348857-add_plans_to_notification_documents.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1694044003667-add_notification_srw_fields.ts create mode 100644 services/apps/alcs/src/providers/typeorm/migrations/1694106690840-add_notification_table_comments.ts diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html index 6e94936da9..8cd539d7f5 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html @@ -73,7 +73,16 @@
-
+
+ + +
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts index 695ab88ad4..2897bc6620 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts @@ -18,6 +18,7 @@ import { scrollToElement } from '../../../shared/utils/scroll-helper'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { ProposalComponent } from './proposal/proposal.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; export enum EditNotificationSteps { @@ -51,6 +52,7 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { @ViewChild(ParcelDetailsComponent) parcelDetailsComponent!: ParcelDetailsComponent; @ViewChild(PrimaryContactComponent) primaryContactComponent!: PrimaryContactComponent; @ViewChild(SelectGovernmentComponent) selectGovernmentComponent!: SelectGovernmentComponent; + @ViewChild(ProposalComponent) proposalComponent!: ProposalComponent; @ViewChild(OtherAttachmentsComponent) otherAttachmentsComponent!: OtherAttachmentsComponent; constructor( @@ -154,6 +156,9 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { } break; case EditNotificationSteps.Proposal: + if (this.proposalComponent) { + await this.proposalComponent.onSave(); + } break; case EditNotificationSteps.Attachments: if (this.otherAttachmentsComponent) { diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts index 08d1f751b4..691f4d8558 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts @@ -14,6 +14,7 @@ import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; +import { ProposalComponent } from './proposal/proposal.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; import { StepComponent } from './step.partial'; import { TransfereeDialogComponent } from './transferees/transferee-dialog/transferee-dialog.component'; @@ -43,6 +44,7 @@ const routes: Routes = [ TransfereeDialogComponent, PrimaryContactComponent, SelectGovernmentComponent, + ProposalComponent, OtherAttachmentsComponent, ], imports: [ diff --git a/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts index 152f173c0a..94ec045c94 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts @@ -1,6 +1,7 @@ import { Component, Input } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { BehaviorSubject } from 'rxjs'; +import { ApplicationDocumentDto } from '../../../services/application-document/application-document.dto'; import { NotificationDocumentDto } from '../../../services/notification-document/notification-document.dto'; import { NotificationDocumentService } from '../../../services/notification-document/notification-document.service'; import { DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; @@ -40,7 +41,8 @@ export abstract class FilesStepComponent extends StepComponent { } } - async onDeleteFile($event: NotificationDocumentDto) { + //Using ApplicationDocumentDto is "correct" here, quack quack + async onDeleteFile($event: ApplicationDocumentDto) { await this.notificationDocumentService.deleteExternalFile($event.uuid); if (this.fileId) { const documents = await this.notificationDocumentService.getByFileId(this.fileId); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.html b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.html new file mode 100644 index 0000000000..f4af76c4cf --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.html @@ -0,0 +1,222 @@ +
+

Purpose of SRW

+

All fields are required unless stated optional.

+
+ + The information entered below will be reflected in the automatically generated notification response and must match + the finalized SRW documents that you will submit to the Land Title Survey Authority + +
+
+
+ +
+ + + + +
+ warning +
This field is required
+
+
+
+
+ +
+ Include why you are placing a SRW and what the SRW will achieve. Include any other associated encumbrances, uses + or ancillary rights over the parcel(s) (e.g. for access through the parcel(s) to get to and from the SRW area). +
+ + + +
+ warning +
This field is required
+
+
Characters left: {{ 4000 - purposeText.textLength }}
+
+ +
+ +
Provide in hectares. If SRW is for the whole parcel, indicate total parcel area
+ + + ha + +
+ warning +
This field is required
+
+
+ +
+ +
The signed and finalized terms that are to be included in the SRW package to LTSA
+ +
+ +
+ +
+ + Yes + + No + + +
+
+ +
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
File Name + {{ element.fileName }} + Survey Plan Number + + + +
+ warning +
+ This field is required +
+
+
Control Number + + + +
+ warning +
+ This field is required +
+
+
Action + +
No attachments
+
+
+
+ +
+ + +
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.scss new file mode 100644 index 0000000000..8223c57759 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.scss @@ -0,0 +1,5 @@ +@use '../../../../../styles/functions' as *; + +section { + margin-top: rem(36); +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.spec.ts new file mode 100644 index 0000000000..21276e7013 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.spec.ts @@ -0,0 +1,61 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { NotificationDocumentDto } from '../../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../../services/notification-document/notification-document.service'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; + +import { ProposalComponent } from './proposal.component'; + +describe('ProposalComponent', () => { + let component: ProposalComponent; + let fixture: ComponentFixture; + let mockNotificationSubmissionService: DeepMocked; + let mockNotificationDocumentService: DeepMocked; + let mockRouter: DeepMocked; + + let documentPipe = new BehaviorSubject([]); + + beforeEach(async () => { + mockNotificationSubmissionService = createMock(); + mockRouter = createMock(); + mockNotificationDocumentService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: NotificationSubmissionService, + useValue: mockNotificationSubmissionService, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: NotificationDocumentService, + useValue: mockNotificationDocumentService, + }, + { + provide: MatDialog, + useValue: {}, + }, + ], + declarations: [ProposalComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ProposalComponent); + component = fixture.componentInstance; + component.$notificationSubmission = new BehaviorSubject(undefined); + component.$notificationDocuments = documentPipe; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts new file mode 100644 index 0000000000..68f2feb014 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts @@ -0,0 +1,150 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { takeUntil } from 'rxjs'; +import { + NotificationDocumentDto, + NotificationDocumentUpdateDto, +} from '../../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../../services/notification-document/notification-document.service'; +import { NotificationSubmissionUpdateDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; +import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; +import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; +import { EditNotificationSteps } from '../edit-submission.component'; +import { FilesStepComponent } from '../files-step.partial'; + +@Component({ + selector: 'app-proposal', + templateUrl: './proposal.component.html', + styleUrls: ['./proposal.component.scss'], +}) +export class ProposalComponent extends FilesStepComponent implements OnInit, OnDestroy { + currentStep = EditNotificationSteps.Proposal; + DOCUMENT = DOCUMENT_TYPE; + allowSurveyPlanUploads = false; + + terms: NotificationDocumentDto[] = []; + surveyPlans: NotificationDocumentDto[] = []; + displayedColumns = ['fileName', 'surveyPlan', 'control', 'actions']; + + purpose = new FormControl(null, [Validators.required]); + totalArea = new FormControl(null, [Validators.required]); + fileNumber = new FormControl(null, [Validators.required]); + hasSurveyPlan = new FormControl(null, [Validators.required]); + + form = new FormGroup({ + fileNumber: this.fileNumber, + purpose: this.purpose, + totalArea: this.totalArea, + hasSurveyPlan: this.hasSurveyPlan, + }); + + private submissionUuid = ''; + private isDirty = false; + surveyForm = new FormGroup({} as any); + + constructor( + private router: Router, + private notificationSubmissionService: NotificationSubmissionService, + notificationDocumentService: NotificationDocumentService, + dialog: MatDialog + ) { + super(notificationDocumentService, dialog); + } + + ngOnInit(): void { + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + if (submission) { + this.fileId = submission.fileNumber; + this.submissionUuid = submission.uuid; + + this.allowSurveyPlanUploads = !!submission.hasSurveyPlan; + + this.form.patchValue({ + fileNumber: submission.submittersFileNumber, + purpose: submission.purpose, + hasSurveyPlan: formatBooleanToString(submission.hasSurveyPlan), + totalArea: submission.totalArea?.toString(), + }); + if (this.showErrors) { + this.form.markAllAsTouched(); + } + } + }); + + this.$notificationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.terms = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.SRW_TERMS); + this.surveyPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.SURVEY_PLAN); + + const newForm = new FormGroup({}); + for (const file of this.surveyPlans) { + newForm.addControl(`${file.uuid}-surveyPlan`, new FormControl(file.surveyPlanNumber, [Validators.required])); + newForm.addControl(`${file.uuid}-control`, new FormControl(file.controlNumber, [Validators.required])); + } + this.surveyForm = newForm; + }); + } + + async onSave() { + await this.save(); + } + + protected async save() { + if (this.fileId && this.form.dirty) { + const purpose = this.purpose.value; + const submittersFileNumber = this.fileNumber.value; + const totalArea = this.totalArea.value; + const hasSurveyPlan = this.hasSurveyPlan.value; + + const updateDto: NotificationSubmissionUpdateDto = { + purpose, + submittersFileNumber, + totalArea: totalArea ? parseFloat(totalArea) : null, + hasSurveyPlan: parseStringToBoolean(hasSurveyPlan), + }; + + const updatedApp = await this.notificationSubmissionService.updatePending(this.submissionUuid, updateDto); + this.$notificationSubmission.next(updatedApp); + } + + if (this.isDirty) { + const updateDtos: NotificationDocumentUpdateDto[] = this.surveyPlans.map((file) => ({ + uuid: file.uuid, + surveyPlanNumber: file.surveyPlanNumber, + controlNumber: file.controlNumber, + })); + await this.notificationDocumentService.update(this.fileId, updateDtos); + } + } + + onChangeHasSurveyPlan(selectedValue: string) { + this.allowSurveyPlanUploads = selectedValue === 'true'; + } + + onChangeControlNumber(uuid: any, event: Event) { + this.isDirty = true; + const input = event.target as HTMLInputElement; + const controlPlanNumber = input.value; + this.surveyPlans = this.surveyPlans.map((file) => { + if (uuid === file.uuid) { + file.controlNumber = controlPlanNumber; + } + return file; + }); + } + + onChangeSurveyPlan(uuid: any, event: Event) { + this.isDirty = true; + const input = event.target as HTMLInputElement; + const surveyPlanNumber = input.value; + this.surveyPlans = this.surveyPlans.map((file) => { + if (uuid === file.uuid) { + file.surveyPlanNumber = surveyPlanNumber; + } + return file; + }); + } +} diff --git a/portal-frontend/src/app/services/notification-document/notification-document.dto.ts b/portal-frontend/src/app/services/notification-document/notification-document.dto.ts index 9e345f8edf..d40228dd49 100644 --- a/portal-frontend/src/app/services/notification-document/notification-document.dto.ts +++ b/portal-frontend/src/app/services/notification-document/notification-document.dto.ts @@ -9,10 +9,14 @@ export interface NotificationDocumentDto { uploadedBy: string; uploadedAt: number; source: DOCUMENT_SOURCE; + surveyPlanNumber: string | null; + controlNumber: string | null; } export interface NotificationDocumentUpdateDto { uuid: string; - type: DOCUMENT_TYPE | null; + type?: DOCUMENT_TYPE | null; description?: string | null; + surveyPlanNumber?: string | null; + controlNumber?: string | null; } diff --git a/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts b/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts index 07ed0ad669..f0507ee88b 100644 --- a/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts +++ b/portal-frontend/src/app/services/notification-submission/notification-submission.dto.ts @@ -44,11 +44,17 @@ export interface NotificationSubmissionDto { export interface NotificationSubmissionDetailedDto extends NotificationSubmissionDto { purpose: string | null; + submittersFileNumber: string | null; + totalArea: number | null; + hasSurveyPlan: boolean | null; } export interface NotificationSubmissionUpdateDto { applicant?: string | null; purpose?: string | null; + submittersFileNumber?: string | null; + totalArea?: number | null; + hasSurveyPlan?: boolean | null; localGovernmentUuid?: string | null; contactFirstName?: string | null; contactLastName?: string | null; diff --git a/portal-frontend/src/app/shared/dto/document.dto.ts b/portal-frontend/src/app/shared/dto/document.dto.ts index b68976bb1e..f859679a1c 100644 --- a/portal-frontend/src/app/shared/dto/document.dto.ts +++ b/portal-frontend/src/app/shared/dto/document.dto.ts @@ -29,6 +29,10 @@ export enum DOCUMENT_TYPE { //NOI DOCUMENTS BUILDING_PLAN = 'BLDP', + + //SRW DOCUMENTS + SRW_TERMS = 'SRTD', + SURVEY_PLAN = 'SURV', } export enum DOCUMENT_SOURCE { diff --git a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.dto.ts b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.dto.ts index 4a909c1efc..0ac94d8e01 100644 --- a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.dto.ts +++ b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.dto.ts @@ -14,9 +14,15 @@ export class NotificationDocumentDto { @AutoMap(() => [String]) visibilityFlags: string[]; - @AutoMap(() => [Number]) + @AutoMap(() => Number) evidentiaryRecordSorting?: number; + @AutoMap(() => String) + surveyPlanNumber: string | null; + + @AutoMap(() => String) + controlNumber: string | null; + //Document Fields documentUuid: string; fileName: string; diff --git a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.entity.ts b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.entity.ts index c8c2fe60d0..22756af305 100644 --- a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.entity.ts +++ b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.entity.ts @@ -41,6 +41,12 @@ export class NotificationDocument extends BaseEntity { @Column({ type: 'text', nullable: true }) description?: string | null; + @Column({ type: 'text', nullable: true }) + surveyPlanNumber?: string | null; + + @Column({ type: 'text', nullable: true }) + controlNumber?: string | null; + @ManyToOne(() => Notification, { nullable: false }) notification: Notification; diff --git a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.ts b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.ts index fbca313a7a..8fe160e506 100644 --- a/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.ts +++ b/services/apps/alcs/src/alcs/notification/notification-document/notification-document.service.ts @@ -224,6 +224,8 @@ export class NotificationDocumentService { file.typeCode = update.type; file.description = update.description; + file.controlNumber = update.controlNumber; + file.surveyPlanNumber = update.surveyPlanNumber; const updatedFile = await this.notificationDocumentRepository.save(file); results.push(updatedFile); } diff --git a/services/apps/alcs/src/document/document-code.entity.ts b/services/apps/alcs/src/document/document-code.entity.ts index b4618f9a4a..b06096c7cc 100644 --- a/services/apps/alcs/src/document/document-code.entity.ts +++ b/services/apps/alcs/src/document/document-code.entity.ts @@ -27,6 +27,8 @@ export enum DOCUMENT_TYPE { REPORT_OF_PUBLIC_HEARING = 'ROPH', PROOF_OF_ADVERTISING = 'POAA', BUILDING_PLAN = 'BLDP', + SRW_TERMS = 'SRTD', + SURVEY_PLAN = 'SURV', ORIGINAL_SUBMISSION = 'SUBO', UPDATED_SUBMISSION = 'SUBU', diff --git a/services/apps/alcs/src/portal/notification-document/notification-document.dto.ts b/services/apps/alcs/src/portal/notification-document/notification-document.dto.ts index d377534e57..0fed402dee 100644 --- a/services/apps/alcs/src/portal/notification-document/notification-document.dto.ts +++ b/services/apps/alcs/src/portal/notification-document/notification-document.dto.ts @@ -1,4 +1,5 @@ -import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { AutoMap } from '@automapper/classes'; +import { IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; import { DOCUMENT_TYPE } from '../../document/document-code.entity'; import { DOCUMENT_SOURCE } from '../../document/document.dto'; @@ -24,7 +25,22 @@ export class AttachExternalDocumentDto { } export class PortalNotificationDocumentUpdateDto { + @IsUUID() uuid: string; - type: DOCUMENT_TYPE | null; - description: string | null; + + @IsString() + @IsOptional() + type?: DOCUMENT_TYPE | null; + + @IsString() + @IsOptional() + description?: string | null; + + @IsString() + @IsOptional() + surveyPlanNumber?: string | null; + + @IsString() + @IsOptional() + controlNumber?: string | null; } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts index 6444eb733b..5c7cf5deba 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.dto.ts @@ -1,5 +1,12 @@ import { AutoMap } from '@automapper/classes'; -import { IsOptional, IsString, IsUUID, MaxLength } from 'class-validator'; +import { + IsBoolean, + IsNumber, + IsOptional, + IsString, + IsUUID, + MaxLength, +} from 'class-validator'; import { Column } from 'typeorm'; import { NoticeOfIntentStatusDto } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NotificationTransfereeDto } from './notification-transferee/notification-transferee.dto'; @@ -55,8 +62,17 @@ export class NotificationSubmissionDto { } export class NotificationSubmissionDetailedDto extends NotificationSubmissionDto { + @AutoMap(() => String) + submittersFileNumber: string | null; + @AutoMap(() => String) purpose: string | null; + + @AutoMap(() => Number) + totalArea: number | null; + + @AutoMap(() => Boolean) + hasSurveyPlan: boolean | null; } export class NotificationSubmissionUpdateDto { @@ -69,6 +85,18 @@ export class NotificationSubmissionUpdateDto { @MaxLength(MAX_DESCRIPTION_FIELD_LENGTH) purpose?: string; + @IsString() + @IsOptional() + submittersFileNumber?: string | null; + + @IsNumber() + @IsOptional() + totalArea?: number | null; + + @IsBoolean() + @IsOptional() + hasSurveyPlan?: boolean | null; + @IsUUID() @IsOptional() localGovernmentUuid?: string; diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts index 7f249ea887..d7251769fc 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.entity.ts @@ -13,6 +13,7 @@ import { NotificationSubmissionToSubmissionStatus } from '../../alcs/notificatio import { Notification } from '../../alcs/notification/notification.entity'; import { Base } from '../../common/entities/base.entity'; import { User } from '../../user/user.entity'; +import { ColumnNumericTransformer } from '../../utils/column-numeric-transform'; import { NotificationParcel } from './notification-parcel/notification-parcel.entity'; import { NotificationTransferee } from './notification-transferee/notification-transferee.entity'; @@ -51,6 +52,14 @@ export class NotificationSubmission extends Base { }) localGovernmentUuid?: string | null; + @AutoMap(() => String) + @Column({ + type: 'varchar', + comment: 'File number provided by Applicant from the LTSA', + nullable: true, + }) + submittersFileNumber?: string | null; + @AutoMap(() => String) @Column({ type: 'varchar', @@ -59,6 +68,20 @@ export class NotificationSubmission extends Base { }) purpose?: string | null; + @AutoMap(() => Number) + @Column({ + type: 'decimal', + nullable: true, + precision: 12, + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + totalArea: number | null; + + @AutoMap(() => Boolean) + @Column({ type: 'boolean', nullable: true }) + hasSurveyPlan: boolean | null; + @AutoMap() @ManyToOne(() => User) createdBy: User; diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts index afc142adbd..83cf02da1f 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts @@ -92,10 +92,23 @@ export class NotificationSubmissionService { const notificationSubmission = await this.getByUuid(submissionUuid, user); notificationSubmission.applicant = updateDto.applicant; + notificationSubmission.submittersFileNumber = filterUndefined( + updateDto.submittersFileNumber, + notificationSubmission.submittersFileNumber, + ); notificationSubmission.purpose = filterUndefined( updateDto.purpose, notificationSubmission.purpose, ); + notificationSubmission.totalArea = filterUndefined( + updateDto.totalArea, + notificationSubmission.totalArea, + ); + notificationSubmission.hasSurveyPlan = filterUndefined( + updateDto.hasSurveyPlan, + notificationSubmission.hasSurveyPlan, + ); + notificationSubmission.localGovernmentUuid = updateDto.localGovernmentUuid; notificationSubmission.contactFirstName = filterUndefined( updateDto.contactFirstName, diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1694042348857-add_plans_to_notification_documents.ts b/services/apps/alcs/src/providers/typeorm/migrations/1694042348857-add_plans_to_notification_documents.ts new file mode 100644 index 0000000000..e3cfffc0f9 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1694042348857-add_plans_to_notification_documents.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addPlansToNotificationDocuments1694042348857 + implements MigrationInterface +{ + name = 'addPlansToNotificationDocuments1694042348857'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" ADD "survey_plan_number" text`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" ADD "control_number" text`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" DROP COLUMN "control_number"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_document" DROP COLUMN "survey_plan_number"`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1694044003667-add_notification_srw_fields.ts b/services/apps/alcs/src/providers/typeorm/migrations/1694044003667-add_notification_srw_fields.ts new file mode 100644 index 0000000000..e057d1ade0 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1694044003667-add_notification_srw_fields.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNotificationSrwFields1694044003667 + implements MigrationInterface +{ + name = 'addNotificationSrwFields1694044003667'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD "submitters_file_number" character varying`, + ); + await queryRunner.query( + `COMMENT ON COLUMN "alcs"."notification_submission"."submitters_file_number" IS 'File number provided by Applicant from the LTSA'`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD "total_area" numeric(12,2)`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" ADD "has_survey_plan" boolean`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP COLUMN "has_survey_plan"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP COLUMN "total_area"`, + ); + await queryRunner.query( + `ALTER TABLE "alcs"."notification_submission" DROP COLUMN "submitters_file_number"`, + ); + } +} diff --git a/services/apps/alcs/src/providers/typeorm/migrations/1694106690840-add_notification_table_comments.ts b/services/apps/alcs/src/providers/typeorm/migrations/1694106690840-add_notification_table_comments.ts new file mode 100644 index 0000000000..f89a8b3b05 --- /dev/null +++ b/services/apps/alcs/src/providers/typeorm/migrations/1694106690840-add_notification_table_comments.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class addNotificationTableComments1694106690840 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notification" IS 'Stores Notification Class Applications such as SRWs'`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notification_parcel" IS 'Parcels Related to Notification Applications'`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notification_transferee" IS 'The Transferees related to Notification Applications'`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notification_submission" IS 'Portal Submissions for Notifications'`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notification_submission_status_type" IS 'Statuses for Notification Submissions'`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notification_submission_to_submission_status" IS 'Links Notifications to their Statuses with Dates'`, + ); + await queryRunner.query( + `COMMENT ON TABLE "alcs"."notification_document" IS 'Documents for Notifications'`, + ); + } + + public async down(): Promise { + //No + } +} From 74cb173c8fd5a7eb6788b46b93938b4e8b73619e Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:19:57 -0700 Subject: [PATCH 356/954] update timeline and decision emails (#950) --- .../release-dialog.component.html | 3 +-- .../release-dialog.component.html | 5 +---- ...application-decision-v2.controller.spec.ts | 6 ------ .../application-decision-v2.controller.ts | 10 --------- .../application-timeline.service.spec.ts | 11 +++++++--- .../application-timeline.service.ts | 19 +++++++++++++---- ...e-of-intent-decision-v2.controller.spec.ts | 18 ++++++---------- ...notice-of-intent-decision-v2.controller.ts | 10 --------- .../notice-of-intent-timeline.service.ts | 18 ++++++++++++++-- .../alcs/src/providers/email/email.service.ts | 21 ++++++++----------- .../decision-released/application.template.ts | 2 +- .../notice-of-intent.template.ts | 6 +++--- 12 files changed, 60 insertions(+), 69 deletions(-) diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.html index 3db6231291..7c0956c1a9 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.html +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/release-dialog/release-dialog.component.html @@ -4,8 +4,7 @@

Confirm Release Decision

- This decision and document will be immediately visible to the Applicant and Local/First Nation Government. The - decision will be visible to the Public after the prescribed amount of days is exhausted. + This decision and document will be immediately visible to the Applicant and Local/First Nation Government.

Upon releasing the decision, the application will be updated to the status: diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.html index 3db6231291..9bf43149ac 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/release-dialog/release-dialog.component.html @@ -3,10 +3,7 @@

Confirm Release Decision

-

- This decision and document will be immediately visible to the Applicant and Local/First Nation Government. The - decision will be visible to the Public after the prescribed amount of days is exhausted. -

+

This decision and document will be immediately visible to the Applicant and Local/First Nation Government.

Upon releasing the decision, the application will be updated to the status: diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts index b9b93ddd3e..7b7562c988 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.spec.ts @@ -328,12 +328,6 @@ describe('ApplicationDecisionV2Controller', () => { parentType: 'application', primaryContact: mockOwner, ccGovernment: true, - decisionDate: new Date().toLocaleDateString('en-CA', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }), }); }); diff --git a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts index 877430a398..ea26ae4f99 100644 --- a/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/application-decision/application-decision-v2/application-decision/application-decision-v2.controller.ts @@ -309,15 +309,6 @@ export class ApplicationDecisionV2Controller { const { applicationSubmission, primaryContact, submissionGovernment } = await this.emailService.getApplicationEmailData(fileNumber); - const date = decision.date ? new Date(decision.date) : new Date(); - - const options: Intl.DateTimeFormatOptions = { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }; - if (primaryContact) { await this.emailService.sendApplicationStatusEmail({ generateStatusHtml: generateALCDApplicationHtml, @@ -327,7 +318,6 @@ export class ApplicationDecisionV2Controller { parentType: PARENT_TYPE.APPLICATION, primaryContact, ccGovernment: true, - decisionDate: date.toLocaleDateString('en-CA', options), }); } } diff --git a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.spec.ts b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.spec.ts index 564840e87a..41523fa130 100644 --- a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.spec.ts +++ b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.spec.ts @@ -29,6 +29,7 @@ describe('ApplicationTimelineService', () => { let mockAppService: DeepMocked; let mockAppMeetingService: DeepMocked; let mockAppStatusService: DeepMocked; + const mockSameDate = new Date(); beforeEach(async () => { mockAppRepo = createMock(); @@ -77,7 +78,9 @@ describe('ApplicationTimelineService', () => { ApplicationTimelineService, ); - mockAppRepo.findOneOrFail.mockResolvedValue(new Application()); + mockAppRepo.findOneOrFail.mockResolvedValue( + new Application({ decisionDate: mockSameDate }), + ); mockAppModificationRepo.find.mockResolvedValue([]); mockAppReconsiderationRepo.find.mockResolvedValue([]); mockAppDecisionRepo.find.mockResolvedValue([]); @@ -128,7 +131,7 @@ describe('ApplicationTimelineService', () => { new ApplicationDecision({ auditDate: new Date(sameDate.getTime() + 1000), chairReviewDate: new Date(sameDate.getTime() + 1000), - date: new Date(sameDate.getTime() + 1000), + date: new Date(mockSameDate.getTime() + 1000), }), new ApplicationDecision({ auditDate: sameDate, @@ -141,7 +144,9 @@ describe('ApplicationTimelineService', () => { expect(res).toBeDefined(); expect(res.length).toEqual(6); - expect(res[5].htmlText).toEqual('Decision #1 Made - Active Days: 6'); + expect(res[5].htmlText).toEqual( + 'Decision #1 Made - Active Days: 6 - Decision Released', + ); expect(res[4].htmlText).toEqual('Audited Decision #1'); expect(res[3].htmlText).toEqual('Chair Reviewed Decision #1'); expect(res[2].htmlText).toEqual('Decision #2 Made'); diff --git a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts index d74fd9633d..bf953c6862 100644 --- a/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts +++ b/services/apps/alcs/src/alcs/application/application-timeline/application-timeline.service.ts @@ -184,10 +184,20 @@ export class ApplicationTimelineService { }); } + const isAddDecisionReleasedTitle = + application.decisionDate !== null && + decision.date !== null && + application.decisionDate?.toDateString() === + decision.date?.toDateString(); + + const decisionReleasedTitle = ' - Decision Released'; + events.push({ htmlText: `Decision #${decisions.length - index} Made${ decisions.length - 1 === index - ? ` - Active Days: ${mappedApplication.activeDays}` + ? ` - Active Days: ${mappedApplication.activeDays}${ + isAddDecisionReleasedTitle ? decisionReleasedTitle : '' + }` : '' }`, startDate: decision.date!.getTime() + SORTING_ORDER.DECISION_MADE, @@ -373,9 +383,10 @@ export class ApplicationTimelineService { const statusesToInclude = statusHistory.filter( (status) => - ![SUBMISSION_STATUS.IN_REVIEW_BY_ALC].includes( - status.statusType.code, - ), + ![ + SUBMISSION_STATUS.IN_REVIEW_BY_ALC, + SUBMISSION_STATUS.ALC_DECISION, + ].includes(status.statusType.code), ); for (const status of statusesToInclude) { if (status.effectiveDate) { diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts index 74a21fc925..8cbd866019 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.spec.ts @@ -3,12 +3,18 @@ import { AutomapperModule } from '@automapper/nestjs'; import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { ClsService } from 'nestjs-cls'; +import { generateALCDNoticeOfIntentHtml } from '../../../../../../templates/emails/decision-released'; import { mockKeyCloakProviders } from '../../../../test/mocks/mockTypes'; import { NoticeOfIntentDecisionProfile } from '../../../common/automapper/notice-of-intent-decision.automapper.profile'; import { NoticeOfIntentProfile } from '../../../common/automapper/notice-of-intent.automapper.profile'; import { UserProfile } from '../../../common/automapper/user.automapper.profile'; +import { NoticeOfIntentOwner } from '../../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NoticeOfIntentSubmissionService } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.service'; import { EmailService } from '../../../providers/email/email.service'; import { CodeService } from '../../code/code.service'; +import { LocalGovernment } from '../../local-government/local-government.entity'; +import { NOI_SUBMISSION_STATUS } from '../../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NoticeOfIntent } from '../../notice-of-intent/notice-of-intent.entity'; import { NoticeOfIntentService } from '../../notice-of-intent/notice-of-intent.service'; import { NoticeOfIntentDecisionOutcome } from '../notice-of-intent-decision-outcome.entity'; @@ -20,12 +26,6 @@ import { NoticeOfIntentDecision } from '../notice-of-intent-decision.entity'; import { NoticeOfIntentModificationService } from '../notice-of-intent-modification/notice-of-intent-modification.service'; import { NoticeOfIntentDecisionV2Controller } from './notice-of-intent-decision-v2.controller'; import { NoticeOfIntentDecisionV2Service } from './notice-of-intent-decision-v2.service'; -import { NoticeOfIntentSubmissionService } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.service'; -import { NoticeOfIntentOwner } from '../../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; -import { NoticeOfIntentSubmission } from '../../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; -import { LocalGovernment } from '../../local-government/local-government.entity'; -import { generateALCDNoticeOfIntentHtml } from '../../../../../../templates/emails/decision-released'; -import { NOI_SUBMISSION_STATUS } from '../../notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; describe('NoticeOfIntentDecisionV2Controller', () => { let controller: NoticeOfIntentDecisionV2Controller; @@ -325,12 +325,6 @@ describe('NoticeOfIntentDecisionV2Controller', () => { parentType: 'notice-of-intent', primaryContact: mockOwner, ccGovernment: true, - decisionDate: new Date().toLocaleDateString('en-CA', { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }), }); }); diff --git a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts index 38edcfad1c..5171a62292 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent-decision/notice-of-intent-decision-v2/notice-of-intent-decision-v2.controller.ts @@ -265,15 +265,6 @@ export class NoticeOfIntentDecisionV2Controller { noticeOfIntentSubmission, ); - const date = decision.date ? new Date(decision.date) : new Date(); - - const options: Intl.DateTimeFormatOptions = { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - }; - if (primaryContact) { await this.emailService.sendNoticeOfIntentStatusEmail({ generateStatusHtml: generateALCDNoticeOfIntentHtml, @@ -283,7 +274,6 @@ export class NoticeOfIntentDecisionV2Controller { parentType: PARENT_TYPE.NOTICE_OF_INTENT, primaryContact, ccGovernment: true, - decisionDate: date.toLocaleDateString('en-CA', options), }); } } diff --git a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts index 8c0d7283bf..bfa5b7051d 100644 --- a/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts +++ b/services/apps/alcs/src/alcs/notice-of-intent/notice-of-intent-timeline/notice-of-intent-timeline.service.ts @@ -148,10 +148,20 @@ export class NoticeOfIntentTimelineService { }); } + const isAddDecisionReleasedTitle = + noticeOfIntent.decisionDate !== null && + decision.date !== null && + noticeOfIntent.decisionDate?.toDateString() === + decision.date?.toDateString(); + + const decisionReleasedTitle = ' - Decision Released'; + events.push({ htmlText: `Decision #${decisions.length - index} Made${ decisions.length - 1 === index - ? ` - Active Days: ${mappedNOI.activeDays}` + ? ` - Active Days: ${mappedNOI.activeDays}${ + isAddDecisionReleasedTitle ? decisionReleasedTitle : '' + }` : '' }`, startDate: decision.date!.getTime() + SORTING_ORDER.DECISION_MADE, @@ -246,7 +256,11 @@ export class NoticeOfIntentTimelineService { noticeOfIntent.fileNumber, ); - for (const status of statusHistory) { + const statusesToInclude = statusHistory.filter( + (status) => NOI_SUBMISSION_STATUS.ALC_DECISION !== status.statusType.code, + ); + + for (const status of statusesToInclude) { if (status.effectiveDate) { let htmlText = `${status.statusType.label}`; diff --git a/services/apps/alcs/src/providers/email/email.service.ts b/services/apps/alcs/src/providers/email/email.service.ts index cf698dfaaa..0afc39f5b6 100644 --- a/services/apps/alcs/src/providers/email/email.service.ts +++ b/services/apps/alcs/src/providers/email/email.service.ts @@ -5,21 +5,21 @@ import { InjectRepository } from '@nestjs/typeorm'; import { MJMLParseResults } from 'mjml-core'; import { firstValueFrom } from 'rxjs'; import { Repository } from 'typeorm'; -import { EmailStatus } from './email-status.entity'; -import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; -import { ApplicationSubmission } from '../../portal/application-submission/application-submission.entity'; import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; +import { ApplicationService } from '../../alcs/application/application.service'; +import { PARENT_TYPE } from '../../alcs/card/card-subtask/card-subtask.dto'; import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; import { ApplicationOwner } from '../../portal/application-submission/application-owner/application-owner.entity'; +import { ApplicationSubmission } from '../../portal/application-submission/application-submission.entity'; import { ApplicationSubmissionService } from '../../portal/application-submission/application-submission.service'; -import { ApplicationService } from '../../alcs/application/application.service'; -import { FALLBACK_APPLICANT_NAME } from '../../utils/owner.constants'; -import { PARENT_TYPE } from '../../alcs/card/card-subtask/card-subtask.dto'; -import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; +import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.service'; -import { NoticeOfIntentService } from '../../alcs/notice-of-intent/notice-of-intent.service'; -import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; +import { FALLBACK_APPLICANT_NAME } from '../../utils/owner.constants'; +import { EmailStatus } from './email-status.entity'; export interface StatusUpdateEmail { fileNumber: string; @@ -35,7 +35,6 @@ type BaseStatusEmailData = { government: LocalGovernment | null; parentType: PARENT_TYPE; ccGovernment?: boolean; - decisionDate?: string; }; type ApplicationEmailData = BaseStatusEmailData & { applicationSubmission: ApplicationSubmission; @@ -296,7 +295,6 @@ export class EmailService { governmentName: data.government?.name, status: status.label, parentTypeLabel: parentTypeLabel[data.parentType], - decisionDate: data?.decisionDate, }); const parentId = await this.applicationService.getUuid(fileNumber); @@ -336,7 +334,6 @@ export class EmailService { governmentName: data.government?.name, status: status.label, parentTypeLabel: parentTypeLabel[data.parentType], - decisionDate: data?.decisionDate, }); const parentId = await this.noticeOfIntentService.getUuid(fileNumber); diff --git a/services/templates/emails/decision-released/application.template.ts b/services/templates/emails/decision-released/application.template.ts index 050496fb37..bfa0d4ea12 100644 --- a/services/templates/emails/decision-released/application.template.ts +++ b/services/templates/emails/decision-released/application.template.ts @@ -28,7 +28,7 @@ const template = ` This email is to advise that the Reasons for Decision for the above noted application has been released. - Please log into the ALC Portal to view the Reasons for Decision. The document can be found by clicking 'View' from the Inbox table and then navigating to the 'ALC Review and Decision' tab. The Reasons for Decision will be available to the public on {{ decisionDate }}. + Please log into the ALC Portal to view the Reasons for Decision. The document can be found by clicking 'View' from the Inbox table and then navigating to the 'ALC Review and Decision' tab. The Reasons for Decision will also be available to the public. Further correspondence with respect to this application should be directed to the ALC Land Use Planner for your region, found on the ALC website Contact Us page. diff --git a/services/templates/emails/decision-released/notice-of-intent.template.ts b/services/templates/emails/decision-released/notice-of-intent.template.ts index e8d0db1b2f..53b74e634c 100644 --- a/services/templates/emails/decision-released/notice-of-intent.template.ts +++ b/services/templates/emails/decision-released/notice-of-intent.template.ts @@ -1,7 +1,7 @@ import { MJMLParseResults } from 'mjml-core'; -import { EmailTemplateService } from '../../../libs/common/src/email-template-service/email-template.service'; -import { header, footer, notificationOnly, portalButton } from '../partials'; import { StatusUpdateEmail } from '../../../apps/alcs/src/providers/email/email.service'; +import { EmailTemplateService } from '../../../libs/common/src/email-template-service/email-template.service'; +import { footer, header, notificationOnly, portalButton } from '../partials'; type DecisionReleasedStatusEmail = StatusUpdateEmail & { decisionDate: number; @@ -28,7 +28,7 @@ const template = ` The decision for the above noted Notice of Intent (NOI) has been released on the the ALC Portal. - The decision document can be found by clicking 'View' from the NOI Inbox table in the ALC Portal, and then navigating to the 'ALC Review and Decision' tab. The decision will be available to the public on {{ decisionDate }}. + The decision document can be found by clicking 'View' from the NOI Inbox table in the ALC Portal, and then navigating to the 'ALC Review and Decision' tab. The decision will also be available to the public. Further correspondence with respect to this NOI should be directed to ALC.Soil@gov.bc.ca. From a5ade17f896ff10f69f9b48820f49cc72a531583 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 7 Sep 2023 10:39:52 -0700 Subject: [PATCH 357/954] Trigger CanDeactivate when URL has no Step Index --- .../notice-of-intents/edit-submission/edit-submission.module.ts | 1 + .../notifications/edit-submission/edit-submission.module.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts index ba969b4f33..6fac8c054b 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/edit-submission.module.ts @@ -11,6 +11,7 @@ const routes: Routes = [ { path: '', component: EditSubmissionComponent, + canDeactivate: [CanDeactivateGuard], }, { path: ':stepInd', diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts index 08d1f751b4..d46d9ac7be 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts @@ -23,6 +23,7 @@ const routes: Routes = [ { path: '', component: EditSubmissionComponent, + canDeactivate: [CanDeactivateGuard], }, { path: ':stepInd', From fd5fded92416dad49b8ba2a202bed14a6f538992 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 7 Sep 2023 11:28:30 -0700 Subject: [PATCH 358/954] MR feedback --- .../submissions/submap/soil_elements.py | 90 ++++++++----------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/bin/migrate-oats-data/submissions/submap/soil_elements.py b/bin/migrate-oats-data/submissions/submap/soil_elements.py index 1fab172e06..f25dab306c 100644 --- a/bin/migrate-oats-data/submissions/submap/soil_elements.py +++ b/bin/migrate-oats-data/submissions/submap/soil_elements.py @@ -1,4 +1,17 @@ from enum import Enum + +class SoilAction(Enum): + RMV = 'REMOVE' + ADD = 'ADD' + +alr_id = 'alr_appl_component_id' +area = 'project_area' +desc = 'material_desc' +origin_desc = 'material_origin_desc' +duration = 'project_duration' +volume = 'volume' +depth = 'depth' + def get_soil_rows(rows, cursor): # fetches adjacent land use data, specifically direction, description and type code component_ids = [dict(item)["alr_appl_component_id"] for item in rows] @@ -13,80 +26,34 @@ def get_soil_rows(rows, cursor): def create_soil_dict(soil_rows): # creates dict contailing fill and remove data - - class SoilAction(Enum): - RMV = 'REMOVE' - ADD = 'ADD' - alr_id = 'alr_appl_component_id' - area = 'project_area' - desc = 'material_desc' - origin_desc = 'material_origin_desc' - duration = 'project_duration' - volume = 'volume' - depth = 'depth' code = 'soil_change_code' - soil_dict = {} for row in soil_rows: app_component_id = row[alr_id] if app_component_id in soil_dict: - if row[code] == SoilAction.RMV.value: if 'RMV' in soil_dict.get(app_component_id, {}): print('ignored element_id:',row['soil_change_element_id']) else: - soil_dict[app_component_id]['remove_type'] = row[desc] - soil_dict[app_component_id]['remove_origin'] = row[origin_desc] - soil_dict[app_component_id]['max_remove_depth'] = row[depth] - soil_dict[app_component_id]['total_remove'] = row[volume] - soil_dict[app_component_id]['remove_duration'] = row[duration] - soil_dict[app_component_id]['remove_area'] = row[area] + dict_rmv_insert(soil_dict, app_component_id, row) if 'import_fill' not in soil_dict.get(app_component_id, {}): soil_dict[app_component_id]['import_fill'] = False - - soil_dict[app_component_id][SoilAction.RMV.name] = True - - + elif row[code] == SoilAction.ADD.value: if 'ADD' in soil_dict.get(app_component_id, {}): print('ignored element_id:',row['soil_change_element_id']) else: - soil_dict[app_component_id]['fill_type'] = row[desc] - soil_dict[app_component_id]['fill_origin'] = row[origin_desc] - soil_dict[app_component_id]['total_fill'] = row[volume] - soil_dict[app_component_id]['max_fill_depth'] = row[depth] - soil_dict[app_component_id]['fill_duration'] = row[duration] - soil_dict[app_component_id]['fill_area'] = row[area] - soil_dict[app_component_id]['import_fill'] = True - - soil_dict[app_component_id][SoilAction.ADD.name] = True - + dict_fill_insert(soil_dict, app_component_id, row) else: print('unknown soil action') else: soil_dict[app_component_id] = {} soil_dict[app_component_id][alr_id] = row[alr_id] if row[code] == SoilAction.RMV.value: - soil_dict[app_component_id]['remove_type'] = row[desc] - soil_dict[app_component_id]['remove_origin'] = row[origin_desc] - soil_dict[app_component_id]['max_remove_depth'] = row[depth] - soil_dict[app_component_id]['total_remove'] = row[volume] - soil_dict[app_component_id]['remove_duration'] = row[duration] - soil_dict[app_component_id]['remove_area'] = row[area] - soil_dict[app_component_id][SoilAction.RMV.name] = True + dict_rmv_insert(soil_dict, app_component_id, row) soil_dict[app_component_id]['import_fill'] = False - - elif row[code] == SoilAction.ADD.value: - soil_dict[app_component_id]['fill_type'] = row[desc] - soil_dict[app_component_id]['fill_origin'] = row[origin_desc] - soil_dict[app_component_id]['total_fill'] = row[volume] - soil_dict[app_component_id]['max_fill_depth'] = row[depth] - soil_dict[app_component_id]['fill_duration'] = row[duration] - soil_dict[app_component_id]['fill_area'] = row[area] - soil_dict[app_component_id]['import_fill'] = True - soil_dict[app_component_id][SoilAction.ADD.name] = True - + dict_fill_insert(soil_dict, app_component_id, row) else: print('unknown soil action') return soil_dict @@ -130,3 +97,24 @@ def add_soil_field(data): data['fill_duration_unit'] = None data['remove_duration_unit'] = None return data + +def dict_fill_insert(soil_dict, app_component_id, row): + soil_dict[app_component_id]['fill_type'] = row[desc] + soil_dict[app_component_id]['fill_origin'] = row[origin_desc] + soil_dict[app_component_id]['total_fill'] = row[volume] + soil_dict[app_component_id]['max_fill_depth'] = row[depth] + soil_dict[app_component_id]['fill_duration'] = row[duration] + soil_dict[app_component_id]['fill_area'] = row[area] + soil_dict[app_component_id]['import_fill'] = True + soil_dict[app_component_id][SoilAction.ADD.name] = True + return + +def dict_rmv_insert(soil_dict, app_component_id, row): + soil_dict[app_component_id]['remove_type'] = row[desc] + soil_dict[app_component_id]['remove_origin'] = row[origin_desc] + soil_dict[app_component_id]['max_remove_depth'] = row[depth] + soil_dict[app_component_id]['total_remove'] = row[volume] + soil_dict[app_component_id]['remove_duration'] = row[duration] + soil_dict[app_component_id]['remove_area'] = row[area] + soil_dict[app_component_id][SoilAction.RMV.name] = True + return \ No newline at end of file From c0cf567f921c76b8509e6eb1220eadc9e36655c7 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Thu, 7 Sep 2023 12:01:01 -0700 Subject: [PATCH 359/954] using refactored soil dict --- .../submissions/submap/soil_elements.py | 42 +++++++------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/bin/migrate-oats-data/submissions/submap/soil_elements.py b/bin/migrate-oats-data/submissions/submap/soil_elements.py index f25dab306c..94061c4159 100644 --- a/bin/migrate-oats-data/submissions/submap/soil_elements.py +++ b/bin/migrate-oats-data/submissions/submap/soil_elements.py @@ -25,37 +25,23 @@ def get_soil_rows(rows, cursor): return soil_rows def create_soil_dict(soil_rows): - # creates dict contailing fill and remove data - code = 'soil_change_code' - soil_dict = {} + # creates dict containing fill & remove data + soil_dict = dict() + for row in soil_rows: app_component_id = row[alr_id] - if app_component_id in soil_dict: - if row[code] == SoilAction.RMV.value: - if 'RMV' in soil_dict.get(app_component_id, {}): - print('ignored element_id:',row['soil_change_element_id']) - else: - dict_rmv_insert(soil_dict, app_component_id, row) - if 'import_fill' not in soil_dict.get(app_component_id, {}): - soil_dict[app_component_id]['import_fill'] = False - - elif row[code] == SoilAction.ADD.value: - if 'ADD' in soil_dict.get(app_component_id, {}): - print('ignored element_id:',row['soil_change_element_id']) - else: - dict_fill_insert(soil_dict, app_component_id, row) - else: - print('unknown soil action') + soil_dict.setdefault(app_component_id, {alr_id: row[alr_id]}) + + if (row['soil_change_code'] == SoilAction.RMV.value and 'RMV' not in soil_dict[app_component_id]) or \ + (row['soil_change_code'] == SoilAction.ADD.value and 'ADD' not in soil_dict[app_component_id]): + action_function = dict_rmv_insert if row['soil_change_code'] == SoilAction.RMV.value else dict_fill_insert + action_function(soil_dict, app_component_id, row) + + elif row['soil_change_code'] not in [SoilAction.ADD.value, SoilAction.RMV.value]: + print('unknown soil action') else: - soil_dict[app_component_id] = {} - soil_dict[app_component_id][alr_id] = row[alr_id] - if row[code] == SoilAction.RMV.value: - dict_rmv_insert(soil_dict, app_component_id, row) - soil_dict[app_component_id]['import_fill'] = False - elif row[code] == SoilAction.ADD.value: - dict_fill_insert(soil_dict, app_component_id, row) - else: - print('unknown soil action') + print('ignored element_id:', row['soil_change_element_id']) + return soil_dict From 760763bb42ad024f143692150d08cc8a8b491c3d Mon Sep 17 00:00:00 2001 From: mhuseinov <61513701+mhuseinov@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:08:50 -0700 Subject: [PATCH 360/954] error outline on noi decision for Decision Maker field (#953) --- .../decision-input/decision-input-v2.component.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html index 8178384585..029b499c77 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-input-v2.component.html @@ -66,8 +66,15 @@

Resolution

- Decision Maker* - + Decision Maker* + CEO Delegate CEO From ae835e9819851f1d00424e740086266dca985035 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Thu, 7 Sep 2023 13:23:33 -0700 Subject: [PATCH 361/954] Review & Submit for Notifications * Setup Step 7 for Review * Fixes for other steps * Add confirmation dialog to Proposal Step. --- .../review-and-submit.component.ts | 4 +- .../edit-submission.component.html | 12 +- .../edit-submission.component.ts | 13 +- .../edit-submission/edit-submission.module.ts | 6 + .../proposal/proposal.component.ts | 22 ++- .../review-and-submit.component.html | 27 ++++ .../review-and-submit.component.scss | 0 .../review-and-submit.component.spec.ts | 57 +++++++ .../review-and-submit.component.ts | 52 ++++++ .../submit-confirmation-dialog.component.html | 45 ++++++ .../submit-confirmation-dialog.component.scss | 18 +++ ...bmit-confirmation-dialog.component.spec.ts | 39 +++++ .../submit-confirmation-dialog.component.ts | 14 ++ .../notification-details.component.html | 105 ++++++++++++ .../notification-details.component.scss | 151 ++++++++++++++++++ .../notification-details.component.spec.ts | 62 +++++++ .../notification-details.component.ts | 91 +++++++++++ .../notification-details.module.ts | 14 ++ .../parcel/parcel.component.html | 71 ++++++++ .../parcel/parcel.component.scss | 39 +++++ .../parcel/parcel.component.spec.ts | 43 +++++ .../parcel/parcel.component.ts | 59 +++++++ .../proposal-details.component.html | 59 +++++++ .../proposal-details.component.scss | 8 + .../proposal-details.component.spec.ts | 35 ++++ .../proposal-details.component.ts | 46 ++++++ .../notification-parcel.controller.spec.ts | 6 +- .../notification-parcel.controller.ts | 2 +- .../notification-parcel.service.ts | 5 +- 29 files changed, 1093 insertions(+), 12 deletions(-) create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts create mode 100644 portal-frontend/src/app/features/notifications/notification-details/notification-details.component.html create mode 100644 portal-frontend/src/app/features/notifications/notification-details/notification-details.component.scss create mode 100644 portal-frontend/src/app/features/notifications/notification-details/notification-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/notification-details/notification-details.component.ts create mode 100644 portal-frontend/src/app/features/notifications/notification-details/notification-details.module.ts create mode 100644 portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.html create mode 100644 portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.scss create mode 100644 portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.ts create mode 100644 portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.html create mode 100644 portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.scss create mode 100644 portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.ts diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts index 7c758da6e2..e6e62f3181 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/review-and-submit/review-and-submit.component.ts @@ -36,9 +36,9 @@ export class ReviewAndSubmitComponent extends StepComponent implements OnInit, O override async onNavigateToStep(step: number) { if (this.draftMode) { - await this.router.navigateByUrl(`alcs/application/${this.noiSubmission?.fileNumber}/edit/${step}?errors=t`); + await this.router.navigateByUrl(`alcs/notice-of-intent/${this.noiSubmission?.fileNumber}/edit/${step}?errors=t`); } else { - await this.router.navigateByUrl(`application/${this.noiSubmission?.fileNumber}/edit/${step}?errors=t`); + await this.router.navigateByUrl(`notice-of-intent/${this.noiSubmission?.fileNumber}/edit/${step}?errors=t`); } } diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html index 8cd539d7f5..78ad8d9568 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.html @@ -99,6 +99,16 @@
-
+
+ + +
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts index 2897bc6620..11d0ada6ee 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.component.ts @@ -3,7 +3,6 @@ import { AfterViewInit, Component, OnDestroy, ViewChild } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, combineLatest, Observable, of, Subject, takeUntil } from 'rxjs'; -import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; import { NotificationDocumentDto } from '../../../services/notification-document/notification-document.dto'; import { NotificationDocumentService } from '../../../services/notification-document/notification-document.service'; import { @@ -19,6 +18,7 @@ import { OtherAttachmentsComponent } from './other-attachments/other-attachments import { ParcelDetailsComponent } from './parcels/parcel-details.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; import { ProposalComponent } from './proposal/proposal.component'; +import { SubmitConfirmationDialogComponent } from './review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; export enum EditNotificationSteps { @@ -180,7 +180,16 @@ export class EditSubmissionComponent implements OnDestroy, AfterViewInit { } async onSubmit() { - //TODO + if (this.notificationSubmission) { + this.dialog + .open(SubmitConfirmationDialogComponent) + .beforeClosed() + .subscribe((didConfirm) => { + if (didConfirm) { + this.submit(); + } + }); + } } private async submit() { diff --git a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts index 691f4d8558..ecc4434f91 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/edit-submission.module.ts @@ -7,6 +7,7 @@ import { RouterModule, Routes } from '@angular/router'; import { NgxMaskPipe } from 'ngx-mask'; import { CanDeactivateGuard } from '../../../shared/guard/can-deactivate.guard'; import { SharedModule } from '../../../shared/shared.module'; +import { NotificationDetailsModule } from '../notification-details/notification-details.module'; import { EditSubmissionComponent } from './edit-submission.component'; import { OtherAttachmentsComponent } from './other-attachments/other-attachments.component'; import { DeleteParcelDialogComponent } from './parcels/delete-parcel/delete-parcel-dialog.component'; @@ -15,6 +16,8 @@ import { ParcelEntryConfirmationDialogComponent } from './parcels/parcel-entry/p import { ParcelEntryComponent } from './parcels/parcel-entry/parcel-entry.component'; import { PrimaryContactComponent } from './primary-contact/primary-contact.component'; import { ProposalComponent } from './proposal/proposal.component'; +import { ReviewAndSubmitComponent } from './review-and-submit/review-and-submit.component'; +import { SubmitConfirmationDialogComponent } from './review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component'; import { SelectGovernmentComponent } from './select-government/select-government.component'; import { StepComponent } from './step.partial'; import { TransfereeDialogComponent } from './transferees/transferee-dialog/transferee-dialog.component'; @@ -46,6 +49,8 @@ const routes: Routes = [ SelectGovernmentComponent, ProposalComponent, OtherAttachmentsComponent, + ReviewAndSubmitComponent, + SubmitConfirmationDialogComponent, ], imports: [ CommonModule, @@ -55,6 +60,7 @@ const routes: Routes = [ MatIconModule, MatTableModule, NgxMaskPipe, + NotificationDetailsModule, ], }) export class EditSubmissionModule {} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts index 68f2feb014..871b326e3a 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts @@ -10,6 +10,7 @@ import { import { NotificationDocumentService } from '../../../../services/notification-document/notification-document.service'; import { NotificationSubmissionUpdateDto } from '../../../../services/notification-submission/notification-submission.dto'; import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; +import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; @@ -50,6 +51,7 @@ export class ProposalComponent extends FilesStepComponent implements OnInit, OnD private router: Router, private notificationSubmissionService: NotificationSubmissionService, notificationDocumentService: NotificationDocumentService, + private confirmationDialogService: ConfirmationDialogService, dialog: MatDialog ) { super(notificationDocumentService, dialog); @@ -121,7 +123,25 @@ export class ProposalComponent extends FilesStepComponent implements OnInit, OnD } onChangeHasSurveyPlan(selectedValue: string) { - this.allowSurveyPlanUploads = selectedValue === 'true'; + if (selectedValue === 'false' && this.surveyPlans.length > 0) { + this.confirmationDialogService + .openDialog({ + body: 'Warning: Changing this answer will remove the uploaded survey plans.', + }) + .subscribe(async (didConfirm) => { + if (didConfirm) { + for (const file of this.surveyPlans) { + await this.onDeleteFile(file); + } + this.allowSurveyPlanUploads = false; + } else { + this.allowSurveyPlanUploads = true; + this.hasSurveyPlan.setValue('true'); + } + }); + } else { + this.allowSurveyPlanUploads = selectedValue === 'true'; + } } onChangeControlNumber(uuid: any, event: Event) { diff --git a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.html b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.html new file mode 100644 index 0000000000..67656e913b --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.html @@ -0,0 +1,27 @@ +
+ +
+ +
+ +
+ + +
+ + +
+
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.spec.ts new file mode 100644 index 0000000000..1b4c879efa --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.spec.ts @@ -0,0 +1,57 @@ +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 { BehaviorSubject } from 'rxjs'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../../services/notification-submission/notification-submission.service'; +import { PdfGenerationService } from '../../../../services/pdf-generation/pdf-generation.service'; +import { ToastService } from '../../../../services/toast/toast.service'; + +import { ReviewAndSubmitComponent } from './review-and-submit.component'; + +describe('ReviewAndSubmitComponent', () => { + let component: ReviewAndSubmitComponent; + let fixture: ComponentFixture; + let mockToastService: DeepMocked; + let mockRouter: DeepMocked; + let mockNotificationSubmissionService: DeepMocked; + + beforeEach(async () => { + mockToastService = createMock(); + mockRouter = createMock(); + mockNotificationSubmissionService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ReviewAndSubmitComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { + provide: ToastService, + useValue: mockToastService, + }, + { + provide: Router, + useValue: mockRouter, + }, + { + provide: NotificationSubmissionService, + useValue: mockNotificationSubmissionService, + }, + { + provide: PdfGenerationService, + useValue: {}, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ReviewAndSubmitComponent); + component = fixture.componentInstance; + component.$notificationSubmission = new BehaviorSubject(undefined); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.ts new file mode 100644 index 0000000000..6a5c201989 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/review-and-submit.component.ts @@ -0,0 +1,52 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Router } from '@angular/router'; +import { BehaviorSubject, takeUntil } from 'rxjs'; +import { NotificationDocumentDto } from '../../../../services/notification-document/notification-document.dto'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { ToastService } from '../../../../services/toast/toast.service'; +import { StepComponent } from '../step.partial'; + +@Component({ + selector: 'app-review-and-submit', + templateUrl: './review-and-submit.component.html', + styleUrls: ['./review-and-submit.component.scss'], +}) +export class ReviewAndSubmitComponent extends StepComponent implements OnInit, OnDestroy { + @Input() $notificationDocuments!: BehaviorSubject; + @Output() submit = new EventEmitter(); + + notificationSubmission: NotificationSubmissionDetailedDto | undefined; + + constructor(private router: Router, private toastService: ToastService) { + super(); + } + + ngOnInit(): void { + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((submission) => { + this.notificationSubmission = submission; + }); + } + + override async onNavigateToStep(step: number) { + await this.router.navigateByUrl(`notification/${this.notificationSubmission?.fileNumber}/edit/${step}?errors=t`); + } + + async onSubmitToAlcs() { + if (this.notificationSubmission) { + const el = document.getElementsByClassName('error'); + if (el && el.length > 0) { + el[0].scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + this.toastService.showErrorToast('Please correct all errors before submitting the form'); + } else { + this.submit.emit(); + } + } + } + + async onDownloadPdf(fileNumber: string | undefined) { + //TODO + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html new file mode 100644 index 0000000000..1e91e2b708 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html @@ -0,0 +1,45 @@ +
+

Submit Notification of SRW

+
+ +
+

Your Notification of SRW will be submitted to the ALC.

+
+
Terms and Conditions:
+ + I/we confirm that the information provided to the ALC matches the SRW package that will be submitted to the Land + Title Survey Authority (LTSA). + + + I/we consent to the use of the information provided in the Notification of SRW and all supporting documents to + process the application in accordance with the Agricultural Land Commission Act, the Agricultural Land Reserve + General Regulation, and the Agricultural Land Reserve Use Regulation. + + + I/we declare that the information provided in the Notification of SRW and all the supporting documents are, to the + best of my/our knowledge, true and correct. + + + check_box I/we understand that the Agricultural Land Commission will take the steps necessary to confirm the + accuracy of the information and documents provided. This information will be available for review by any member of + the public. + +
+

+ If you have any questions about the collection or use of this information, please contact the Agricultural Land + Commission. +

+
+
+ + +
+
+
diff --git a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss new file mode 100644 index 0000000000..93f22cbbcb --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.scss @@ -0,0 +1,18 @@ +@use '../../../../../../styles/functions' as *; + +.checkbox { + margin: rem(10) 0; +} + +p { + margin: rem(28) 0 !important; +} + +.step-controls { + display: flex; + justify-content: flex-end; + + button:not(:last-child) { + margin-right: rem(16) !important; + } +} diff --git a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts new file mode 100644 index 0000000000..7d6d06ed68 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.spec.ts @@ -0,0 +1,39 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { SubmitConfirmationDialogComponent } from './submit-confirmation-dialog.component'; + +describe('SubmitConfirmationDialogComponent', () => { + let component: SubmitConfirmationDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SubmitConfirmationDialogComponent], + providers: [ + { + provide: MatDialog, + useValue: {}, + }, + { + provide: MatDialogRef, + useValue: {}, + }, + { + provide: MAT_DIALOG_DATA, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmitConfirmationDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts new file mode 100644 index 0000000000..79c353d727 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.ts @@ -0,0 +1,14 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +@Component({ + selector: 'app-submit-confirmation-dialog', + templateUrl: './submit-confirmation-dialog.component.html', + styleUrls: ['./submit-confirmation-dialog.component.scss'], +}) +export class SubmitConfirmationDialogComponent { + constructor( + @Inject(MAT_DIALOG_DATA) + protected data: {} + ) {} +} diff --git a/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.html b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.html new file mode 100644 index 0000000000..412f8e83a0 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.html @@ -0,0 +1,105 @@ +
+ +
+
+

3. Primary Contact

+
+
First Name
+
+ {{ notificationSubmission.contactFirstName }} + +
+
Last Name
+
+ {{ notificationSubmission.contactLastName }} + +
+
+ Organization (optional) +
+
+ {{ notificationSubmission.contactOrganization }} + +
+
Phone
+
+ {{ notificationSubmission.contactPhone }} + + Invalid Format +
+
Email
+
+ {{ notificationSubmission.contactEmail }} + + Invalid Format +
+
+ +
+
+
+
+

4. Government

+
+
Local or First Nation Government
+
+ {{ localGovernment?.name }} + + + This Local/First Nation Government has not yet been set up with the ALC Portal to receive notice of intents. To + submit, you will need to contact the ALC directly:  ALC.Portal@gov.bc.ca / 236-468-3342 + +
+
+ +
+
+
+
+

5. Proposal

+ +
+
+

6. Optional Attachments

+
+
+
Type
+
Description
+
File Name
+ +
+ {{ file.type?.label }} + +
+
+ {{ file.description }} + +
+ +
+
+ +
+
+
+ +
+
+
diff --git a/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.scss b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.scss new file mode 100644 index 0000000000..7ef4dbb6b8 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.scss @@ -0,0 +1,151 @@ +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; + +: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; + } + + .no-data-text { + text-align: center; + color: colors.$grey; + padding-top: rem(12); + + .error { + justify-content: center; + } + } + + .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; + } + + .soil-table { + display: grid; + grid-template-columns: max-content max-content; + overflow-x: auto; + grid-column-gap: rem(36); + grid-row-gap: rem(12); + } + + .other-attachments { + display: grid; + grid-template-columns: max-content max-content max-content; + overflow-x: auto; + grid-column-gap: rem(36); + grid-row-gap: rem(12); + + .full-width { + grid-column: 1/3; + } + } + + .adjacent-parcels { + display: grid; + grid-template-columns: max-content max-content max-content; + overflow-x: auto; + grid-column-gap: rem(36); + grid-row-gap: rem(12); + + .full-width { + grid-column: 1/4; + } + } + + .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; + } + + .edit-button { + grid-column: 1/5; + } + } + } +} diff --git a/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.spec.ts b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.spec.ts new file mode 100644 index 0000000000..69b527af07 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.spec.ts @@ -0,0 +1,62 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { CodeService } from '../../../services/code/code.service'; +import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; +import { NotificationDocumentDto } from '../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../services/notification-document/notification-document.service'; +import { NotificationSubmissionDetailedDto } from '../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; +import { ToastService } from '../../../services/toast/toast.service'; + +import { NotificationDetailsComponent } from './notification-details.component'; + +describe('NoticeOfIntentDetailsComponent', () => { + let component: NotificationDetailsComponent; + let fixture: ComponentFixture; + let mockCodeService: DeepMocked; + let mockDocumentService: DeepMocked; + let mockRouter: DeepMocked; + + let documentPipe = new BehaviorSubject([]); + + beforeEach(async () => { + mockCodeService = createMock(); + mockDocumentService = createMock(); + mockRouter = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: CodeService, + useValue: mockCodeService, + }, + { + provide: NotificationDocumentService, + useValue: mockDocumentService, + }, + { + provide: Router, + useValue: mockRouter, + }, + ], + declarations: [NotificationDetailsComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(NotificationDetailsComponent); + component = fixture.componentInstance; + component.$notificationSubmission = new BehaviorSubject(undefined); + component.$notificationDocuments = documentPipe; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.ts b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.ts new file mode 100644 index 0000000000..ff6fdc05f1 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.ts @@ -0,0 +1,91 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; +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 { NotificationDocumentDto } from '../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../services/notification-document/notification-document.service'; +import { NotificationSubmissionDetailedDto } from '../../../services/notification-submission/notification-submission.dto'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../shared/dto/document.dto'; +import { OWNER_TYPE } from '../../../shared/dto/owner.dto'; + +@Component({ + selector: 'app-notification-details', + templateUrl: './notification-details.component.html', + styleUrls: ['./notification-details.component.scss'], +}) +export class NotificationDetailsComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + + @Input() $notificationSubmission!: BehaviorSubject; + @Input() $notificationDocuments!: BehaviorSubject; + @Input() showErrors = true; + @Input() showEdit = true; + + parcelType = PARCEL_TYPE; + notificationSubmission: NotificationSubmissionDetailedDto | undefined; + localGovernment: LocalGovernmentDto | undefined; + otherFiles: NotificationDocumentDto[] = []; + notificationDocuments: NotificationDocumentDto[] = []; + OWNER_TYPE = OWNER_TYPE; + + private localGovernments: LocalGovernmentDto[] = []; + private otherFileTypes = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; + + constructor( + private codeService: CodeService, + private notificationDocumentService: NotificationDocumentService, + private router: Router + ) {} + + ngOnInit(): void { + this.loadGovernments(); + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((notificationSubmission) => { + this.notificationSubmission = notificationSubmission; + if (notificationSubmission) { + this.populateLocalGovernment(notificationSubmission.localGovernmentUuid); + } + }); + + this.$notificationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.otherFiles = documents + .filter((file) => (file.type ? this.otherFileTypes.includes(file.type.code) : true)) + .filter((file) => file.source === DOCUMENT_SOURCE.APPLICANT) + .sort((a, b) => { + return a.uploadedAt - b.uploadedAt; + }); + + this.notificationDocuments = documents; + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + async openFile(uuid: string) { + const res = await this.notificationDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } + + async onNavigateToStep(step: number) { + await this.router.navigateByUrl(`notification/${this.notificationSubmission?.fileNumber}/edit/${step}?errors=t`); + } + + private async loadGovernments() { + const codes = await this.codeService.loadCodes(); + this.localGovernments = codes.localGovernments.sort((a, b) => (a.name > b.name ? 1 : -1)); + if (this.notificationSubmission?.localGovernmentUuid) { + this.populateLocalGovernment(this.notificationSubmission?.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/notifications/notification-details/notification-details.module.ts b/portal-frontend/src/app/features/notifications/notification-details/notification-details.module.ts new file mode 100644 index 0000000000..c7499b1cfe --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/notification-details.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NgxMaskPipe } from 'ngx-mask'; +import { SharedModule } from '../../../shared/shared.module'; +import { NotificationDetailsComponent } from './notification-details.component'; +import { ParcelComponent } from './parcel/parcel.component'; +import { ProposalDetailsComponent } from './proposal-details/proposal-details.component'; + +@NgModule({ + declarations: [ParcelComponent, ProposalDetailsComponent, NotificationDetailsComponent], + imports: [CommonModule, SharedModule, NgxMaskPipe], + exports: [NotificationDetailsComponent], +}) +export class NotificationDetailsModule {} diff --git a/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.html b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.html new file mode 100644 index 0000000000..2f315378b6 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.html @@ -0,0 +1,71 @@ +

1. Identify Parcel(s) Under Application

+
+ +
+

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

+
+ +
+
+
Parcel 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' }} + + + Invalid Format + +
+ +
PIN (optional)
+
+ {{ parcel.pin }} + +
+
+
Civic Address
+
+ {{ parcel.civicAddress }} + +
+
+ +
+
+
+ +
+
diff --git a/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.scss b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.scss new file mode 100644 index 0000000000..ecf9d4a24c --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.scss @@ -0,0 +1,39 @@ +@use '../../../../../styles/functions' as *; + +.owner-information { + display: grid; + grid-template-columns: max-content max-content max-content max-content max-content max-content; + overflow-x: auto; + grid-column-gap: rem(36); + grid-row-gap: rem(12); + + .full-width { + grid-column: 1/3; + } +} + +.review-table { + grid-template-columns: 1fr 1fr !important; + + .full-width { + grid-column: 1/3; + } + + .edit-button { + grid-column: 1/3; + } + + @media screen and (min-width: $tabletBreakpoint) { + .full-width { + grid-column: 1/5; + } + + .edit-button { + grid-column: 1/5; + } + } +} + +.crown-land { + text-transform: capitalize; +} diff --git a/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.spec.ts b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.spec.ts new file mode 100644 index 0000000000..6ba3abd1dc --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.spec.ts @@ -0,0 +1,43 @@ +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 { BehaviorSubject } from 'rxjs'; +import { NotificationParcelService } from '../../../../services/notification-parcel/notification-parcel.service'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { ParcelComponent } from './parcel.component'; + +describe('ParcelComponent', () => { + let component: ParcelComponent; + let fixture: ComponentFixture; + + let mockNotificationParcelService: DeepMocked; + + beforeEach(async () => { + mockNotificationParcelService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ParcelComponent], + providers: [ + { + provide: NotificationParcelService, + useValue: mockNotificationParcelService, + }, + { + provides: Router, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelComponent); + component = fixture.componentInstance; + component.$notificationSubmission = new BehaviorSubject(undefined); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.ts b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.ts new file mode 100644 index 0000000000..c7430df8dd --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.ts @@ -0,0 +1,59 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; +import { PARCEL_OWNERSHIP_TYPE } from '../../../../services/application-parcel/application-parcel.dto'; +import { NotificationParcelDto } from '../../../../services/notification-parcel/notification-parcel.dto'; +import { NotificationParcelService } from '../../../../services/notification-parcel/notification-parcel.service'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; + +@Component({ + selector: 'app-parcel', + templateUrl: './parcel.component.html', + styleUrls: ['./parcel.component.scss'], +}) +export class ParcelComponent { + $destroy = new Subject(); + + @Input() $notificationSubmission!: BehaviorSubject; + @Input() showErrors = true; + @Input() showEdit = true; + + PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; + + fileId = ''; + submissionUuid = ''; + parcels: NotificationParcelDto[] = []; + noticeOfIntentSubmission!: NotificationSubmissionDetailedDto; + updatedFields: string[] = []; + + constructor(private noticeOfIntentParcelService: NotificationParcelService, private router: Router) {} + + ngOnInit(): void { + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + if (noiSubmission) { + this.fileId = noiSubmission.fileNumber; + this.submissionUuid = noiSubmission.uuid; + this.noticeOfIntentSubmission = noiSubmission; + this.loadParcels(); + } + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + async loadParcels() { + this.parcels = (await this.noticeOfIntentParcelService.fetchBySubmissionUuid(this.submissionUuid)) || []; + } + + async onEditParcelsClick($event: any) { + $event.stopPropagation(); + await this.router.navigateByUrl(`notification/${this.fileId}/edit/0?errors=t`); + } + + async onEditParcelClick(uuid: string) { + await this.router.navigateByUrl(`notification/${this.fileId}/edit/0?parcelUuid=${uuid}&errors=t`); + } +} diff --git a/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.html b/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.html new file mode 100644 index 0000000000..6e8283ed1c --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.html @@ -0,0 +1,59 @@ +
+
Submitter’s File Number
+
+ {{ _notificationSubmission.submittersFileNumber }} + +
+ +
What is the purpose of the SRW?
+
+ {{ _notificationSubmission.purpose }} + +
+ +
Total area of the SRW
+
+ {{ _notificationSubmission.totalArea }} + +
+ +
Upload Terms of the SRW
+ + +
Is there a survey plan associated with the SRW?
+
+ + {{ _notificationSubmission.hasSurveyPlan ? 'Yes' : 'No' }} + + +
+ + +
+
File Name
+
Survey Plan Number
+
Control Number
+ +
{{ file.fileName }}
+
+ {{ file.surveyPlanNumber }} + +
+
+ {{ file.controlNumber }} + +
+
+ +
+
+ +
+ +
+
diff --git a/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.scss b/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.scss new file mode 100644 index 0000000000..8ce457dba6 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.scss @@ -0,0 +1,8 @@ +@use '../../../../../styles/functions' as *; + +.survey-table { + display: grid; + grid-column-gap: rem(16); + grid-row-gap: rem(8); + grid-template-columns: max-content max-content max-content; +} diff --git a/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.spec.ts b/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.spec.ts new file mode 100644 index 0000000000..6bc8714fa7 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.spec.ts @@ -0,0 +1,35 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent-parcel/notice-of-intent-parcel.service'; +import { NotificationDocumentService } from '../../../../services/notification-document/notification-document.service'; + +import { ProposalDetailsComponent } from './proposal-details.component'; + +describe('ProposalDetailsComponent', () => { + let component: ProposalDetailsComponent; + let fixture: ComponentFixture; + let mockNoiDocumentService: DeepMocked; + + beforeEach(async () => { + mockNoiDocumentService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [ProposalDetailsComponent], + providers: [ + { + provide: NotificationDocumentService, + useValue: mockNoiDocumentService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ProposalDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.ts b/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.ts new file mode 100644 index 0000000000..5db233e2db --- /dev/null +++ b/portal-frontend/src/app/features/notifications/notification-details/proposal-details/proposal-details.component.ts @@ -0,0 +1,46 @@ +import { Component, Input } from '@angular/core'; +import { Router } from '@angular/router'; +import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NoticeOfIntentSubmissionDetailedDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { NotificationDocumentDto } from '../../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../../services/notification-document/notification-document.service'; +import { NotificationSubmissionDetailedDto } from '../../../../services/notification-submission/notification-submission.dto'; +import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; + +@Component({ + selector: 'app-proposal-details[notificationSubmission]', + templateUrl: './proposal-details.component.html', + styleUrls: ['./proposal-details.component.scss'], +}) +export class ProposalDetailsComponent { + @Input() showErrors = true; + @Input() showEdit = true; + + _notificationSubmission: NotificationSubmissionDetailedDto | undefined; + + @Input() set notificationSubmission(notificationSubmission: NotificationSubmissionDetailedDto | undefined) { + if (notificationSubmission) { + this._notificationSubmission = notificationSubmission; + } + } + + @Input() set notificationDocuments(documents: NotificationDocumentDto[]) { + this.surveyPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.SURVEY_PLAN); + this.srwTerms = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.SRW_TERMS); + } + + surveyPlans: NotificationDocumentDto[] = []; + srwTerms: NotificationDocumentDto[] = []; + + constructor(private router: Router, private notificationDocumentService: NotificationDocumentService) {} + + async onEditSection(step: number) { + await this.router.navigateByUrl(`notification/${this._notificationSubmission?.fileNumber}/edit/${step}?errors=t`); + } + + async openFile(uuid: string) { + const res = await this.notificationDocumentService.openFile(uuid); + window.open(res?.url, '_blank'); + } +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts index 5415df50ea..2061680d07 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.spec.ts @@ -79,15 +79,13 @@ describe('NotificationParcelController', () => { }); it('should call out to service when fetching parcels', async () => { - mockNotificationParcelService.fetchByApplicationSubmissionUuid.mockResolvedValue( - [], - ); + mockNotificationParcelService.fetchBySubmissionUuid.mockResolvedValue([]); const parcels = await controller.fetchByFileId('mockFileID'); expect(parcels).toBeDefined(); expect( - mockNotificationParcelService.fetchByApplicationSubmissionUuid, + mockNotificationParcelService.fetchBySubmissionUuid, ).toHaveBeenCalledTimes(1); }); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts index b71ae3cedd..94c8f4d285 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.controller.ts @@ -36,7 +36,7 @@ export class NotificationParcelController { async fetchByFileId( @Param('submissionUuid') submissionUuid: string, ): Promise { - const parcels = await this.parcelService.fetchByApplicationSubmissionUuid( + const parcels = await this.parcelService.fetchBySubmissionUuid( submissionUuid, ); return this.mapper.mapArrayAsync( diff --git a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts index 9dacb9fa06..376b93981e 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-parcel/notification-parcel.service.ts @@ -21,10 +21,13 @@ export class NotificationParcelService { }); } - async fetchByApplicationSubmissionUuid(uuid: string) { + async fetchBySubmissionUuid(uuid: string) { return this.parcelRepository.find({ where: { notificationSubmissionUuid: uuid }, order: { auditCreatedAt: 'ASC' }, + relations: { + ownershipType: true, + }, }); } From 4d9b7751a91f674dea165a19d6c7feacd440cb7d Mon Sep 17 00:00:00 2001 From: "to. sandra" <76515860+sandratoh@users.noreply.github.com> Date: Thu, 7 Sep 2023 15:43:32 -0700 Subject: [PATCH 362/954] Update top nav with advanced search (#955) * Update searchbar with icon toggle and advanced search button * Close input when clicked outside of search component * Add search bar slide in animation and update toggle behaviour --- .../search-bar/search-bar.component.html | 42 ++++++--- .../search-bar/search-bar.component.scss | 90 ++++++++++++++----- .../header/search-bar/search-bar.component.ts | 32 ++++++- 3 files changed, 127 insertions(+), 37 deletions(-) diff --git a/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.html b/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.html index bdc29dafa4..16290e8d95 100644 --- a/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.html +++ b/alcs-frontend/src/app/shared/header/search-bar/search-bar.component.html @@ -1,13 +1,31 @@ -
diff --git a/portal-frontend/src/app/features/home/noi-list/noi-list.component.html b/portal-frontend/src/app/features/home/noi-list/noi-list.component.html index e22f232d71..e9e9d22972 100644 --- a/portal-frontend/src/app/features/home/noi-list/noi-list.component.html +++ b/portal-frontend/src/app/features/home/noi-list/noi-list.component.html @@ -61,6 +61,6 @@ - +
diff --git a/portal-frontend/src/app/features/home/notification-list/notification-list.component.html b/portal-frontend/src/app/features/home/notification-list/notification-list.component.html new file mode 100644 index 0000000000..f0613608bb --- /dev/null +++ b/portal-frontend/src/app/features/home/notification-list/notification-list.component.html @@ -0,0 +1,61 @@ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No NOIs Created
Notification ID{{ row.fileNumber }}Date Created{{ row.createdAt | date }}Name{{ row.applicant || '(Unknown)' }}Type{{ row.type }}Status +
+ {{ row.status.label }} +
+
Last Updated{{ row.updatedAt | date }}Actions + +
+ +
+
diff --git a/portal-frontend/src/app/features/home/notification-list/notification-list.component.scss b/portal-frontend/src/app/features/home/notification-list/notification-list.component.scss new file mode 100644 index 0000000000..5eb8d7a15e --- /dev/null +++ b/portal-frontend/src/app/features/home/notification-list/notification-list.component.scss @@ -0,0 +1,31 @@ +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; + +.label { + display: inline-block; + padding: rem(4) rem(16); + border-radius: rem(16); + font-weight: bold; +} + +.table-wrapper { + overflow-x: auto; + + .table-container { + display: table; + width: 100%; + + table { + width: 100%; + } + + ::ng-deep .mat-table { + ::ng-deep .mat-cell, + ::ng-deep .mat-header-cell, + ::ng-deep .mat-row, + ::ng-deep .mat-header-row { + min-width: rem(150); + } + } + } +} diff --git a/portal-frontend/src/app/features/home/notification-list/notification-list.component.spec.ts b/portal-frontend/src/app/features/home/notification-list/notification-list.component.spec.ts new file mode 100644 index 0000000000..a5b90d4182 --- /dev/null +++ b/portal-frontend/src/app/features/home/notification-list/notification-list.component.spec.ts @@ -0,0 +1,36 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; + +import { NotificationListComponent } from './notification-list.component'; + +describe('NotificationListComponent', () => { + let component: NotificationListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [NotificationListComponent], + providers: [ + { + provide: NotificationSubmissionService, + useValue: {}, + }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(NotificationListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/home/notification-list/notification-list.component.ts b/portal-frontend/src/app/features/home/notification-list/notification-list.component.ts new file mode 100644 index 0000000000..ca930f1122 --- /dev/null +++ b/portal-frontend/src/app/features/home/notification-list/notification-list.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatTableDataSource } from '@angular/material/table'; +import { NotificationSubmissionDto } from '../../../services/notification-submission/notification-submission.dto'; +import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; + +@Component({ + selector: 'app-notification-list', + templateUrl: './notification-list.component.html', + styleUrls: ['./notification-list.component.scss'], +}) +export class NotificationListComponent implements OnInit { + dataSource: MatTableDataSource = new MatTableDataSource(); + displayedColumns: string[] = [ + 'fileNumber', + 'dateCreated', + 'applicant', + 'applicationType', + 'status', + 'lastUpdated', + 'actions', + ]; + + @ViewChild(MatPaginator) paginator!: MatPaginator; + + constructor(private notificationSubmissionService: NotificationSubmissionService) {} + + ngOnInit(): void { + this.loadNotifications(); + } + + async loadNotifications() { + const notifications = await this.notificationSubmissionService.getNotifications(); + this.dataSource = new MatTableDataSource(notifications); + this.dataSource.paginator = this.paginator; + } +} diff --git a/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.html b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.html index 2f315378b6..e0a1efdf77 100644 --- a/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.html +++ b/portal-frontend/src/app/features/notifications/notification-details/parcel/parcel.component.html @@ -1,4 +1,4 @@ -

1. Identify Parcel(s) Under Application

+

1. Identify Parcel(s) Under The Statutory Right of Way

@@ -48,10 +48,7 @@

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

PIN (optional)
{{ parcel.pin }} - +
Civic Address
diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html index 1809b0a6e6..aa28457dfe 100644 --- a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html @@ -54,7 +54,14 @@

Applicant Submission

-
TODO
+
+ +
@@ -75,8 +82,8 @@

Applicant Submission

- -
To Add Later
+ +
//TODO
diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.spec.ts b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.spec.ts index bbbfa7a822..f105bb2fcd 100644 --- a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.spec.ts +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.spec.ts @@ -3,7 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { BehaviorSubject } from 'rxjs'; +import { NotificationDocumentService } from '../../../services/notification-document/notification-document.service'; import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { ViewNotificationSubmissionComponent } from './view-notification-submission.component'; @@ -12,10 +14,12 @@ describe('ViewNotificationSubmissionComponent', () => { let fixture: ComponentFixture; let mockNotificationSubmissionService: DeepMocked; + let mockNotificationDocumentService: DeepMocked; let mockActivatedRoute: DeepMocked; beforeEach(async () => { mockNotificationSubmissionService = createMock(); + mockNotificationDocumentService = createMock(); mockActivatedRoute = createMock(); mockActivatedRoute.paramMap = new BehaviorSubject(new Map()); @@ -27,10 +31,18 @@ describe('ViewNotificationSubmissionComponent', () => { provide: NotificationSubmissionService, useValue: mockNotificationSubmissionService, }, + { + provide: NotificationDocumentService, + useValue: mockNotificationDocumentService, + }, { provide: ActivatedRoute, useValue: mockActivatedRoute, }, + { + provide: ConfirmationDialogService, + useValue: {}, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts index b14c06927f..b21120939e 100644 --- a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.ts @@ -1,8 +1,14 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; -import { NotificationSubmissionDto } from '../../../services/notification-submission/notification-submission.dto'; +import { NotificationDocumentDto } from '../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../services/notification-document/notification-document.service'; +import { + NotificationSubmissionDetailedDto, + NotificationSubmissionDto, +} from '../../../services/notification-submission/notification-submission.dto'; import { NotificationSubmissionService } from '../../../services/notification-submission/notification-submission.service'; +import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; @Component({ selector: 'app-view-notification-submission', @@ -11,11 +17,14 @@ import { NotificationSubmissionService } from '../../../services/notification-su }) export class ViewNotificationSubmissionComponent implements OnInit, OnDestroy { $destroy = new Subject(); - $notificationSubmission = new BehaviorSubject(undefined); - submission: NotificationSubmissionDto | undefined; + $notificationSubmission = new BehaviorSubject(undefined); + $notificationDocuments = new BehaviorSubject([]); + submission: NotificationSubmissionDetailedDto | undefined; constructor( private notificationSubmissionService: NotificationSubmissionService, + private notificationDocumentService: NotificationDocumentService, + private confirmationDialogService: ConfirmationDialogService, private route: ActivatedRoute, private router: Router ) {} @@ -37,10 +46,10 @@ export class ViewNotificationSubmissionComponent implements OnInit, OnDestroy { } async loadDocuments(fileId: string) { - // const documents = await this.noiDocumentService.getByFileId(fileId); - // if (documents) { - // this.$noiDocuments.next(documents); - // } + const documents = await this.notificationDocumentService.getByFileId(fileId); + if (documents) { + this.$notificationDocuments.next(documents); + } } ngOnDestroy(): void { @@ -53,8 +62,18 @@ export class ViewNotificationSubmissionComponent implements OnInit, OnDestroy { } async onCancel(uuid: string) { - await this.notificationSubmissionService.cancel(uuid); - await this.router.navigateByUrl(`home`); + const dialog = this.confirmationDialogService.openDialog({ + body: 'Are you sure you want to cancel the notification? A cancelled notification cannot be edited or submitted to the ALC. This cannot be undone.', + confirmAction: 'Confirm', + cancelAction: 'Return', + }); + + dialog.subscribe(async (isConfirmed) => { + if (isConfirmed) { + await this.notificationSubmissionService.cancel(uuid); + await this.router.navigateByUrl(`home`); + } + }); } onDownloadSubmissionPdf(fileNumber: string) { diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts index 700cda7875..8e9c36c202 100644 --- a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts @@ -3,6 +3,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; import { SharedModule } from '../../../shared/shared.module'; +import { NotificationDetailsModule } from '../notification-details/notification-details.module'; import { ViewNotificationSubmissionComponent } from './view-notification-submission.component'; const routes: Routes = [ @@ -13,7 +14,14 @@ const routes: Routes = [ ]; @NgModule({ - imports: [CommonModule, SharedModule, RouterModule.forChild(routes), NgxMaskDirective, NgxMaskPipe], + imports: [ + CommonModule, + SharedModule, + RouterModule.forChild(routes), + NgxMaskDirective, + NgxMaskPipe, + NotificationDetailsModule, + ], declarations: [ViewNotificationSubmissionComponent], }) export class ViewNotificationSubmissionModule {} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts index 83cf02da1f..77257a787e 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts @@ -11,7 +11,9 @@ import { Not, Repository, } from 'typeorm'; +import { SUBMISSION_STATUS } from '../../alcs/application/application-submission-status/submission-status.dto'; import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NOI_SUBMISSION_STATUS } from '../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { NOTIFICATION_STATUS } from '../../alcs/notification/notification-submission-status/notification-status.dto'; import { NotificationSubmissionStatusService } from '../../alcs/notification/notification-submission-status/notification-submission-status.service'; import { NotificationService } from '../../alcs/notification/notification.service'; @@ -264,21 +266,30 @@ export class NotificationSubmissionService { async mapToDTOs(submissions: NotificationSubmission[], user: User) { const types = await this.notificationService.listTypes(); - return submissions.map((noiSubmission) => { - const isCreator = noiSubmission.createdBy.uuid === user.uuid; + return submissions.map((notificationSubmission) => { + const isCreator = notificationSubmission.createdBy.uuid === user.uuid; const isSameAccount = user.bceidBusinessGuid && - noiSubmission.createdBy.bceidBusinessGuid === user.bceidBusinessGuid; + notificationSubmission.createdBy.bceidBusinessGuid === + user.bceidBusinessGuid; return { ...this.mapper.map( - noiSubmission, + notificationSubmission, NotificationSubmission, NotificationSubmissionDto, ), - type: types.find((type) => type.code === noiSubmission.typeCode)!.label, - canEdit: isCreator || isSameAccount, - canView: true, + type: types.find( + (type) => type.code === notificationSubmission.typeCode, + )!.label, + canEdit: + [NOTIFICATION_STATUS.IN_PROGRESS].includes( + notificationSubmission.status.statusTypeCode as NOTIFICATION_STATUS, + ) && + (isCreator || isSameAccount), + canView: + notificationSubmission.status.statusTypeCode !== + SUBMISSION_STATUS.CANCELLED, }; }); } @@ -303,8 +314,14 @@ export class NotificationSubmissionService { ...mappedApp, type: types.find((type) => type.code === notificationSubmission.typeCode)! .label, - canEdit: isCreator || isSameAccount, - canView: true, + canEdit: + [NOTIFICATION_STATUS.IN_PROGRESS].includes( + notificationSubmission.status.statusTypeCode as NOTIFICATION_STATUS, + ) && + (isCreator || isSameAccount), + canView: + notificationSubmission.status.statusTypeCode !== + SUBMISSION_STATUS.CANCELLED, }; } From 0b1acf749cac51d5e370a460d67a04ea473029b4 Mon Sep 17 00:00:00 2001 From: Liam Stoddard Date: Fri, 8 Sep 2023 09:52:48 -0700 Subject: [PATCH 366/954] renamed no data string --- bin/migrate-oats-data/submissions/submap/direction_mapping.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/migrate-oats-data/submissions/submap/direction_mapping.py b/bin/migrate-oats-data/submissions/submap/direction_mapping.py index ac086e8507..b61c27c721 100644 --- a/bin/migrate-oats-data/submissions/submap/direction_mapping.py +++ b/bin/migrate-oats-data/submissions/submap/direction_mapping.py @@ -25,7 +25,7 @@ def get_directions_rows(rows, cursor): def map_direction_values(data, direction_data): # adds direction field values into data row - no_data = 'No data found' + no_data = 'No data found in OATS' app_id = "alr_application_id" data['east_land_use_type_description'] = direction_data.get(data[app_id], {}).get('east_description', no_data) data['east_land_use_type'] = direction_data.get(data[app_id], {}).get('east_type_code', None) From 0f88b00a07b2135d460763f371920e851f36b619 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 8 Sep 2023 10:48:48 -0700 Subject: [PATCH 367/954] Add Required Errors to Soil and Structure Tables --- .../subd-proposal.component.html | 123 +++++++++------- .../subd-proposal.component.scss | 4 + .../subd-proposal/subd-proposal.component.ts | 44 ++++-- .../additional-information.component.html | 135 ++++++++++-------- .../additional-information.component.ts | 57 +++++--- 5 files changed, 231 insertions(+), 132 deletions(-) diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html index 8defadda06..a6f6417554 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html @@ -60,58 +60,85 @@
Documents needed for this step:
- - - - - - + + +
#{{ i + 1 }}
+ + + + - - - - + + + + - - - - + + + + - - + + - - - -
#{{ i + 1 }}Type - - - Lot - Road Dedication - - - Type + + + Lot + Road Dedication + + +
+ warning +
This field is required
+
+
Size - - - ha - - Size + + + ha + +
+ warning +
This field is required
+
+
No Proposed Lots Entered
-
Total area of the proposed lots: {{ totalAcres }} ha
-
- warning -
The total lot area proposed must match {{ totalTargetAcres }} hectares as provided in Step 1
-
+ + No Proposed Lots Entered + + +
Total area of the proposed lots: {{ totalAcres }} ha
+
+ warning +
The total lot area proposed must match {{ totalTargetAcres }} hectares as provided in Step 1
+
+
diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.scss b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.scss index c56600402d..1dd31dde9d 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.scss +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.scss @@ -7,3 +7,7 @@ section { .toggle-button { min-width: rem(170); } + +.proposed-lots { + margin-bottom: rem(16); +} diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts index 9410136d51..fe3ffdb90d 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts @@ -8,13 +8,16 @@ import { ApplicationDocumentDto } from '../../../../../services/application-docu import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; import { PARCEL_TYPE } from '../../../../../services/application-parcel/application-parcel.dto'; import { ApplicationParcelService } from '../../../../../services/application-parcel/application-parcel.service'; -import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; +import { + ApplicationSubmissionUpdateDto, + ProposedLot, +} from '../../../../../services/application-submission/application-submission.dto'; import { ApplicationSubmissionService } from '../../../../../services/application-submission/application-submission.service'; import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; -type ProposedLot = { type: 'Lot' | 'Road Dedication' | null; size: string | null }; +type FormProposedLot = { type: 'Lot' | 'Road Dedication' | null; size: string | null }; @Component({ selector: 'app-subd-proposal', @@ -35,7 +38,7 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, totalTargetAcres = '0'; totalAcres = '0'; - proposedLots: ProposedLot[] = []; + proposedLots: FormProposedLot[] = []; lotsSource = new MatTableDataSource(this.proposedLots); displayedColumns = ['index', 'type', 'size']; @@ -46,6 +49,7 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, agriculturalSupport: this.agriculturalSupport, isHomeSiteSeverance: this.isHomeSiteSeverance, }); + lotsForm = new FormGroup({} as any); private submissionUuid = ''; constructor( @@ -81,6 +85,14 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, size: lot.size ? lot.size.toString(10) : null, })); this.lotsSource = new MatTableDataSource(this.proposedLots); + + const newForm = new FormGroup({}); + for (const [index, lot] of applicationSubmission.subdProposedLots.entries()) { + newForm.addControl(`${index}-type`, new FormControl(lot.type, [Validators.required])); + newForm.addControl(`${index}-size`, new FormControl(lot.size, [Validators.required])); + } + this.lotsForm = newForm; + this.calculateLotSize(); if (this.showErrors) { @@ -102,21 +114,28 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, } protected async save() { - if (this.fileId && this.form.dirty) { + if (this.fileId && (this.form.dirty || this.lotsForm.dirty)) { const purpose = this.purpose.getRawValue(); const subdSuitability = this.suitability.getRawValue(); const subdAgricultureSupport = this.agriculturalSupport.getRawValue(); const subdIsHomeSiteSeverance = this.isHomeSiteSeverance.getRawValue(); + const updatedStructures: ProposedLot[] = []; + for (const [index, lot] of this.proposedLots.entries()) { + const lotType = this.lotsForm.controls[`${index}-type`].value; + const lotSize = this.lotsForm.controls[`${index}-size`].value; + updatedStructures.push({ + type: lotType, + size: lotSize ? parseFloat(lotSize) : null, + }); + } + const updateDto: ApplicationSubmissionUpdateDto = { purpose, subdSuitability, subdAgricultureSupport, subdIsHomeSiteSeverance: subdIsHomeSiteSeverance !== null ? subdIsHomeSiteSeverance === 'true' : null, - subdProposedLots: this.proposedLots.map((lot) => ({ - ...lot, - size: lot.size ? parseFloat(lot.size) : null, - })), + subdProposedLots: updatedStructures, }; const updatedApp = await this.applicationService.updatePending(this.submissionUuid, updateDto); @@ -128,12 +147,21 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, const targetString = (event.target as HTMLInputElement).value; const targetCount = parseInt(targetString); + for (let index = this.proposedLots.length; index > targetCount; index--) { + this.lotsForm.removeControl(`${index}-type`); + this.lotsForm.removeControl(`${index}-size`); + } + this.proposedLots = this.proposedLots.slice(0, targetCount); while (this.proposedLots.length < targetCount) { this.proposedLots.push({ size: '0', type: null, }); + + const index = this.proposedLots.length - 1; + this.lotsForm.addControl(`${index}-type`, new FormControl(null, [Validators.required])); + this.lotsForm.addControl(`${index}-size`, new FormControl(null, [Validators.required])); } this.lotsSource = new MatTableDataSource(this.proposedLots); this.calculateLotSize(); diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html index 40cdb0efee..1cdb76bb5a 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html @@ -58,75 +58,96 @@

Additional Proposal Information

Note: The form will be updated with additional required questions if you are building a structure + >Note: The form will be updated with additional required questions if you are building a structure +
- - - - - - + + +
#{{ i + 1 }}
+ + + + - - - + - + warning +
This field is required
+ + + - - - - + + + + - - - - + + + + - - + + - - - -
#{{ i + 1 }}Type - - + Type + + + + {{ type }} + + + +
- - {{ type }} - - - -
Total Floor Area - - - m2 - - Total Floor Area + + + m2 + +
+ warning +
This field is required
+
+
Action - - Action + +
- No Proposed Structures Entered. Use the button below to add your first structure. -
+ + + No Proposed Structures Entered. Use the button below to add your first structure. + + + +
diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts index 82ff5f4a63..fb08fa5f3a 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts @@ -6,7 +6,10 @@ import { MatTableDataSource } from '@angular/material/table'; import { takeUntil } from 'rxjs'; import { NoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; import { NoticeOfIntentDocumentService } from '../../../../services/notice-of-intent-document/notice-of-intent-document.service'; -import { NoticeOfIntentSubmissionUpdateDto } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; +import { + NoticeOfIntentSubmissionUpdateDto, + ProposedStructure, +} from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent-submission/notice-of-intent-submission.service'; import { DOCUMENT_TYPE } from '../../../../shared/dto/document.dto'; import { formatBooleanToString } from '../../../../shared/utils/boolean-helper'; @@ -23,7 +26,7 @@ export enum STRUCTURE_TYPES { ACCESSORY_STRUCTURE = 'Residential - Accessory Structure', } -type ProposedStructure = { type: STRUCTURE_TYPES | null; area: string | null }; +type FormProposedStructure = { type: STRUCTURE_TYPES | null; area: string | null }; export const RESIDENTIAL_STRUCTURE_TYPES = [ STRUCTURE_TYPES.ACCESSORY_STRUCTURE, @@ -52,7 +55,7 @@ export class AdditionalInformationComponent extends FilesStepComponent implement confirmRemovalOfSoil = false; buildingPlans: NoticeOfIntentDocumentDto[] = []; - proposedStructures: ProposedStructure[] = []; + proposedStructures: FormProposedStructure[] = []; structuresSource = new MatTableDataSource(this.proposedStructures); displayedColumns = ['index', 'type', 'area', 'action']; @@ -75,6 +78,8 @@ export class AdditionalInformationComponent extends FilesStepComponent implement soilStructureResidentialAccessoryUseReason: this.soilStructureResidentialAccessoryUseReason, }); + lotsForm = new FormGroup({} as any); + firstQuestion: string = 'FIX THIS'; constructor( @@ -119,10 +124,19 @@ export class AdditionalInformationComponent extends FilesStepComponent implement ...structure, area: structure.area ? structure.area.toString(10) : null, })); + + const newForm = new FormGroup({}); + for (const [index, lot] of noiSubmission.soilProposedStructures.entries()) { + newForm.addControl(`${index}-type`, new FormControl(lot.type, [Validators.required])); + newForm.addControl(`${index}-area`, new FormControl(lot.area, [Validators.required])); + } + this.lotsForm = newForm; + this.structuresSource = new MatTableDataSource(this.proposedStructures); this.prepareStructureSpecificTextInputs(); if (this.showErrors) { + this.lotsForm.markAllAsTouched(); this.form.markAllAsTouched(); } } @@ -186,23 +200,30 @@ export class AdditionalInformationComponent extends FilesStepComponent implement } protected async save(): Promise { - if (this.fileId && this.form.dirty) { + if (this.fileId && (this.form.dirty || this.lotsForm.dirty)) { const isRemovingSoilForNewStructure = this.isRemovingSoilForNewStructure.getRawValue(); const soilStructureFarmUseReason = this.soilStructureFarmUseReason.getRawValue(); const soilStructureResidentialUseReason = this.soilStructureResidentialUseReason.getRawValue(); const soilAgriParcelActivity = this.soilAgriParcelActivity.getRawValue(); const soilStructureResidentialAccessoryUseReason = this.soilStructureResidentialAccessoryUseReason.getRawValue(); + const updatedStructures: ProposedStructure[] = []; + for (const [index, lot] of this.proposedStructures.entries()) { + const lotType = this.lotsForm.controls[`${index}-type`].value; + const lotArea = this.lotsForm.controls[`${index}-area`].value; + updatedStructures.push({ + type: lotType, + area: lotArea ? parseFloat(lotArea) : null, + }); + } + const updateDto: NoticeOfIntentSubmissionUpdateDto = { soilStructureFarmUseReason, soilStructureResidentialUseReason, soilIsRemovingSoilForNewStructure: parseStringToBoolean(isRemovingSoilForNewStructure), soilAgriParcelActivity, soilStructureResidentialAccessoryUseReason, - soilProposedStructures: this.proposedStructures.map((structure) => ({ - ...structure, - area: structure.area ? parseFloat(structure.area) : null, - })), + soilProposedStructures: updatedStructures, }; const updatedApp = await this.noticeOfIntentSubmissionService.updatePending(this.submissionUuid, updateDto); @@ -242,11 +263,8 @@ export class AdditionalInformationComponent extends FilesStepComponent implement } } - onChangeStructureType(index: number, value: STRUCTURE_TYPES) { - this.proposedStructures[index].type = value; - + onChangeStructureType() { this.prepareStructureSpecificTextInputs(); - this.form.markAsDirty(); } @@ -265,8 +283,11 @@ export class AdditionalInformationComponent extends FilesStepComponent implement } private deleteStructure(index: number) { - const deletedStructure: ProposedStructure = this.proposedStructures.splice(index, 1)[0]; + const deletedStructure: FormProposedStructure = this.proposedStructures.splice(index, 1)[0]; this.structuresSource = new MatTableDataSource(this.proposedStructures); + this.lotsForm.removeControl(`${index}-type`); + this.lotsForm.removeControl(`${index}-area`); + this.lotsForm.markAsDirty(); if (deletedStructure.type === STRUCTURE_TYPES.FARM_STRUCTURE) { this.setVisibilityAndValidatorsForFarmFields(); @@ -279,18 +300,16 @@ export class AdditionalInformationComponent extends FilesStepComponent implement if (deletedStructure.type && RESIDENTIAL_STRUCTURE_TYPES.includes(deletedStructure.type)) { this.setVisibilityAndValidatorsForResidentialFields(); } - - this.form.markAsDirty(); } onStructureAdd() { this.proposedStructures.push({ type: null, area: '' }); this.structuresSource = new MatTableDataSource(this.proposedStructures); - this.form.markAsDirty(); - } - onAreaChange() { - this.form.markAsDirty(); + const index = this.proposedStructures.length - 1; + this.lotsForm.addControl(`${index}-type`, new FormControl(null, [Validators.required])); + this.lotsForm.addControl(`${index}-area`, new FormControl(null, [Validators.required])); + this.lotsForm.markAsDirty(); } private setRequired(formControl: FormControl) { From 47ec80099732ab312eaceb70be04b90002ead776 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 8 Sep 2023 11:52:45 -0700 Subject: [PATCH 368/954] Add Validation to Notification Submission * Add ALC Tab content and documents --- .../submit-confirmation-dialog.component.html | 2 +- .../transferees/transferees.component.html | 2 +- .../notification-details.component.html | 2 +- .../alc-review/alc-review.component.html | 26 ++ .../alc-review/alc-review.component.scss | 8 + .../alc-review/alc-review.component.spec.ts | 23 ++ .../alc-review/alc-review.component.ts | 35 +++ .../submission-documents.component.html | 43 +++ .../submission-documents.component.scss | 25 ++ .../submission-documents.component.spec.ts | 36 +++ .../submission-documents.component.ts | 45 +++ ...iew-notification-submission.component.html | 9 +- .../view-notification-submission.module.ts | 4 +- ...ation-submission-validator.service.spec.ts | 258 ++++++++++++++++++ ...tification-submission-validator.service.ts | 233 ++++++++++++++++ ...notification-submission.controller.spec.ts | 15 + .../notification-submission.controller.ts | 42 ++- .../notification-submission.module.ts | 2 + .../notification-submission.service.spec.ts | 7 +- .../notification-submission.service.ts | 7 +- .../notification-transferee.controller.ts | 1 + .../notification-transferee.service.spec.ts | 3 + .../notification-transferee.service.ts | 6 + 23 files changed, 810 insertions(+), 24 deletions(-) create mode 100644 portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.html create mode 100644 portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.scss create mode 100644 portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.ts create mode 100644 portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.html create mode 100644 portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.scss create mode 100644 portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts create mode 100644 portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-submission-validator.service.spec.ts create mode 100644 services/apps/alcs/src/portal/notification-submission/notification-submission-validator.service.ts diff --git a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html index 1e91e2b708..865e3302a1 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/review-and-submit/submit-confirmation-dialog/submit-confirmation-dialog.component.html @@ -20,7 +20,7 @@

Submit Notification of SRW

best of my/our knowledge, true and correct. - check_box I/we understand that the Agricultural Land Commission will take the steps necessary to confirm the + I/we understand that the Agricultural Land Commission will take the steps necessary to confirm the accuracy of the information and documents provided. This information will be available for review by any member of the public. diff --git a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.html b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.html index 318f8f0a10..3b3bbd8596 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/transferees/transferees.component.html @@ -26,7 +26,7 @@

All Transferees

- + diff --git a/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.html b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.html index 412f8e83a0..22ffb74cb1 100644 --- a/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.html +++ b/portal-frontend/src/app/features/notifications/notification-details/notification-details.component.html @@ -23,7 +23,7 @@

3. Primary Contact

{{ notificationSubmission.contactOrganization }} - +
Phone
diff --git a/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.html b/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.html new file mode 100644 index 0000000000..3194abed39 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.html @@ -0,0 +1,26 @@ +
+
+

ALC Review and Response

+
+ +
+
+
+ This section will update after the notification is submitted to the ALC. +
+
+ +
+
diff --git a/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.scss b/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.scss new file mode 100644 index 0000000000..7ca8d1c637 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/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/notifications/view-submission/alc-review/alc-review.component.spec.ts b/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.spec.ts new file mode 100644 index 0000000000..5c4cd95735 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AlcReviewComponent } from './alc-review.component'; + +describe('AlcsReviewComponent', () => { + let component: AlcReviewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [], + declarations: [AlcReviewComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AlcReviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.ts b/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.ts new file mode 100644 index 0000000000..8961327a4f --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/alc-review/alc-review.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; +import { NotificationDocumentDto } from '../../../../services/notification-document/notification-document.dto'; +import { + NOTIFICATION_STATUS, + NotificationSubmissionDetailedDto, +} from '../../../../services/notification-submission/notification-submission.dto'; + +@Component({ + selector: 'app-alc-review', + templateUrl: './alc-review.component.html', + styleUrls: ['./alc-review.component.scss'], +}) +export class AlcReviewComponent implements OnInit, OnDestroy { + private $destroy = new Subject(); + + @Input() $notificationSubmission = new BehaviorSubject(undefined); + @Input() $notificationDocuments = new BehaviorSubject([]); + + notificationSubmission: NotificationSubmissionDetailedDto | undefined; + NOTIFICATION_STATUS = NOTIFICATION_STATUS; + + constructor() {} + + ngOnInit(): void { + this.$notificationSubmission.pipe(takeUntil(this.$destroy)).subscribe((noiSubmission) => { + this.notificationSubmission = noiSubmission; + }); + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.html b/portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.html new file mode 100644 index 0000000000..e359f0710a --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.html @@ -0,0 +1,43 @@ +

SRW Documents

+
+
Type{{ element.type.code === 'INVD' ? 'Individual' : 'Organization' }}{{ element.type.code === 'INDV' ? 'Individual' : 'Organization' }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
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/notifications/view-submission/alc-review/submission-documents/submission-documents.component.scss b/portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.scss new file mode 100644 index 0000000000..0027146765 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/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/notifications/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts b/portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.spec.ts new file mode 100644 index 0000000000..3359ec78d2 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.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 { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { NotificationDocumentService } from '../../../../../services/notification-document/notification-document.service'; + +import { SubmissionDocumentsComponent } from './submission-documents.component'; + +describe('SubmissionDocumentsComponent', () => { + let component: SubmissionDocumentsComponent; + let fixture: ComponentFixture; + let mockNotificationDocumentService: DeepMocked; + + beforeEach(async () => { + mockNotificationDocumentService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [SubmissionDocumentsComponent], + providers: [ + { + provide: NotificationDocumentService, + useValue: mockNotificationDocumentService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmissionDocumentsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.ts b/portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.ts new file mode 100644 index 0000000000..2410063047 --- /dev/null +++ b/portal-frontend/src/app/features/notifications/view-submission/alc-review/submission-documents/submission-documents.component.ts @@ -0,0 +1,45 @@ +import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { BehaviorSubject, Subject, takeUntil } from 'rxjs'; +import { NotificationDocumentDto } from '../../../../../services/notification-document/notification-document.dto'; +import { NotificationDocumentService } from '../../../../../services/notification-document/notification-document.service'; + +@Component({ + selector: 'app-submission-documents', + templateUrl: './submission-documents.component.html', + styleUrls: ['./submission-documents.component.scss'], +}) +export class SubmissionDocumentsComponent implements OnInit, OnDestroy { + private $destroy = new Subject(); + + displayedColumns: string[] = ['type', 'fileName', 'source', 'uploadedAt', 'actions']; + documents: NotificationDocumentDto[] = []; + + @Input() $notificationDocuments = new BehaviorSubject([]); + + @ViewChild(MatSort) sort!: MatSort; + dataSource: MatTableDataSource = new MatTableDataSource(); + + constructor(private notificationDocumentService: NotificationDocumentService) {} + + ngOnInit(): void { + this.$notificationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { + this.dataSource = new MatTableDataSource(documents); + }); + } + + async openFile(uuid: string) { + const res = await this.notificationDocumentService.openFile(uuid); + if (res) { + window.open(res.url, '_blank'); + } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + protected readonly open = open; +} diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html index aa28457dfe..066ca7591b 100644 --- a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.component.html @@ -82,8 +82,13 @@

Applicant Submission

- -
//TODO
+ +
+ +
diff --git a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts index 8e9c36c202..695ed72b7f 100644 --- a/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts +++ b/portal-frontend/src/app/features/notifications/view-submission/view-notification-submission.module.ts @@ -4,6 +4,8 @@ import { RouterModule, Routes } from '@angular/router'; import { NgxMaskDirective, NgxMaskPipe } from 'ngx-mask'; import { SharedModule } from '../../../shared/shared.module'; import { NotificationDetailsModule } from '../notification-details/notification-details.module'; +import { AlcReviewComponent } from './alc-review/alc-review.component'; +import { SubmissionDocumentsComponent } from './alc-review/submission-documents/submission-documents.component'; import { ViewNotificationSubmissionComponent } from './view-notification-submission.component'; const routes: Routes = [ @@ -22,6 +24,6 @@ const routes: Routes = [ NgxMaskPipe, NotificationDetailsModule, ], - declarations: [ViewNotificationSubmissionComponent], + declarations: [ViewNotificationSubmissionComponent, AlcReviewComponent, SubmissionDocumentsComponent], }) export class ViewNotificationSubmissionModule {} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission-validator.service.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission-validator.service.spec.ts new file mode 100644 index 0000000000..611feac0c9 --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission-validator.service.spec.ts @@ -0,0 +1,258 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LocalGovernment } from '../../alcs/local-government/local-government.entity'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NotificationDocument } from '../../alcs/notification/notification-document/notification-document.entity'; +import { NotificationDocumentService } from '../../alcs/notification/notification-document/notification-document.service'; +import { + DOCUMENT_TYPE, + DocumentCode, +} from '../../document/document-code.entity'; +import { DOCUMENT_SOURCE } from '../../document/document.dto'; +import { Document } from '../../document/document.entity'; +import { NotificationParcel } from './notification-parcel/notification-parcel.entity'; +import { NotificationParcelService } from './notification-parcel/notification-parcel.service'; +import { NotificationSubmissionValidatorService } from './notification-submission-validator.service'; +import { NotificationSubmission } from './notification-submission.entity'; + +function includesError(errors: Error[], target: Error) { + return errors.some((error) => error.message === target.message); +} + +describe('NotificationSubmissionValidatorService', () => { + let service: NotificationSubmissionValidatorService; + let mockLGService: DeepMocked; + let mockParcelService: DeepMocked; + let mockNotificationDocumentService: DeepMocked; + + beforeEach(async () => { + mockLGService = createMock(); + mockParcelService = createMock(); + mockNotificationDocumentService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + NotificationSubmissionValidatorService, + { + provide: LocalGovernmentService, + useValue: mockLGService, + }, + { + provide: NotificationParcelService, + useValue: mockParcelService, + }, + { + provide: NotificationDocumentService, + useValue: mockNotificationDocumentService, + }, + ], + }).compile(); + + mockLGService.list.mockResolvedValue([]); + mockParcelService.fetchByFileId.mockResolvedValue([]); + mockNotificationDocumentService.getApplicantDocuments.mockResolvedValue([]); + + service = module.get( + NotificationSubmissionValidatorService, + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('should return an error for missing applicant', async () => { + const noticeOfIntentSubmission = new NotificationSubmission({}); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect(includesError(res.errors, new Error('Missing applicant'))).toBe( + true, + ); + }); + + it('should return an error for missing purpose', async () => { + const noticeOfIntentSubmission = new NotificationSubmission({}); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect(includesError(res.errors, new Error('Missing purpose'))).toBe(true); + }); + + it('should return an error for no parcels', async () => { + const noticeOfIntentSubmission = new NotificationSubmission({}); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect(includesError(res.errors, new Error('Missing applicant'))).toBe( + true, + ); + }); + + it('provide errors for invalid parcel', async () => { + const noticeOfIntentSubmission = new NotificationSubmission({}); + const parcel = new NotificationParcel({ + uuid: 'parcel-1', + ownershipTypeCode: 'SMPL', + legalDescription: null, + }); + + mockParcelService.fetchByFileId.mockResolvedValue([parcel]); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new ServiceValidationException(`Invalid Parcel ${parcel.uuid}`), + ), + ).toBe(true); + expect( + includesError( + res.errors, + new ServiceValidationException( + `Fee Simple Parcel ${parcel.uuid} has no PID`, + ), + ), + ).toBe(true); + }); + + it('should report an invalid PID', async () => { + const noticeOfIntentSubmission = new NotificationSubmission({}); + const parcel = new NotificationParcel({ + uuid: 'parcel-1', + ownershipTypeCode: 'SMPL', + pid: '1251251', + }); + + mockParcelService.fetchByFileId.mockResolvedValue([parcel]); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new ServiceValidationException(`Parcel ${parcel.uuid} has invalid PID`), + ), + ).toBe(true); + }); + + it('should return an error for invalid primary contact', async () => { + const noticeOfIntentSubmission = new NotificationSubmission({}); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Invalid Primary Contact Information`), + ), + ).toBe(true); + }); + + it('should return an error for incomplete proposal', async () => { + const noticeOfIntentSubmission = new NotificationSubmission({ + typeCode: 'SRW', + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError(res.errors, new Error(`Incomplete Proposal Information`)), + ).toBe(true); + expect( + includesError(res.errors, new Error(`SRW proposal missing SRW Terms`)), + ).toBe(true); + }); + + it('should produce an error for missing local government', async () => { + const noticeOfIntentSubmission = new NotificationSubmission({}); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error('Notification has no local government'), + ), + ).toBe(true); + }); + + it('should accept local government when its valid', async () => { + const mockLg = new LocalGovernment({ + uuid: 'lg-uuid', + name: 'lg', + bceidBusinessGuid: 'CATS', + isFirstNation: false, + }); + mockLGService.list.mockResolvedValue([mockLg]); + + const noticeOfIntentSubmission = new NotificationSubmission({ + localGovernmentUuid: mockLg.uuid, + }); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error( + `Selected local government is setup in portal ${mockLg.name}`, + ), + ), + ).toBe(false); + }); + + it('should report error for document missing type', async () => { + const incompleteDocument = new NotificationDocument({ + type: undefined, + document: new Document({ + source: DOCUMENT_SOURCE.APPLICANT, + }), + }); + + const documents = [incompleteDocument]; + mockNotificationDocumentService.getApplicantDocuments.mockResolvedValue( + documents, + ); + + const noticeOfIntentSubmission = new NotificationSubmission({}); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Document ${incompleteDocument.uuid} missing type`), + ), + ).toBe(true); + }); + + it('should report error for other document missing description', async () => { + const incompleteDocument = new NotificationDocument({ + type: new DocumentCode({ + code: DOCUMENT_TYPE.OTHER, + }), + document: new Document({ + source: DOCUMENT_SOURCE.APPLICANT, + }), + description: undefined, + }); + const noticeOfIntentSubmission = new NotificationSubmission({}); + + const documents = [incompleteDocument]; + mockNotificationDocumentService.getApplicantDocuments.mockResolvedValue( + documents, + ); + + const res = await service.validateSubmission(noticeOfIntentSubmission); + + expect( + includesError( + res.errors, + new Error(`Document ${incompleteDocument.uuid} missing description`), + ), + ).toBe(true); + }); +}); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission-validator.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission-validator.service.ts new file mode 100644 index 0000000000..bb93adff0e --- /dev/null +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission-validator.service.ts @@ -0,0 +1,233 @@ +import { ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { Injectable, Logger } from '@nestjs/common'; +import { LocalGovernmentService } from '../../alcs/local-government/local-government.service'; +import { NotificationDocument } from '../../alcs/notification/notification-document/notification-document.entity'; +import { NotificationDocumentService } from '../../alcs/notification/notification-document/notification-document.service'; +import { DOCUMENT_TYPE } from '../../document/document-code.entity'; +import { NotificationParcelService } from './notification-parcel/notification-parcel.service'; +import { NotificationSubmission } from './notification-submission.entity'; + +export class ValidatedNotificationSubmission extends NotificationSubmission { + applicant: string; + localGovernmentUuid: string; +} + +@Injectable() +export class NotificationSubmissionValidatorService { + private logger: Logger = new Logger( + NotificationSubmissionValidatorService.name, + ); + + constructor( + private localGovernmentService: LocalGovernmentService, + private notificationParcelService: NotificationParcelService, + private notificationDocumentService: NotificationDocumentService, + ) {} + + async validateSubmission(notificationSubmission: NotificationSubmission) { + const errors: Error[] = []; + + if (!notificationSubmission.applicant) { + errors.push(new ServiceValidationException('Missing applicant')); + } + + if (!notificationSubmission.purpose) { + errors.push(new ServiceValidationException('Missing purpose')); + } + + await this.validateParcels(notificationSubmission, errors); + + const applicantDocuments = + await this.notificationDocumentService.getApplicantDocuments( + notificationSubmission.fileNumber, + ); + + await this.validatePrimaryContact(notificationSubmission, errors); + + await this.validateLocalGovernment(notificationSubmission, errors); + await this.validateProposal( + notificationSubmission, + applicantDocuments, + errors, + ); + await this.validateOptionalDocuments(applicantDocuments, errors); + + return { + errors, + noticeOfIntentSubmission: + errors.length === 0 + ? (notificationSubmission as ValidatedNotificationSubmission) + : undefined, + }; + } + + private async validateParcels( + notificationSubmission: NotificationSubmission, + errors: Error[], + ) { + const parcels = await this.notificationParcelService.fetchByFileId( + notificationSubmission.fileNumber, + ); + + if (parcels.length === 0) { + errors.push( + new ServiceValidationException(`Notification has no parcels`), + ); + } + + for (const parcel of parcels) { + if ( + parcel.ownershipTypeCode === null || + parcel.legalDescription === null || + parcel.mapAreaHectares === null || + parcel.civicAddress === null + ) { + errors.push( + new ServiceValidationException(`Invalid Parcel ${parcel.uuid}`), + ); + } + + if (parcel.ownershipTypeCode === 'SMPL' && !parcel.pid) { + errors.push( + new ServiceValidationException( + `Fee Simple Parcel ${parcel.uuid} has no PID`, + ), + ); + } + + if (parcel.pid && parcel.pid.length !== 9) { + errors.push( + new ServiceValidationException( + `Parcel ${parcel.uuid} has invalid PID`, + ), + ); + } + } + + if (errors.length === 0) { + return parcels; + } + } + + private async validatePrimaryContact( + notificationSubmission: NotificationSubmission, + errors: Error[], + ) { + if ( + !notificationSubmission.contactFirstName || + !notificationSubmission.contactLastName || + !notificationSubmission.contactPhone || + !notificationSubmission.contactEmail + ) { + errors.push( + new ServiceValidationException(`Invalid Primary Contact Information`), + ); + } + } + + private async validateLocalGovernment( + notificationSubmission: NotificationSubmission, + errors: Error[], + ) { + const localGovernments = await this.localGovernmentService.list(); + const matchingLg = localGovernments.find( + (lg) => lg.uuid === notificationSubmission.localGovernmentUuid, + ); + if (!notificationSubmission.localGovernmentUuid) { + errors.push( + new ServiceValidationException('Notification has no local government'), + ); + } + + if (!matchingLg) { + errors.push( + new ServiceValidationException( + 'Cannot find local government set on Notification', + ), + ); + return; + } + + if (!matchingLg.bceidBusinessGuid) { + errors.push( + new ServiceValidationException( + `Selected local government is setup in portal ${matchingLg.name}`, + ), + ); + } + } + + private async validateOptionalDocuments( + applicantDocuments: NotificationDocument[], + errors: Error[], + ) { + const untypedDocuments = applicantDocuments.filter( + (document) => !document.type, + ); + for (const document of untypedDocuments) { + errors.push( + new ServiceValidationException( + `Document ${document.uuid} missing type`, + ), + ); + } + + const optionalDocuments = applicantDocuments.filter((document) => + [ + DOCUMENT_TYPE.OTHER, + DOCUMENT_TYPE.PHOTOGRAPH, + DOCUMENT_TYPE.PROFESSIONAL_REPORT, + ].includes(document.type?.code as DOCUMENT_TYPE), + ); + for (const document of optionalDocuments) { + if (!document.description) { + errors.push( + new ServiceValidationException( + `Document ${document.uuid} missing description`, + ), + ); + } + } + } + + private validateProposal( + notificationSubmission: NotificationSubmission, + applicantDocuments: NotificationDocument[], + errors: Error[], + ) { + if ( + !notificationSubmission.submittersFileNumber || + !notificationSubmission.purpose || + !notificationSubmission.totalArea || + notificationSubmission.hasSurveyPlan === null + ) { + errors.push( + new ServiceValidationException(`Incomplete Proposal Information`), + ); + } + + const srwTerms = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.SRW_TERMS, + ); + if (srwTerms.length === 0) { + errors.push( + new ServiceValidationException( + `${notificationSubmission.typeCode} proposal missing SRW Terms`, + ), + ); + } + + if (notificationSubmission.hasSurveyPlan) { + const surveyPlans = applicantDocuments.filter( + (document) => document.typeCode === DOCUMENT_TYPE.SURVEY_PLAN, + ); + if (surveyPlans.length === 0) { + errors.push( + new ServiceValidationException( + `${notificationSubmission.typeCode} proposal missing Survey Plans`, + ), + ); + } + } + } +} diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts index 73f24fdeac..e813a9cc24 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.spec.ts @@ -14,6 +14,10 @@ import { Notification } from '../../alcs/notification/notification.entity'; import { NotificationSubmissionProfile } from '../../common/automapper/notification-submission.automapper.profile'; import { EmailService } from '../../providers/email/email.service'; import { User } from '../../user/user.entity'; +import { + NotificationSubmissionValidatorService, + ValidatedNotificationSubmission, +} from './notification-submission-validator.service'; import { NotificationSubmissionController } from './notification-submission.controller'; import { NotificationSubmissionDetailedDto, @@ -26,6 +30,7 @@ import { NotificationTransferee } from './notification-transferee/notification-t describe('NotificationSubmissionController', () => { let controller: NotificationSubmissionController; let mockNotificationSubmissionService: DeepMocked; + let mockNotificationValidationService: DeepMocked; let mockDocumentService: DeepMocked; let mockLgService: DeepMocked; let mockEmailService: DeepMocked; @@ -40,6 +45,7 @@ describe('NotificationSubmissionController', () => { mockDocumentService = createMock(); mockLgService = createMock(); mockEmailService = createMock(); + mockNotificationValidationService = createMock(); const module: TestingModule = await Test.createTestingModule({ controllers: [NotificationSubmissionController], @@ -53,6 +59,10 @@ describe('NotificationSubmissionController', () => { provide: NoticeOfIntentDocumentService, useValue: mockDocumentService, }, + { + provide: NotificationSubmissionValidatorService, + useValue: mockNotificationValidationService, + }, { provide: LocalGovernmentService, useValue: mockLgService, @@ -275,6 +285,11 @@ describe('NotificationSubmissionController', () => { mockNotificationSubmissionService.mapToDetailedDTO.mockResolvedValue( {} as NotificationSubmissionDetailedDto, ); + mockNotificationValidationService.validateSubmission.mockResolvedValue({ + noticeOfIntentSubmission: + mockSubmission as ValidatedNotificationSubmission, + errors: [], + }); await controller.submitAsApplicant(mockFileId, { user: { diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts index 6be4e1d581..13f1e08f46 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.controller.ts @@ -13,6 +13,7 @@ import { import { NOTIFICATION_STATUS } from '../../alcs/notification/notification-submission-status/notification-status.dto'; import { PortalAuthGuard } from '../../common/authorization/portal-auth-guard.service'; import { User } from '../../user/user.entity'; +import { NotificationSubmissionValidatorService } from './notification-submission-validator.service'; import { NotificationSubmissionUpdateDto } from './notification-submission.dto'; import { NotificationSubmissionService } from './notification-submission.service'; @@ -23,6 +24,7 @@ export class NotificationSubmissionController { constructor( private notificationSubmissionService: NotificationSubmissionService, + private notificationValidationService: NotificationSubmissionValidatorService, ) {} @Get() @@ -130,18 +132,32 @@ export class NotificationSubmissionController { const notificationSubmission = await this.notificationSubmissionService.getByUuid(uuid, req.user.entity); - await this.notificationSubmissionService.submitToAlcs( - notificationSubmission, - ); - - const finalSubmission = await this.notificationSubmissionService.getByUuid( - uuid, - req.user.entity, - ); - - return await this.notificationSubmissionService.mapToDetailedDTO( - finalSubmission, - req.user.entity, - ); + const validationResult = + await this.notificationValidationService.validateSubmission( + notificationSubmission, + ); + + if (validationResult.noticeOfIntentSubmission) { + const validatedApplicationSubmission = + validationResult.noticeOfIntentSubmission; + + await this.notificationSubmissionService.submitToAlcs( + validatedApplicationSubmission, + ); + + const finalSubmission = + await this.notificationSubmissionService.getByUuid( + uuid, + req.user.entity, + ); + + return await this.notificationSubmissionService.mapToDetailedDTO( + finalSubmission, + req.user.entity, + ); + } else { + this.logger.debug(validationResult.errors); + throw new BadRequestException('Invalid Notification'); + } } } diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts index 62d5b44899..095ccccaa1 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.module.ts @@ -14,6 +14,7 @@ import { FileNumberModule } from '../../file-number/file-number.module'; import { NotificationParcelController } from './notification-parcel/notification-parcel.controller'; import { NotificationParcel } from './notification-parcel/notification-parcel.entity'; import { NotificationParcelService } from './notification-parcel/notification-parcel.service'; +import { NotificationSubmissionValidatorService } from './notification-submission-validator.service'; import { NotificationSubmissionController } from './notification-submission.controller'; import { NotificationSubmission } from './notification-submission.entity'; import { NotificationSubmissionService } from './notification-submission.service'; @@ -44,6 +45,7 @@ import { NotificationTransfereeService } from './notification-transferee/notific ], providers: [ NotificationSubmissionService, + NotificationSubmissionValidatorService, NotificationParcelService, NotificationTransfereeService, NotificationSubmissionProfile, diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts index c98e98df0d..e01a708832 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.spec.ts @@ -14,6 +14,7 @@ import { NotificationService } from '../../alcs/notification/notification.servic import { NotificationSubmissionProfile } from '../../common/automapper/notification-submission.automapper.profile'; import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; +import { ValidatedNotificationSubmission } from './notification-submission-validator.service'; import { NotificationSubmission } from './notification-submission.entity'; import { NotificationSubmissionService } from './notification-submission.service'; @@ -193,7 +194,7 @@ describe('NotificationSubmissionService', () => { expect(res[0].applicant).toEqual(applicant); }); - it('should fail on submitToAlcs if error', async () => { + it('should fail on submitToAlcs if validation fails', async () => { const applicant = 'Bruce Wayne'; const typeCode = 'fake-code'; const fileNumber = 'fake'; @@ -208,7 +209,9 @@ describe('NotificationSubmissionService', () => { mockNotificationService.submit.mockRejectedValue(new Error()); await expect( - service.submitToAlcs(noticeOfIntentSubmission), + service.submitToAlcs( + noticeOfIntentSubmission as ValidatedNotificationSubmission, + ), ).rejects.toMatchObject( new BaseServiceException(`Failed to submit notification: ${fileNumber}`), ); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts index 77257a787e..d3f658a872 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-submission.service.ts @@ -22,6 +22,7 @@ import { FileNumberService } from '../../file-number/file-number.service'; import { User } from '../../user/user.entity'; import { FALLBACK_APPLICANT_NAME } from '../../utils/owner.constants'; import { filterUndefined } from '../../utils/undefined'; +import { ValidatedNotificationSubmission } from './notification-submission-validator.service'; import { NotificationSubmissionDetailedDto, NotificationSubmissionDto, @@ -325,12 +326,12 @@ export class NotificationSubmissionService { }; } - async submitToAlcs(notificationSubmission: NotificationSubmission) { + async submitToAlcs(notificationSubmission: ValidatedNotificationSubmission) { try { const submittedNotification = await this.notificationService.submit({ fileNumber: notificationSubmission.fileNumber, - applicant: notificationSubmission.applicant!, - localGovernmentUuid: notificationSubmission.localGovernmentUuid!, + applicant: notificationSubmission.applicant, + localGovernmentUuid: notificationSubmission.localGovernmentUuid, typeCode: notificationSubmission.typeCode, dateSubmittedToAlc: new Date(), }); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts index bbac88444a..43a68adba9 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.controller.ts @@ -69,6 +69,7 @@ export class NotificationTransfereeController { const owner = await this.ownerService.create( createDto, noticeOfIntentSubmission, + req.user.entity, ); return this.mapper.mapAsync( diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.spec.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.spec.ts index 822dd90d5e..269b9edc7f 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.spec.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { Notification } from '../../../alcs/notification/notification.entity'; import { NotificationService } from '../../../alcs/notification/notification.service'; import { OwnerType } from '../../../common/owner-type/owner-type.entity'; import { User } from '../../../user/user.entity'; @@ -70,6 +71,7 @@ describe('NotificationTransfereeService', () => { it('should load the type and then call save for create', async () => { mockRepo.save.mockResolvedValue(new NotificationTransferee()); mockTypeRepo.findOneOrFail.mockResolvedValue(new OwnerType()); + mockRepo.find.mockResolvedValue([]); await service.create( { @@ -79,6 +81,7 @@ describe('NotificationTransfereeService', () => { typeCode: '', }, new NotificationSubmission(), + new User(), ); expect(mockRepo.save).toHaveBeenCalledTimes(1); diff --git a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.ts b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.ts index 9359664545..da5b942f18 100644 --- a/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.ts +++ b/services/apps/alcs/src/portal/notification-submission/notification-transferee/notification-transferee.service.ts @@ -41,6 +41,7 @@ export class NotificationTransfereeService { async create( createDto: NotificationTransfereeCreateDto, notificationSubmission: NotificationSubmission, + user: User, ) { const type = await this.typeRepository.findOneOrFail({ where: { @@ -58,6 +59,11 @@ export class NotificationTransfereeService { type, }); + await this.updateSubmissionApplicant( + newOwner.notificationSubmissionUuid, + user, + ); + return await this.repository.save(newOwner); } From c3cec43988ddfd1bbec76524cf96ab89a0e29994 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Fri, 8 Sep 2023 15:04:13 -0700 Subject: [PATCH 369/954] Add Preview State for NOIs --- .../notice-of-intent.component.html | 15 ++++- .../notice-of-intent.component.ts | 33 ++++++++-- .../notice-of-intent-timeline.service.ts | 62 ++++++++++--------- 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.html b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.html index 12df253cf8..843d18baa1 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.html +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.component.html @@ -10,7 +10,7 @@ [submissionStatusService]="noticeOfIntentStatusService" >
-