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
-
- Change NOI Type
-
+
Change NOI Type
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.
+
+
+
+
+
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)
+
+
+
+
+
+
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)
+
+
+
+
+
+ Primary Contact Authorization Letters
+
+
+ An authorization letter must be provided if:
+
+ the parcel under notice of intent is owned by more than one person;
+ the parcel(s) is owned by an organization; or
+ the parcel(s) is owned by a corporation (private, Crown, local government, First Nations); or
+ the notice of intent is being submitted by a third-party agent on behalf of the land owner(s)
+
+
+ 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.
+
+
+
+
+
+ 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,
+ );
}
}