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 + + + {{ 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, + ); } }