diff --git a/alcs-frontend/src/app/features/application/application.module.ts b/alcs-frontend/src/app/features/application/application.module.ts index e347318a34..826bf2f72d 100644 --- a/alcs-frontend/src/app/features/application/application.module.ts +++ b/alcs-frontend/src/app/features/application/application.module.ts @@ -15,7 +15,6 @@ import { appChildRoutes, ApplicationComponent } from './application.component'; import { BoundaryAmendmentComponent } from './boundary-amendment/boundary-amendment.component'; import { EditBoundaryAmendmentDialogComponent } from './boundary-amendment/edit-boundary-amendment-dialog/edit-boundary-amendment-dialog.component'; import { DecisionModule } from './decision/decision.module'; -import { DocumentUploadDialogComponent } from './documents/document-upload-dialog/document-upload-dialog.component'; import { DocumentsComponent } from './documents/documents.component'; import { InfoRequestsComponent } from './info-requests/info-requests.component'; import { InfoRequestDialogComponent } from './info-requests/info-request-dialog/info-request-dialog.component'; @@ -73,7 +72,6 @@ const routes: Routes = [ ApplicantInfoComponent, LfngInfoComponent, DocumentsComponent, - DocumentUploadDialogComponent, ProposalComponent, NfuProposalComponent, SubdProposalComponent, diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts index 1435b8b0ef..7bd93f30be 100644 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts +++ b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-documents/decision-documents.component.ts @@ -8,7 +8,7 @@ import { ApplicationDecisionV2Service } from '../../../../../services/applicatio import { ApplicationDecisionDocumentDto } from '../../../../../services/application/decision/application-decision-v2/application-decision.dto'; import { ToastService } from '../../../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { DecisionDocumentUploadDialogComponent } from '../decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component'; +import { DocumentUploadDialogComponent } from '../../../../../shared/document-upload-dialog/document-upload-dialog.component'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../../../shared/constants'; @Component({ @@ -102,7 +102,7 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { private openFileDialog(existingDocument?: ApplicationDecisionDocumentDto) { if (this.decision) { this.dialog - .open(DecisionDocumentUploadDialogComponent, { + .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', @@ -110,6 +110,8 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { fileId: this.fileId, decisionUuid: this.decision?.uuid, existingDocument: existingDocument, + decisionService: this.decisionService, + allowedVisibilityFlags: ['A', 'C', 'G', 'P'], }, }) .beforeClosed() diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html deleted file mode 100644 index 7ea010a0d0..0000000000 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html +++ /dev/null @@ -1,111 +0,0 @@ -
-

{{ title }} Document

-
-
-
-
-
- Document Upload* -
- -
- -
or drag and drop them here
- -
-
-
- {{ pendingFile.name }} -  ({{ pendingFile.size | filesize }}) -
- -
-
-
- {{ existingFile }} -
- -
- - warning A virus was detected in the file. Choose another file and try again. - -
- -
- - Document Name - - {{ extension }} - -
- -
- - -
-
- - Source - - {{ source }} - - -
-
- Visible To: -
- Applicant, L/FNG, and Commissioner -
-
- Public -
-
-
- - -
- - - -
-
-
diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss deleted file mode 100644 index 7c95496402..0000000000 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss +++ /dev/null @@ -1,82 +0,0 @@ -@use '../../../../../../../styles/colors'; - -.form { - display: grid; - grid-template-columns: 1fr 1fr; - row-gap: 32px; - column-gap: 32px; - - .double { - grid-column: 1/3; - } -} - -.full-width { - width: 100%; -} - -a { - word-break: break-all; -} - -.file { - border: 1px solid #000; - border-radius: 8px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; -} - -.upload-button { - margin-top: 6px !important; - - &.error { - border: 2px solid colors.$error-color; - } -} - -.spinner { - display: inline-block; - margin-right: 4px; -} - -:host::ng-deep { - .mdc-button__label { - display: flex; - align-items: center; - } -} - -.file-drag-drop { - background: colors.$white; - border-radius: 4px; - - &:hover { - background: colors.$grey-light !important; - } - - button:nth-child(1) { - width: 100%; - background: colors.$white; - padding: 24px; - border: none; - - &:hover { - background: colors.$grey-light !important; - } - } - - .drag-text { - margin-top: 14px; - color: colors.$grey; - } - - .icon { - color: colors.$grey; - font-size: 36px; - height: 36px; - align-content: center; - margin-bottom: 4px; - } -} \ No newline at end of file diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts deleted file mode 100644 index 759e0e8534..0000000000 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; -import { ToastService } from '../../../../../../services/toast/toast.service'; - -import { DecisionDocumentUploadDialogComponent } from './decision-document-upload-dialog.component'; - -describe('DecisionDocumentUploadDialogComponent', () => { - let component: DecisionDocumentUploadDialogComponent; - let fixture: ComponentFixture; - - let mockAppDecService: DeepMocked; - - beforeEach(async () => { - mockAppDecService = createMock(); - - const mockDialogRef = { - close: jest.fn(), - afterClosed: jest.fn(), - subscribe: jest.fn(), - backdropClick: () => new EventEmitter(), - }; - - await TestBed.configureTestingModule({ - declarations: [DecisionDocumentUploadDialogComponent], - providers: [ - { - provide: ApplicationDecisionV2Service, - useValue: mockAppDecService, - }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: ToastService, useValue: {} }, - ], - imports: [MatDialogModule], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(DecisionDocumentUploadDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts deleted file mode 100644 index 651bb281c4..0000000000 --- a/alcs-frontend/src/app/features/application/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { ApplicationDecisionDocumentDto } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.dto'; -import { ApplicationDecisionV2Service } from '../../../../../../services/application/decision/application-decision-v2/application-decision-v2.service'; -import { ToastService } from '../../../../../../services/toast/toast.service'; -import { DOCUMENT_SOURCE } from '../../../../../../shared/document/document.dto'; -import { FileHandle } from '../../../../../../shared/drag-drop-file/drag-drop-file.directive'; -import { splitExtension } from '../../../../../../shared/utils/file'; - -@Component({ - selector: 'app-app-decision-document-upload-dialog', - templateUrl: './decision-document-upload-dialog.component.html', - styleUrls: ['./decision-document-upload-dialog.component.scss'], -}) -export class DecisionDocumentUploadDialogComponent implements OnInit { - title = 'Create'; - isDirty = false; - isSaving = false; - allowsFileEdit = true; - documentType = 'Decision Package'; - - @Output() uploadFiles: EventEmitter = new EventEmitter(); - - name = new FormControl('', [Validators.required]); - type = new FormControl({ disabled: true, value: undefined }, [Validators.required]); - source = new FormControl({ disabled: true, value: DOCUMENT_SOURCE.ALC }, [Validators.required]); - - visibleToInternal = new FormControl({ disabled: true, value: true }, [Validators.required]); - visibleToPublic = new FormControl({ disabled: true, value: true }, [Validators.required]); - - documentSources = Object.values(DOCUMENT_SOURCE); - - form = new FormGroup({ - name: this.name, - type: this.type, - source: this.source, - visibleToInternal: this.visibleToInternal, - visibleToPublic: this.visibleToPublic, - }); - - pendingFile: File | undefined; - existingFile: string | undefined; - showVirusError = false; - extension = ''; - - constructor( - @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; decisionUuid: string; existingDocument?: ApplicationDecisionDocumentDto }, - protected dialog: MatDialogRef, - private decisionService: ApplicationDecisionV2Service, - private toastService: ToastService, - ) {} - - ngOnInit(): void { - if (this.data.existingDocument) { - const document = this.data.existingDocument; - this.title = 'Edit'; - - const { fileName, extension } = splitExtension(document.fileName); - this.extension = extension; - this.form.patchValue({ - name: fileName, - }); - this.existingFile = document.fileName; - } - } - - async onSubmit() { - const file = this.pendingFile; - if (file) { - const renamedFile = new File([file], this.name.value + this.extension ?? file.name, { type: file.type }) - this.isSaving = true; - if (this.data.existingDocument) { - await this.decisionService.deleteFile(this.data.decisionUuid, this.data.existingDocument.uuid); - } - - try { - await this.decisionService.uploadFile(this.data.decisionUuid, renamedFile); - } catch (err) { - this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - this.showVirusError = true; - this.isSaving = false; - this.pendingFile = undefined; - return; - } - } - - this.dialog.close(true); - this.isSaving = false; - } else if (this.data.existingDocument) { - this.isSaving = true; - await this.decisionService.updateFile( - this.data.decisionUuid, - this.data.existingDocument.uuid, - this.name.value! + this.extension, - ); - - this.dialog.close(true); - this.isSaving = false; - } - } - - uploadFile(event: Event) { - const element = event.target as HTMLInputElement; - const selectedFiles = element.files; - if (selectedFiles && selectedFiles[0]) { - this.pendingFile = selectedFiles[0]; - const { fileName, extension } = splitExtension(selectedFiles[0].name); - this.name.setValue(fileName); - this.extension = extension; - this.showVirusError = false; - } - } - - onRemoveFile() { - this.pendingFile = undefined; - this.existingFile = undefined; - this.extension = ''; - this.name.setValue(''); - } - - openFile() { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile); - window.open(fileURL, '_blank'); - } - } - - async openExistingFile() { - if (this.data.existingDocument) { - await this.decisionService.downloadFile( - this.data.decisionUuid, - this.data.existingDocument.uuid, - this.data.existingDocument.fileName, - ); - } - } - - filesDropped($event: FileHandle) { - this.pendingFile = $event.file; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - this.uploadFiles.emit($event); - } -} diff --git a/alcs-frontend/src/app/features/application/decision/decision.module.ts b/alcs-frontend/src/app/features/application/decision/decision.module.ts index 9ae5697437..ddaf336411 100644 --- a/alcs-frontend/src/app/features/application/decision/decision.module.ts +++ b/alcs-frontend/src/app/features/application/decision/decision.module.ts @@ -28,7 +28,6 @@ import { SubdInputComponent } from './decision-v2/decision-input/decision-compon import { DecisionComponentsComponent } from './decision-v2/decision-input/decision-components/decision-components.component'; import { DecisionConditionComponent } from './decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component'; import { DecisionConditionsComponent } from './decision-v2/decision-input/decision-conditions/decision-conditions.component'; -import { DecisionDocumentUploadDialogComponent } from './decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component'; import { DecisionInputV2Component } from './decision-v2/decision-input/decision-input-v2.component'; import { DecisionV2Component } from './decision-v2/decision-v2.component'; import { ReleaseDialogComponent } from './decision-v2/release-dialog/release-dialog.component'; @@ -72,7 +71,6 @@ export const decisionChildRoutes = [ DecisionConditionDateDialogComponent, DecisionComponentComponent, DecisionComponentsComponent, - DecisionDocumentUploadDialogComponent, RevertToDraftDialogComponent, DecisionDocumentsComponent, NfuInputComponent, diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.scss b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.scss deleted file mode 100644 index ba93743c38..0000000000 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.scss +++ /dev/null @@ -1,88 +0,0 @@ -@use '../../../../../styles/colors'; - -.form { - display: grid; - grid-template-columns: 1fr 1fr; - row-gap: 32px; - column-gap: 32px; - - .double { - grid-column: 1/3; - } -} - -.full-width { - width: 100%; -} - -a { - word-break: break-all; -} - -.file { - border: 1px solid #000; - border-radius: 8px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; -} - -.upload-button { - margin-top: 6px !important; - - &.error { - border: 2px solid colors.$error-color; - } -} - -.spinner { - display: inline-block; - margin-right: 4px; -} - -:host::ng-deep { - .mdc-button__label { - display: flex; - align-items: center; - } -} - -.superseded-warning { - background-color: colors.$secondary-color-dark; - color: #fff; - padding: 0 4px; -} - -.file-drag-drop { - background: colors.$white; - border-radius: 4px; - - &:hover { - background: colors.$grey-light !important; - } - - button:nth-child(1) { - width: 100%; - background: colors.$white; - padding: 24px; - border: none; - - &:hover { - background: colors.$grey-light !important; - } - } - - .drag-text { - margin-top: 14px; - color: colors.$grey; - } - - .icon { - color: colors.$grey; - font-size: 36px; - height: 36px; - align-content: center; - margin-bottom: 4px; - } -} \ No newline at end of file diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts deleted file mode 100644 index 793a43da4a..0000000000 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.ts +++ /dev/null @@ -1,297 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Subject } from 'rxjs'; -import { - ApplicationDocumentDto, - UpdateDocumentDto, -} from '../../../../services/application/application-document/application-document.dto'; -import { ApplicationDocumentService } from '../../../../services/application/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../../services/application/application-parcel/application-parcel.service'; -import { ApplicationSubmissionService } from '../../../../services/application/application-submission/application-submission.service'; -import { ToastService } from '../../../../services/toast/toast.service'; -import { - DOCUMENT_SOURCE, - DOCUMENT_SYSTEM, - DOCUMENT_TYPE, - DocumentTypeDto, -} from '../../../../shared/document/document.dto'; -import { splitExtension } from '../../../../shared/utils/file'; -import { FileHandle } from '../../../../shared/drag-drop-file/drag-drop-file.directive'; - -@Component({ - selector: 'app-document-upload-dialog', - templateUrl: './document-upload-dialog.component.html', - styleUrls: ['./document-upload-dialog.component.scss'], -}) -export class DocumentUploadDialogComponent implements OnInit, OnDestroy { - $destroy = new Subject(); - DOCUMENT_TYPE = DOCUMENT_TYPE; - - @Output() uploadFiles: EventEmitter = new EventEmitter(); - - title = 'Create'; - isDirty = false; - isSaving = false; - allowsFileEdit = true; - documentTypeAhead: string | undefined = undefined; - - name = new FormControl('', [Validators.required]); - type = new FormControl(undefined, [Validators.required]); - source = new FormControl('', [Validators.required]); - - parcelId = new FormControl(null); - ownerId = new FormControl(null); - - visibleToInternal = new FormControl(false, [Validators.required]); - visibleToPublic = new FormControl(false, [Validators.required]); - - documentTypes: DocumentTypeDto[] = []; - documentSources = Object.values(DOCUMENT_SOURCE); - selectableParcels: { uuid: string; index: number; pid?: string }[] = []; - selectableOwners: { uuid: string; label: string }[] = []; - - form = new FormGroup({ - name: this.name, - type: this.type, - source: this.source, - visibleToInternal: this.visibleToInternal, - visibleToPublic: this.visibleToPublic, - parcelId: this.parcelId, - ownerId: this.ownerId, - }); - - pendingFile: File | undefined; - existingFile: { name: string; size: number } | undefined; - showSupersededWarning = false; - showVirusError = false; - extension = ''; - - constructor( - @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; existingDocument?: ApplicationDocumentDto }, - protected dialog: MatDialogRef, - private applicationDocumentService: ApplicationDocumentService, - private parcelService: ApplicationParcelService, - private submissionService: ApplicationSubmissionService, - private toastService: ToastService, - ) {} - - ngOnInit(): void { - this.loadDocumentTypes(); - - if (this.data.existingDocument) { - const document = this.data.existingDocument; - this.title = 'Edit'; - this.allowsFileEdit = document.system === DOCUMENT_SYSTEM.ALCS; - if (document.type?.code === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { - this.prepareCertificateOfTitleUpload(document.uuid); - this.allowsFileEdit = false; - } - if (document.type?.code === DOCUMENT_TYPE.CORPORATE_SUMMARY) { - this.allowsFileEdit = false; - this.prepareCorporateSummaryUpload(document.uuid); - } - - const { fileName, extension } = splitExtension(document.fileName); - this.extension = extension; - this.form.patchValue({ - name: fileName, - type: document.type?.code, - source: document.source, - visibleToInternal: document.visibilityFlags.includes('C') || document.visibilityFlags.includes('A'), - visibleToPublic: document.visibilityFlags.includes('P'), - }); - this.documentTypeAhead = document.type!.code; - this.existingFile = { - name: document.fileName, - size: 0, - }; - } - } - - async onSubmit() { - const visibilityFlags: ('A' | 'C' | 'G' | 'P')[] = []; - - if (this.visibleToInternal.getRawValue()) { - visibilityFlags.push('A'); - visibilityFlags.push('G'); - visibilityFlags.push('C'); - } - - if (this.visibleToPublic.getRawValue()) { - visibilityFlags.push('P'); - } - - const file = this.pendingFile; - const dto: UpdateDocumentDto = { - fileName: this.name.value! + this.extension, - source: this.source.value as DOCUMENT_SOURCE, - typeCode: this.type.value as DOCUMENT_TYPE, - visibilityFlags, - parcelUuid: this.parcelId.value ?? undefined, - ownerUuid: this.ownerId.value ?? undefined, - file, - }; - - this.isSaving = true; - if (this.data.existingDocument) { - await this.applicationDocumentService.update(this.data.existingDocument.uuid, dto); - } else if (file !== undefined) { - try { - await this.applicationDocumentService.upload(this.data.fileId, { - ...dto, - file, - }); - } catch (err) { - this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - this.showVirusError = true; - this.isSaving = false; - this.pendingFile = undefined; - return; - } - } - this.showVirusError = false; - } - - this.dialog.close(true); - this.isSaving = false; - } - - ngOnDestroy(): void { - this.$destroy.next(); - this.$destroy.complete(); - } - - filterDocumentTypes(term: string, item: DocumentTypeDto) { - const termLower = term.toLocaleLowerCase(); - return ( - item.label.toLocaleLowerCase().indexOf(termLower) > -1 || - item.oatsCode.toLocaleLowerCase().indexOf(termLower) > -1 - ); - } - - async prepareCertificateOfTitleUpload(uuid?: string) { - const parcels = await this.parcelService.fetchParcels(this.data.fileId); - if (parcels.length > 0) { - this.parcelId.setValidators([Validators.required]); - this.parcelId.updateValueAndValidity(); - this.source.setValue(DOCUMENT_SOURCE.APPLICANT); - - const selectedParcel = parcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); - if (selectedParcel) { - this.parcelId.setValue(selectedParcel.uuid); - } else if (uuid) { - this.showSupersededWarning = true; - } - - this.selectableParcels = parcels.map((parcel, index) => ({ - uuid: parcel.uuid, - pid: parcel.pid, - index: index, - })); - } - } - - async prepareCorporateSummaryUpload(uuid?: string) { - const submission = await this.submissionService.fetchSubmission(this.data.fileId); - if (submission.owners.length > 0) { - const owners = submission.owners; - this.ownerId.setValidators([Validators.required]); - this.ownerId.updateValueAndValidity(); - this.source.setValue(DOCUMENT_SOURCE.APPLICANT); - - const selectedOwner = owners.find((owner) => owner.corporateSummaryUuid === uuid); - if (selectedOwner) { - this.ownerId.setValue(selectedOwner.uuid); - } else if (uuid) { - this.showSupersededWarning = true; - } - - this.selectableOwners = owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner, index) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })); - } - } - - async onDocTypeSelected($event?: DocumentTypeDto) { - if ($event) { - this.type.setValue($event.code); - } else { - this.type.setValue(undefined); - } - - if (this.type.value === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { - await this.prepareCertificateOfTitleUpload(); - this.visibleToInternal.setValue(true); - } else { - this.parcelId.setValue(null); - this.parcelId.setValidators([]); - this.parcelId.updateValueAndValidity(); - } - - if (this.type.value === DOCUMENT_TYPE.CORPORATE_SUMMARY) { - await this.prepareCorporateSummaryUpload(); - this.visibleToInternal.setValue(true); - } else { - this.ownerId.setValue(null); - this.ownerId.setValidators([]); - this.ownerId.updateValueAndValidity(); - } - } - - uploadFile(event: Event) { - const element = event.target as HTMLInputElement; - const selectedFiles = element.files; - if (selectedFiles && selectedFiles[0]) { - this.pendingFile = selectedFiles[0]; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - } - } - - onRemoveFile() { - this.pendingFile = undefined; - this.existingFile = undefined; - this.extension = ''; - this.name.setValue(''); - } - - openFile() { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile); - window.open(fileURL, '_blank'); - } - } - - async openExistingFile() { - if (this.data.existingDocument) { - await this.applicationDocumentService.download( - this.data.existingDocument.uuid, - this.data.existingDocument.fileName, - ); - } - } - - filesDropped($event: FileHandle) { - this.pendingFile = $event.file; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - this.uploadFiles.emit($event); - } - - private async loadDocumentTypes() { - const docTypes = await this.applicationDocumentService.fetchTypes(); - docTypes.sort((a, b) => (a.label > b.label ? 1 : -1)); - this.documentTypes = docTypes.filter((type) => type.code !== DOCUMENT_TYPE.ORIGINAL_APPLICATION); - } -} diff --git a/alcs-frontend/src/app/features/application/documents/documents.component.spec.ts b/alcs-frontend/src/app/features/application/documents/documents.component.spec.ts index 4ddbc4a6c3..adba41a180 100644 --- a/alcs-frontend/src/app/features/application/documents/documents.component.spec.ts +++ b/alcs-frontend/src/app/features/application/documents/documents.component.spec.ts @@ -10,6 +10,8 @@ import { ApplicationDto } from '../../../services/application/application.dto'; import { ToastService } from '../../../services/toast/toast.service'; import { DocumentsComponent } from './documents.component'; +import { ApplicationParcelService } from '../../../services/application/application-parcel/application-parcel.service'; +import { ApplicationSubmissionService } from '../../../services/application/application-submission/application-submission.service'; describe('DocumentsComponent', () => { let component: DocumentsComponent; @@ -19,6 +21,8 @@ describe('DocumentsComponent', () => { let mockDialog: DeepMocked; let mockToastService: DeepMocked; let mockAppSubStatusService: DeepMocked; + let mockAppSubService: DeepMocked; + let mockAppParcelService: DeepMocked; beforeEach(async () => { mockAppDocService = createMock(); @@ -50,6 +54,14 @@ describe('DocumentsComponent', () => { provide: ApplicationSubmissionStatusService, useValue: mockAppSubStatusService, }, + { + provide: ApplicationSubmissionService, + useValue: mockAppSubService, + }, + { + provide: ApplicationParcelService, + useValue: mockAppParcelService, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/alcs-frontend/src/app/features/application/documents/documents.component.ts b/alcs-frontend/src/app/features/application/documents/documents.component.ts index 3bf2cd2ab0..14b01d9870 100644 --- a/alcs-frontend/src/app/features/application/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/application/documents/documents.component.ts @@ -5,14 +5,19 @@ import { MatTableDataSource } from '@angular/material/table'; import { ApplicationSubmissionToSubmissionStatusDto } from '../../../services/application/application-submission-status/application-submission-status.dto'; import { ApplicationSubmissionStatusService } from '../../../services/application/application-submission-status/application-submission-status.service'; import { SUBMISSION_STATUS } from '../../../services/application/application.dto'; -import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; +import { DOCUMENT_SYSTEM, DOCUMENT_TYPE } from '../../../shared/document/document.dto'; import { ApplicationDetailService } from '../../../services/application/application-detail.service'; import { ApplicationDocumentDto } from '../../../services/application/application-document/application-document.dto'; import { ApplicationDocumentService } from '../../../services/application/application-document/application-document.service'; import { ToastService } from '../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { DocumentUploadDialogComponent } from './document-upload-dialog/document-upload-dialog.component'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; +import { ApplicationSubmissionService } from '../../../services/application/application-submission/application-submission.service'; +import { ApplicationParcelService } from '../../../services/application/application-parcel/application-parcel.service'; +import { + DocumentUploadDialogComponent, + VisibilityGroup, +} from '../../../shared/document-upload-dialog/document-upload-dialog.component'; @Component({ selector: 'app-documents', @@ -39,7 +44,9 @@ export class DocumentsComponent implements OnInit { private applicationDocumentService: ApplicationDocumentService, private applicationDetailService: ApplicationDetailService, private confirmationDialogService: ConfirmationDialogService, + private applicationSubmissionService: ApplicationSubmissionService, private applicationSubmissionStatusService: ApplicationSubmissionStatusService, + private applicationParcelService: ApplicationParcelService, private toastService: ToastService, public dialog: MatDialog, ) {} @@ -56,6 +63,9 @@ export class DocumentsComponent implements OnInit { } async onUploadFile() { + const submission = await this.applicationSubmissionService.fetchSubmission(this.fileId); + const parcels = await this.applicationParcelService.fetchParcels(this.fileId); + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', @@ -63,6 +73,19 @@ export class DocumentsComponent implements OnInit { width: '70%', data: { fileId: this.fileId, + documentService: this.applicationDocumentService, + selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), + selectableOwners: submission.owners + .filter((owner) => owner.type.code === 'ORGZ') + .map((owner) => ({ + label: owner.organizationName ?? owner.displayName, + uuid: owner.uuid, + })), + allowedVisibilityFlags: ['A', 'C', 'G', 'P'], + documentTypeToVisibilityGroupsMap: { + [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: [VisibilityGroup.INTERNAL], + [DOCUMENT_TYPE.CORPORATE_SUMMARY]: [VisibilityGroup.INTERNAL], + }, }, }) .beforeClosed() @@ -95,7 +118,10 @@ export class DocumentsComponent implements OnInit { this.dataSource.sort = this.sort; } - onEditFile(element: ApplicationDocumentDto) { + async onEditFile(element: ApplicationDocumentDto) { + const submission = await this.applicationSubmissionService.fetchSubmission(this.fileId); + const parcels = await this.applicationParcelService.fetchParcels(this.fileId); + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', @@ -104,6 +130,18 @@ export class DocumentsComponent implements OnInit { data: { fileId: this.fileId, existingDocument: element, + documentService: this.applicationDocumentService, + selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), + selectableOwners: submission.owners + .filter((owner) => owner.type.code === 'ORGZ') + .map((owner) => ({ + label: owner.organizationName ?? owner.displayName, + uuid: owner.uuid, + })), + documentTypeToVisibilityGroupsMap: { + [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: [VisibilityGroup.INTERNAL], + [DOCUMENT_TYPE.CORPORATE_SUMMARY]: [VisibilityGroup.INTERNAL], + }, }, }) .beforeClosed() diff --git a/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.html b/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.html deleted file mode 100644 index fdc228b65b..0000000000 --- a/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.html +++ /dev/null @@ -1,108 +0,0 @@ -
-

{{ title }} Document

-
-
-
-
-
- Document Upload* -
- -
- -
or drag and drop them here
- -
-
-
- {{ pendingFile.name }} -  ({{ pendingFile.size | filesize }}) -
- -
-
-
- {{ existingFile.name }} -  ({{ existingFile.size | filesize }}) -
- -
- - warning A virus was detected in the file. Choose another file and try again. - -
- -
- - Document Name - - {{ extension }} - -
- -
- - -
-
- - Source - - {{ source }} - - -
-
- - -
- - - -
-
-
diff --git a/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.scss b/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.scss deleted file mode 100644 index fcdc947261..0000000000 --- a/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.scss +++ /dev/null @@ -1,89 +0,0 @@ -@use '../../../../../styles/colors'; - -.form { - display: grid; - grid-template-columns: 1fr 1fr; - row-gap: 32px; - column-gap: 32px; - - .double { - grid-column: 1/3; - } -} - -.full-width { - width: 100%; -} - -a { - word-break: break-all; -} - -.file { - border: 1px solid #000; - border-radius: 8px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; -} - -.upload-button { - margin-top: 6px !important; - - &.error { - border: 2px solid colors.$error-color; - } -} - -.spinner { - display: inline-block; - margin-right: 4px; -} - -:host::ng-deep { - .mdc-button__label { - display: flex; - align-items: center; - } -} - -.superseded-warning { - background-color: colors.$secondary-color-dark; - color: #fff; - padding: 0 4px; -} - - -.file-drag-drop { - background: colors.$white; - border-radius: 4px; - - &:hover { - background: colors.$grey-light !important; - } - - button:nth-child(1) { - width: 100%; - background: colors.$white; - padding: 24px; - border: none; - - &:hover { - background: colors.$grey-light !important; - } - } - - .drag-text { - margin-top: 14px; - color: colors.$grey; - } - - .icon { - color: colors.$grey; - font-size: 36px; - height: 36px; - align-content: center; - margin-bottom: 4px; - } -} \ No newline at end of file diff --git a/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.spec.ts deleted file mode 100644 index 1571be1108..0000000000 --- a/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { InquiryDocumentService } from '../../../../services/inquiry/inquiry-document/inquiry-document.service'; -import { ToastService } from '../../../../services/toast/toast.service'; - -import { DocumentUploadDialogComponent } from './document-upload-dialog.component'; - -describe('DocumentUploadDialogComponent', () => { - let component: DocumentUploadDialogComponent; - let fixture: ComponentFixture; - - let mockAppDocService: DeepMocked; - - beforeEach(async () => { - mockAppDocService = createMock(); - - const mockDialogRef = { - close: jest.fn(), - afterClosed: jest.fn(), - subscribe: jest.fn(), - backdropClick: () => new EventEmitter(), - }; - - await TestBed.configureTestingModule({ - declarations: [DocumentUploadDialogComponent], - providers: [ - { - provide: InquiryDocumentService, - useValue: mockAppDocService, - }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: ToastService, useValue: {} }, - ], - imports: [MatDialogModule], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(DocumentUploadDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.ts deleted file mode 100644 index 1b0c751f46..0000000000 --- a/alcs-frontend/src/app/features/inquiry/documents/document-upload-dialog/document-upload-dialog.component.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Subject } from 'rxjs'; -import { - InquiryDocumentDto, - UpdateDocumentDto, -} from '../../../../services/inquiry/inquiry-document/inquiry-document.dto'; -import { InquiryDocumentService } from '../../../../services/inquiry/inquiry-document/inquiry-document.service'; -import { ToastService } from '../../../../services/toast/toast.service'; -import { - DOCUMENT_SOURCE, - DOCUMENT_SYSTEM, - DOCUMENT_TYPE, - DocumentTypeDto, -} from '../../../../shared/document/document.dto'; -import { splitExtension } from '../../../../shared/utils/file'; -import { FileHandle } from '../../../../shared/drag-drop-file/drag-drop-file.directive'; - -@Component({ - selector: 'app-document-upload-dialog', - templateUrl: './document-upload-dialog.component.html', - styleUrls: ['./document-upload-dialog.component.scss'], -}) -export class DocumentUploadDialogComponent implements OnInit, OnDestroy { - $destroy = new Subject(); - DOCUMENT_TYPE = DOCUMENT_TYPE; - - @Output() uploadFiles: EventEmitter = new EventEmitter(); - - title = 'Create'; - isDirty = false; - isSaving = false; - allowsFileEdit = true; - documentTypeAhead: string | undefined = undefined; - - name = new FormControl('', [Validators.required]); - type = new FormControl(undefined, [Validators.required]); - source = new FormControl('', [Validators.required]); - - documentTypes: DocumentTypeDto[] = []; - documentSources = Object.values(DOCUMENT_SOURCE); - - form = new FormGroup({ - name: this.name, - type: this.type, - source: this.source, - }); - - pendingFile: File | undefined; - existingFile: { name: string; size: number } | undefined; - showVirusError = false; - extension = ''; - - constructor( - @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; existingDocument?: InquiryDocumentDto }, - protected dialog: MatDialogRef, - private inquiryDocumentService: InquiryDocumentService, - private toastService: ToastService, - ) {} - - ngOnInit(): void { - this.loadDocumentTypes(); - - if (this.data.existingDocument) { - const document = this.data.existingDocument; - this.title = 'Edit'; - this.allowsFileEdit = document.system === DOCUMENT_SYSTEM.ALCS; - const { fileName, extension } = splitExtension(document.fileName); - this.extension = extension; - - this.form.patchValue({ - name: fileName, - type: document.type?.code, - source: document.source, - }); - this.documentTypeAhead = document.type!.code; - - this.existingFile = { - name: document.fileName, - size: 0, - }; - } - } - - async onSubmit() { - const file = this.pendingFile; - const dto: UpdateDocumentDto = { - fileName: this.name.value! + this.extension, - source: this.source.value as DOCUMENT_SOURCE, - typeCode: this.type.value as DOCUMENT_TYPE, - file, - }; - - this.isSaving = true; - if (this.data.existingDocument) { - await this.inquiryDocumentService.update(this.data.existingDocument.uuid, dto); - } else if (file !== undefined) { - try { - await this.inquiryDocumentService.upload(this.data.fileId, { - ...dto, - file, - }); - } catch (err) { - this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - this.showVirusError = true; - this.isSaving = false; - this.pendingFile = undefined; - return; - } - } - this.showVirusError = false; - } - - this.dialog.close(true); - this.isSaving = false; - } - - ngOnDestroy(): void { - this.$destroy.next(); - this.$destroy.complete(); - } - - filterDocumentTypes(term: string, item: DocumentTypeDto) { - const termLower = term.toLocaleLowerCase(); - return ( - item.label.toLocaleLowerCase().indexOf(termLower) > -1 || - item.oatsCode.toLocaleLowerCase().indexOf(termLower) > -1 - ); - } - - async onDocTypeSelected($event?: DocumentTypeDto) { - if ($event) { - this.type.setValue($event.code); - } else { - this.type.setValue(undefined); - } - } - - uploadFile(event: Event) { - const element = event.target as HTMLInputElement; - const selectedFiles = element.files; - if (selectedFiles && selectedFiles[0]) { - this.pendingFile = selectedFiles[0]; - - const documentName = selectedFiles[0].name; - const { fileName, extension } = splitExtension(documentName); - this.name.setValue(fileName); - this.extension = extension; - this.showVirusError = false; - } - } - - onRemoveFile() { - this.pendingFile = undefined; - this.existingFile = undefined; - this.extension = ''; - this.name.setValue(''); - } - - openFile() { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile); - window.open(fileURL, '_blank'); - } - } - - async openExistingFile() { - if (this.data.existingDocument) { - await this.inquiryDocumentService.download(this.data.existingDocument.uuid, this.data.existingDocument.fileName); - } - } - - filesDropped($event: FileHandle) { - this.pendingFile = $event.file; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - this.uploadFiles.emit($event); - } - - private async loadDocumentTypes() { - const docTypes = await this.inquiryDocumentService.fetchTypes(); - docTypes.sort((a, b) => (a.label > b.label ? 1 : -1)); - this.documentTypes = docTypes.filter((type) => type.code !== DOCUMENT_TYPE.ORIGINAL_APPLICATION); - } -} diff --git a/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts b/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts index b842c43b3f..902eb78d0a 100644 --- a/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/inquiry/documents/documents.component.ts @@ -9,8 +9,8 @@ import { PlanningReviewDocumentDto } from '../../../services/planning-review/pla import { ToastService } from '../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; -import { DocumentUploadDialogComponent } from './document-upload-dialog/document-upload-dialog.component'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; +import { DocumentUploadDialogComponent } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; @Component({ selector: 'app-documents', @@ -28,9 +28,9 @@ export class DocumentsComponent implements OnInit { dataSource: MatTableDataSource = new MatTableDataSource(); readonly fileNameTruncLen = FILE_NAME_TRUNCATE_LENGTH; - + constructor( - private planningReviewDocumentService: InquiryDocumentService, + private inquiryDocumentService: InquiryDocumentService, private inquiryDetailService: InquiryDetailService, private confirmationDialogService: ConfirmationDialogService, private toastService: ToastService, @@ -54,6 +54,7 @@ export class DocumentsComponent implements OnInit { width: '70%', data: { fileId: this.fileId, + documentService: this.inquiryDocumentService, }, }) .beforeClosed() @@ -65,15 +66,15 @@ export class DocumentsComponent implements OnInit { } async openFile(uuid: string, fileName: string) { - await this.planningReviewDocumentService.download(uuid, fileName); + await this.inquiryDocumentService.download(uuid, fileName); } async downloadFile(uuid: string, fileName: string) { - await this.planningReviewDocumentService.download(uuid, fileName, false); + await this.inquiryDocumentService.download(uuid, fileName, false); } private async loadDocuments(fileNumber: string) { - this.documents = await this.planningReviewDocumentService.listAll(fileNumber); + this.documents = await this.inquiryDocumentService.listAll(fileNumber); this.dataSource = new MatTableDataSource(this.documents); this.dataSource.sortingDataAccessor = (item, property) => { switch (property) { @@ -95,6 +96,7 @@ export class DocumentsComponent implements OnInit { data: { fileId: this.fileId, existingDocument: element, + documentService: this.inquiryDocumentService, }, }) .beforeClosed() @@ -112,7 +114,7 @@ export class DocumentsComponent implements OnInit { }) .subscribe(async (accepted) => { if (accepted) { - await this.planningReviewDocumentService.delete(element.uuid); + await this.inquiryDocumentService.delete(element.uuid); this.loadDocuments(this.fileId); this.toastService.showSuccessToast('Document deleted'); } diff --git a/alcs-frontend/src/app/features/inquiry/inquiry.module.ts b/alcs-frontend/src/app/features/inquiry/inquiry.module.ts index 1f6340c586..5d58217b8f 100644 --- a/alcs-frontend/src/app/features/inquiry/inquiry.module.ts +++ b/alcs-frontend/src/app/features/inquiry/inquiry.module.ts @@ -5,7 +5,6 @@ import { RouterModule, Routes } from '@angular/router'; import { InquiryDetailService } from '../../services/inquiry/inquiry-detail.service'; import { SharedModule } from '../../shared/shared.module'; import { DetailsComponent } from './detail/details.component'; -import { DocumentUploadDialogComponent } from './documents/document-upload-dialog/document-upload-dialog.component'; import { DocumentsComponent } from './documents/documents.component'; import { HeaderComponent } from './header/header.component'; import { InquiryComponent, childRoutes } from './inquiry.component'; @@ -29,7 +28,6 @@ const routes: Routes = [ DetailsComponent, ParcelsComponent, DocumentsComponent, - DocumentUploadDialogComponent, ], imports: [CommonModule, SharedModule, RouterModule.forChild(routes), CdkDropList, CdkDrag], exports: [HeaderComponent], diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts index 3bcbfb3485..492e909828 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-documents/decision-documents.component.ts @@ -10,8 +10,8 @@ import { } from '../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision.dto'; import { ToastService } from '../../../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { DecisionDocumentUploadDialogComponent } from '../decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../../../shared/constants'; +import { DocumentUploadDialogComponent } from '../../../../../shared/document-upload-dialog/document-upload-dialog.component'; @Component({ selector: 'app-decision-documents', @@ -37,7 +37,7 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { dataSource = new MatTableDataSource(); readonly fileNameTruncLen = FILE_NAME_TRUNCATE_LENGTH; - + constructor( private decisionService: NoticeOfIntentDecisionV2Service, private dialog: MatDialog, @@ -85,7 +85,7 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { private openFileDialog(existingDocument?: NoticeOfIntentDecisionDocumentDto) { if (this.decision) { this.dialog - .open(DecisionDocumentUploadDialogComponent, { + .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', @@ -93,6 +93,8 @@ export class DecisionDocumentsComponent implements OnInit, OnDestroy { fileId: this.fileId, decisionUuid: this.decision?.uuid, existingDocument: existingDocument, + decisionService: this.decisionService, + allowedVisibilityFlags: ['A', 'C', 'G', 'P'], }, }) .beforeClosed() diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html deleted file mode 100644 index 7ea010a0d0..0000000000 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html +++ /dev/null @@ -1,111 +0,0 @@ -
-

{{ title }} Document

-
-
-
-
-
- Document Upload* -
- -
- -
or drag and drop them here
- -
-
-
- {{ pendingFile.name }} -  ({{ pendingFile.size | filesize }}) -
- -
-
- - -
- - warning A virus was detected in the file. Choose another file and try again. - -
- -
- - Document Name - - {{ extension }} - -
- -
- - -
-
- - Source - - {{ source }} - - -
-
- Visible To: -
- Applicant, L/FNG, and Commissioner -
-
- Public -
-
-
- - -
- - - -
-
-
diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss deleted file mode 100644 index 27b3b52f32..0000000000 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss +++ /dev/null @@ -1,82 +0,0 @@ -@use '../../../../../../../styles/colors'; - -.form { - display: grid; - grid-template-columns: 1fr 1fr; - row-gap: 32px; - column-gap: 32px; - - .double { - grid-column: 1/3; - } -} - -.full-width { - width: 100%; -} - -a { - word-break: break-all; -} - -.file { - border: 1px solid #000; - border-radius: 8px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; -} - -.upload-button { - margin-top: 6px !important; - - &.error { - border: 2px solid colors.$error-color; - } -} - -.spinner { - display: inline-block; - margin-right: 4px; -} - -:host::ng-deep { - .mdc-button__label { - display: flex; - align-items: center; - } -} - -.file-drag-drop { - background: colors.$white; - border-radius: 4px; - - &:hover { - background: colors.$grey-light !important; - } - - button:nth-child(1) { - width: 100%; - background: colors.$white; - padding: 24px; - border: none; - - &:hover { - background: colors.$grey-light !important; - } - } - - .drag-text { - margin-top: 14px; - color: colors.$grey; - } - - .icon { - color: colors.$grey; - font-size: 36px; - height: 36px; - align-content: center; - margin-bottom: 4px; - } -} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts deleted file mode 100644 index 10697accdf..0000000000 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { NoticeOfIntentDecisionV2Service } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; -import { ToastService } from '../../../../../../services/toast/toast.service'; - -import { DecisionDocumentUploadDialogComponent } from './decision-document-upload-dialog.component'; - -describe('DecisionDocumentUploadDialogComponent', () => { - let component: DecisionDocumentUploadDialogComponent; - let fixture: ComponentFixture; - - let mockNOIDecService: DeepMocked; - - beforeEach(async () => { - mockNOIDecService = createMock(); - - const mockDialogRef = { - close: jest.fn(), - afterClosed: jest.fn(), - subscribe: jest.fn(), - backdropClick: () => new EventEmitter(), - }; - - await TestBed.configureTestingModule({ - declarations: [DecisionDocumentUploadDialogComponent], - providers: [ - { - provide: NoticeOfIntentDecisionV2Service, - useValue: mockNOIDecService, - }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: ToastService, useValue: {} }, - ], - imports: [MatDialogModule], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(DecisionDocumentUploadDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts deleted file mode 100644 index 97ada1a8c9..0000000000 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { NoticeOfIntentDecisionV2Service } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision-v2.service'; -import { NoticeOfIntentDecisionDocumentDto } from '../../../../../../services/notice-of-intent/decision-v2/notice-of-intent-decision.dto'; -import { ToastService } from '../../../../../../services/toast/toast.service'; -import { DOCUMENT_SOURCE } from '../../../../../../shared/document/document.dto'; -import { FileHandle } from '../../../../../../shared/drag-drop-file/drag-drop-file.directive'; -import { splitExtension } from '../../../../../../shared/utils/file'; - -@Component({ - selector: 'app-noi-decision-document-upload-dialog', - templateUrl: './decision-document-upload-dialog.component.html', - styleUrls: ['./decision-document-upload-dialog.component.scss'], -}) -export class DecisionDocumentUploadDialogComponent implements OnInit { - title = 'Create'; - isDirty = false; - isSaving = false; - allowsFileEdit = true; - documentType = 'Decision Package'; - - @Output() uploadFiles: EventEmitter = new EventEmitter(); - - name = new FormControl('', [Validators.required]); - type = new FormControl({ disabled: true, value: undefined }, [Validators.required]); - source = new FormControl({ disabled: true, value: DOCUMENT_SOURCE.ALC }, [Validators.required]); - - visibleToInternal = new FormControl({ disabled: true, value: true }, [Validators.required]); - visibleToPublic = new FormControl({ disabled: true, value: true }, [Validators.required]); - - documentSources = Object.values(DOCUMENT_SOURCE); - - form = new FormGroup({ - name: this.name, - type: this.type, - source: this.source, - visibleToInternal: this.visibleToInternal, - visibleToPublic: this.visibleToPublic, - }); - - pendingFile: File | undefined; - existingFile: string | undefined; - showVirusError = false; - extension = ''; - - constructor( - @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; decisionUuid: string; existingDocument?: NoticeOfIntentDecisionDocumentDto }, - protected dialog: MatDialogRef, - private decisionService: NoticeOfIntentDecisionV2Service, - private toastService: ToastService, - ) {} - - ngOnInit(): void { - if (this.data.existingDocument) { - const document = this.data.existingDocument; - this.title = 'Edit'; - const { fileName, extension } = splitExtension(document.fileName); - this.extension = extension; - this.form.patchValue({ - name: fileName, - }); - this.existingFile = document.fileName; - } - } - - async onSubmit() { - const file = this.pendingFile; - if (file) { - const renamedFile = new File([file], this.name.value + this.extension ?? file.name, { type: file.type }); - this.isSaving = true; - if (this.data.existingDocument) { - await this.decisionService.deleteFile(this.data.decisionUuid, this.data.existingDocument.uuid); - } - - try { - await this.decisionService.uploadFile(this.data.decisionUuid, renamedFile); - } catch (err) { - this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - this.showVirusError = true; - this.isSaving = false; - this.pendingFile = undefined; - return; - } - } - - this.dialog.close(true); - this.isSaving = false; - } else if (this.data.existingDocument) { - this.isSaving = true; - await this.decisionService.updateFile( - this.data.decisionUuid, - this.data.existingDocument.uuid, - this.name.value! + this.extension, - ); - - this.dialog.close(true); - this.isSaving = false; - } - } - - uploadFile(event: Event) { - const element = event.target as HTMLInputElement; - const selectedFiles = element.files; - if (selectedFiles && selectedFiles[0]) { - this.pendingFile = selectedFiles[0]; - - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.name.setValue(fileName); - this.extension = extension; - this.showVirusError = false; - } - } - - onRemoveFile() { - this.pendingFile = undefined; - this.existingFile = undefined; - this.extension = ''; - this.name.setValue(''); - } - - openFile() { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile); - window.open(fileURL, '_blank'); - } - } - - async openExistingFile() { - if (this.data.existingDocument) { - await this.decisionService.downloadFile( - this.data.decisionUuid, - this.data.existingDocument.uuid, - this.data.existingDocument.fileName, - ); - } - } - - filesDropped($event: FileHandle) { - this.pendingFile = $event.file; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - this.uploadFiles.emit($event); - } -} diff --git a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.module.ts b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.module.ts index 505be59742..8abc523ea5 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/decision/decision.module.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/decision/decision.module.ts @@ -17,7 +17,6 @@ import { RosoInputComponent } from './decision-v2/decision-input/decision-compon import { DecisionComponentsComponent } from './decision-v2/decision-input/decision-components/decision-components.component'; import { DecisionConditionComponent } from './decision-v2/decision-input/decision-conditions/decision-condition/decision-condition.component'; import { DecisionConditionsComponent } from './decision-v2/decision-input/decision-conditions/decision-conditions.component'; -import { DecisionDocumentUploadDialogComponent } from './decision-v2/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component'; import { DecisionInputV2Component } from './decision-v2/decision-input/decision-input-v2.component'; import { DecisionV2Component } from './decision-v2/decision-v2.component'; import { ReleaseDialogComponent } from './decision-v2/release-dialog/release-dialog.component'; @@ -59,7 +58,6 @@ export const decisionChildRoutes = [ ReleaseDialogComponent, DecisionComponentComponent, DecisionComponentsComponent, - DecisionDocumentUploadDialogComponent, RevertToDraftDialogComponent, DecisionDocumentsComponent, DecisionConditionComponent, diff --git a/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.html b/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.html deleted file mode 100644 index 25a0845a98..0000000000 --- a/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.html +++ /dev/null @@ -1,142 +0,0 @@ -
-

{{ title }} Document

- Superseded - Not associated with Applicant Submission in Portal -
-
-
-
-
- Document Upload* -
- -
- -
or drag and drop them here
- -
-
-
- {{ pendingFile.name }} -  ({{ pendingFile.size | filesize }}) -
- -
-
-
- {{ existingFile.name }} -  ({{ existingFile.size | filesize }}) -
- -
- - warning A virus was detected in the file. Choose another file and try again. - -
- -
- - Document Name - - {{ this.extension }} - -
- -
- - -
-
- - Source - - {{ source }} - - -
-
- - Associated Parcel - - - #{{ parcel.index + 1 }} PID: - {{ parcel.pid | mask: '000-000-000' }} - No Data - - -
-
- - Associated Organization - - - {{ owner.label }} - - - -
-
- Visible To: -
- Applicant, L/FNG -
-
- Public -
-
-
- - -
- - - -
-
-
diff --git a/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.scss b/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.scss deleted file mode 100644 index ba93743c38..0000000000 --- a/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.scss +++ /dev/null @@ -1,88 +0,0 @@ -@use '../../../../../styles/colors'; - -.form { - display: grid; - grid-template-columns: 1fr 1fr; - row-gap: 32px; - column-gap: 32px; - - .double { - grid-column: 1/3; - } -} - -.full-width { - width: 100%; -} - -a { - word-break: break-all; -} - -.file { - border: 1px solid #000; - border-radius: 8px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; -} - -.upload-button { - margin-top: 6px !important; - - &.error { - border: 2px solid colors.$error-color; - } -} - -.spinner { - display: inline-block; - margin-right: 4px; -} - -:host::ng-deep { - .mdc-button__label { - display: flex; - align-items: center; - } -} - -.superseded-warning { - background-color: colors.$secondary-color-dark; - color: #fff; - padding: 0 4px; -} - -.file-drag-drop { - background: colors.$white; - border-radius: 4px; - - &:hover { - background: colors.$grey-light !important; - } - - button:nth-child(1) { - width: 100%; - background: colors.$white; - padding: 24px; - border: none; - - &:hover { - background: colors.$grey-light !important; - } - } - - .drag-text { - margin-top: 14px; - color: colors.$grey; - } - - .icon { - color: colors.$grey; - font-size: 36px; - height: 36px; - align-content: center; - margin-bottom: 4px; - } -} \ No newline at end of file diff --git a/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.spec.ts deleted file mode 100644 index 113f491adb..0000000000 --- a/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { NoiDocumentService } from '../../../../services/notice-of-intent/noi-document/noi-document.service'; -import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service'; -import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; -import { ToastService } from '../../../../services/toast/toast.service'; - -import { DocumentUploadDialogComponent } from './document-upload-dialog.component'; - -describe('DocumentUploadDialogComponent', () => { - let component: DocumentUploadDialogComponent; - let fixture: ComponentFixture; - - let mockNoiDocService: DeepMocked; - let mockParcelService: DeepMocked; - let mockSubmissionService: DeepMocked; - - beforeEach(async () => { - mockNoiDocService = createMock(); - - const mockDialogRef = { - close: jest.fn(), - afterClosed: jest.fn(), - subscribe: jest.fn(), - backdropClick: () => new EventEmitter(), - }; - - await TestBed.configureTestingModule({ - declarations: [DocumentUploadDialogComponent], - providers: [ - { - provide: NoiDocumentService, - useValue: mockNoiDocService, - }, - { - provide: NoticeOfIntentParcelService, - useValue: mockParcelService, - }, - { - provide: NoticeOfIntentSubmissionService, - useValue: mockSubmissionService, - }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: ToastService, useValue: {} }, - ], - imports: [MatDialogModule], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(DocumentUploadDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.ts deleted file mode 100644 index 2f4a9ff3a1..0000000000 --- a/alcs-frontend/src/app/features/notice-of-intent/documents/document-upload-dialog/document-upload-dialog.component.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Subject } from 'rxjs'; -import { - NoticeOfIntentDocumentDto, - UpdateNoticeOfIntentDocumentDto, -} from '../../../../services/notice-of-intent/noi-document/noi-document.dto'; -import { NoiDocumentService } from '../../../../services/notice-of-intent/noi-document/noi-document.service'; -import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service'; -import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; -import { ToastService } from '../../../../services/toast/toast.service'; -import { - DOCUMENT_SOURCE, - DOCUMENT_SYSTEM, - DOCUMENT_TYPE, - DocumentTypeDto, -} from '../../../../shared/document/document.dto'; -import { FileHandle } from '../../../../shared/drag-drop-file/drag-drop-file.directive'; -import { splitExtension } from '../../../../shared/utils/file'; - -@Component({ - selector: 'app-document-upload-dialog', - templateUrl: './document-upload-dialog.component.html', - styleUrls: ['./document-upload-dialog.component.scss'], -}) -export class DocumentUploadDialogComponent implements OnInit, OnDestroy { - $destroy = new Subject(); - DOCUMENT_TYPE = DOCUMENT_TYPE; - - @Output() uploadFiles: EventEmitter = new EventEmitter(); - - title = 'Create'; - isDirty = false; - isSaving = false; - allowsFileEdit = true; - documentTypeAhead: string | undefined = undefined; - - name = new FormControl('', [Validators.required]); - type = new FormControl(undefined, [Validators.required]); - source = new FormControl('', [Validators.required]); - - parcelId = new FormControl(null); - ownerId = new FormControl(null); - - visibleToInternal = new FormControl(false, [Validators.required]); - visibleToPublic = new FormControl(false, [Validators.required]); - - documentTypes: DocumentTypeDto[] = []; - documentSources = Object.values(DOCUMENT_SOURCE); - selectableParcels: { uuid: string; index: number; pid?: string }[] = []; - selectableOwners: { uuid: string; label: string }[] = []; - - form = new FormGroup({ - name: this.name, - type: this.type, - source: this.source, - visibleToInternal: this.visibleToInternal, - visibleToPublic: this.visibleToPublic, - parcelId: this.parcelId, - ownerId: this.ownerId, - }); - - pendingFile: File | undefined; - existingFile: { name: string; size: number } | undefined; - showSupersededWarning = false; - showVirusError = false; - extension = ''; - - constructor( - @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; existingDocument?: NoticeOfIntentDocumentDto }, - protected dialog: MatDialogRef, - private noiDocumentService: NoiDocumentService, - private noiSubmissionService: NoticeOfIntentSubmissionService, - private noiParcelService: NoticeOfIntentParcelService, - private toastService: ToastService, - ) {} - - ngOnInit(): void { - this.loadDocumentTypes(); - - if (this.data.existingDocument) { - const document = this.data.existingDocument; - this.title = 'Edit'; - this.allowsFileEdit = document.system === DOCUMENT_SYSTEM.ALCS; - - if (document.type?.code === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { - this.prepareCertificateOfTitleUpload(document.uuid); - this.allowsFileEdit = false; - } - if (document.type?.code === DOCUMENT_TYPE.CORPORATE_SUMMARY) { - this.allowsFileEdit = false; - this.prepareCorporateSummaryUpload(document.uuid); - } - - const { fileName, extension } = splitExtension(document.fileName); - this.extension = extension; - this.form.patchValue({ - name: fileName, - type: document.type?.code, - source: document.source, - visibleToInternal: document.visibilityFlags.includes('A'), - visibleToPublic: document.visibilityFlags.includes('P'), - }); - this.documentTypeAhead = document.type!.code; - this.existingFile = { - name: document.fileName, - size: 0, - }; - } - } - - async onSubmit() { - const visibilityFlags: ('A' | 'G' | 'P')[] = []; - - if (this.visibleToInternal.getRawValue()) { - visibilityFlags.push('A'); - visibilityFlags.push('G'); - } - - if (this.visibleToPublic.getRawValue()) { - visibilityFlags.push('P'); - } - - const file = this.pendingFile; - const dto: UpdateNoticeOfIntentDocumentDto = { - fileName: this.name.value! + this.extension, - source: this.source.value as DOCUMENT_SOURCE, - typeCode: this.type.value as DOCUMENT_TYPE, - visibilityFlags, - parcelUuid: this.parcelId.value ?? undefined, - ownerUuid: this.ownerId.value ?? undefined, - file, - }; - - this.isSaving = true; - if (this.data.existingDocument) { - await this.noiDocumentService.update(this.data.existingDocument.uuid, dto); - } else if (file !== undefined) { - try { - await this.noiDocumentService.upload(this.data.fileId, { - ...dto, - file, - }); - } catch (err) { - this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - this.showVirusError = true; - this.isSaving = false; - this.pendingFile = undefined; - return; - } - } - } - this.dialog.close(true); - this.isSaving = false; - } - - ngOnDestroy(): void { - this.$destroy.next(); - this.$destroy.complete(); - } - - filterDocumentTypes(term: string, item: DocumentTypeDto) { - const termLower = term.toLocaleLowerCase(); - return ( - item.label.toLocaleLowerCase().indexOf(termLower) > -1 || - item.oatsCode.toLocaleLowerCase().indexOf(termLower) > -1 - ); - } - - async onDocTypeSelected($event?: DocumentTypeDto) { - if ($event) { - this.type.setValue($event.code); - } else { - this.type.setValue(undefined); - } - - if (this.type.value === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { - await this.prepareCertificateOfTitleUpload(); - this.visibleToInternal.setValue(true); - } else { - this.parcelId.setValue(null); - this.parcelId.setValidators([]); - this.parcelId.updateValueAndValidity(); - } - - if (this.type.value === DOCUMENT_TYPE.CORPORATE_SUMMARY) { - await this.prepareCorporateSummaryUpload(); - this.visibleToInternal.setValue(true); - } else { - this.ownerId.setValue(null); - this.ownerId.setValidators([]); - this.ownerId.updateValueAndValidity(); - } - } - - uploadFile(event: Event) { - const element = event.target as HTMLInputElement; - const selectedFiles = element.files; - if (selectedFiles && selectedFiles[0]) { - this.pendingFile = selectedFiles[0]; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.name.setValue(fileName); - this.extension = extension; - } - } - - onRemoveFile() { - this.pendingFile = undefined; - this.existingFile = undefined; - this.extension = ''; - this.name.setValue(''); - } - - openFile() { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile); - window.open(fileURL, '_blank'); - } - } - - async openExistingFile() { - if (this.data.existingDocument) { - await this.noiDocumentService.download(this.data.existingDocument.uuid, this.data.existingDocument.fileName); - } - } - - filesDropped($event: FileHandle) { - this.pendingFile = $event.file; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - this.uploadFiles.emit($event); - } - - private async prepareCertificateOfTitleUpload(uuid?: string) { - const parcels = await this.noiParcelService.fetchParcels(this.data.fileId); - if (parcels.length > 0) { - this.parcelId.setValidators([Validators.required]); - this.parcelId.updateValueAndValidity(); - this.source.setValue(DOCUMENT_SOURCE.APPLICANT); - - const selectedParcel = parcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); - if (selectedParcel) { - this.parcelId.setValue(selectedParcel.uuid); - } else if (uuid) { - this.showSupersededWarning = true; - } - - this.selectableParcels = parcels.map((parcel, index) => ({ - uuid: parcel.uuid, - pid: parcel.pid, - index: index, - })); - } - } - - private async prepareCorporateSummaryUpload(uuid?: string) { - const submission = await this.noiSubmissionService.fetchSubmission(this.data.fileId); - if (submission.owners.length > 0) { - const owners = submission.owners; - this.ownerId.setValidators([Validators.required]); - this.ownerId.updateValueAndValidity(); - this.source.setValue(DOCUMENT_SOURCE.APPLICANT); - - const selectedOwner = owners.find((owner) => owner.corporateSummaryUuid === uuid); - if (selectedOwner) { - this.ownerId.setValue(selectedOwner.uuid); - } else if (uuid) { - this.showSupersededWarning = true; - } - - this.selectableOwners = owners - .filter((owner) => owner.type.code === 'ORGZ') - .map((owner, index) => ({ - label: owner.organizationName ?? owner.displayName, - uuid: owner.uuid, - })); - } - } - - private async loadDocumentTypes() { - const docTypes = await this.noiDocumentService.fetchTypes(); - docTypes.sort((a, b) => (a.label > b.label ? 1 : -1)); - this.documentTypes = docTypes.filter((type) => type.code !== DOCUMENT_TYPE.ORIGINAL_APPLICATION); - } -} diff --git a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.spec.ts b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.spec.ts index 35bc315067..d058c65757 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.spec.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.spec.ts @@ -10,6 +10,8 @@ import { NoticeOfIntentDto } from '../../../services/notice-of-intent/notice-of- import { ToastService } from '../../../services/toast/toast.service'; import { NoiDocumentsComponent } from './documents.component'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentParcelService } from '../../../services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service'; describe('NoiDocumentsComponent', () => { let component: NoiDocumentsComponent; @@ -19,6 +21,8 @@ describe('NoiDocumentsComponent', () => { let mockDialog: DeepMocked; let mockToastService: DeepMocked; let mockNoiSubStatusService: DeepMocked; + let mockAppSubService: DeepMocked; + let mockAppParcelService: DeepMocked; beforeEach(async () => { mockNoiDocService = createMock(); @@ -51,6 +55,14 @@ describe('NoiDocumentsComponent', () => { provide: NoticeOfIntentSubmissionStatusService, useValue: mockNoiSubStatusService, }, + { + provide: NoticeOfIntentSubmissionService, + useValue: mockAppSubService, + }, + { + provide: NoticeOfIntentParcelService, + useValue: mockAppParcelService, + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts index beaef018cb..ff06f77ff8 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/documents/documents.component.ts @@ -9,15 +9,20 @@ import { NOI_SUBMISSION_STATUS, NoticeOfIntentSubmissionToSubmissionStatusDto, } from '../../../services/notice-of-intent/notice-of-intent.dto'; -import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; +import { DOCUMENT_SYSTEM, DOCUMENT_TYPE } from '../../../shared/document/document.dto'; import { ApplicationDocumentDto } from '../../../services/application/application-document/application-document.dto'; import { NoticeOfIntentDocumentDto } from '../../../services/notice-of-intent/noi-document/noi-document.dto'; import { NoiDocumentService } from '../../../services/notice-of-intent/noi-document/noi-document.service'; import { NoticeOfIntentDetailService } from '../../../services/notice-of-intent/notice-of-intent-detail.service'; import { ToastService } from '../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { DocumentUploadDialogComponent } from './document-upload-dialog/document-upload-dialog.component'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; +import { + DocumentUploadDialogComponent, + VisibilityGroup, +} from '../../../shared/document-upload-dialog/document-upload-dialog.component'; +import { NoticeOfIntentSubmissionService } from '../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; +import { NoticeOfIntentParcelService } from '../../../services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service'; @Component({ selector: 'app-noi-documents', @@ -43,7 +48,9 @@ export class NoiDocumentsComponent implements OnInit { private noiDocumentService: NoiDocumentService, private noticeOfIntentDetailService: NoticeOfIntentDetailService, private confirmationDialogService: ConfirmationDialogService, + private noiSubmissionService: NoticeOfIntentSubmissionService, private noiSubmissionStatusService: NoticeOfIntentSubmissionStatusService, + private noiParcelService: NoticeOfIntentParcelService, private toastService: ToastService, public dialog: MatDialog, ) {} @@ -60,6 +67,9 @@ export class NoiDocumentsComponent implements OnInit { } async onUploadFile() { + const submission = await this.noiSubmissionService.fetchSubmission(this.fileId); + const parcels = await this.noiParcelService.fetchParcels(this.fileId); + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', @@ -67,6 +77,19 @@ export class NoiDocumentsComponent implements OnInit { width: '70%', data: { fileId: this.fileId, + documentService: this.noiDocumentService, + selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), + selectableOwners: submission.owners + .filter((owner) => owner.type.code === 'ORGZ') + .map((owner) => ({ + label: owner.organizationName ?? owner.displayName, + uuid: owner.uuid, + })), + allowedVisibilityFlags: ['A', 'C', 'G', 'P'], + documentTypeToVisibilityGroupsMap: { + [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: [VisibilityGroup.INTERNAL], + [DOCUMENT_TYPE.CORPORATE_SUMMARY]: [VisibilityGroup.INTERNAL], + }, }, }) .beforeClosed() @@ -85,7 +108,10 @@ export class NoiDocumentsComponent implements OnInit { await this.noiDocumentService.download(uuid, fileName, false); } - onEditFile(element: NoticeOfIntentDocumentDto) { + async onEditFile(element: NoticeOfIntentDocumentDto) { + const submission = await this.noiSubmissionService.fetchSubmission(this.fileId); + const parcels = await this.noiParcelService.fetchParcels(this.fileId); + this.dialog .open(DocumentUploadDialogComponent, { minWidth: '600px', @@ -94,6 +120,19 @@ export class NoiDocumentsComponent implements OnInit { data: { fileId: this.fileId, existingDocument: element, + documentService: this.noiDocumentService, + selectableParcels: parcels.map((parcel, index) => ({ ...parcel, index })), + selectableOwners: submission.owners + .filter((owner) => owner.type.code === 'ORGZ') + .map((owner) => ({ + label: owner.organizationName ?? owner.displayName, + uuid: owner.uuid, + })), + allowedVisibilityFlags: ['A', 'C', 'G', 'P'], + documentTypeToVisibilityGroupsMap: { + [DOCUMENT_TYPE.CERTIFICATE_OF_TITLE]: [VisibilityGroup.INTERNAL], + [DOCUMENT_TYPE.CORPORATE_SUMMARY]: [VisibilityGroup.INTERNAL], + }, }, }) .beforeClosed() diff --git a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts index f1fe9e3613..40d7c57843 100644 --- a/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts +++ b/alcs-frontend/src/app/features/notice-of-intent/notice-of-intent.module.ts @@ -4,7 +4,6 @@ import { NoticeOfIntentDetailService } from '../../services/notice-of-intent/not import { SharedModule } from '../../shared/shared.module'; import { ApplicantInfoComponent } from './applicant-info/applicant-info.component'; import { NoticeOfIntentDetailsModule } from './applicant-info/notice-of-intent-details/notice-of-intent-details.module'; -import { DocumentUploadDialogComponent } from './documents/document-upload-dialog/document-upload-dialog.component'; import { NoiDocumentsComponent } from './documents/documents.component'; import { InfoRequestDialogComponent } from './info-requests/info-request-dialog/info-request-dialog.component'; import { InfoRequestsComponent } from './info-requests/info-requests.component'; @@ -41,7 +40,6 @@ const routes: Routes = [ PostDecisionComponent, EditModificationDialogComponent, NoiDocumentsComponent, - DocumentUploadDialogComponent, ApplicantInfoComponent, ProposalComponent, ParcelPrepComponent, diff --git a/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.html b/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.html deleted file mode 100644 index 56e04b839c..0000000000 --- a/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.html +++ /dev/null @@ -1,117 +0,0 @@ -
-

{{ title }} Document

-
-
-
-
-
- Document Upload* -
- -
- -
or drag and drop them here
- -
-
-
- {{ pendingFile.name }} -  ({{ pendingFile.size | filesize }}) -
- -
-
-
- {{ existingFile.name }} -  ({{ existingFile.size | filesize }}) -
- -
- - warning A virus was detected in the file. Choose another file and try again. - -
- -
- - Document Name - - {{ extension }} - -
- -
- - -
-
- - Source - - {{ source }} - - -
-
- Visible To: -
- Applicant, L/FNG -
-
- Public -
-
-
- - -
- - - -
-
-
diff --git a/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.scss b/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.scss deleted file mode 100644 index ba93743c38..0000000000 --- a/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.scss +++ /dev/null @@ -1,88 +0,0 @@ -@use '../../../../../styles/colors'; - -.form { - display: grid; - grid-template-columns: 1fr 1fr; - row-gap: 32px; - column-gap: 32px; - - .double { - grid-column: 1/3; - } -} - -.full-width { - width: 100%; -} - -a { - word-break: break-all; -} - -.file { - border: 1px solid #000; - border-radius: 8px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; -} - -.upload-button { - margin-top: 6px !important; - - &.error { - border: 2px solid colors.$error-color; - } -} - -.spinner { - display: inline-block; - margin-right: 4px; -} - -:host::ng-deep { - .mdc-button__label { - display: flex; - align-items: center; - } -} - -.superseded-warning { - background-color: colors.$secondary-color-dark; - color: #fff; - padding: 0 4px; -} - -.file-drag-drop { - background: colors.$white; - border-radius: 4px; - - &:hover { - background: colors.$grey-light !important; - } - - button:nth-child(1) { - width: 100%; - background: colors.$white; - padding: 24px; - border: none; - - &:hover { - background: colors.$grey-light !important; - } - } - - .drag-text { - margin-top: 14px; - color: colors.$grey; - } - - .icon { - color: colors.$grey; - font-size: 36px; - height: 36px; - align-content: center; - margin-bottom: 4px; - } -} \ No newline at end of file diff --git a/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.spec.ts deleted file mode 100644 index 1ccefa2049..0000000000 --- a/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { NoiDocumentService } from '../../../../services/notice-of-intent/noi-document/noi-document.service'; -import { NoticeOfIntentParcelService } from '../../../../services/notice-of-intent/notice-of-intent-parcel/notice-of-intent-parcel.service'; -import { NoticeOfIntentSubmissionService } from '../../../../services/notice-of-intent/notice-of-intent-submission/notice-of-intent-submission.service'; -import { NotificationDocumentService } from '../../../../services/notification/notification-document/notification-document.service'; -import { ToastService } from '../../../../services/toast/toast.service'; - -import { DocumentUploadDialogComponent } from './document-upload-dialog.component'; - -describe('DocumentUploadDialogComponent', () => { - let component: DocumentUploadDialogComponent; - let fixture: ComponentFixture; - - let mockNotificationDocumentService: DeepMocked; - - beforeEach(async () => { - mockNotificationDocumentService = createMock(); - - const mockDialogRef = { - close: jest.fn(), - afterClosed: jest.fn(), - subscribe: jest.fn(), - backdropClick: () => new EventEmitter(), - }; - - await TestBed.configureTestingModule({ - declarations: [DocumentUploadDialogComponent], - providers: [ - { - provide: NotificationDocumentService, - useValue: mockNotificationDocumentService, - }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: ToastService, useValue: {} }, - ], - imports: [MatDialogModule], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(DocumentUploadDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.ts deleted file mode 100644 index 0118619579..0000000000 --- a/alcs-frontend/src/app/features/notification/documents/document-upload-dialog/document-upload-dialog.component.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Subject } from 'rxjs'; -import { UpdateNoticeOfIntentDocumentDto } from '../../../../services/notice-of-intent/noi-document/noi-document.dto'; -import { NotificationDocumentDto } from '../../../../services/notification/notification-document/notification-document.dto'; -import { NotificationDocumentService } from '../../../../services/notification/notification-document/notification-document.service'; -import { ToastService } from '../../../../services/toast/toast.service'; -import { - DOCUMENT_SOURCE, - DOCUMENT_SYSTEM, - DOCUMENT_TYPE, - DocumentTypeDto, -} from '../../../../shared/document/document.dto'; -import { splitExtension } from '../../../../shared/utils/file'; -import { FileHandle } from '../../../../shared/drag-drop-file/drag-drop-file.directive'; - -@Component({ - selector: 'app-document-upload-dialog', - templateUrl: './document-upload-dialog.component.html', - styleUrls: ['./document-upload-dialog.component.scss'], -}) -export class DocumentUploadDialogComponent implements OnInit, OnDestroy { - $destroy = new Subject(); - DOCUMENT_TYPE = DOCUMENT_TYPE; - - @Output() uploadFiles: EventEmitter = new EventEmitter(); - - title = 'Create'; - isDirty = false; - isSaving = false; - allowsFileEdit = true; - documentTypeAhead: string | undefined = undefined; - - name = new FormControl('', [Validators.required]); - type = new FormControl(undefined, [Validators.required]); - source = new FormControl('', [Validators.required]); - - visibleToInternal = new FormControl(false, [Validators.required]); - visibleToPublic = new FormControl(false, [Validators.required]); - - documentTypes: DocumentTypeDto[] = []; - documentSources = Object.values(DOCUMENT_SOURCE); - - form = new FormGroup({ - name: this.name, - type: this.type, - source: this.source, - visibleToInternal: this.visibleToInternal, - visibleToPublic: this.visibleToPublic, - }); - - pendingFile: File | undefined; - existingFile: { name: string; size: number } | undefined; - showVirusError = false; - extension = ''; - - constructor( - @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; existingDocument?: NotificationDocumentDto }, - protected dialog: MatDialogRef, - private notificationDocumentService: NotificationDocumentService, - private toastService: ToastService, - ) {} - - ngOnInit(): void { - this.loadDocumentTypes(); - - if (this.data.existingDocument) { - const document = this.data.existingDocument; - this.title = 'Edit'; - this.allowsFileEdit = document.system === DOCUMENT_SYSTEM.ALCS; - - const { fileName, extension } = splitExtension(document.fileName); - this.extension = extension; - this.form.patchValue({ - name: fileName, - type: document.type?.code, - source: document.source, - visibleToInternal: document.visibilityFlags.includes('A'), - visibleToPublic: document.visibilityFlags.includes('P'), - }); - this.documentTypeAhead = document.type!.code; - this.existingFile = { - name: document.fileName, - size: 0, - }; - } - } - - async onSubmit() { - const visibilityFlags: ('A' | 'G' | 'P')[] = []; - - if (this.visibleToInternal.getRawValue()) { - visibilityFlags.push('A'); - visibilityFlags.push('G'); - } - - if (this.visibleToPublic.getRawValue()) { - visibilityFlags.push('P'); - } - - const file = this.pendingFile; - const dto: UpdateNoticeOfIntentDocumentDto = { - fileName: this.name.value! + this.extension, - source: this.source.value as DOCUMENT_SOURCE, - typeCode: this.type.value as DOCUMENT_TYPE, - visibilityFlags, - file, - }; - - this.isSaving = true; - if (this.data.existingDocument) { - await this.notificationDocumentService.update(this.data.existingDocument.uuid, dto); - } else if (file !== undefined) { - try { - await this.notificationDocumentService.upload(this.data.fileId, { - ...dto, - file, - }); - } catch (err) { - this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - this.showVirusError = true; - this.isSaving = false; - this.pendingFile = undefined; - return; - } - } - } - this.dialog.close(true); - this.isSaving = false; - } - - ngOnDestroy(): void { - this.$destroy.next(); - this.$destroy.complete(); - } - - filterDocumentTypes(term: string, item: DocumentTypeDto) { - const termLower = term.toLocaleLowerCase(); - return ( - item.label.toLocaleLowerCase().indexOf(termLower) > -1 || - item.oatsCode.toLocaleLowerCase().indexOf(termLower) > -1 - ); - } - - async onDocTypeSelected($event?: DocumentTypeDto) { - if ($event) { - this.type.setValue($event.code); - } else { - this.type.setValue(undefined); - } - } - - uploadFile(event: Event) { - const element = event.target as HTMLInputElement; - const selectedFiles = element.files; - if (selectedFiles && selectedFiles[0]) { - const { fileName, extension } = splitExtension(selectedFiles[0].name); - this.pendingFile = selectedFiles[0]; - this.name.setValue(fileName); - this.extension = extension; - } - } - - onRemoveFile() { - this.pendingFile = undefined; - this.existingFile = undefined; - this.extension = ''; - this.name.setValue(''); - } - - openFile() { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile); - window.open(fileURL, '_blank'); - } - } - - async openExistingFile() { - if (this.data.existingDocument) { - await this.notificationDocumentService.download( - this.data.existingDocument.uuid, - this.data.existingDocument.fileName, - ); - } - } - - filesDropped($event: FileHandle) { - this.pendingFile = $event.file; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - this.uploadFiles.emit($event); - } - - private async loadDocumentTypes() { - const docTypes = await this.notificationDocumentService.fetchTypes(); - docTypes.sort((a, b) => (a.label > b.label ? 1 : -1)); - this.documentTypes = docTypes.filter((type) => type.code !== DOCUMENT_TYPE.ORIGINAL_APPLICATION); - } -} diff --git a/alcs-frontend/src/app/features/notification/documents/documents.component.ts b/alcs-frontend/src/app/features/notification/documents/documents.component.ts index bb416cf00e..570202de5e 100644 --- a/alcs-frontend/src/app/features/notification/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/notification/documents/documents.component.ts @@ -10,8 +10,8 @@ import { NotificationDocumentService } from '../../../services/notification/noti import { ToastService } from '../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; -import { DocumentUploadDialogComponent } from './document-upload-dialog/document-upload-dialog.component'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; +import { DocumentUploadDialogComponent } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; @Component({ selector: 'app-notification-documents', @@ -55,6 +55,8 @@ export class NotificationDocumentsComponent implements OnInit { width: '70%', data: { fileId: this.fileId, + documentService: this.notificationDocumentService, + allowedVisibilityFlags: ['A', 'G', 'P'], }, }) .beforeClosed() @@ -96,6 +98,8 @@ export class NotificationDocumentsComponent implements OnInit { data: { fileId: this.fileId, existingDocument: element, + documentService: this.notificationDocumentService, + allowedVisibilityFlags: ['A', 'G', 'P'], }, }) .beforeClosed() diff --git a/alcs-frontend/src/app/features/notification/notification.module.ts b/alcs-frontend/src/app/features/notification/notification.module.ts index ad79308bbe..f1435650d1 100644 --- a/alcs-frontend/src/app/features/notification/notification.module.ts +++ b/alcs-frontend/src/app/features/notification/notification.module.ts @@ -4,7 +4,6 @@ import { NotificationDetailService } from '../../services/notification/notificat import { SharedModule } from '../../shared/shared.module'; import { ApplicantInfoComponent } from './applicant-info/applicant-info.component'; import { NotificationDetailsModule } from './applicant-info/notification-details/notification-details.module'; -import { DocumentUploadDialogComponent } from './documents/document-upload-dialog/document-upload-dialog.component'; import { NotificationDocumentsComponent } from './documents/documents.component'; import { IntakeComponent } from './intake/intake.component'; import { NotificationComponent, postSubmissionRoutes } from './notification.component'; @@ -25,7 +24,6 @@ const routes: Routes = [ NotificationComponent, OverviewComponent, NotificationDocumentsComponent, - DocumentUploadDialogComponent, ApplicantInfoComponent, UncancelNotificationDialogComponent, IntakeComponent, diff --git a/alcs-frontend/src/app/features/planning-review/decision/decision-documents/decision-documents.component.ts b/alcs-frontend/src/app/features/planning-review/decision/decision-documents/decision-documents.component.ts index 0428b65070..f704bbf285 100644 --- a/alcs-frontend/src/app/features/planning-review/decision/decision-documents/decision-documents.component.ts +++ b/alcs-frontend/src/app/features/planning-review/decision/decision-documents/decision-documents.component.ts @@ -10,8 +10,8 @@ import { import { PlanningReviewDecisionService } from '../../../../services/planning-review/planning-review-decision/planning-review-decision.service'; import { ToastService } from '../../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../../shared/confirmation-dialog/confirmation-dialog.service'; -import { DecisionDocumentUploadDialogComponent } from '../decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../../shared/constants'; +import { DocumentUploadDialogComponent } from '../../../../shared/document-upload-dialog/document-upload-dialog.component'; @Component({ selector: 'app-decision-documents', @@ -35,7 +35,7 @@ export class DecisionDocumentsComponent implements OnDestroy, OnChanges { new MatTableDataSource(); readonly fileNameTruncLen = FILE_NAME_TRUNCATE_LENGTH; - + constructor( private decisionService: PlanningReviewDecisionService, private dialog: MatDialog, @@ -67,7 +67,7 @@ export class DecisionDocumentsComponent implements OnDestroy, OnChanges { private openFileDialog(existingDocument?: PlanningReviewDecisionDocumentDto) { if (this.decision) { this.dialog - .open(DecisionDocumentUploadDialogComponent, { + .open(DocumentUploadDialogComponent, { minWidth: '600px', maxWidth: '800px', width: '70%', @@ -75,6 +75,7 @@ export class DecisionDocumentsComponent implements OnDestroy, OnChanges { fileId: this.fileId, decisionUuid: this.decision?.uuid, existingDocument: existingDocument, + decisionService: this.decisionService, }, }) .beforeClosed() diff --git a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html b/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html deleted file mode 100644 index 380a50982f..0000000000 --- a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.html +++ /dev/null @@ -1,102 +0,0 @@ -
-

{{ title }} Document

-
-
-
-
-
- Document Upload* -
- -
- -
or drag and drop them here
- -
-
-
- {{ pendingFile.name }} -  ({{ pendingFile.size | filesize }}) -
- -
-
- - -
- - warning A virus was detected in the file. Choose another file and try again. - -
- -
- - Document Name - - {{ extension }} - -
- -
- - -
-
- - Source - - {{ source }} - - -
-
- - -
- - - -
-
-
diff --git a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts deleted file mode 100644 index 33c3c65019..0000000000 --- a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { PlanningReviewDecisionService } from '../../../../../services/planning-review/planning-review-decision/planning-review-decision.service'; -import { ToastService } from '../../../../../services/toast/toast.service'; - -import { DecisionDocumentUploadDialogComponent } from './decision-document-upload-dialog.component'; - -describe('DecisionDocumentUploadDialogComponent', () => { - let component: DecisionDocumentUploadDialogComponent; - let fixture: ComponentFixture; - - let mockPRDecService: DeepMocked; - - beforeEach(async () => { - mockPRDecService = createMock(); - - const mockDialogRef = { - close: jest.fn(), - afterClosed: jest.fn(), - subscribe: jest.fn(), - backdropClick: () => new EventEmitter(), - }; - - await TestBed.configureTestingModule({ - declarations: [DecisionDocumentUploadDialogComponent], - providers: [ - { - provide: PlanningReviewDecisionService, - useValue: mockPRDecService, - }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: ToastService, useValue: {} }, - ], - imports: [MatDialogModule], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(DecisionDocumentUploadDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts b/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts deleted file mode 100644 index 050c883714..0000000000 --- a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { PlanningReviewDecisionDocumentDto } from '../../../../../services/planning-review/planning-review-decision/planning-review-decision.dto'; -import { PlanningReviewDecisionService } from '../../../../../services/planning-review/planning-review-decision/planning-review-decision.service'; -import { ToastService } from '../../../../../services/toast/toast.service'; -import { DOCUMENT_SOURCE } from '../../../../../shared/document/document.dto'; -import { FileHandle } from '../../../../../shared/drag-drop-file/drag-drop-file.directive'; -import { splitExtension } from '../../../../../shared/utils/file'; - -@Component({ - selector: 'app-app-decision-document-upload-dialog', - templateUrl: './decision-document-upload-dialog.component.html', - styleUrls: ['./decision-document-upload-dialog.component.scss'], -}) -export class DecisionDocumentUploadDialogComponent implements OnInit { - title = 'Create'; - isDirty = false; - isSaving = false; - allowsFileEdit = true; - documentType = 'Decision Package'; - - @Output() uploadFiles: EventEmitter = new EventEmitter(); - - name = new FormControl('', [Validators.required]); - type = new FormControl({ disabled: true, value: undefined }, [Validators.required]); - source = new FormControl({ disabled: true, value: DOCUMENT_SOURCE.ALC }, [Validators.required]); - - documentSources = Object.values(DOCUMENT_SOURCE); - - form = new FormGroup({ - name: this.name, - type: this.type, - source: this.source, - }); - - pendingFile: File | undefined; - existingFile: string | undefined; - showVirusError = false; - extension = ''; - - constructor( - @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; decisionUuid: string; existingDocument?: PlanningReviewDecisionDocumentDto }, - protected dialog: MatDialogRef, - private decisionService: PlanningReviewDecisionService, - private toastService: ToastService, - ) {} - - ngOnInit(): void { - if (this.data.existingDocument) { - const document = this.data.existingDocument; - this.title = 'Edit'; - const { fileName, extension } = splitExtension(document.fileName); - this.extension = extension; - this.form.patchValue({ - name: fileName, - }); - this.existingFile = document.fileName; - } - } - - async onSubmit() { - const file = this.pendingFile; - if (file) { - const renamedFile = new File([file], this.name.value + this.extension ?? file.name, { type: file.type }); - this.isSaving = true; - if (this.data.existingDocument) { - await this.decisionService.deleteFile(this.data.decisionUuid, this.data.existingDocument.uuid); - } - - try { - await this.decisionService.uploadFile(this.data.decisionUuid, renamedFile); - } catch (err) { - this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - this.showVirusError = true; - this.isSaving = false; - this.pendingFile = undefined; - return; - } - } - - this.dialog.close(true); - this.isSaving = false; - } else if (this.data.existingDocument) { - this.isSaving = true; - await this.decisionService.updateFile( - this.data.decisionUuid, - this.data.existingDocument.uuid, - this.name.value! + this.extension, - ); - - this.dialog.close(true); - this.isSaving = false; - } - } - - uploadFile(event: Event) { - const element = event.target as HTMLInputElement; - const selectedFiles = element.files; - if (selectedFiles && selectedFiles[0]) { - this.pendingFile = selectedFiles[0]; - - const { fileName, extension } = splitExtension(selectedFiles[0].name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - } - } - - onRemoveFile() { - this.pendingFile = undefined; - this.existingFile = undefined; - this.extension = ''; - this.name.setValue(''); - } - - openFile() { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile); - window.open(fileURL, '_blank'); - } - } - - async openExistingFile() { - if (this.data.existingDocument) { - await this.decisionService.downloadFile( - this.data.decisionUuid, - this.data.existingDocument.uuid, - this.data.existingDocument.fileName, - ); - } - } - - filesDropped($event: FileHandle) { - this.pendingFile = $event.file; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - this.uploadFiles.emit($event); - } -} diff --git a/alcs-frontend/src/app/features/planning-review/decision/decision.module.ts b/alcs-frontend/src/app/features/planning-review/decision/decision.module.ts index be0cf9b7a4..d923cf225a 100644 --- a/alcs-frontend/src/app/features/planning-review/decision/decision.module.ts +++ b/alcs-frontend/src/app/features/planning-review/decision/decision.module.ts @@ -4,7 +4,6 @@ import { MatTabsModule } from '@angular/material/tabs'; import { RouterModule } from '@angular/router'; import { SharedModule } from '../../../shared/shared.module'; import { DecisionDocumentsComponent } from './decision-documents/decision-documents.component'; -import { DecisionDocumentUploadDialogComponent } from './decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component'; import { DecisionInputComponent } from './decision-input/decision-input.component'; import { DecisionComponent } from './decision.component'; import { ReleaseDialogComponent } from './release-dialog/release-dialog.component'; @@ -39,7 +38,6 @@ export const decisionChildRoutes = [ DecisionDocumentsComponent, RevertToDraftDialogComponent, ReleaseDialogComponent, - DecisionDocumentUploadDialogComponent, ], imports: [SharedModule, RouterModule.forChild(decisionChildRoutes), MatTabsModule, MatOptionModule], }) diff --git a/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.html b/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.html deleted file mode 100644 index e1e82bbaa9..0000000000 --- a/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.html +++ /dev/null @@ -1,114 +0,0 @@ -
-

{{ title }} Document

-
-
-
-
-
- Document Upload* -
- -
- -
or drag and drop them here
- -
-
-
- {{ pendingFile.name }} -  ({{ pendingFile.size | filesize }}) -
- -
-
-
- {{ existingFile.name }} -  ({{ existingFile.size | filesize }}) -
- -
- - warning A virus was detected in the file. Choose another file and try again. - -
- -
- - Document Name - - {{ extension }} - -
- -
- - -
-
- - Source - - {{ source }} - - -
-
- Visible To: -
- Commissioner -
-
-
- - -
- - - -
-
-
diff --git a/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.scss b/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.scss deleted file mode 100644 index 312d8fadba..0000000000 --- a/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.scss +++ /dev/null @@ -1,88 +0,0 @@ -@use '../../../../../styles/colors'; - -.form { - display: grid; - grid-template-columns: 1fr 1fr; - row-gap: 32px; - column-gap: 32px; - - .double { - grid-column: 1/3; - } -} - -.full-width { - width: 100%; -} - -a { - word-break: break-all; -} - -.file { - border: 1px solid #000; - border-radius: 8px; - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; -} - -.upload-button { - margin-top: 6px !important; - - &.error { - border: 2px solid colors.$error-color; - } -} - -.spinner { - display: inline-block; - margin-right: 4px; -} - -:host::ng-deep { - .mdc-button__label { - display: flex; - align-items: center; - } -} - -.superseded-warning { - background-color: colors.$secondary-color-dark; - color: #fff; - padding: 0 4px; -} - -.file-drag-drop { - background: colors.$white; - border-radius: 4px; - - &:hover { - background: colors.$grey-light !important; - } - - button:nth-child(1) { - width: 100%; - background: colors.$white; - padding: 24px; - border: none; - - &:hover { - background: colors.$grey-light !important; - } - } - - .drag-text { - margin-top: 14px; - color: colors.$grey; - } - - .icon { - color: colors.$grey; - font-size: 36px; - height: 36px; - align-content: center; - margin-bottom: 4px; - } -} diff --git a/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.spec.ts deleted file mode 100644 index 614aa11ccc..0000000000 --- a/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.spec.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { PlanningReviewDocumentService } from '../../../../services/planning-review/planning-review-document/planning-review-document.service'; -import { ToastService } from '../../../../services/toast/toast.service'; - -import { DocumentUploadDialogComponent } from './document-upload-dialog.component'; - -describe('DocumentUploadDialogComponent', () => { - let component: DocumentUploadDialogComponent; - let fixture: ComponentFixture; - - let mockAppDocService: DeepMocked; - - beforeEach(async () => { - mockAppDocService = createMock(); - - const mockDialogRef = { - close: jest.fn(), - afterClosed: jest.fn(), - subscribe: jest.fn(), - backdropClick: () => new EventEmitter(), - }; - - await TestBed.configureTestingModule({ - declarations: [DocumentUploadDialogComponent], - providers: [ - { - provide: PlanningReviewDocumentService, - useValue: mockAppDocService, - }, - { provide: MatDialogRef, useValue: mockDialogRef }, - { provide: MAT_DIALOG_DATA, useValue: {} }, - { provide: ToastService, useValue: {} }, - ], - imports: [MatDialogModule], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); - - fixture = TestBed.createComponent(DocumentUploadDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.ts deleted file mode 100644 index 705de633f9..0000000000 --- a/alcs-frontend/src/app/features/planning-review/documents/document-upload-dialog/document-upload-dialog.component.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { HttpErrorResponse } from '@angular/common/http'; -import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { Subject } from 'rxjs'; -import { - PlanningReviewDocumentDto, - UpdateDocumentDto, -} from '../../../../services/planning-review/planning-review-document/planning-review-document.dto'; -import { PlanningReviewDocumentService } from '../../../../services/planning-review/planning-review-document/planning-review-document.service'; -import { ToastService } from '../../../../services/toast/toast.service'; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../shared/document/document.dto'; -import { FileHandle } from '../../../../shared/drag-drop-file/drag-drop-file.directive'; -import { splitExtension } from '../../../../shared/utils/file'; - -@Component({ - selector: 'app-document-upload-dialog', - templateUrl: './document-upload-dialog.component.html', - styleUrls: ['./document-upload-dialog.component.scss'], -}) -export class DocumentUploadDialogComponent implements OnInit, OnDestroy { - $destroy = new Subject(); - DOCUMENT_TYPE = DOCUMENT_TYPE; - - @Output() uploadFiles: EventEmitter = new EventEmitter(); - - title = 'Create'; - isDirty = false; - isSaving = false; - documentTypeAhead: string | undefined = undefined; - - name = new FormControl('', [Validators.required]); - type = new FormControl(undefined, [Validators.required]); - source = new FormControl('', [Validators.required]); - visibleToCommissioner = new FormControl(false, [Validators.required]); - - documentTypes: DocumentTypeDto[] = []; - documentSources = Object.values(DOCUMENT_SOURCE); - - form = new FormGroup({ - name: this.name, - type: this.type, - source: this.source, - visibleToCommissioner: this.visibleToCommissioner, - }); - - pendingFile: File | undefined; - existingFile: { name: string; size: number } | undefined; - showVirusError = false; - extension = ''; - - constructor( - @Inject(MAT_DIALOG_DATA) - public data: { fileId: string; existingDocument?: PlanningReviewDocumentDto }, - protected dialog: MatDialogRef, - private planningReviewDocumentService: PlanningReviewDocumentService, - private toastService: ToastService, - ) {} - - ngOnInit(): void { - this.loadDocumentTypes(); - - if (this.data.existingDocument) { - const document = this.data.existingDocument; - this.title = 'Edit'; - const { fileName, extension } = splitExtension(document.fileName); - this.extension = extension; - - this.form.patchValue({ - name: fileName, - type: document.type?.code, - source: document.source, - visibleToCommissioner: document.visibilityFlags.includes('C'), - }); - this.documentTypeAhead = document.type!.code; - this.existingFile = { - name: document.fileName, - size: 0, - }; - } - } - - async onSubmit() { - const visibilityFlags: 'C'[] = []; - - if (this.visibleToCommissioner.getRawValue()) { - visibilityFlags.push('C'); - } - - const file = this.pendingFile; - const dto: UpdateDocumentDto = { - fileName: this.name.value! + this.extension, - source: this.source.value as DOCUMENT_SOURCE, - typeCode: this.type.value as DOCUMENT_TYPE, - visibilityFlags, - file, - }; - - this.isSaving = true; - if (this.data.existingDocument) { - await this.planningReviewDocumentService.update(this.data.existingDocument.uuid, dto); - } else if (file !== undefined) { - try { - await this.planningReviewDocumentService.upload(this.data.fileId, { - ...dto, - file, - }); - } catch (err) { - this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - this.showVirusError = true; - this.isSaving = false; - this.pendingFile = undefined; - return; - } - } - this.showVirusError = false; - } - - this.dialog.close(true); - this.isSaving = false; - } - - ngOnDestroy(): void { - this.$destroy.next(); - this.$destroy.complete(); - } - - filterDocumentTypes(term: string, item: DocumentTypeDto) { - const termLower = term.toLocaleLowerCase(); - return ( - item.label.toLocaleLowerCase().indexOf(termLower) > -1 || - item.oatsCode.toLocaleLowerCase().indexOf(termLower) > -1 - ); - } - - async onDocTypeSelected($event?: DocumentTypeDto) { - if ($event) { - this.type.setValue($event.code); - } else { - this.type.setValue(undefined); - } - } - - uploadFile(event: Event) { - const element = event.target as HTMLInputElement; - const selectedFiles = element.files; - if (selectedFiles && selectedFiles[0]) { - this.pendingFile = selectedFiles[0]; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - } - } - - onRemoveFile() { - this.pendingFile = undefined; - this.existingFile = undefined; - this.extension = ''; - this.name.setValue(''); - } - - openFile() { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile); - window.open(fileURL, '_blank'); - } - } - - async openExistingFile() { - if (this.data.existingDocument) { - await this.planningReviewDocumentService.download( - this.data.existingDocument.uuid, - this.data.existingDocument.fileName, - ); - } - } - - filesDropped($event: FileHandle) { - this.pendingFile = $event.file; - const { fileName, extension } = splitExtension(this.pendingFile.name); - this.extension = extension; - this.name.setValue(fileName); - this.showVirusError = false; - this.uploadFiles.emit($event); - } - - private async loadDocumentTypes() { - const docTypes = await this.planningReviewDocumentService.fetchTypes(); - docTypes.sort((a, b) => (a.label > b.label ? 1 : -1)); - this.documentTypes = docTypes.filter((type) => type.code !== DOCUMENT_TYPE.ORIGINAL_APPLICATION); - } -} diff --git a/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts b/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts index aaa52334bf..eb0e6e7337 100644 --- a/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts +++ b/alcs-frontend/src/app/features/planning-review/documents/documents.component.ts @@ -8,8 +8,8 @@ import { PlanningReviewDocumentService } from '../../../services/planning-review import { ToastService } from '../../../services/toast/toast.service'; import { ConfirmationDialogService } from '../../../shared/confirmation-dialog/confirmation-dialog.service'; import { DOCUMENT_SYSTEM } from '../../../shared/document/document.dto'; -import { DocumentUploadDialogComponent } from './document-upload-dialog/document-upload-dialog.component'; import { FILE_NAME_TRUNCATE_LENGTH } from '../../../shared/constants'; +import { DocumentUploadDialogComponent } from '../../../shared/document-upload-dialog/document-upload-dialog.component'; @Component({ selector: 'app-documents', @@ -55,6 +55,8 @@ export class DocumentsComponent implements OnInit { width: '70%', data: { fileId: this.fileId, + documentService: this.planningReviewDocumentService, + allowedVisibilityFlags: ['C'], }, }) .beforeClosed() @@ -96,6 +98,8 @@ export class DocumentsComponent implements OnInit { data: { fileId: this.fileId, existingDocument: element, + documentService: this.planningReviewDocumentService, + allowedVisibilityFlags: ['C'], }, }) .beforeClosed() diff --git a/alcs-frontend/src/app/features/planning-review/planning-review.module.ts b/alcs-frontend/src/app/features/planning-review/planning-review.module.ts index 8cfc1b0c47..894135131c 100644 --- a/alcs-frontend/src/app/features/planning-review/planning-review.module.ts +++ b/alcs-frontend/src/app/features/planning-review/planning-review.module.ts @@ -4,7 +4,6 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { PlanningReviewDetailService } from '../../services/planning-review/planning-review-detail.service'; import { SharedModule } from '../../shared/shared.module'; -import { DocumentUploadDialogComponent } from './documents/document-upload-dialog/document-upload-dialog.component'; import { DocumentsComponent } from './documents/documents.component'; import { HeaderComponent } from './header/header.component'; import { OverviewComponent } from './overview/overview.component'; @@ -31,7 +30,6 @@ const routes: Routes = [ OverviewComponent, HeaderComponent, DocumentsComponent, - DocumentUploadDialogComponent, ReferralComponent, CreatePlanningReferralDialogComponent, EvidentiaryRecordComponent, diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.html b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html similarity index 77% rename from alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.html rename to alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html index e3b824a029..0931a4cc91 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.html +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.html @@ -1,8 +1,5 @@

{{ title }} Document

- Superseded - Not associated with Applicant Submission in Portal
@@ -15,7 +12,7 @@

{{ title }} Document

*ngIf="!pendingFile && !existingFile" [ngClass]="{ 'file-drag-drop': true, - error: showVirusError + error: showHasVirusError || showVirusScanFailedError, }" class="full-width upload-button" appDragDropFile @@ -43,16 +40,19 @@

{{ title }} Document

{{ existingFile.name }} -  ({{ existingFile.size | filesize }})
- + + warning A virus was detected in the file. Choose another file and try again. + + warning There was a problem scanning the file for viruses. Please try again. +
@@ -65,20 +65,19 @@

{{ title }} Document

+
Source @@ -87,11 +86,11 @@

{{ title }} Document

-
+
Associated Parcel - + #{{ parcel.index + 1 }} PID: {{ parcel.pid | mask: '000-000-000' }} No Data{{ title }} Document
-
+
Associated Organization - + {{ owner.label }}
-
+
Visible To: -
- Applicant, L/FNG, and Commissioner +
+ {{ internalVisibilityLabel }}
-
+
Public
diff --git a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.scss similarity index 96% rename from alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss rename to alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.scss index 5a86cf7e2c..85f904a71a 100644 --- a/alcs-frontend/src/app/features/planning-review/decision/decision-input/decision-file-upload-dialog/decision-document-upload-dialog.component.scss +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../../styles/colors'; +@use '../../../styles/colors'; .form { display: grid; @@ -79,4 +79,4 @@ a { align-content: center; margin-bottom: 4px; } -} \ No newline at end of file +} diff --git a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.spec.ts similarity index 81% rename from alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts rename to alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.spec.ts index 88572c1418..e750d2ccf2 100644 --- a/alcs-frontend/src/app/features/application/documents/document-upload-dialog/document-upload-dialog.component.spec.ts +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.spec.ts @@ -2,10 +2,10 @@ import { EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../../services/application/application-document/application-document.service'; -import { ApplicationParcelService } from '../../../../services/application/application-parcel/application-parcel.service'; -import { ApplicationSubmissionService } from '../../../../services/application/application-submission/application-submission.service'; -import { ToastService } from '../../../../services/toast/toast.service'; +import { ApplicationDocumentService } from '../../services/application/application-document/application-document.service'; +import { ApplicationParcelService } from '../../services/application/application-parcel/application-parcel.service'; +import { ApplicationSubmissionService } from '../../services/application/application-submission/application-submission.service'; +import { ToastService } from '../../services/toast/toast.service'; import { DocumentUploadDialogComponent } from './document-upload-dialog.component'; diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts new file mode 100644 index 0000000000..c3d28fe017 --- /dev/null +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.component.ts @@ -0,0 +1,406 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { ToastService } from '../../services/toast/toast.service'; +import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, DOCUMENT_TYPE, DocumentTypeDto } from '../document/document.dto'; +import { FileHandle } from '../drag-drop-file/drag-drop-file.directive'; +import { splitExtension } from '../utils/file'; +import { DecisionService, DocumentService } from './document-upload-dialog.interface'; +import { + CreateDocumentDto, + DocumentDto, + SelectableOwnerDto, + SelectableParcelDto, + UpdateDocumentDto, +} from './document-upload-dialog.dto'; +import { Subject } from 'rxjs'; + +export enum VisibilityGroup { + INTERNAL = 'Internal', + PUBLIC = 'Public', +} + +@Component({ + selector: 'app-document-upload-dialog', + templateUrl: './document-upload-dialog.component.html', + styleUrls: ['./document-upload-dialog.component.scss'], +}) +export class DocumentUploadDialogComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + DOCUMENT_TYPE = DOCUMENT_TYPE; + + title = 'Create'; + isDirty = false; + isSaving = false; + allowsFileEdit = true; + + @Output() uploadFiles: EventEmitter = new EventEmitter(); + + name = new FormControl('', [Validators.required]); + type = new FormControl(undefined, [Validators.required]); + source = new FormControl('', [Validators.required]); + + parcelId = new FormControl(null); + ownerId = new FormControl(null); + + visibleToInternal = new FormControl(false, [Validators.required]); + visibleToPublic = new FormControl(false, [Validators.required]); + + documentTypes: DocumentTypeDto[] = []; + documentSources = Object.values(DOCUMENT_SOURCE); + + form = new FormGroup({ + name: this.name, + type: this.type, + source: this.source, + visibleToInternal: this.visibleToInternal, + visibleToPublic: this.visibleToPublic, + parcelId: this.parcelId, + ownerId: this.ownerId, + }); + + pendingFile: File | undefined; + existingFile: { name: string; size: number } | undefined; + showSupersededWarning = false; + showHasVirusError = false; + showVirusScanFailedError = false; + extension = ''; + + internalVisibilityLabel = ''; + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: { + fileId: string; + decisionUuid?: string; + existingDocument?: DocumentDto; + decisionService?: DecisionService; + documentService?: DocumentService; + selectableParcels?: SelectableParcelDto[]; + selectableOwners?: SelectableOwnerDto[]; + allowedVisibilityFlags?: ('A' | 'C' | 'G' | 'P')[]; + documentTypeToVisibilityGroupsMap?: Record; + }, + protected dialog: MatDialogRef, + private toastService: ToastService, + ) {} + + ngOnInit(): void { + this.loadDocumentTypes(); + + this.internalVisibilityLabel = this.buildInternalVisibilityLabel(); + + if (this.data.existingDocument) { + const document = this.data.existingDocument; + this.title = 'Edit'; + this.allowsFileEdit = !!(this.data.decisionService || document.system === DOCUMENT_SYSTEM.ALCS); + + if (document.type?.code === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { + this.prepareCertificateOfTitleUpload(document.uuid); + this.allowsFileEdit = false; + } + if (document.type?.code === DOCUMENT_TYPE.CORPORATE_SUMMARY) { + this.prepareCorporateSummaryUpload(document.uuid); + this.allowsFileEdit = false; + } + + const { fileName, extension } = splitExtension(document.fileName); + this.extension = extension; + + this.form.patchValue({ + name: fileName, + source: document.source, + visibleToInternal: + document.visibilityFlags?.includes('A') || + document.visibilityFlags?.includes('C') || + document.visibilityFlags?.includes('G'), + visibleToPublic: document.visibilityFlags?.includes('P'), + }); + + this.existingFile = { name: document.fileName, size: 0 }; + + if (this.data.documentService) { + this.type.setValue(document.type!.code); + } + } + + if (this.data.decisionService) { + this.type.disable(); + this.source.disable(); + this.visibleToInternal.disable(); + this.visibleToPublic.disable(); + + this.type.setValue(DOCUMENT_TYPE.DECISION_DOCUMENT); + this.source.setValue(DOCUMENT_SOURCE.ALC); + this.visibleToInternal.setValue(true); + this.visibleToPublic.setValue(true); + } + } + + buildInternalVisibilityLabel(): string { + const ordinalsByWord = { + Applicant: 0, + 'L/FNG': 1, + Commissioner: 2, + }; + + type Word = keyof typeof ordinalsByWord; + + const wordsByFlag: { + A: Word; + G: Word; + C: Word; + } = { + A: 'Applicant', + G: 'L/FNG', + C: 'Commissioner', + }; + + const words = ( + this.data.allowedVisibilityFlags?.reduce((words, flag) => { + if (flag !== 'P') { + words.push(wordsByFlag[flag]); + } + return words; + }, [] as Word[]) ?? [] + ).sort((word1, word2) => ordinalsByWord[word1] - ordinalsByWord[word2]); + + if (words.length === 0) { + return ''; + } + + if (words.length === 1) { + return words[0]; + } + + if (words.length === 2) { + return `${words[0]} and ${words[1]}`; + } + + return `${words.slice(0, -1).join(', ')}, and ${words[words.length - 1]}`; + } + + async onSubmit() { + const file = this.pendingFile; + const visibilityFlags: ('A' | 'C' | 'G' | 'P')[] = []; + + if (this.visibleToInternal.getRawValue()) { + for (const flag of this.data.allowedVisibilityFlags ?? []) { + if (flag !== 'P') { + visibilityFlags.push(flag); + } + } + } + + if (this.visibleToPublic.getRawValue() && this.data.allowedVisibilityFlags?.includes('P')) { + visibilityFlags.push('P'); + } + + const dto: UpdateDocumentDto = { + fileName: this.name.value! + this.extension, + source: this.source.value as DOCUMENT_SOURCE, + typeCode: this.type.value as DOCUMENT_TYPE, + visibilityFlags, + parcelUuid: this.parcelId.value ?? undefined, + ownerUuid: this.ownerId.value ?? undefined, + }; + + if (file) { + const renamedFile = new File([file], this.name.value! + this.extension, { type: file.type }); + this.isSaving = true; + if (this.data.existingDocument) { + if (this.data.decisionService && this.data.decisionUuid) { + this.data.decisionService.deleteFile(this.data.decisionUuid, this.data.existingDocument.uuid); + } + } + + try { + if (this.data.decisionService && this.data.decisionUuid) { + await this.data.decisionService.uploadFile(this.data.decisionUuid, renamedFile); + } else if (this.data.documentService) { + await this.data.documentService.upload(this.data.fileId, { ...dto, file } as CreateDocumentDto); + } + } catch (err) { + this.toastService.showErrorToast('Document upload failed'); + if (err instanceof HttpErrorResponse) { + if (err.status === 400) { + this.showHasVirusError = true; + } else if (err.status === 500) { + this.showVirusScanFailedError = true; + } + this.isSaving = false; + this.pendingFile = undefined; + return; + } + } + + this.dialog.close(true); + this.isSaving = false; + } else if (this.data.existingDocument) { + this.isSaving = true; + if (this.data.decisionService && this.data.decisionUuid) { + await this.data.decisionService.updateFile( + this.data.decisionUuid, + this.data.existingDocument.uuid, + this.name.value! + this.extension, + ); + } else if (this.data.documentService) { + this.data.documentService.update(this.data.existingDocument.uuid, dto); + } + + this.dialog.close(true); + this.isSaving = false; + } + } + + async prepareCertificateOfTitleUpload(uuid?: string) { + if (this.data.selectableParcels && this.data.selectableParcels.length > 0) { + this.parcelId.setValidators([Validators.required]); + this.parcelId.updateValueAndValidity(); + this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + + const selectedParcel = this.data.selectableParcels.find((parcel) => parcel.certificateOfTitleUuid === uuid); + if (selectedParcel) { + this.parcelId.setValue(selectedParcel.uuid); + } else if (uuid) { + this.showSupersededWarning = true; + } + } + } + + async prepareCorporateSummaryUpload(uuid?: string) { + if (this.data.selectableOwners && this.data.selectableOwners.length > 0) { + this.ownerId.setValidators([Validators.required]); + this.ownerId.updateValueAndValidity(); + this.source.setValue(DOCUMENT_SOURCE.APPLICANT); + + const selectedOwner = this.data.selectableOwners.find((owner) => owner.corporateSummaryUuid === uuid); + if (selectedOwner) { + this.ownerId.setValue(selectedOwner.uuid); + } else if (uuid) { + this.showSupersededWarning = true; + } + } + } + + async onDocTypeSelected($event?: DocumentTypeDto) { + if (!$event) { + return; + } + + if (this.data.documentTypeToVisibilityGroupsMap && this.data.documentTypeToVisibilityGroupsMap[$event.code]) { + for (const visibilityGroup of this.data.documentTypeToVisibilityGroupsMap[$event.code]) { + if (visibilityGroup === VisibilityGroup.INTERNAL) { + this.visibleToInternal.setValue(true); + } + + if (visibilityGroup === VisibilityGroup.PUBLIC) { + this.visibleToPublic.setValue(true); + } + } + } + + if ($event.code === DOCUMENT_TYPE.CERTIFICATE_OF_TITLE) { + await this.prepareCertificateOfTitleUpload(); + } else { + this.parcelId.setValue(null); + this.parcelId.setValidators([]); + this.parcelId.updateValueAndValidity(); + } + + if ($event.code === DOCUMENT_TYPE.CORPORATE_SUMMARY) { + await this.prepareCorporateSummaryUpload(); + } else { + this.ownerId.setValue(null); + this.ownerId.setValidators([]); + this.ownerId.updateValueAndValidity(); + } + } + + filterDocumentTypes(term: string, item: DocumentTypeDto) { + const termLower = term.toLocaleLowerCase(); + return ( + item.label.toLocaleLowerCase().indexOf(termLower) > -1 || + item.oatsCode.toLocaleLowerCase().indexOf(termLower) > -1 + ); + } + + uploadFile(event: Event) { + const element = event.target as HTMLInputElement; + const selectedFiles = element.files; + if (selectedFiles && selectedFiles[0]) { + this.pendingFile = selectedFiles[0]; + const { fileName, extension } = splitExtension(selectedFiles[0].name); + this.name.setValue(fileName); + this.extension = extension; + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + } + } + + onRemoveFile() { + this.pendingFile = undefined; + this.existingFile = undefined; + this.extension = ''; + this.name.setValue(''); + } + + openFile() { + if (this.pendingFile) { + const fileURL = URL.createObjectURL(this.pendingFile); + window.open(fileURL, '_blank'); + } + } + + async openExistingFile() { + if (this.data.existingDocument) { + if (this.data.decisionService && this.data.decisionUuid) { + await this.data.decisionService.downloadFile( + this.data.decisionUuid, + this.data.existingDocument.uuid, + this.data.existingDocument.fileName, + true, + ); + } else if (this.data.documentService) { + await this.data.documentService.download( + this.data.existingDocument.uuid, + this.data.existingDocument.fileName, + true, + ); + } + } + } + + filesDropped($event: FileHandle) { + this.pendingFile = $event.file; + const { fileName, extension } = splitExtension(this.pendingFile.name); + this.extension = extension; + this.name.setValue(fileName); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + this.uploadFiles.emit($event); + } + + private async loadDocumentTypes() { + if (this.data.documentService) { + const docTypes = await this.data.documentService.fetchTypes(); + docTypes.sort((a, b) => (a.label > b.label ? 1 : -1)); + this.documentTypes = docTypes.filter((type) => type.code !== DOCUMENT_TYPE.ORIGINAL_APPLICATION); + } else if (this.data.decisionService) { + this.documentTypes = [ + { + code: DOCUMENT_TYPE.DECISION_DOCUMENT, + label: 'Decision Package', + description: '', + oatsCode: '', + }, + ]; + } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts new file mode 100644 index 0000000000..73bb4085ff --- /dev/null +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.dto.ts @@ -0,0 +1,44 @@ +import { DOCUMENT_SOURCE, DOCUMENT_SYSTEM, DOCUMENT_TYPE, DocumentTypeDto } from '../../shared/document/document.dto'; + +export interface UpdateDocumentDto { + file?: File; + parcelUuid?: string; + ownerUuid?: string; + fileName: string; + typeCode: DOCUMENT_TYPE; + source: DOCUMENT_SOURCE; + visibilityFlags?: ('A' | 'C' | 'G' | 'P')[]; +} + +export interface CreateDocumentDto extends UpdateDocumentDto { + file: File; +} + +export interface DocumentDto { + uuid: string; + documentUuid: string; + type?: DocumentTypeDto; + description?: string; + visibilityFlags?: string[]; + source: DOCUMENT_SOURCE; + system: DOCUMENT_SYSTEM; + fileName: string; + mimeType: string; + uploadedBy: string; + uploadedAt: number; + evidentiaryRecordSorting?: number; + fileSize?: number; +} + +export interface SelectableParcelDto { + uuid: string; + pid: string; + certificateOfTitleUuid: string; + index: string; +} + +export interface SelectableOwnerDto { + label: string; + uuid: string; + corporateSummaryUuid: string; +} diff --git a/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts new file mode 100644 index 0000000000..388f12d61c --- /dev/null +++ b/alcs-frontend/src/app/shared/document-upload-dialog/document-upload-dialog.interface.ts @@ -0,0 +1,16 @@ +import { DocumentTypeDto } from '../document/document.dto'; +import { CreateDocumentDto, UpdateDocumentDto } from './document-upload-dialog.dto'; + +export interface DecisionService { + uploadFile(decisionUuid: string, file: File): Promise; + downloadFile(decisionUuid: string, documentUuid: string, fileName: string, isInline: boolean): Promise; + updateFile(decisionUuid: string, documentUuid: string, fileName: string): Promise; + deleteFile(decisionUuid: string, documentUuid: string): Promise<{ url: string }>; +} + +export interface DocumentService { + update(uuid: string, updateDto: UpdateDocumentDto): Promise; + upload(fileNumber: string, createDto: CreateDocumentDto): Promise; + download(uuid: string, fileName: string, isInline: boolean): Promise; + fetchTypes(): Promise; +} diff --git a/alcs-frontend/src/app/shared/shared.module.ts b/alcs-frontend/src/app/shared/shared.module.ts index be7cf1ea2f..701c6dabb6 100644 --- a/alcs-frontend/src/app/shared/shared.module.ts +++ b/alcs-frontend/src/app/shared/shared.module.ts @@ -79,6 +79,7 @@ import { MatChipsModule } from '@angular/material/chips'; import { TagChipComponent } from './tags/tag-chip/tag-chip.component'; import { DomSanitizer } from '@angular/platform-browser'; import { CommissionerTagsHeaderComponent } from './tags/commissioner-tags-header/commissioner-tags-header.component'; +import { DocumentUploadDialogComponent } from './document-upload-dialog/document-upload-dialog.component'; @NgModule({ declarations: [ @@ -123,6 +124,7 @@ import { CommissionerTagsHeaderComponent } from './tags/commissioner-tags-header TagsHeaderComponent, TagChipComponent, CommissionerTagsHeaderComponent, + DocumentUploadDialogComponent, ], imports: [ CommonModule, @@ -150,6 +152,7 @@ import { CommissionerTagsHeaderComponent } from './tags/commissioner-tags-header MatSlideToggleModule, MatChipsModule, MatAutocompleteModule, + MatCheckboxModule, ], exports: [ CommonModule, @@ -226,6 +229,7 @@ import { CommissionerTagsHeaderComponent } from './tags/commissioner-tags-header TruncatePipe, TagsHeaderComponent, TagChipComponent, + DocumentUploadDialogComponent, ], }) export class SharedModule { diff --git a/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts index b57fbfa6a2..f3fcaf1be9 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/files-step.partial.ts @@ -38,24 +38,22 @@ export abstract class FilesStepComponent extends StepComponent { await this.save(); const mappedFiles = file.file; - let res; try { - res = await this.applicationDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); + const res = await this.applicationDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); + + if (res) { + this.toastService.showSuccessToast('Document uploaded'); + const documents = await this.applicationDocumentService.getByFileId(this.fileId); + if (documents) { + this.$applicationDocuments.next(documents); + } + } } catch (err) { this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - return false; - } - } - if (res) { - const documents = await this.applicationDocumentService.getByFileId(this.fileId); - if (documents) { - this.$applicationDocuments.next(documents); - } + throw err; } } - return true; } async onDeleteFile($event: ApplicationDocumentDto) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html index 8622a33a24..a915d3aa02 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html @@ -1,79 +1,79 @@
-

{{title}} optional attachment

+

{{ title }} optional attachment

-
- - -
- -
-
- - - - {{ type.label }} - - - -
- warning -
- This field is required -
-
-
-
- - - -
- warning -
- This field is required -
-
-
+
+ + +
+ +
+
+ + + + {{ type.label }} + + + +
+ warning +
This field is required
+
+
+
+ + + +
+ warning +
This field is required
- +
+
+
-
- - - -
-
\ No newline at end of file +
+ + + +
+ diff --git a/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts index a84f186148..451db086cf 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts @@ -1,212 +1,232 @@ -import { CommonModule } from "@angular/common"; +import { CommonModule } from '@angular/common'; import { Component, Inject, OnInit } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; -import { ApplicationDocumentDto, ApplicationDocumentUpdateDto } from "../../../../../services/application-document/application-document.dto"; -import { ToastService } from "../../../../../services/toast/toast.service"; -import { ApplicationDocumentService } from "../../../../../services/application-document/application-document.service"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { OtherAttachmentsComponent } from "../other-attachments.component"; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from "../../../../../shared/dto/document.dto"; -import { CodeService } from "../../../../../services/code/code.service"; -import { FileHandle } from "../../../../../shared/file-drag-drop/drag-drop.directive"; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + ApplicationDocumentDto, + ApplicationDocumentUpdateDto, +} from '../../../../../services/application-document/application-document.dto'; +import { ToastService } from '../../../../../services/toast/toast.service'; +import { ApplicationDocumentService } from '../../../../../services/application-document/application-document.service'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { OtherAttachmentsComponent } from '../other-attachments.component'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../../shared/dto/document.dto'; +import { CodeService } from '../../../../../services/code/code.service'; +import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; +import { HttpErrorResponse } from '@angular/common/http'; const USER_CONTROLLED_TYPES = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; @Component({ - selector: 'app-other-attachments-upload-dialog', - templateUrl: './other-attachments-upload-dialog.component.html', - styleUrl: './other-attachments-upload-dialog.component.scss', + selector: 'app-other-attachments-upload-dialog', + templateUrl: './other-attachments-upload-dialog.component.html', + styleUrl: './other-attachments-upload-dialog.component.scss', }) export class OtherAttachmentsUploadDialogComponent implements OnInit { - isDirty = false; - isFileDirty = false; - isSaving = false; - showVirusError = false; - showFileRequiredError = false; - title: string = ''; - isEditing = false; - - attachment: ApplicationDocumentDto[] = []; - attachmentForDelete: ApplicationDocumentDto[] = []; - pendingFile: FileHandle | undefined; - selectableTypes: DocumentTypeDto[] = []; - private documentCodes: DocumentTypeDto[] = []; - - fileDescription = new FormControl(null, [Validators.required]); - fileType = new FormControl(null, [Validators.required]); - currentDescription: string | null = null; - currentType: DocumentTypeDto | null = null; - - form = new FormGroup({ - fileDescription: this.fileDescription, - fileType: this.fileType, - }); - - constructor( - private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) - public data: { - otherAttachmentsComponent: OtherAttachmentsComponent, - existingDocument?: ApplicationDocumentDto, - fileId: string, - }, - private applicationDcoumentService: ApplicationDocumentService, - private codeService: CodeService, - private toastService: ToastService - ) {} - - ngOnInit(): void { - this.loadDocumentCodes(); - if (this.data.existingDocument) { - this.title = 'Edit'; - this.isEditing = true; - this.fileType.setValue(this.data.existingDocument.type!.code); - this.fileDescription.setValue(this.data.existingDocument.description!); - this.currentDescription = this.data.existingDocument.description!; - this.currentType = this.data.existingDocument.type; - this.attachment = [this.data.existingDocument]; - } else { - this.title = 'Add'; - } - } - - async attachDocument(file: FileHandle) { - this.pendingFile = file; - this.attachment = [{uuid: '', - fileName: file.file.name, - type: null, - fileSize: file.file.size, - uploadedBy: '', - uploadedAt: file.file.lastModified, - source: DOCUMENT_SOURCE.APPLICANT, - }]; - this.isFileDirty = true; - this.showFileRequiredError = false; + isDirty = false; + isFileDirty = false; + isSaving = false; + showHasVirusError = false; + showVirusScanFailedError = false; + showFileRequiredError = false; + title: string = ''; + isEditing = false; + + attachment: ApplicationDocumentDto[] = []; + attachmentForDelete: ApplicationDocumentDto[] = []; + pendingFile: FileHandle | undefined; + selectableTypes: DocumentTypeDto[] = []; + private documentCodes: DocumentTypeDto[] = []; + + fileDescription = new FormControl(null, [Validators.required]); + fileType = new FormControl(null, [Validators.required]); + currentDescription: string | null = null; + currentType: DocumentTypeDto | null = null; + + form = new FormGroup({ + fileDescription: this.fileDescription, + fileType: this.fileType, + }); + + constructor( + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) + public data: { + otherAttachmentsComponent: OtherAttachmentsComponent; + existingDocument?: ApplicationDocumentDto; + fileId: string; + }, + private applicationDcoumentService: ApplicationDocumentService, + private codeService: CodeService, + private toastService: ToastService, + ) {} + + ngOnInit(): void { + this.loadDocumentCodes(); + if (this.data.existingDocument) { + this.title = 'Edit'; + this.isEditing = true; + this.fileType.setValue(this.data.existingDocument.type!.code); + this.fileDescription.setValue(this.data.existingDocument.description!); + this.currentDescription = this.data.existingDocument.description!; + this.currentType = this.data.existingDocument.type; + this.attachment = [this.data.existingDocument]; + } else { + this.title = 'Add'; } - - openFile() { - if (this.isEditing && this.pendingFile === undefined) { - this.data.otherAttachmentsComponent.openFile(this.attachment[0]); - } else { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile.file); - window.open(fileURL, '_blank'); - } - } + } + + async attachDocument(file: FileHandle) { + this.pendingFile = file; + this.attachment = [ + { + uuid: '', + fileName: file.file.name, + type: null, + fileSize: file.file.size, + uploadedBy: '', + uploadedAt: file.file.lastModified, + source: DOCUMENT_SOURCE.APPLICANT, + }, + ]; + this.isFileDirty = true; + this.showFileRequiredError = false; + } + + openFile() { + if (this.isEditing && this.pendingFile === undefined) { + this.data.otherAttachmentsComponent.openFile(this.attachment[0]); + } else { + if (this.pendingFile) { + const fileURL = URL.createObjectURL(this.pendingFile.file); + window.open(fileURL, '_blank'); + } } + } - deleteFile() { - this.pendingFile = undefined; - if (this.isEditing) { - this.attachmentForDelete = this.attachment; - } - this.attachment = []; + deleteFile() { + this.pendingFile = undefined; + if (this.isEditing) { + this.attachmentForDelete = this.attachment; } - - onChangeDescription() { - this.isDirty = true; - this.currentDescription = this.fileDescription.value; + this.attachment = []; + } + + onChangeDescription() { + this.isDirty = true; + this.currentDescription = this.fileDescription.value; + } + + onChangeType(selectedValue: DOCUMENT_TYPE) { + this.isDirty = true; + const newType = this.documentCodes.find((code) => code.code === selectedValue); + this.currentType = newType !== undefined ? newType : null; + } + + validateForm() { + if (this.form.valid && this.attachment.length !== 0) { + return true; } - onChangeType(selectedValue: DOCUMENT_TYPE) { - this.isDirty = true; - const newType = this.documentCodes.find((code) => code.code === selectedValue); - this.currentType = newType !== undefined ? newType : null; + if (this.form.invalid) { + this.form.markAllAsTouched(); } - validateForm() { - if (this.form.valid && this.attachment.length !== 0) { - return true; - } - - if (this.form.invalid) { - this.form.markAllAsTouched(); - } + if (this.attachment.length == 0) { + this.showFileRequiredError = true; + } + return false; + } - if (this.attachment.length == 0) { - this.showFileRequiredError = true; - } - return false; + async onAdd() { + if (this.validateForm()) { + await this.add(); } - - async onAdd() { - if (this.validateForm()) { - await this.add(); + } + + protected async add() { + if (this.isFileDirty) { + this.isSaving = true; + try { + await this.data.otherAttachmentsComponent.attachFile(this.pendingFile!, null); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + + const documents = await this.applicationDcoumentService.getByFileId(this.data.fileId); + + if (documents) { + const sortedDocuments = documents.sort((a, b) => { + return b.uploadedAt - a.uploadedAt; + }); + const updateDtos: ApplicationDocumentUpdateDto[] = sortedDocuments.map((file) => ({ + uuid: file.uuid, + description: file.description, + type: file.type?.code ?? null, + })); + updateDtos[0] = { + ...updateDtos[0], + description: this.currentDescription, + type: this.currentType?.code ?? null, + }; + await this.applicationDcoumentService.update(this.data.fileId, updateDtos); + this.toastService.showSuccessToast('Attachment added successfully'); + this.dialogRef.close(); + } else { + this.toastService.showErrorToast('Could not read attached documents'); } - } - - protected async add() { - if (this.isFileDirty) { - this.isSaving = true; - const res = await this.data.otherAttachmentsComponent.attachFile(this.pendingFile!, null); - this.showVirusError = !res; - if (res) { - const documents = await this.applicationDcoumentService.getByFileId(this.data.fileId); - if (documents) { - const sortedDocuments = documents.sort((a, b) => {return b.uploadedAt - a.uploadedAt}); - const updateDtos: ApplicationDocumentUpdateDto[] = sortedDocuments.map((file) => ({ - uuid: file.uuid, - description: file.description, - type: file.type?.code ?? null, - })); - updateDtos[0] = { - ...updateDtos[0], - description: this.currentDescription, - type: this.currentType?.code ?? null, - } - await this.applicationDcoumentService.update(this.data.fileId, updateDtos); - this.toastService.showSuccessToast('Attachment added successfully'); - this.dialogRef.close(); - } else { - this.toastService.showErrorToast("Could not read attached documents"); - } - } + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; } + } + this.isDirty = false; + this.isFileDirty = true; + this.isSaving = false; } + } - async onEdit() { - if (this.validateForm()) { - this.edit(); - } + async onEdit() { + if (this.validateForm()) { + this.edit(); } - - protected async edit() { - if (this.isFileDirty) { - this.data.otherAttachmentsComponent.onDeleteFile(this.attachmentForDelete[0]); - await this.add(); - } else { - if (this.isDirty) { - this.isSaving = true; - const documents = await this.applicationDcoumentService.getByFileId(this.data.fileId); - if (documents) { - const updateDtos: ApplicationDocumentUpdateDto[] = documents.map((file) => ({ - uuid: file.uuid, - description: file.description, - type: file.type?.code ?? null, - })); - for (let i = 0; i < updateDtos.length; i++) { - if (updateDtos[i].uuid === this.data.existingDocument?.uuid) { - updateDtos[i] = { - ...updateDtos[i], - description: this.currentDescription, - type: this.currentType?.code ?? null, - } - } - } - await this.applicationDcoumentService.update(this.data.fileId, updateDtos); - this.toastService.showSuccessToast('Attachment updated successully'); - } else { - this.toastService.showErrorToast("Could not read attached documents"); - } + } + + protected async edit() { + if (this.isFileDirty) { + this.data.otherAttachmentsComponent.onDeleteFile(this.attachmentForDelete[0]); + await this.add(); + } else { + if (this.isDirty) { + this.isSaving = true; + const documents = await this.applicationDcoumentService.getByFileId(this.data.fileId); + if (documents) { + const updateDtos: ApplicationDocumentUpdateDto[] = documents.map((file) => ({ + uuid: file.uuid, + description: file.description, + type: file.type?.code ?? null, + })); + for (let i = 0; i < updateDtos.length; i++) { + if (updateDtos[i].uuid === this.data.existingDocument?.uuid) { + updateDtos[i] = { + ...updateDtos[i], + description: this.currentDescription, + type: this.currentType?.code ?? null, + }; } - this.dialogRef.close(); + } + await this.applicationDcoumentService.update(this.data.fileId, updateDtos); + this.toastService.showSuccessToast('Attachment updated successully'); + } else { + this.toastService.showErrorToast('Could not read attached documents'); } + } + this.dialogRef.close(); } + } - 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)); - } - + 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/applications/edit-submission/other-attachments/other-attachments.component.ts b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts index 51ad7db354..e6ca352239 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/other-attachments/other-attachments.component.ts @@ -17,6 +17,7 @@ import { EditApplicationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; import { OtherAttachmentsUploadDialogComponent } from './other-attachments-upload-dialog/other-attachments-upload-dialog.component'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; +import { HttpErrorResponse } from '@angular/common/http'; const USER_CONTROLLED_TYPES = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; @@ -33,7 +34,8 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI otherFiles: ApplicationDocumentDto[] = []; private isDirty = false; - showVirusError = false; + showHasVirusError = false; + showVirusScanFailedError = false; isMobile = window.innerWidth <= MOBILE_BREAKPOINT; form = new FormGroup({} as any); @@ -45,7 +47,7 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI private codeService: CodeService, applicationDocumentService: ApplicationDocumentService, dialog: MatDialog, - toastService: ToastService + toastService: ToastService, ) { super(applicationDocumentService, dialog, toastService); } @@ -74,8 +76,16 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI } async attachDocument(file: FileHandle) { - const res = await this.attachFile(file, null); - this.showVirusError = !res; + try { + await this.attachFile(file, null); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { @@ -98,15 +108,17 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI onAddEditAttachment(attachment: ApplicationDocumentDto | undefined) { this.dialog .open(OtherAttachmentsUploadDialogComponent, { - width: this.isMobile? '90%' : '50%', + width: this.isMobile ? '90%' : '50%', data: { fileId: this.fileId, otherAttachmentsComponent: this, existingDocument: attachment, - } - }).afterClosed().subscribe(async res => { - await this.refreshFiles(); - }); + }, + }) + .afterClosed() + .subscribe(async (res) => { + await this.refreshFiles(); + }); } @HostListener('window:resize', ['$event']) diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html index 34240f1843..0d225d5bb8 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.html @@ -241,7 +241,8 @@ (beforeFileUploadOpened)="saveParcelProgress()" [showErrors]="showErrors" [isRequired]="isCertificateOfTitleRequired" - [showVirusError]="showVirusError" + [showHasVirusError]="showHasVirusError" + [showVirusScanFailedError]="showVirusScanFailedError" [disabled]="parcelForm.controls.pid.disabled" >
@@ -353,11 +354,12 @@
OR
+ (editClicked)="onEditCrownOwner(selectedOwner)" + >
@@ -376,18 +378,20 @@
OR
- - + [ngClass]="{ error: ownerInput.errors && ownerInput.errors['required'] }" + > +
- {{owner.firstName + ' ' + owner.lastName}} + {{ owner.firstName + ' ' + owner.lastName }}
@@ -407,7 +411,7 @@
OR
'error-outline': enableUserSignOff && isConfirmedByApplicant.invalid && - (isConfirmedByApplicant.dirty || isConfirmedByApplicant.touched) + (isConfirmedByApplicant.dirty || isConfirmedByApplicant.touched), }" > diff --git a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts index 617204efdf..ebadf41620 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/parcel-details/parcel-entry/parcel-entry.component.ts @@ -23,6 +23,7 @@ import { openFileInline } from '../../../../../shared/utils/file'; import { RemoveFileConfirmationDialogComponent } from '../../../alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; +import { HttpErrorResponse } from '@angular/common/http'; export interface ParcelEntryFormData { uuid: string; @@ -57,7 +58,8 @@ export class ParcelEntryComponent implements OnInit { @Input() _disabled = false; @Input() isDraft = false; - showVirusError = false; + showHasVirusError = false; + showVirusScanFailedError = false; @Output() private onFormGroupChange = new EventEmitter>(); @Output() private onSaveProgress = new EventEmitter(); @@ -301,18 +303,25 @@ export class ParcelEntryComponent implements OnInit { async attachFile(file: FileHandle, parcelUuid: string) { if (parcelUuid) { const mappedFiles = file.file; + try { this.parcel.certificateOfTitle = await this.applicationParcelService.attachCertificateOfTitle( this.fileId, parcelUuid, mappedFiles, ); - } catch (e) { - this.showVirusError = true; - this.toastService.showErrorToast('Document upload failed'); + this.toastService.showSuccessToast('Document uploaded'); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; return; + } catch (err) { + this.toastService.showErrorToast('Document upload failed'); + + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } } - this.showVirusError = false; } } diff --git a/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.html b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.html index 50ab41d8fb..8016f86310 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.html @@ -82,7 +82,7 @@

Primary Contact

Phone Number:
-
{{ phoneNumber.value ?? '' | mask : '(000) 000-0000' }}
+
{{ phoneNumber.value ?? '' | mask: '(000) 000-0000' }}
Email:
@@ -217,7 +217,8 @@

(openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="needsAuthorizationLetter" - [showVirusError]="showVirusError" + [showHasVirusError]="showHasVirusError" + [showVirusScanFailedError]="showVirusScanFailedError" >

diff --git a/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts index eb7c05bd35..1207be60b2 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/primary-contact/primary-contact.component.ts @@ -22,6 +22,7 @@ import { CrownOwnerDialogComponent } from '../../../../shared/owner-dialogs/crow import { scrollToElement } from '../../../../shared/utils/scroll-helper'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { strictEmailValidator } from '../../../../shared/validators/email-validator'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-primary-contact', @@ -43,7 +44,8 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni isGovernmentUser = false; governmentName: string | undefined; isDirty = false; - showVirusError = false; + showHasVirusError = false; + showVirusScanFailedError = false; hasCrownParcels = false; ownersList = new FormControl(null, [Validators.required]); @@ -102,8 +104,16 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } async attachAuthorizationLetter(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.AUTHORIZATION_LETTER); - this.showVirusError = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.AUTHORIZATION_LETTER); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } set selectedOwnerUuid(value: string | undefined) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/cove-proposal/cove-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/cove-proposal/cove-proposal.component.html index 3a405b9f67..04928a197d 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/cove-proposal/cove-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/cove-proposal/cove-proposal.component.html @@ -32,7 +32,7 @@

Proposal

mat-flat-button color="accent" [ngClass]="{ - 'error-outline': transferees.length === 0 && showErrors + 'error-outline': transferees.length === 0 && showErrors, }" (click)="onAdd()" > @@ -185,7 +185,9 @@

Proposal

(openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" >
@@ -201,7 +203,7 @@

Proposal

Yes @@ -209,7 +211,7 @@

Proposal

No @@ -229,7 +231,8 @@

Proposal

[showErrors]="showErrors" [isRequired]="true" [disabled]="!canUploadDraft" - [showVirusError]="showDraftCovenantVirus" + [showHasVirusError]="showDraftCovenantHasVirusError" + [showVirusScanFailedError]="showDraftCovenantVirusScanFailedError" >
diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/cove-proposal/cove-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/cove-proposal/cove-proposal.component.ts index 91ba649d56..2f97b0e10b 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/cove-proposal/cove-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/cove-proposal/cove-proposal.component.ts @@ -20,6 +20,7 @@ import { CovenantTransfereeDialogComponent } from './transferee-dialog/transfere import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; import { VISIBLE_COUNT_INCREMENT } from '../../../../../shared/constants'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-cove-proposal', @@ -49,9 +50,11 @@ export class CoveProposalComponent extends FilesStepComponent implements OnInit, }); proposalMap: ApplicationDocumentDto[] = []; - showProposalMapVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; draftCovenant: ApplicationDocumentDto[] = []; - showDraftCovenantVirus = false; + showDraftCovenantHasVirusError = false; + showDraftCovenantVirusScanFailedError = false; isMobile = false; visibleCount = VISIBLE_COUNT_INCREMENT; @@ -127,13 +130,29 @@ export class CoveProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachDraftCovenant(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.SRW_TERMS); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.SRW_TERMS); + this.showDraftCovenantHasVirusError = false; + this.showDraftCovenantVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showDraftCovenantHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showDraftCovenantVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } onAdd() { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.html index c9b3eae36c..d01819fae6 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.html @@ -134,7 +134,9 @@

Proposal

(openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" > @@ -176,7 +178,9 @@

Notification and Public Hearing Requirements

[showErrors]="showErrors" [allowMultiple]="true" [isRequired]="true" - [showVirusError]="showProofOfAdvertisingVirus" + [showHasVirusError]="showProofOfAdvertisingHasVirusError" + [showVirusScanFailedError]="showProofOfAdvertisingVirusScanFailedError" + [showVirusScanFailedError]="showProofOfAdvertisingVirusScanFailedError" >
@@ -194,7 +198,9 @@

Notification and Public Hearing Requirements

[showErrors]="showErrors" [isRequired]="true" [allowMultiple]="true" - [showVirusError]="showProofOfSignageVirus" + [showHasVirusError]="showProofOfSignageHasVirusError" + [showVirusScanFailedError]="showProofOfSignageVirusScanFailedError" + [showVirusScanFailedError]="showProofOfSignageVirusScanFailedError" >
@@ -209,7 +215,8 @@

Notification and Public Hearing Requirements

[showErrors]="showErrors" [isRequired]="true" [allowMultiple]="true" - [showVirusError]="showReportOfPublicHearingVirus" + [showHasVirusError]="showReportOfPublicHearingHasVirusError" + [showVirusScanFailedError]="showReportOfPublicHearingVirusScanFailedError" >
diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts index 4bf9578a1c..c2b799792c 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/excl-proposal/excl-proposal.component.ts @@ -13,6 +13,7 @@ import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.direc import { parseStringToBoolean } from '../../../../../shared/utils/string-helper'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-excl-proposal', @@ -25,10 +26,14 @@ export class ExclProposalComponent extends FilesStepComponent implements OnInit, currentStep = EditApplicationSteps.Proposal; prescribedBody: string | null = null; - showProposalMapVirus = false; - showProofOfAdvertisingVirus = false; - showProofOfSignageVirus = false; - showReportOfPublicHearingVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showProofOfAdvertisingHasVirusError = false; + showProofOfAdvertisingVirusScanFailedError = false; + showProofOfSignageHasVirusError = false; + showProofOfSignageVirusScanFailedError = false; + showReportOfPublicHearingHasVirusError = false; + showReportOfPublicHearingVirusScanFailedError = false; hectares = new FormControl(null, [Validators.required]); shareProperty = new FormControl(null, [Validators.required]); @@ -52,7 +57,7 @@ export class ExclProposalComponent extends FilesStepComponent implements OnInit, private pdfGenerationService: PdfGenerationService, applicationDocumentService: ApplicationDocumentService, dialog: MatDialog, - toastService: ToastService + toastService: ToastService, ) { super(applicationDocumentService, dialog, toastService); } @@ -83,11 +88,11 @@ export class ExclProposalComponent extends FilesStepComponent implements OnInit, this.$applicationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); this.noticeOfPublicHearing = documents.filter( - (document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_ADVERTISING + (document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_ADVERTISING, ); this.proofOfSignage = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_SIGNAGE); this.reportOfPublicHearing = documents.filter( - (document) => document.type?.code === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING + (document) => document.type?.code === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING, ); }); } @@ -97,23 +102,55 @@ export class ExclProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachProofOfAdvertising(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROOF_OF_ADVERTISING); - this.showProofOfAdvertisingVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROOF_OF_ADVERTISING); + this.showProofOfAdvertisingHasVirusError = false; + this.showProofOfAdvertisingVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProofOfAdvertisingHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProofOfAdvertisingVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachProofOfSignage(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROOF_OF_SIGNAGE); - this.showProofOfSignageVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROOF_OF_SIGNAGE); + this.showProofOfSignageHasVirusError = false; + this.showProofOfSignageVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProofOfSignageHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProofOfSignageVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachReportOfPublicHearing(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING); - this.showReportOfPublicHearingVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING); + this.showReportOfPublicHearingHasVirusError = false; + this.showReportOfPublicHearingVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showReportOfPublicHearingHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showReportOfPublicHearingVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async onDownloadPdf() { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.html index fc4f8ff429..7173b614d6 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.html @@ -125,7 +125,8 @@

Proposal

(openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" >
@@ -156,7 +157,7 @@

Proposal

value="true" [ngClass]="{ 'error-outline': - governmentOwnsAllParcels.invalid && (governmentOwnsAllParcels.dirty || governmentOwnsAllParcels.touched) + governmentOwnsAllParcels.invalid && (governmentOwnsAllParcels.dirty || governmentOwnsAllParcels.touched), }" >Yes @@ -164,7 +165,7 @@

Proposal

value="false" [ngClass]="{ 'error-outline': - governmentOwnsAllParcels.invalid && (governmentOwnsAllParcels.dirty || governmentOwnsAllParcels.touched) + governmentOwnsAllParcels.invalid && (governmentOwnsAllParcels.dirty || governmentOwnsAllParcels.touched), }" >No @@ -214,7 +215,8 @@

Notification and Public Hearing Requirements

[isRequired]="!disableNotificationFileUploads" [disabled]="disableNotificationFileUploads" [allowMultiple]="true" - [showVirusError]="showProofOfAdvertisingVirus" + [showHasVirusError]="showProofOfAdvertisingHasVirusError" + [showVirusScanFailedError]="showProofOfAdvertisingVirusScanFailedError" >
@@ -233,7 +235,9 @@

Notification and Public Hearing Requirements

[isRequired]="!disableNotificationFileUploads" [disabled]="disableNotificationFileUploads" [allowMultiple]="true" - [showVirusError]="showProofOfSignageVirus" + [showHasVirusError]="showProofOfSignageHasVirusError" + [showVirusScanFailedError]="showProofOfSignageVirusScanFailedError" + [showVirusScanFailedError]="showProofOfSignageVirusScanFailedError" >
@@ -248,7 +252,8 @@

Notification and Public Hearing Requirements

[showErrors]="showErrors" [isRequired]="!disableNotificationFileUploads" [disabled]="disableNotificationFileUploads" - [showVirusError]="showReportOfPublicHearingVirus" + [showHasVirusError]="showReportOfPublicHearingHasVirusError" + [showVirusScanFailedError]="showReportOfPublicHearingVirusScanFailedError" [allowMultiple]="true" >
diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts index b292bcd70d..240618bd47 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/incl-proposal/incl-proposal.component.ts @@ -15,6 +15,7 @@ import { ApplicationDocumentDto } from '../../../../../services/application-docu import { EditApplicationSteps } from '../../edit-submission.component'; import { takeUntil } from 'rxjs'; import { ApplicationSubmissionUpdateDto } from '../../../../../services/application-submission/application-submission.dto'; +import { HttpErrorResponse } from '@angular/common/http'; interface InclForm { hectares: FormControl; @@ -37,10 +38,14 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, governmentName? = ''; disableNotificationFileUploads = true; - showProposalMapVirus = false; - showProofOfAdvertisingVirus = false; - showProofOfSignageVirus = false; - showReportOfPublicHearingVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showProofOfAdvertisingHasVirusError = false; + showProofOfAdvertisingVirusScanFailedError = false; + showProofOfSignageHasVirusError = false; + showProofOfSignageVirusScanFailedError = false; + showReportOfPublicHearingHasVirusError = false; + showReportOfPublicHearingVirusScanFailedError = false; hectares = new FormControl(null, [Validators.required]); purpose = new FormControl(null, [Validators.required]); @@ -65,7 +70,7 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, private authenticationService: AuthenticationService, applicationDocumentService: ApplicationDocumentService, dialog: MatDialog, - toastService: ToastService + toastService: ToastService, ) { super(applicationDocumentService, dialog, toastService); } @@ -86,7 +91,7 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, if (applicationSubmission.inclGovernmentOwnsAllParcels !== null) { this.showGovernmentQuestions = true; this.governmentOwnsAllParcels.setValue( - formatBooleanToString(applicationSubmission.inclGovernmentOwnsAllParcels) + formatBooleanToString(applicationSubmission.inclGovernmentOwnsAllParcels), ); this.disableNotificationFileUploads = applicationSubmission.inclGovernmentOwnsAllParcels; this.form.setControl('governmentOwnsAllParcels', this.governmentOwnsAllParcels); @@ -101,11 +106,11 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, this.$applicationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { this.proposalMap = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROPOSAL_MAP); this.noticeOfPublicHearing = documents.filter( - (document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_ADVERTISING + (document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_ADVERTISING, ); this.proofOfSignage = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.PROOF_OF_SIGNAGE); this.reportOfPublicHearing = documents.filter( - (document) => document.type?.code === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING + (document) => document.type?.code === DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING, ); }); @@ -129,23 +134,55 @@ export class InclProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachProofOfAdvertising(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROOF_OF_ADVERTISING); - this.showProofOfAdvertisingVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROOF_OF_ADVERTISING); + this.showProofOfAdvertisingHasVirusError = false; + this.showProofOfAdvertisingVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProofOfAdvertisingHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProofOfAdvertisingVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachProofOfSignage(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROOF_OF_SIGNAGE); - this.showProofOfSignageVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROOF_OF_SIGNAGE); + this.showProofOfSignageHasVirusError = false; + this.showProofOfSignageVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProofOfSignageHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProofOfSignageVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachReportOfPublicHearing(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING); - this.showReportOfPublicHearingVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.REPORT_OF_PUBLIC_HEARING); + this.showReportOfPublicHearingHasVirusError = false; + this.showReportOfPublicHearingVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showReportOfPublicHearingHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showReportOfPublicHearingVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.html index fdad5b04ed..91d0423303 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.html @@ -23,10 +23,7 @@

Proposal

>
  • - + Housing in the ALR
  • @@ -642,7 +639,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" > @@ -659,7 +657,8 @@

    Proposal

    (deleteFile)="onDeleteFile($event)" (openFile)="openFile($event)" [showErrors]="showErrors" - [showVirusError]="showBuildingPlanVirus" + [showHasVirusError]="showBuildingPlanHasVirusError" + [showVirusScanFailedError]="showBuildingPlanVirusScanFailedError" > diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts index 31dc7d862d..b7296fdb4f 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/naru-proposal/naru-proposal.component.ts @@ -21,6 +21,7 @@ import { ResidenceDialogComponent } from './residence-dialog/residence-dialog.co import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; import { isTruncated, truncate } from '../../../../../shared/utils/string-helper'; import { EXISTING_RESIDENCE_DESCRIPTION_CHAR_LIMIT } from '../../../../../shared/constants'; +import { HttpErrorResponse } from '@angular/common/http'; export type FormExisingResidence = { id?: number; floorArea: number; description: string; isExpanded?: boolean }; export type FormProposedResidence = { id?: number; floorArea: number; description: string; isExpanded?: boolean }; @@ -33,8 +34,10 @@ export type FormProposedResidence = { id?: number; floorArea: number; descriptio export class NaruProposalComponent extends FilesStepComponent implements OnInit, OnDestroy { currentStep = EditApplicationSteps.Proposal; - showProposalMapVirus = false; - showBuildingPlanVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showBuildingPlanHasVirusError = false; + showBuildingPlanVirusScanFailedError = false; willBeOverFiveHundredM2 = new FormControl(null, [Validators.required]); willRetainResidence = new FormControl(null, [Validators.required]); @@ -179,13 +182,29 @@ export class NaruProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachBuildingPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); - this.showBuildingPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); + this.showBuildingPlanHasVirusError = false; + this.showBuildingPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showBuildingPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showBuildingPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } onChangeOver500m2(answerIsYes: boolean) { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html index f21a41495c..19fc39d22e 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.html @@ -120,7 +120,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" >
    diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts index 09fd87a73c..13e030b3fa 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/nfu-proposal/nfu-proposal.component.ts @@ -14,6 +14,7 @@ import { SoilTableData } from '../../../../../shared/soil-table/soil-table.compo import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; import { ConfirmationDialogService } from '../../../../../shared/confirmation-dialog/confirmation-dialog.service'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-nfu-proposal', @@ -25,7 +26,8 @@ export class NfuProposalComponent extends FilesStepComponent implements OnInit, fillTableData: SoilTableData = {}; fillTableDisabled = true; - showProposalMapVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; hectares = new FormControl(null, [Validators.required]); purpose = new FormControl(null, [Validators.required]); @@ -103,8 +105,16 @@ export class NfuProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html index a75d8cf34f..0ee431e81c 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.html @@ -314,12 +314,22 @@

    Proposal

    - + - - -
    #{{ i + 1 }} + {{ i + 1 }} + Type + Proposal Total Floor Area + Proposal Action + @@ -683,7 +701,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" > @@ -709,7 +728,8 @@

    Proposal

    [showErrors]="showErrors" [isRequired]="true" [allowMultiple]="true" - [showVirusError]="showCrossSectionVirus" + [showHasVirusError]="showCrossSectionHasVirusError" + [showVirusScanFailedError]="showCrossSectionVirusScanFailedError" > @@ -737,7 +757,8 @@

    Proposal

    [showErrors]="showErrors" [isRequired]="true" [allowMultiple]="true" - [showVirusError]="showReclamationPlanVirus" + [showHasVirusError]="showReclamationPlanHasVirusError" + [showVirusScanFailedError]="showReclamationPlanVirusScanFailedError" > @@ -756,7 +777,8 @@

    Proposal

    (deleteFile)="onDeleteFile($event)" (openFile)="openFile($event)" [showErrors]="showErrors" - [showVirusError]="showBuildingPlanVirus" + [showHasVirusError]="showBuildingPlanHasVirusError" + [showVirusScanFailedError]="showBuildingPlanVirusScanFailedError" [isRequired]="true" [allowMultiple]="true" > @@ -862,7 +884,8 @@

    Proposal

    [isRequired]="true" [disabled]="!requiresNoticeOfWork" [allowMultiple]="true" - [showVirusError]="showNoticeOfWorkVirus" + [showHasVirusError]="showNoticeOfWorkHasVirusError" + [showVirusScanFailedError]="showNoticeOfWorkVirusScanFailedError" > diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts index e887069f3d..a30c6f11c3 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pfrs-proposal/pfrs-proposal.component.ts @@ -25,6 +25,7 @@ import { } from '../../../../notice-of-intents/edit-submission/additional-information/additional-information.component'; import { ProposedStructure } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; import { AddStructureDialogComponent } from '../../../../notice-of-intents/edit-submission/additional-information/add-structure-dialog/add-structure-dialog.component'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-pfrs-proposal', @@ -54,11 +55,16 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, noticeOfWork: ApplicationDocumentDto[] = []; areComponentsDirty = false; - showProposalMapVirus = false; - showCrossSectionVirus = false; - showReclamationPlanVirus = false; - showBuildingPlanVirus = false; - showNoticeOfWorkVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showCrossSectionHasVirusError = false; + showCrossSectionVirusScanFailedError = false; + showReclamationPlanHasVirusError = false; + showReclamationPlanVirusScanFailedError = false; + showBuildingPlanHasVirusError = false; + showBuildingPlanVirusScanFailedError = false; + showNoticeOfWorkHasVirusError = false; + showNoticeOfWorkVirusScanFailedError = false; isNewStructure = new FormControl(null, [Validators.required]); isFollowUp = new FormControl(null, [Validators.required]); @@ -227,28 +233,68 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachCrossSection(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); - this.showCrossSectionVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); + this.showCrossSectionHasVirusError = false; + this.showCrossSectionVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showCrossSectionHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showCrossSectionVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachReclamationPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); - this.showReclamationPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); + this.showReclamationPlanHasVirusError = false; + this.showReclamationPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showReclamationPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showReclamationPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachBuildingPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); - this.showBuildingPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); + this.showBuildingPlanHasVirusError = false; + this.showBuildingPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showBuildingPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showBuildingPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachNoticeOfWork(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.NOTICE_OF_WORK); - this.showNoticeOfWorkVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.NOTICE_OF_WORK); + this.showNoticeOfWorkHasVirusError = false; + this.showNoticeOfWorkVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showNoticeOfWorkHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showNoticeOfWorkVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html index 80c3e64ae8..694be894c0 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.html @@ -222,12 +222,22 @@

    Proposal

    - + - - -
    #{{ i + 1 }} + {{ i + 1 }} + Type + Proposal Total Floor Area + Proposal Action + @@ -593,7 +611,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" > @@ -619,7 +638,8 @@

    Proposal

    [showErrors]="showErrors" [isRequired]="true" [allowMultiple]="true" - [showVirusError]="showCrossSectionVirus" + [showHasVirusError]="showCrossSectionHasVirusError" + [showVirusScanFailedError]="showCrossSectionVirusScanFailedError" > @@ -647,7 +667,8 @@

    Proposal

    [showErrors]="showErrors" [isRequired]="true" [allowMultiple]="true" - [showVirusError]="showReclamationPlanVirus" + [showHasVirusError]="showReclamationPlanHasVirusError" + [showVirusScanFailedError]="showReclamationPlanVirusScanFailedError" > @@ -666,7 +687,8 @@

    Proposal

    (deleteFile)="onDeleteFile($event)" (openFile)="openFile($event)" [showErrors]="showErrors" - [showVirusError]="showBuildingPlanVirus" + [showHasVirusError]="showBuildingPlanHasVirusError" + [showVirusScanFailedError]="showBuildingPlanVirusScanFailedError" [isRequired]="true" [allowMultiple]="true" > diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts index a9635c9777..28f920fcac 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/pofo-proposal/pofo-proposal.component.ts @@ -26,6 +26,7 @@ import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; import { AddStructureDialogComponent } from '../../../../../features/notice-of-intents/edit-submission/additional-information/add-structure-dialog/add-structure-dialog.component'; import { ProposedStructure } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; import { v4 } from 'uuid'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-pofo-proposal', @@ -53,10 +54,14 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, reclamationPlan: ApplicationDocumentDto[] = []; buildingPlans: ApplicationDocumentDto[] = []; - showProposalMapVirus = false; - showCrossSectionVirus = false; - showReclamationPlanVirus = false; - showBuildingPlanVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showCrossSectionHasVirusError = false; + showCrossSectionVirusScanFailedError = false; + showReclamationPlanHasVirusError = false; + showReclamationPlanVirusScanFailedError = false; + showBuildingPlanHasVirusError = false; + showBuildingPlanVirusScanFailedError = false; isNewStructure = new FormControl(null, [Validators.required]); isFollowUp = new FormControl(null, [Validators.required]); @@ -188,23 +193,55 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachCrossSection(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); - this.showCrossSectionVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); + this.showCrossSectionHasVirusError = false; + this.showCrossSectionVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showCrossSectionHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showCrossSectionVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachReclamationPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); - this.showReclamationPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); + this.showReclamationPlanHasVirusError = false; + this.showReclamationPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showReclamationPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showReclamationPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachBuildingPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); - this.showBuildingPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); + this.showBuildingPlanHasVirusError = false; + this.showBuildingPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showBuildingPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showBuildingPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html index 91f79ea8ae..097f44ab52 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.html @@ -224,12 +224,22 @@

    Proposal

    - + - - -
    #{{ i + 1 }} + {{ i + 1 }} + Type + Proposal Total Floor Area + Proposal Action + @@ -561,7 +579,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" > @@ -587,7 +606,8 @@

    Proposal

    [showErrors]="showErrors" [isRequired]="true" [allowMultiple]="true" - [showVirusError]="showCrossSectionVirus" + [showHasVirusError]="showCrossSectionHasVirusError" + [showVirusScanFailedError]="showCrossSectionVirusScanFailedError" > @@ -615,7 +635,8 @@

    Proposal

    [showErrors]="showErrors" [isRequired]="true" [allowMultiple]="true" - [showVirusError]="showReclamationPlanVirus" + [showHasVirusError]="showReclamationPlanHasVirusError" + [showVirusScanFailedError]="showReclamationPlanVirusScanFailedError" > @@ -634,7 +655,8 @@

    Proposal

    (deleteFile)="onDeleteFile($event)" (openFile)="openFile($event)" [showErrors]="showErrors" - [showVirusError]="showBuildingPlanVirus" + [showHasVirusError]="showBuildingPlanHasVirusError" + [showVirusScanFailedError]="showBuildingPlanVirusScanFailedError" [isRequired]="true" [allowMultiple]="true" > diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts index 3bea4eb978..2f1ee76a8d 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/roso-proposal/roso-proposal.component.ts @@ -26,6 +26,7 @@ import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; import { AddStructureDialogComponent } from '../../../../../features/notice-of-intents/edit-submission/additional-information/add-structure-dialog/add-structure-dialog.component'; import { ProposedStructure } from '../../../../../services/notice-of-intent-submission/notice-of-intent-submission.dto'; import { v4 } from 'uuid'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-roso-proposal', @@ -53,10 +54,14 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, reclamationPlan: ApplicationDocumentDto[] = []; buildingPlans: ApplicationDocumentDto[] = []; - showProposalMapVirus = false; - showCrossSectionVirus = false; - showReclamationPlanVirus = false; - showBuildingPlanVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showCrossSectionHasVirusError = false; + showCrossSectionVirusScanFailedError = false; + showReclamationPlanHasVirusError = false; + showReclamationPlanVirusScanFailedError = false; + showBuildingPlanHasVirusError = false; + showBuildingPlanVirusScanFailedError = false; isNewStructure = new FormControl(null, [Validators.required]); isFollowUp = new FormControl(null, [Validators.required]); @@ -185,23 +190,55 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachCrossSection(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); - this.showCrossSectionVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); + this.showCrossSectionHasVirusError = false; + this.showCrossSectionVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showCrossSectionHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showCrossSectionVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachReclamationPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); - this.showReclamationPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); + this.showReclamationPlanHasVirusError = false; + this.showReclamationPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showReclamationPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showReclamationPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachBuildingPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); - this.showBuildingPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); + this.showBuildingPlanHasVirusError = false; + this.showBuildingPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showBuildingPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showBuildingPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html index 9029fd3495..db08232046 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.html @@ -215,7 +215,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" >
    @@ -240,7 +241,7 @@

    Proposal

    Yes @@ -248,7 +249,7 @@

    Proposal

    No @@ -271,7 +272,8 @@

    Proposal

    [isRequired]="isHomeSiteSeverance.getRawValue() !== 'true'" [allowMultiple]="true" [disabled]="isHomeSiteSeverance.getRawValue() !== 'true'" - [showVirusError]="showHomesiteSeveranceVirus" + [showHasVirusError]="showHomesiteSeveranceHasVirusError" + [showVirusScanFailedError]="showHomesiteSeveranceVirusScanFailedError" >
    diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts index 8149f2a71a..2b9c66617f 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/subd-proposal/subd-proposal.component.ts @@ -17,6 +17,7 @@ import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; +import { HttpErrorResponse } from '@angular/common/http'; type FormProposedLot = { type: 'Lot' | 'Road Dedication' | null; size: string | null }; @@ -31,8 +32,10 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, homesiteSeverance: ApplicationDocumentDto[] = []; proposalMap: ApplicationDocumentDto[] = []; - showHomesiteSeveranceVirus = false; - showProposalMapVirus = false; + showHomesiteSeveranceHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showProposalMapHasVirusError = false; + showHomesiteSeveranceVirusScanFailedError = false; lotsProposed = new FormControl(null, [Validators.required]); purpose = new FormControl(null, [Validators.required]); @@ -62,7 +65,7 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, private parcelService: ApplicationParcelService, applicationDocumentService: ApplicationDocumentService, dialog: MatDialog, - toastService: ToastService + toastService: ToastService, ) { super(applicationDocumentService, dialog, toastService); } @@ -120,13 +123,29 @@ export class SubdProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachHomesiteSeverance(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.HOMESITE_SEVERANCE); - this.showHomesiteSeveranceVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.HOMESITE_SEVERANCE); + this.showHomesiteSeveranceHasVirusError = false; + this.showHomesiteSeveranceVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHomesiteSeveranceHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showHomesiteSeveranceVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.html b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.html index 730f9cf721..f72b17cfab 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.html +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.html @@ -149,7 +149,7 @@

    Proposal

    [formControl]="allOwnersNotified" [ngClass]="{ 'parcel-checkbox': true, - 'error-outline': allOwnersNotified.invalid && (allOwnersNotified.dirty || allOwnersNotified.touched) + 'error-outline': allOwnersNotified.invalid && (allOwnersNotified.dirty || allOwnersNotified.touched), }" >I confirm that all affected property owners with land in the ALR have been notified. @@ -178,7 +178,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showServingNoticeVirus" + [showHasVirusError]="showServingNoticeHasVirusError" + [showVirusScanFailedError]="showServingNoticeVirusScanFailedError" data-testid="proof-of-serving-notice-filechooser" > @@ -193,7 +194,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" data-testid="proposal-map-filechooser" > diff --git a/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts index adab6177fe..b640d60b21 100644 --- a/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts +++ b/portal-frontend/src/app/features/applications/edit-submission/proposal/tur-proposal/tur-proposal.component.ts @@ -12,6 +12,7 @@ import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; import { EditApplicationSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-tur-proposal', @@ -26,8 +27,10 @@ export class TurProposalComponent extends FilesStepComponent implements OnInit, servingNotice: ApplicationDocumentDto[] = []; proposalMap: ApplicationDocumentDto[] = []; - showServingNoticeVirus = false; - showProposalMapVirus = false; + showServingNoticeHasVirusError = false; + showServingNoticeVirusScanFailedError = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; purpose = new FormControl(null, [Validators.required]); outsideLands = new FormControl(null, [Validators.required]); @@ -51,7 +54,7 @@ export class TurProposalComponent extends FilesStepComponent implements OnInit, private applicationService: ApplicationSubmissionService, applicationDocumentService: ApplicationDocumentService, dialog: MatDialog, - toastService: ToastService + toastService: ToastService, ) { super(applicationDocumentService, dialog, toastService); } @@ -84,13 +87,29 @@ export class TurProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachServinceNotice(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.SERVING_NOTICE); - this.showServingNoticeVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.SERVING_NOTICE); + this.showServingNoticeHasVirusError = false; + this.showServingNoticeVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showServingNoticeHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showServingNoticeVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async onSave() { diff --git a/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.html b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.html index 945a20c66d..b2656719a3 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.html +++ b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.html @@ -18,7 +18,8 @@

    Attachments

    (openFile)="openFile($event)" [isRequired]="true" [showErrors]="showErrors" - [showVirusError]="showResolutionVirusError" + [showHasVirusError]="showResolutionHasVirusError" + [showVirusScanFailedError]="showResolutionVirusScanFailedError" >
    @@ -31,7 +32,8 @@

    Attachments

    (openFile)="openFile($event)" [isRequired]="isAuthorized" [showErrors]="showErrors" - [showVirusError]="showStaffReportVirusError" + [showHasVirusError]="showStaffReportHasVirusError" + [showVirusScanFailedError]="showStaffReportVirusScanFailedError" >
    @@ -43,7 +45,8 @@

    Attachments

    (deleteFile)="deleteFile($event)" (openFile)="openFile($event)" [allowMultiple]="true" - [showVirusError]="showOtherVirusError" + [showHasVirusError]="showOtherHasVirusError" + [showVirusScanFailedError]="showOtherVirusScanFailedError" >
    diff --git a/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts index f0156a2999..0fae15d00d 100644 --- a/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts +++ b/portal-frontend/src/app/features/applications/review-submission/review-attachments/review-attachments.component.ts @@ -8,6 +8,7 @@ import { DOCUMENT_SOURCE, DOCUMENT_TYPE } from '../../../../shared/dto/document. import { FileHandle } from '../../../../shared/file-drag-drop/drag-drop.directive'; import { ReviewApplicationFngSteps, ReviewApplicationSteps } from '../review-submission.component'; import { openFileInline } from '../../../../shared/utils/file'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-review-attachments', @@ -32,14 +33,18 @@ export class ReviewAttachmentsComponent implements OnInit, OnDestroy { isAuthorized = false; showMandatoryUploads = false; hasCompletedPreviousSteps = false; - showResolutionVirusError = false; - showStaffReportVirusError = false; - showOtherVirusError = false; + + showResolutionHasVirusError = false; + showResolutionVirusScanFailedError = false; + showStaffReportHasVirusError = false; + showStaffReportVirusScanFailedError = false; + showOtherHasVirusError = false; + showOtherVirusScanFailedError = false; constructor( private applicationReviewService: ApplicationSubmissionReviewService, private applicationDocumentService: ApplicationDocumentService, - private toastService: ToastService + private toastService: ToastService, ) {} ngOnInit(): void { @@ -69,11 +74,11 @@ export class ReviewAttachmentsComponent implements OnInit, OnDestroy { this.$applicationDocuments.pipe(takeUntil(this.$destroy)).subscribe((documents) => { this.resolutionDocument = documents.filter( - (document) => document.type?.code === DOCUMENT_TYPE.RESOLUTION_DOCUMENT + (document) => document.type?.code === DOCUMENT_TYPE.RESOLUTION_DOCUMENT, ); this.staffReport = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.STAFF_REPORT); this.otherAttachments = documents.filter( - (document) => document.type?.code === DOCUMENT_TYPE.OTHER && document.source === DOCUMENT_SOURCE.LFNG + (document) => document.type?.code === DOCUMENT_TYPE.OTHER && document.source === DOCUMENT_SOURCE.LFNG, ); }); } @@ -86,18 +91,42 @@ export class ReviewAttachmentsComponent implements OnInit, OnDestroy { } async attachStaffReport(fileHandle: FileHandle) { - const res = await this.attachFile(fileHandle, DOCUMENT_TYPE.STAFF_REPORT); - this.showStaffReportVirusError = !res; + try { + await this.attachFile(fileHandle, DOCUMENT_TYPE.STAFF_REPORT); + this.showStaffReportHasVirusError = false; + this.showStaffReportVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showStaffReportHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showStaffReportVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachResolutionDocument(fileHandle: FileHandle) { - const res = await this.attachFile(fileHandle, DOCUMENT_TYPE.RESOLUTION_DOCUMENT); - this.showResolutionVirusError = !res; + try { + await this.attachFile(fileHandle, DOCUMENT_TYPE.RESOLUTION_DOCUMENT); + this.showResolutionHasVirusError = false; + this.showResolutionVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showResolutionHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showResolutionVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachOtherDocument(fileHandle: FileHandle) { - const res = await this.attachFile(fileHandle, DOCUMENT_TYPE.OTHER); - this.showOtherVirusError = !res; + try { + await this.attachFile(fileHandle, DOCUMENT_TYPE.OTHER); + this.showOtherHasVirusError = false; + this.showOtherVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showOtherHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showOtherVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } private async attachFile(fileHandle: FileHandle, documentType: DOCUMENT_TYPE) { @@ -107,15 +136,16 @@ export class ReviewAttachmentsComponent implements OnInit, OnDestroy { this.fileId, fileHandle.file, documentType, - DOCUMENT_SOURCE.LFNG + DOCUMENT_SOURCE.LFNG, ); - } catch (e) { + this.toastService.showSuccessToast('Document uploaded'); + } catch (err) { this.toastService.showErrorToast('Document upload failed'); - return false; + + throw err; } await this.loadApplicationDocuments(this.fileId); } - return true; } async deleteFile($event: ApplicationDocumentDto) { diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html index a79f4e2a60..8861eeceff 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.html @@ -40,7 +40,7 @@

    Additional Proposal Information

    [ngClass]="{ 'error-outline': isRemovingSoilForNewStructure.invalid && - (isRemovingSoilForNewStructure.dirty || isRemovingSoilForNewStructure.touched) + (isRemovingSoilForNewStructure.dirty || isRemovingSoilForNewStructure.touched), }" value="true" >Yes @@ -50,7 +50,7 @@

    Additional Proposal Information

    [ngClass]="{ 'error-outline': isRemovingSoilForNewStructure.invalid && - (isRemovingSoilForNewStructure.dirty || isRemovingSoilForNewStructure.touched) + (isRemovingSoilForNewStructure.dirty || isRemovingSoilForNewStructure.touched), }" value="false" >No @@ -68,7 +68,7 @@

    Additional Proposal Information

    Provide the total floor area (m2) for each of the proposed structure(s)
    - Additional Proposal Information [isReviewStep]="false" (removeClicked)="onStructureRemove(structure.id)" (editClicked)="onStructureEdit(structure.id)" - > + >
    - + - - - @@ -381,7 +402,8 @@

    Additional Proposal Information

    (deleteFile)="onDeleteFile($event)" (openFile)="openFile($event)" [showErrors]="showErrors" - [showVirusError]="showBuildingPlanVirus" + [showHasVirusError]="showBuildingPlanHasVirusError" + [showVirusScanFailedError]="showBuildingPlanVirusScanFailedError" [isRequired]="confirmRemovalOfSoil" [allowMultiple]="true" [disabled]="!confirmRemovalOfSoil" diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts index cce35f4b30..8f068dd154 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/additional-information/additional-information.component.ts @@ -24,6 +24,7 @@ import { DeleteStructureConfirmationDialogComponent } from './delete-structure-c import { SoilRemovalConfirmationDialogComponent } from './soil-removal-confirmation-dialog/soil-removal-confirmation-dialog.component'; import { AddStructureDialogComponent } from './add-structure-dialog/add-structure-dialog.component'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; +import { HttpErrorResponse } from '@angular/common/http'; export enum STRUCTURE_TYPES { FARM_STRUCTURE = 'Farm Structure', @@ -95,7 +96,8 @@ export class AdditionalInformationComponent extends FilesStepComponent implement typeCode: string = ''; confirmRemovalOfSoil = false; - showBuildingPlanVirus = false; + showBuildingPlanHasVirusError = false; + showBuildingPlanVirusScanFailedError = false; buildingPlans: NoticeOfIntentDocumentDto[] = []; proposedStructures: FormProposedStructure[] = []; @@ -195,8 +197,16 @@ export class AdditionalInformationComponent extends FilesStepComponent implement } async attachBuildingPlan(file: FileHandle) { - const attachmentSucceeded = await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); - this.showBuildingPlanVirus = !attachmentSucceeded; + try { + await this.attachFile(file, DOCUMENT_TYPE.BUILDING_PLAN); + this.showBuildingPlanHasVirusError = false; + this.showBuildingPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showBuildingPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showBuildingPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } prepareStructureSpecificTextInputs() { 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 index a3dcebd03f..b57663de3f 100644 --- 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 @@ -38,24 +38,22 @@ export abstract class FilesStepComponent extends StepComponent { await this.save(); const mappedFiles = file.file; - let res; try { - res = await this.noticeOfIntentDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); + const res = await this.noticeOfIntentDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); + + if (res) { + this.toastService.showSuccessToast('Document uploaded'); + const documents = await this.noticeOfIntentDocumentService.getByFileId(this.fileId); + if (documents) { + this.$noiDocuments.next(documents); + } + } } catch (err) { this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - return false; - } - } - if (res) { - const documents = await this.noticeOfIntentDocumentService.getByFileId(this.fileId); - if (documents) { - this.$noiDocuments.next(documents); - } + throw err; } } - return true; } async onDeleteFile($event: NoticeOfIntentDocumentDto) { diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html index 8622a33a24..a915d3aa02 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html @@ -1,79 +1,79 @@
    -

    {{title}} optional attachment

    +

    {{ title }} optional attachment

    -
    - - -
    -
    -
    -
    - - - - {{ type.label }} - - - -
    - warning -
    - This field is required -
    -
    -
    -
    - - - -
    - warning -
    - This field is required -
    -
    -
    +
    + + +
    + +
    +
    + + + + {{ type.label }} + + + +
    + warning +
    This field is required
    +
    +
    +
    + + + +
    + warning +
    This field is required
    - +
    +
    +
    -
    - - - -
    -
    \ No newline at end of file +
    + + + +
    + diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts index fc9d0fd864..bf241c192a 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts @@ -1,210 +1,230 @@ import { Component, Inject, type OnInit } from '@angular/core'; -import { NoticeOfIntentDocumentDto, NoticeOfIntentDocumentUpdateDto } from "../../../../../services/notice-of-intent-document/notice-of-intent-document.dto"; -import { FileHandle } from "../../../../../shared/file-drag-drop/drag-drop.directive"; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from "../../../../../shared/dto/document.dto"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; -import { OtherAttachmentsComponent } from "../other-attachments.component"; -import { NoticeOfIntentDocumentService } from "../../../../../services/notice-of-intent-document/notice-of-intent-document.service"; -import { CodeService } from "../../../../../services/code/code.service"; -import { ToastService } from "../../../../../services/toast/toast.service"; +import { + NoticeOfIntentDocumentDto, + NoticeOfIntentDocumentUpdateDto, +} from '../../../../../services/notice-of-intent-document/notice-of-intent-document.dto'; +import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../../shared/dto/document.dto'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { OtherAttachmentsComponent } from '../other-attachments.component'; +import { NoticeOfIntentDocumentService } from '../../../../../services/notice-of-intent-document/notice-of-intent-document.service'; +import { CodeService } from '../../../../../services/code/code.service'; +import { ToastService } from '../../../../../services/toast/toast.service'; +import { HttpErrorResponse } from '@angular/common/http'; const USER_CONTROLLED_TYPES = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; @Component({ - selector: 'app-other-attachments-upload-dialog', - templateUrl: './other-attachments-upload-dialog.component.html', - styleUrl: './other-attachments-upload-dialog.component.scss', + selector: 'app-other-attachments-upload-dialog', + templateUrl: './other-attachments-upload-dialog.component.html', + styleUrl: './other-attachments-upload-dialog.component.scss', }) export class OtherAttachmentsUploadDialogComponent implements OnInit { - isDirty = false; - isFileDirty = false; - isSaving = false; - showVirusError = false; - showFileRequiredError = false; - title: string = ''; - isEditing = false; - - attachment: NoticeOfIntentDocumentDto[] = []; - attachmentForDelete: NoticeOfIntentDocumentDto[] = []; - pendingFile: FileHandle | undefined; - selectableTypes: DocumentTypeDto[] = []; - private documentCodes: DocumentTypeDto[] = []; - - fileDescription = new FormControl(null, [Validators.required]); - fileType = new FormControl(null, [Validators.required]); - currentDescription: string | null = null; - currentType: DocumentTypeDto | null = null; - - form = new FormGroup({ - fileDescription: this.fileDescription, - fileType: this.fileType, - }); - - constructor( - private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) - public data: { - otherAttachmentsComponent: OtherAttachmentsComponent, - existingDocument?: NoticeOfIntentDocumentDto, - fileId: string, - }, - private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, - private codeService: CodeService, - private toastService: ToastService - ) {} - - ngOnInit(): void { - this.loadDocumentCodes(); - if (this.data.existingDocument) { - this.title = 'Edit'; - this.isEditing = true; - this.fileType.setValue(this.data.existingDocument.type!.code); - this.fileDescription.setValue(this.data.existingDocument.description!); - this.currentDescription = this.data.existingDocument.description!; - this.currentType = this.data.existingDocument.type; - this.attachment = [this.data.existingDocument]; - } else { - this.title = 'Add'; - } + isDirty = false; + isFileDirty = false; + isSaving = false; + showHasVirusError = false; + showVirusScanFailedError = false; + showFileRequiredError = false; + title: string = ''; + isEditing = false; + + attachment: NoticeOfIntentDocumentDto[] = []; + attachmentForDelete: NoticeOfIntentDocumentDto[] = []; + pendingFile: FileHandle | undefined; + selectableTypes: DocumentTypeDto[] = []; + private documentCodes: DocumentTypeDto[] = []; + + fileDescription = new FormControl(null, [Validators.required]); + fileType = new FormControl(null, [Validators.required]); + currentDescription: string | null = null; + currentType: DocumentTypeDto | null = null; + + form = new FormGroup({ + fileDescription: this.fileDescription, + fileType: this.fileType, + }); + + constructor( + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) + public data: { + otherAttachmentsComponent: OtherAttachmentsComponent; + existingDocument?: NoticeOfIntentDocumentDto; + fileId: string; + }, + private noticeOfIntentDocumentService: NoticeOfIntentDocumentService, + private codeService: CodeService, + private toastService: ToastService, + ) {} + + ngOnInit(): void { + this.loadDocumentCodes(); + if (this.data.existingDocument) { + this.title = 'Edit'; + this.isEditing = true; + this.fileType.setValue(this.data.existingDocument.type!.code); + this.fileDescription.setValue(this.data.existingDocument.description!); + this.currentDescription = this.data.existingDocument.description!; + this.currentType = this.data.existingDocument.type; + this.attachment = [this.data.existingDocument]; + } else { + this.title = 'Add'; } - - async attachDocument(file: FileHandle) { - this.pendingFile = file; - this.attachment = [{uuid: '', - fileName: file.file.name, - type: null, - fileSize: file.file.size, - uploadedBy: '', - uploadedAt: file.file.lastModified, - source: DOCUMENT_SOURCE.APPLICANT, - }]; - this.isFileDirty = true; - this.showFileRequiredError = false; + } + + async attachDocument(file: FileHandle) { + this.pendingFile = file; + this.attachment = [ + { + uuid: '', + fileName: file.file.name, + type: null, + fileSize: file.file.size, + uploadedBy: '', + uploadedAt: file.file.lastModified, + source: DOCUMENT_SOURCE.APPLICANT, + }, + ]; + this.isFileDirty = true; + this.showFileRequiredError = false; + } + + openFile() { + if (this.isEditing && this.pendingFile === undefined) { + this.data.otherAttachmentsComponent.openFile(this.attachment[0]); + } else { + if (this.pendingFile) { + const fileURL = URL.createObjectURL(this.pendingFile.file); + window.open(fileURL, '_blank'); + } } + } - openFile() { - if (this.isEditing && this.pendingFile === undefined) { - this.data.otherAttachmentsComponent.openFile(this.attachment[0]); - } else { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile.file); - window.open(fileURL, '_blank'); - } - } + deleteFile() { + this.pendingFile = undefined; + if (this.isEditing) { + this.attachmentForDelete = this.attachment; } - - deleteFile() { - this.pendingFile = undefined; - if (this.isEditing) { - this.attachmentForDelete = this.attachment; - } - this.attachment = []; + this.attachment = []; + } + + onChangeDescription() { + this.isDirty = true; + this.currentDescription = this.fileDescription.value; + } + + onChangeType(selectedValue: DOCUMENT_TYPE) { + this.isDirty = true; + const newType = this.documentCodes.find((code) => code.code === selectedValue); + this.currentType = newType !== undefined ? newType : null; + } + + validateForm() { + if (this.form.valid && this.attachment.length !== 0) { + return true; } - onChangeDescription() { - this.isDirty = true; - this.currentDescription = this.fileDescription.value; + if (this.form.invalid) { + this.form.markAllAsTouched(); } - onChangeType(selectedValue: DOCUMENT_TYPE) { - this.isDirty = true; - const newType = this.documentCodes.find((code) => code.code === selectedValue); - this.currentType = newType !== undefined ? newType : null; + if (this.attachment.length == 0) { + this.showFileRequiredError = true; } + return false; + } - validateForm() { - if (this.form.valid && this.attachment.length !== 0) { - return true; - } - - if (this.form.invalid) { - this.form.markAllAsTouched(); - } - - if (this.attachment.length == 0) { - this.showFileRequiredError = true; - } - return false; + async onAdd() { + if (this.validateForm()) { + await this.add(); } - - async onAdd() { - if (this.validateForm()) { - await this.add(); + } + + protected async add() { + if (this.isFileDirty) { + this.isSaving = true; + try { + await this.data.otherAttachmentsComponent.attachFile(this.pendingFile!, null); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + + const documents = await this.noticeOfIntentDocumentService.getByFileId(this.data.fileId); + if (documents) { + const sortedDocuments = documents.sort((a, b) => { + return b.uploadedAt - a.uploadedAt; + }); + const updateDtos: NoticeOfIntentDocumentUpdateDto[] = sortedDocuments.map((file) => ({ + uuid: file.uuid, + description: file.description, + type: file.type?.code ?? null, + })); + updateDtos[0] = { + ...updateDtos[0], + description: this.currentDescription, + type: this.currentType?.code ?? null, + }; + await this.noticeOfIntentDocumentService.update(this.data.fileId, updateDtos); + this.toastService.showSuccessToast('Attachment added successfully'); + this.dialogRef.close(); + } else { + this.toastService.showErrorToast('Could not read attached documents'); } - } - - protected async add() { - if (this.isFileDirty) { - this.isSaving = true; - const res = await this.data.otherAttachmentsComponent.attachFile(this.pendingFile!, null); - this.showVirusError = !res; - if (res) { - const documents = await this.noticeOfIntentDocumentService.getByFileId(this.data.fileId); - if (documents) { - const sortedDocuments = documents.sort((a, b) => {return b.uploadedAt - a.uploadedAt}); - const updateDtos: NoticeOfIntentDocumentUpdateDto[] = sortedDocuments.map((file) => ({ - uuid: file.uuid, - description: file.description, - type: file.type?.code ?? null, - })); - updateDtos[0] = { - ...updateDtos[0], - description: this.currentDescription, - type: this.currentType?.code ?? null, - } - await this.noticeOfIntentDocumentService.update(this.data.fileId, updateDtos); - this.toastService.showSuccessToast('Attachment added successfully'); - this.dialogRef.close(); - } else { - this.toastService.showErrorToast("Could not read attached documents"); - } - } + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; } + } + this.isDirty = false; + this.isFileDirty = true; + this.isSaving = false; } + } - async onEdit() { - if (this.validateForm()) { - await this.edit(); - } + async onEdit() { + if (this.validateForm()) { + await this.edit(); } - - protected async edit() { - if (this.isFileDirty) { - this.data.otherAttachmentsComponent.onDeleteFile(this.attachmentForDelete[0]); - await this.add(); - } else { - if (this.isDirty) { - this.isSaving = true; - const documents = await this.noticeOfIntentDocumentService.getByFileId(this.data.fileId); - if (documents) { - const updateDtos: NoticeOfIntentDocumentUpdateDto[] = documents.map((file) => ({ - uuid: file.uuid, - description: file.description, - type: file.type?.code ?? null, - })); - for (let i = 0; i < updateDtos.length; i++) { - if (updateDtos[i].uuid === this.data.existingDocument?.uuid) { - updateDtos[i] = { - ...updateDtos[i], - description: this.currentDescription, - type: this.currentType?.code ?? null, - } - } - } - await this.noticeOfIntentDocumentService.update(this.data.fileId, updateDtos); - this.toastService.showSuccessToast('Attachment updated successully'); - } else { - this.toastService.showErrorToast("Could not read attached documents"); - } + } + + protected async edit() { + if (this.isFileDirty) { + this.data.otherAttachmentsComponent.onDeleteFile(this.attachmentForDelete[0]); + await this.add(); + } else { + if (this.isDirty) { + this.isSaving = true; + const documents = await this.noticeOfIntentDocumentService.getByFileId(this.data.fileId); + if (documents) { + const updateDtos: NoticeOfIntentDocumentUpdateDto[] = documents.map((file) => ({ + uuid: file.uuid, + description: file.description, + type: file.type?.code ?? null, + })); + for (let i = 0; i < updateDtos.length; i++) { + if (updateDtos[i].uuid === this.data.existingDocument?.uuid) { + updateDtos[i] = { + ...updateDtos[i], + description: this.currentDescription, + type: this.currentType?.code ?? null, + }; } - this.dialogRef.close(); + } + await this.noticeOfIntentDocumentService.update(this.data.fileId, updateDtos); + this.toastService.showSuccessToast('Attachment updated successully'); + } else { + this.toastService.showErrorToast('Could not read attached documents'); } + } + this.dialogRef.close(); } + } - 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)); - } + 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/other-attachments/other-attachments.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/other-attachments/other-attachments.component.ts index 09b6c7ac79..1ff2735298 100644 --- 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 @@ -15,6 +15,7 @@ import { EditNoiSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; import { OtherAttachmentsUploadDialogComponent } from './other-attachments-upload-dialog/other-attachments-upload-dialog.component'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; +import { HttpErrorResponse } from '@angular/common/http'; const USER_CONTROLLED_TYPES = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; @@ -33,14 +34,15 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI private isDirty = false; private documentCodes: DocumentTypeDto[] = []; - showVirusError = false; + showHasVirusError = false; + showVirusScanFailedError = false; isMobile = window.innerWidth <= MOBILE_BREAKPOINT; constructor( private codeService: CodeService, noticeOfIntentDocumentService: NoticeOfIntentDocumentService, dialog: MatDialog, - toastService: ToastService + toastService: ToastService, ) { super(noticeOfIntentDocumentService, dialog, toastService); } @@ -69,8 +71,16 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI } async attachDocument(file: FileHandle) { - const res = await this.attachFile(file, null); - this.showVirusError = !res; + try { + await this.attachFile(file, null); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { @@ -93,15 +103,17 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI onAddEditAttachment(attachment: NoticeOfIntentDocumentDto | undefined) { this.dialog .open(OtherAttachmentsUploadDialogComponent, { - width: this.isMobile? '90%' : '50%', + width: this.isMobile ? '90%' : '50%', data: { fileId: this.fileId, otherAttachmentsComponent: this, existingDocument: attachment, - } - }).afterClosed().subscribe(async res => { - await this.refreshFiles(); - }); + }, + }) + .afterClosed() + .subscribe(async (res) => { + await this.refreshFiles(); + }); } @HostListener('window:resize', ['$event']) diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html index 31b643215e..216735720f 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.html @@ -238,7 +238,8 @@ (beforeFileUploadOpened)="saveParcelProgress()" [showErrors]="showErrors" [isRequired]="isCertificateOfTitleRequired" - [showVirusError]="showVirusError" + [showHasVirusError]="showHasVirusError" + [showVirusScanFailedError]="showVirusScanFailedError" [disabled]="parcelForm.controls.pid.disabled" >
    @@ -347,11 +348,12 @@
    OR
    + (editClicked)="onEditCrownOwner(selectedOwner)" + >
    @@ -370,18 +372,20 @@
    OR
    - - + [ngClass]="{ error: ownerInput.errors && ownerInput.errors['required'] }" + > +
    - {{owner.firstName + ' ' + owner.lastName}} + {{ owner.firstName + ' ' + owner.lastName }}
    @@ -401,7 +405,7 @@
    OR
    'error-outline': enableUserSignOff && isConfirmedByApplicant.invalid && - (isConfirmedByApplicant.dirty || isConfirmedByApplicant.touched) + (isConfirmedByApplicant.dirty || isConfirmedByApplicant.touched), }" > diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts index aab4dbddbb..37416f4df0 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/parcels/parcel-entry/parcel-entry.component.ts @@ -22,6 +22,7 @@ import { scrollToElement } from '../../../../../shared/utils/scroll-helper'; import { RemoveFileConfirmationDialogComponent } from '../../../../applications/alcs-edit-submission/remove-file-confirmation-dialog/remove-file-confirmation-dialog.component'; import { ParcelEntryConfirmationDialogComponent } from './parcel-entry-confirmation-dialog/parcel-entry-confirmation-dialog.component'; import { MOBILE_BREAKPOINT } from '../../../../../shared/utils/breakpoints'; +import { HttpErrorResponse } from '@angular/common/http'; export interface ParcelEntryFormData { uuid: string; @@ -66,7 +67,8 @@ export class ParcelEntryComponent implements OnInit { searchBy = new FormControl(null); isCrownLand: boolean | null = null; isCertificateOfTitleRequired = true; - showVirusError = false; + showHasVirusError = false; + showVirusScanFailedError = false; isMobile = false; parcelType = new FormControl(null, [Validators.required]); @@ -311,12 +313,18 @@ export class ParcelEntryComponent implements OnInit { parcelUuid, mappedFiles, ); - } catch (e) { - this.showVirusError = true; - this.toastService.showErrorToast('Document upload failed'); + this.toastService.showSuccessToast('Document uploaded'); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; return; + } catch (err) { + this.toastService.showErrorToast('Document upload failed'); + + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } } - this.showVirusError = false; } } 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 index c30b61a893..a84922a9e7 100644 --- 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 @@ -84,7 +84,7 @@

    Primary Contact

    Phone Number:
    -
    {{ phoneNumber.value ?? '' | mask : '(000) 000-0000' }}
    +
    {{ phoneNumber.value ?? '' | mask: '(000) 000-0000' }}
    Email:
    @@ -218,7 +218,8 @@

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="needsAuthorizationLetter" - [showVirusError]="showVirusError" + [showHasVirusError]="showHasVirusError" + [showVirusScanFailedError]="showVirusScanFailedError" >

    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 index 31eaaa9fbd..989b908965 100644 --- 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 @@ -23,6 +23,7 @@ import { CrownOwnerDialogComponent } from '../../../../shared/owner-dialogs/crow import { scrollToElement } from '../../../../shared/utils/scroll-helper'; import { MatButtonToggleChange } from '@angular/material/button-toggle'; import { strictEmailValidator } from '../../../../shared/validators/email-validator'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-primary-contact', @@ -37,7 +38,8 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni files: (NoticeOfIntentDocumentDto & { errorMessage?: string })[] = []; needsAuthorizationLetter = false; - showVirusError = false; + showHasVirusError = false; + showVirusScanFailedError = false; selectedThirdPartyAgent: boolean | null = false; selectedLocalGovernment = false; _selectedOwnerUuid: string | undefined = undefined; @@ -104,8 +106,16 @@ export class PrimaryContactComponent extends FilesStepComponent implements OnIni } async attachAuthorizationLetter(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.AUTHORIZATION_LETTER); - this.showVirusError = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.AUTHORIZATION_LETTER); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } set selectedOwnerUuid(value: string | undefined) { diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.html index 6b8472096d..608c39f794 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.html @@ -40,7 +40,7 @@

    Proposal

    Yes @@ -48,7 +48,7 @@

    Proposal

    No @@ -277,7 +277,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" > @@ -293,7 +294,7 @@

    Proposal

    Yes @@ -301,7 +302,7 @@

    Proposal

    No @@ -325,7 +326,7 @@

    Proposal

    class="toggle-button" [ngClass]="{ 'error-outline': - isExtractionOrMining.invalid && (isExtractionOrMining.dirty || isExtractionOrMining.touched) + isExtractionOrMining.invalid && (isExtractionOrMining.dirty || isExtractionOrMining.touched), }" value="true" >Yes @@ -334,7 +335,7 @@

    Proposal

    class="toggle-button" [ngClass]="{ 'error-outline': - isExtractionOrMining.invalid && (isExtractionOrMining.dirty || isExtractionOrMining.touched) + isExtractionOrMining.invalid && (isExtractionOrMining.dirty || isExtractionOrMining.touched), }" value="false" >No @@ -372,7 +373,8 @@

    Proposal

    [isRequired]="true" [allowMultiple]="true" [disabled]="!allowMiningUploads" - [showVirusError]="showCrossSectionVirus" + [showHasVirusError]="showCrossSectionHasVirusError" + [showVirusScanFailedError]="showCrossSectionVirusScanFailedError" >
    @@ -400,7 +402,8 @@

    Proposal

    [isRequired]="true" [allowMultiple]="true" [disabled]="!allowMiningUploads" - [showVirusError]="showReclamationPlanVirus" + [showHasVirusError]="showReclamationPlanHasVirusError" + [showVirusScanFailedError]="showReclamationPlanVirusScanFailedError" >
    @@ -419,7 +422,7 @@

    Proposal

    Yes @@ -427,7 +430,7 @@

    Proposal

    No @@ -464,7 +467,8 @@

    Proposal

    [isRequired]="true" [disabled]="!requiresNoticeOfWork" [allowMultiple]="true" - [showVirusError]="showNoticeOfWorkVirus" + [showHasVirusError]="showNoticeOfWorkHasVirusError" + [showVirusScanFailedError]="showNoticeOfWorkVirusScanFailedError" > diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.ts index dc2ab961cd..dc811f568d 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pfrs/pfrs-proposal.component.ts @@ -16,6 +16,7 @@ import { parseStringToBoolean } from '../../../../../shared/utils/string-helper' import { SoilTableData } from '../../../../../shared/soil-table/soil-table.component'; import { EditNoiSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-pfrs-proposal', @@ -34,10 +35,14 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, allowMiningUploads = false; requiresNoticeOfWork = false; - showProposalMapVirus = false; - showCrossSectionVirus = false; - showReclamationPlanVirus = false; - showNoticeOfWorkVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showCrossSectionHasVirusError = false; + showCrossSectionVirusScanFailedError = false; + showReclamationPlanHasVirusError = false; + showReclamationPlanVirusScanFailedError = false; + showNoticeOfWorkHasVirusError = false; + showNoticeOfWorkVirusScanFailedError = false; proposalMap: NoticeOfIntentDocumentDto[] = []; crossSections: NoticeOfIntentDocumentDto[] = []; @@ -81,7 +86,7 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, noticeOfIntentDocumentService: NoticeOfIntentDocumentService, dialog: MatDialog, - toastService: ToastService + toastService: ToastService, ) { super(noticeOfIntentDocumentService, dialog, toastService); } @@ -166,23 +171,55 @@ export class PfrsProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachCrossSection(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); - this.showCrossSectionVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); + this.showCrossSectionHasVirusError = false; + this.showCrossSectionVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showCrossSectionHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showCrossSectionVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachReclamationPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); - this.showReclamationPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); + this.showReclamationPlanHasVirusError = false; + this.showReclamationPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showReclamationPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showReclamationPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachNoticeOfWork(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.NOTICE_OF_WORK); - this.showNoticeOfWorkVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.NOTICE_OF_WORK); + this.showNoticeOfWorkHasVirusError = false; + this.showNoticeOfWorkVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showNoticeOfWorkHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showNoticeOfWorkVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.html index 5b4768f031..bd4462b0ce 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.html @@ -40,7 +40,7 @@

    Proposal

    Yes @@ -48,7 +48,7 @@

    Proposal

    No @@ -195,7 +195,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" > @@ -211,7 +212,7 @@

    Proposal

    Yes @@ -219,7 +220,7 @@

    Proposal

    No @@ -250,7 +251,8 @@

    Proposal

    [isRequired]="true" [allowMultiple]="true" [disabled]="!allowMiningUploads" - [showVirusError]="showCrossSectionVirus" + [showHasVirusError]="showCrossSectionHasVirusError" + [showVirusScanFailedError]="showCrossSectionVirusScanFailedError" >
    @@ -278,7 +280,8 @@

    Proposal

    [isRequired]="true" [allowMultiple]="true" [disabled]="!allowMiningUploads" - [showVirusError]="showReclamationPlanVirus" + [showHasVirusError]="showReclamationPlanHasVirusError" + [showVirusScanFailedError]="showReclamationPlanVirusScanFailedError" >
    diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.ts index ab5f87f5a5..731d365a74 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/pofo/pofo-proposal.component.ts @@ -15,6 +15,7 @@ import { parseStringToBoolean } from '../../../../../shared/utils/string-helper' import { SoilTableData } from '../../../../../shared/soil-table/soil-table.component'; import { EditNoiSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-pofo-proposal', @@ -27,9 +28,12 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, DOCUMENT = DOCUMENT_TYPE; allowMiningUploads = false; - showProposalMapVirus = false; - showCrossSectionVirus = false; - showReclamationPlanVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showCrossSectionHasVirusError = false; + showCrossSectionVirusScanFailedError = false; + showReclamationPlanHasVirusError = false; + showReclamationPlanVirusScanFailedError = false; proposalMap: NoticeOfIntentDocumentDto[] = []; crossSections: NoticeOfIntentDocumentDto[] = []; @@ -117,18 +121,42 @@ export class PofoProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachCrossSection(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); - this.showCrossSectionVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); + this.showCrossSectionHasVirusError = false; + this.showCrossSectionVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showCrossSectionHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showCrossSectionVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachReclamationPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); - this.showReclamationPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); + this.showReclamationPlanHasVirusError = false; + this.showReclamationPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showReclamationPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showReclamationPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html index 7f9b20bc25..777fdfd5fe 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.html @@ -40,7 +40,7 @@

    Proposal

    Yes @@ -48,7 +48,7 @@

    Proposal

    No @@ -180,7 +180,8 @@

    Proposal

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showProposalMapVirus" + [showHasVirusError]="showProposalMapHasVirusError" + [showVirusScanFailedError]="showProposalMapVirusScanFailedError" > @@ -199,7 +200,7 @@

    Proposal

    class="toggle-button" [ngClass]="{ 'error-outline': - isExtractionOrMining.invalid && (isExtractionOrMining.dirty || isExtractionOrMining.touched) + isExtractionOrMining.invalid && (isExtractionOrMining.dirty || isExtractionOrMining.touched), }" value="true" >Yes @@ -208,7 +209,7 @@

    Proposal

    class="toggle-button" [ngClass]="{ 'error-outline': - isExtractionOrMining.invalid && (isExtractionOrMining.dirty || isExtractionOrMining.touched) + isExtractionOrMining.invalid && (isExtractionOrMining.dirty || isExtractionOrMining.touched), }" value="false" >No @@ -239,7 +240,8 @@

    Proposal

    [isRequired]="true" [allowMultiple]="true" [disabled]="!allowMiningUploads" - [showVirusError]="showCrossSectionVirus" + [showHasVirusError]="showCrossSectionHasVirusError" + [showVirusScanFailedError]="showCrossSectionVirusScanFailedError" >
    @@ -267,7 +269,8 @@

    Proposal

    [isRequired]="true" [allowMultiple]="true" [disabled]="!allowMiningUploads" - [showVirusError]="showReclamationPlanVirus" + [showHasVirusError]="showReclamationPlanHasVirusError" + [showVirusScanFailedError]="showReclamationPlanVirusScanFailedError" >
    @@ -286,7 +289,7 @@

    Proposal

    Yes @@ -294,7 +297,7 @@

    Proposal

    No @@ -324,7 +327,8 @@

    Proposal

    [isRequired]="true" [disabled]="!requiresNoticeOfWork" [allowMultiple]="true" - [showVirusError]="showNoticeOfWorkVirus" + [showHasVirusError]="showNoticeOfWorkHasVirusError" + [showVirusScanFailedError]="showNoticeOfWorkVirusScanFailedError" > diff --git a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts index 356fe65147..023f382523 100644 --- a/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts +++ b/portal-frontend/src/app/features/notice-of-intents/edit-submission/proposal/roso/roso-proposal.component.ts @@ -15,6 +15,7 @@ import { parseStringToBoolean } from '../../../../../shared/utils/string-helper' import { SoilTableData } from '../../../../../shared/soil-table/soil-table.component'; import { EditNoiSteps } from '../../edit-submission.component'; import { FilesStepComponent } from '../../files-step.partial'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-roso-proposal', @@ -28,10 +29,14 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, allowMiningUploads = false; requiresNoticeOfWork = false; - showProposalMapVirus = false; - showCrossSectionVirus = false; - showReclamationPlanVirus = false; - showNoticeOfWorkVirus = false; + showProposalMapHasVirusError = false; + showProposalMapVirusScanFailedError = false; + showCrossSectionHasVirusError = false; + showCrossSectionVirusScanFailedError = false; + showReclamationPlanHasVirusError = false; + showReclamationPlanVirusScanFailedError = false; + showNoticeOfWorkHasVirusError = false; + showNoticeOfWorkVirusScanFailedError = false; proposalMap: NoticeOfIntentDocumentDto[] = []; crossSections: NoticeOfIntentDocumentDto[] = []; @@ -66,7 +71,7 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, private noticeOfIntentSubmissionService: NoticeOfIntentSubmissionService, noticeOfIntentDocumentService: NoticeOfIntentDocumentService, dialog: MatDialog, - toastService: ToastService + toastService: ToastService, ) { super(noticeOfIntentDocumentService, dialog, toastService); } @@ -132,23 +137,55 @@ export class RosoProposalComponent extends FilesStepComponent implements OnInit, } async attachProposalMap(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); - this.showProposalMapVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.PROPOSAL_MAP); + this.showProposalMapHasVirusError = false; + this.showProposalMapVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showProposalMapHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showProposalMapVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachCrossSection(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); - this.showCrossSectionVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.CROSS_SECTIONS); + this.showCrossSectionHasVirusError = false; + this.showCrossSectionVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showCrossSectionHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showCrossSectionVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachReclamationPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); - this.showReclamationPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.RECLAMATION_PLAN); + this.showReclamationPlanHasVirusError = false; + this.showReclamationPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showReclamationPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showReclamationPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachNoticeOfWork(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.NOTICE_OF_WORK); - this.showNoticeOfWorkVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.NOTICE_OF_WORK); + this.showNoticeOfWorkHasVirusError = false; + this.showNoticeOfWorkVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showNoticeOfWorkHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showNoticeOfWorkVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { diff --git a/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts index 9cc0e19699..a79c762e00 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/files-step.partial.ts @@ -38,24 +38,22 @@ export abstract class FilesStepComponent extends StepComponent { await this.save(); const mappedFiles = file.file; - let res; try { - res = await this.notificationDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); + const res = await this.notificationDocumentService.attachExternalFile(this.fileId, mappedFiles, documentType); + + if (res) { + this.toastService.showSuccessToast('Document uploaded'); + const documents = await this.notificationDocumentService.getByFileId(this.fileId); + if (documents) { + this.$notificationDocuments.next(documents); + } + } } catch (err) { this.toastService.showErrorToast('Document upload failed'); - if (err instanceof HttpErrorResponse && err.status === 403) { - return false; - } - } - if (res) { - const documents = await this.notificationDocumentService.getByFileId(this.fileId); - if (documents) { - this.$notificationDocuments.next(documents); - } + throw err; } } - return true; } //Using ApplicationDocumentDto is "correct" here, quack quack diff --git a/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html index 015de6d742..c83eb82cf4 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.html @@ -1,82 +1,82 @@
    -

    {{title}} optional attachment

    +

    {{ title }} optional attachment

    -
    - - -
    - - warning A virus was detected in the file. Choose another file and try again. - -
    -
    -
    - - - - {{ type.label }} - - - -
    - warning -
    - This field is required -
    -
    -
    -
    - - - -
    - warning -
    - This field is required -
    -
    -
    +
    + + +
    + + warning A virus was detected in the file. Choose another file and try again. + + +
    +
    + + + + {{ type.label }} + + + +
    + warning +
    This field is required
    +
    +
    +
    + + + +
    + warning +
    This field is required
    - +
    +
    +
    -
    - - - -
    +
    + + + +
    diff --git a/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts index 65666c4581..4031f5008e 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments-upload-dialog/other-attachments-upload-dialog.component.ts @@ -1,215 +1,232 @@ import { Component, Inject, type OnInit } from '@angular/core'; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; -import { NotificationDocumentDto, NotificationDocumentUpdateDto } from "../../../../../services/notification-document/notification-document.dto"; -import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from "../../../../../shared/dto/document.dto"; -import { FileHandle } from "../../../../../shared/file-drag-drop/drag-drop.directive"; -import { OtherAttachmentsComponent } from "../other-attachments.component"; -import { NotificationDocumentService } from "../../../../../services/notification-document/notification-document.service"; -import { CodeService } from "../../../../../services/code/code.service"; -import { ToastService } from "../../../../../services/toast/toast.service"; +import { FormControl, FormGroup, Validators } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { + NotificationDocumentDto, + NotificationDocumentUpdateDto, +} from '../../../../../services/notification-document/notification-document.dto'; +import { DOCUMENT_SOURCE, DOCUMENT_TYPE, DocumentTypeDto } from '../../../../../shared/dto/document.dto'; +import { FileHandle } from '../../../../../shared/file-drag-drop/drag-drop.directive'; +import { OtherAttachmentsComponent } from '../other-attachments.component'; +import { NotificationDocumentService } from '../../../../../services/notification-document/notification-document.service'; +import { CodeService } from '../../../../../services/code/code.service'; +import { ToastService } from '../../../../../services/toast/toast.service'; +import { HttpErrorResponse } from '@angular/common/http'; const USER_CONTROLLED_TYPES = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; @Component({ - selector: 'app-other-attachments-upload-dialog', - templateUrl: './other-attachments-upload-dialog.component.html', - styleUrl: './other-attachments-upload-dialog.component.scss', + selector: 'app-other-attachments-upload-dialog', + templateUrl: './other-attachments-upload-dialog.component.html', + styleUrl: './other-attachments-upload-dialog.component.scss', }) export class OtherAttachmentsUploadDialogComponent implements OnInit { - isDirty = false; - isFileDirty = false; - isSaving = false; - showVirusError = false; - showFileRequiredError = false; - title: string = ''; - isEditing = false; - - attachment: NotificationDocumentDto[] = []; - attachmentForDelete: NotificationDocumentDto[] = []; - pendingFile: FileHandle | undefined; - selectableTypes: DocumentTypeDto[] = []; - private documentCodes: DocumentTypeDto[] = []; - - fileDescription = new FormControl(null, [Validators.required]); - fileType = new FormControl(null, [Validators.required]); - currentDescription: string | null = null; - currentType: DocumentTypeDto | null = null; - - form = new FormGroup({ - fileDescription: this.fileDescription, - fileType: this.fileType, - }); - - constructor( - private dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) - public data: { - otherAttachmentsComponent: OtherAttachmentsComponent, - existingDocument?: NotificationDocumentDto, - fileId: string, - }, - private notificationDocumentService: NotificationDocumentService, - private codeService: CodeService, - private toastService: ToastService - ) {} - - ngOnInit(): void { - this.loadDocumentCodes(); - if (this.data.existingDocument) { - this.title = 'Edit'; - this.isEditing = true; - this.fileType.setValue(this.data.existingDocument.type!.code); - this.fileDescription.setValue(this.data.existingDocument.description!); - this.currentDescription = this.data.existingDocument.description!; - this.currentType = this.data.existingDocument.type; - this.attachment = [this.data.existingDocument]; - } else { - this.title = 'Add'; - } + isDirty = false; + isFileDirty = false; + isSaving = false; + showHasVirusError = false; + showVirusScanFailedError = false; + showFileRequiredError = false; + title: string = ''; + isEditing = false; + + attachment: NotificationDocumentDto[] = []; + attachmentForDelete: NotificationDocumentDto[] = []; + pendingFile: FileHandle | undefined; + selectableTypes: DocumentTypeDto[] = []; + private documentCodes: DocumentTypeDto[] = []; + + fileDescription = new FormControl(null, [Validators.required]); + fileType = new FormControl(null, [Validators.required]); + currentDescription: string | null = null; + currentType: DocumentTypeDto | null = null; + + form = new FormGroup({ + fileDescription: this.fileDescription, + fileType: this.fileType, + }); + + constructor( + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) + public data: { + otherAttachmentsComponent: OtherAttachmentsComponent; + existingDocument?: NotificationDocumentDto; + fileId: string; + }, + private notificationDocumentService: NotificationDocumentService, + private codeService: CodeService, + private toastService: ToastService, + ) {} + + ngOnInit(): void { + this.loadDocumentCodes(); + if (this.data.existingDocument) { + this.title = 'Edit'; + this.isEditing = true; + this.fileType.setValue(this.data.existingDocument.type!.code); + this.fileDescription.setValue(this.data.existingDocument.description!); + this.currentDescription = this.data.existingDocument.description!; + this.currentType = this.data.existingDocument.type; + this.attachment = [this.data.existingDocument]; + } else { + this.title = 'Add'; } - - async attachDocument(file: FileHandle) { - this.pendingFile = file; - this.attachment = [{uuid: '', - fileName: file.file.name, - type: null, - fileSize: file.file.size, - uploadedBy: '', - uploadedAt: file.file.lastModified, - source: DOCUMENT_SOURCE.APPLICANT, - surveyPlanNumber: null, - controlNumber: null, - }]; - this.isFileDirty = true; - this.showFileRequiredError = true; + } + + async attachDocument(file: FileHandle) { + this.pendingFile = file; + this.attachment = [ + { + uuid: '', + fileName: file.file.name, + type: null, + fileSize: file.file.size, + uploadedBy: '', + uploadedAt: file.file.lastModified, + source: DOCUMENT_SOURCE.APPLICANT, + surveyPlanNumber: null, + controlNumber: null, + }, + ]; + this.isFileDirty = true; + this.showFileRequiredError = true; + } + + openFile() { + if (this.isEditing && this.pendingFile === undefined) { + this.data.otherAttachmentsComponent.openFile(this.attachment[0]); + } else { + if (this.pendingFile) { + const fileURL = URL.createObjectURL(this.pendingFile.file); + window.open(fileURL, '_blank'); + } } + } - openFile() { - if (this.isEditing && this.pendingFile === undefined) { - this.data.otherAttachmentsComponent.openFile(this.attachment[0]); - } else { - if (this.pendingFile) { - const fileURL = URL.createObjectURL(this.pendingFile.file); - window.open(fileURL, '_blank'); - } - } + deleteFile() { + this.pendingFile = undefined; + if (this.isEditing) { + this.attachmentForDelete = this.attachment; } - - deleteFile() { - this.pendingFile = undefined; - if (this.isEditing) { - this.attachmentForDelete = this.attachment; - } - this.attachment = []; + this.attachment = []; + } + + onChangeDescription() { + this.isDirty = true; + this.currentDescription = this.fileDescription.value; + } + + onChangeType(selectedValue: DOCUMENT_TYPE) { + this.isDirty = true; + const newType = this.documentCodes.find((code) => code.code === selectedValue); + this.currentType = newType !== undefined ? newType : null; + } + + validateForm() { + if (this.form.valid && this.attachment.length !== 0) { + return true; } - onChangeDescription() { - this.isDirty = true; - this.currentDescription = this.fileDescription.value; + if (this.form.invalid) { + this.form.markAllAsTouched(); } - onChangeType(selectedValue: DOCUMENT_TYPE) { - this.isDirty = true; - const newType = this.documentCodes.find((code) => code.code === selectedValue); - this.currentType = newType !== undefined ? newType : null; + if (this.attachment.length == 0) { + this.showFileRequiredError = true; } + return false; + } - validateForm() { - if (this.form.valid && this.attachment.length !== 0) { - return true; - } - - if (this.form.invalid) { - this.form.markAllAsTouched(); - } - - if (this.attachment.length == 0) { - this.showFileRequiredError = true; - } - return false; + async onAdd() { + if (this.validateForm()) { + await this.add(); } - - async onAdd() { - if (this.validateForm()) { - await this.add(); + } + + protected async add() { + if (this.isFileDirty) { + this.isSaving = true; + try { + await this.data.otherAttachmentsComponent.attachFile(this.pendingFile!, null); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + + const documents = await this.notificationDocumentService.getByFileId(this.data.fileId); + if (documents) { + const sortedDocuments = documents.sort((a, b) => { + return b.uploadedAt - a.uploadedAt; + }); + const updateDtos: NotificationDocumentUpdateDto[] = sortedDocuments.map((file) => ({ + uuid: file.uuid, + description: file.description, + type: file.type?.code ?? null, + })); + updateDtos[0] = { + ...updateDtos[0], + description: this.currentDescription, + type: this.currentType?.code ?? null, + }; + await this.notificationDocumentService.update(this.data.fileId, updateDtos); + this.toastService.showSuccessToast('Attachment added successfully'); + this.dialogRef.close(); + } else { + this.toastService.showErrorToast('Could not read attached documents'); } - } - - protected async add() { - if (this.isFileDirty) { - this.isSaving = true; - const res = await this.data.otherAttachmentsComponent.attachFile(this.pendingFile!, null); - this.showVirusError = !res; - if (res) { - const documents = await this.notificationDocumentService.getByFileId(this.data.fileId); - if (documents) { - const sortedDocuments = documents.sort((a, b) => {return b.uploadedAt - a.uploadedAt}); - const updateDtos: NotificationDocumentUpdateDto[] = sortedDocuments.map((file) => ({ - uuid: file.uuid, - description: file.description, - type: file.type?.code ?? null, - })); - updateDtos[0] = { - ...updateDtos[0], - description: this.currentDescription, - type: this.currentType?.code ?? null, - } - await this.notificationDocumentService.update(this.data.fileId, updateDtos); - this.toastService.showSuccessToast('Attachment added successfully'); - this.dialogRef.close(); - } else { - this.toastService.showErrorToast("Could not read attached documents"); - } - } - this.isDirty = false; - this.isFileDirty = true; - this.isSaving = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; } + } + this.isDirty = false; + this.isFileDirty = true; + this.isSaving = false; } + } - async onEdit() { - if (this.validateForm()) { - this.edit(); - } + async onEdit() { + if (this.validateForm()) { + this.edit(); } - - protected async edit() { - if (this.isFileDirty) { - this.data.otherAttachmentsComponent.onDeleteFile(this.attachmentForDelete[0]); - await this.onAdd(); - } else { - if (this.isDirty) { - this.isSaving = true; - const documents = await this.notificationDocumentService.getByFileId(this.data.fileId); - if (documents) { - const updateDtos: NotificationDocumentUpdateDto[] = documents.map((file) => ({ - uuid: file.uuid, - description: file.description, - type: file.type?.code ?? null, - })); - for (let i = 0; i < updateDtos.length; i++) { - if (updateDtos[i].uuid === this.data.existingDocument?.uuid) { - updateDtos[i] = { - ...updateDtos[i], - description: this.currentDescription, - type: this.currentType?.code ?? null, - } - } - } - await this.notificationDocumentService.update(this.data.fileId, updateDtos); - this.toastService.showSuccessToast('Attachment updated successully'); - } else { - this.toastService.showErrorToast("Could not read attached documents"); - } + } + + protected async edit() { + if (this.isFileDirty) { + this.data.otherAttachmentsComponent.onDeleteFile(this.attachmentForDelete[0]); + await this.onAdd(); + } else { + if (this.isDirty) { + this.isSaving = true; + const documents = await this.notificationDocumentService.getByFileId(this.data.fileId); + if (documents) { + const updateDtos: NotificationDocumentUpdateDto[] = documents.map((file) => ({ + uuid: file.uuid, + description: file.description, + type: file.type?.code ?? null, + })); + for (let i = 0; i < updateDtos.length; i++) { + if (updateDtos[i].uuid === this.data.existingDocument?.uuid) { + updateDtos[i] = { + ...updateDtos[i], + description: this.currentDescription, + type: this.currentType?.code ?? null, + }; } - this.dialogRef.close(); + } + await this.notificationDocumentService.update(this.data.fileId, updateDtos); + this.toastService.showSuccessToast('Attachment updated successully'); + } else { + this.toastService.showErrorToast('Could not read attached documents'); } + } + this.dialogRef.close(); } + } - 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)); - } + 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/notifications/edit-submission/other-attachments/other-attachments.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.ts index 23a7abf237..bef9e6d4cc 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/other-attachments/other-attachments.component.ts @@ -17,6 +17,7 @@ import { EditNotificationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; import { OtherAttachmentsUploadDialogComponent } from './other-attachments-upload-dialog/other-attachments-upload-dialog.component'; import { MOBILE_BREAKPOINT } from '../../../../shared/utils/breakpoints'; +import { HttpErrorResponse } from '@angular/common/http'; const USER_CONTROLLED_TYPES = [DOCUMENT_TYPE.PHOTOGRAPH, DOCUMENT_TYPE.PROFESSIONAL_REPORT, DOCUMENT_TYPE.OTHER]; @@ -33,7 +34,8 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI otherFiles: NotificationDocumentDto[] = []; private isDirty = false; - showVirusError = false; + showHasVirusError = false; + showVirusScanFailedError = false; isMobile = window.innerWidth <= MOBILE_BREAKPOINT; private documentCodes: DocumentTypeDto[] = []; @@ -42,7 +44,7 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI private codeService: CodeService, toastService: ToastService, notificationDocumentService: NotificationDocumentService, - dialog: MatDialog + dialog: MatDialog, ) { super(notificationDocumentService, dialog, toastService); } @@ -71,8 +73,16 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI } async attachDocument(file: FileHandle) { - const res = await this.attachFile(file, null); - this.showVirusError = !res; + try { + await this.attachFile(file, null); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } protected async save() { @@ -95,15 +105,17 @@ export class OtherAttachmentsComponent extends FilesStepComponent implements OnI onAddEditAttachment(attachment: NotificationDocumentDto | undefined) { this.dialog .open(OtherAttachmentsUploadDialogComponent, { - width: this.isMobile? '90%' : '50%', + width: this.isMobile ? '90%' : '50%', data: { fileId: this.fileId, otherAttachmentsComponent: this, existingDocument: attachment, - } - }).afterClosed().subscribe(async res => { - await this.refreshFiles(); - }); + }, + }) + .afterClosed() + .subscribe(async (res) => { + await this.refreshFiles(); + }); } @HostListener('window:resize', ['$event']) diff --git a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.html b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.html index 3f8d717121..9c9d155aa0 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.html +++ b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.html @@ -77,7 +77,8 @@

    Purpose of SRW

    (openFile)="openFile($event)" [showErrors]="showErrors" [isRequired]="true" - [showVirusError]="showSRWTermsVirus" + [showHasVirusError]="showSRWTermsHasVirusError" + [showVirusScanFailedError]="showSRWTermsVirusScanFailedError" >
    @@ -93,7 +94,7 @@

    Purpose of SRW

    Yes @@ -101,7 +102,7 @@

    Purpose of SRW

    No @@ -122,7 +123,8 @@

    Purpose of SRW

    [isRequired]="surveyPlans.length === 0" [disabled]="!allowSurveyPlanUploads" [allowMultiple]="true" - [showVirusError]="showSurveyPlanVirus" + [showHasVirusError]="showSurveyPlanHasVirusError" + [showVirusScanFailedError]="showSurveyPlanVirusScanFailedError" > diff --git a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts index 9e9dd6c25b..74f3cc5639 100644 --- a/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts +++ b/portal-frontend/src/app/features/notifications/edit-submission/proposal/proposal.component.ts @@ -18,6 +18,7 @@ import { parseStringToBoolean } from '../../../../shared/utils/string-helper'; import { EditNotificationSteps } from '../edit-submission.component'; import { FilesStepComponent } from '../files-step.partial'; import { ChangeSurveyPlanConfirmationDialogComponent } from './change-survey-plan-confirmation-dialog/change-survey-plan-confirmation-dialog.component'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-proposal', @@ -48,15 +49,17 @@ export class ProposalComponent extends FilesStepComponent implements OnInit, OnD private submissionUuid = ''; private isDirty = false; surveyForm = new FormGroup({} as any); - showSRWTermsVirus = false; - showSurveyPlanVirus = false; + showSRWTermsHasVirusError = false; + showSRWTermsVirusScanFailedError = false; + showSurveyPlanHasVirusError = false; + showSurveyPlanVirusScanFailedError = false; constructor( private router: Router, private notificationSubmissionService: NotificationSubmissionService, notificationDocumentService: NotificationDocumentService, dialog: MatDialog, - toastService: ToastService + toastService: ToastService, ) { super(notificationDocumentService, dialog, toastService); } @@ -98,13 +101,29 @@ export class ProposalComponent extends FilesStepComponent implements OnInit, OnD } async attachSRWTerms(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.SRW_TERMS); - this.showSRWTermsVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.SRW_TERMS); + this.showSRWTermsHasVirusError = false; + this.showSRWTermsVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showSRWTermsHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showSRWTermsVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async attachSurveyPlan(file: FileHandle) { - const res = await this.attachFile(file, DOCUMENT_TYPE.SURVEY_PLAN); - this.showSurveyPlanVirus = !res; + try { + await this.attachFile(file, DOCUMENT_TYPE.SURVEY_PLAN); + this.showSurveyPlanHasVirusError = false; + this.showSurveyPlanVirusScanFailedError = false; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showSurveyPlanHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showSurveyPlanVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } + } } async onSave() { diff --git a/portal-frontend/src/app/services/application-document/application-document.service.ts b/portal-frontend/src/app/services/application-document/application-document.service.ts index b7b69eab01..a4ce5b2153 100644 --- a/portal-frontend/src/app/services/application-document/application-document.service.ts +++ b/portal-frontend/src/app/services/application-document/application-document.service.ts @@ -27,26 +27,13 @@ export class ApplicationDocumentService { documentType: DOCUMENT_TYPE | null, source = DOCUMENT_SOURCE.APPLICANT ) { - try { - const res = await this.documentService.uploadFile( - fileNumber, - file, - documentType, - source, - `${this.serviceUrl}/application/${fileNumber}/attachExternal` - ); - if (res) { - this.toastService.showSuccessToast('Document uploaded'); - } - return res; - } catch (e) { - if (e instanceof HttpErrorResponse && e.status === 403) { - throw e; - } - console.error(e); - this.toastService.showErrorToast('Failed to attach document to Application, please try again'); - } - return undefined; + return await this.documentService.uploadFile( + fileNumber, + file, + documentType, + source, + `${this.serviceUrl}/application/${fileNumber}/attachExternal`, + ); } async openFile(fileUuid: string) { diff --git a/portal-frontend/src/app/services/application-owner/application-owner.service.ts b/portal-frontend/src/app/services/application-owner/application-owner.service.ts index 9124cb404f..feafe5b433 100644 --- a/portal-frontend/src/app/services/application-owner/application-owner.service.ts +++ b/portal-frontend/src/app/services/application-owner/application-owner.service.ts @@ -117,21 +117,12 @@ export class ApplicationOwnerService { } async uploadCorporateSummary(applicationFileId: string, file: File) { - try { - return await this.documentService.uploadFile<{ uuid: string }>( - applicationFileId, - file, - DOCUMENT_TYPE.CORPORATE_SUMMARY, - DOCUMENT_SOURCE.APPLICANT, - `${this.serviceUrl}/attachCorporateSummary` - ); - } catch (e) { - if (e instanceof HttpErrorResponse && e.status === 403) { - throw e; - } - console.error(e); - this.toastService.showErrorToast('Failed to attach document to Owner, please try again'); - } - return undefined; + return await this.documentService.uploadFile<{ uuid: string }>( + applicationFileId, + file, + DOCUMENT_TYPE.CORPORATE_SUMMARY, + DOCUMENT_SOURCE.APPLICANT, + `${this.serviceUrl}/attachCorporateSummary`, + ); } } diff --git a/portal-frontend/src/app/services/application-parcel/application-parcel.service.spec.ts b/portal-frontend/src/app/services/application-parcel/application-parcel.service.spec.ts index e9c2afcd58..c96eef6a09 100644 --- a/portal-frontend/src/app/services/application-parcel/application-parcel.service.spec.ts +++ b/portal-frontend/src/app/services/application-parcel/application-parcel.service.spec.ts @@ -1,4 +1,4 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; @@ -99,31 +99,12 @@ describe('ApplicationParcelService', () => { expect(mockHttpClient.put.mock.calls[0][0]).toContain('application-parcel'); }); - it('should show an error toast if updating a parcel fails', async () => { - mockHttpClient.put.mockReturnValue(throwError(() => ({}))); - - await service.update([{}] as ApplicationParcelUpdateDto[]); - - expect(mockHttpClient.put).toHaveBeenCalledTimes(1); - expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); - }); - it('should call document service for attaching certificate of title', async () => { mockDocumentService.uploadFile.mockResolvedValue({}); await service.attachCertificateOfTitle('fileId', 'parcelUuid', {} as File); expect(mockDocumentService.uploadFile).toHaveBeenCalledTimes(1); - expect(mockToastService.showSuccessToast).toHaveBeenCalledTimes(1); - }); - - it('should show an error toast if document service fails', async () => { - mockDocumentService.uploadFile.mockRejectedValue({}); - - await service.attachCertificateOfTitle('fileId', 'parcelUuid', {} as File); - - expect(mockDocumentService.uploadFile).toHaveBeenCalledTimes(1); - expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); }); it('should make a delete request and show the overlay for removing all parcels', async () => { @@ -147,7 +128,7 @@ describe('ApplicationParcelService', () => { mockHttpClient.delete.mockReturnValue( throwError(() => { new Error(''); - }) + }), ); await service.deleteMany([]); diff --git a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts index 65c8e9ac45..8891275130 100644 --- a/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts +++ b/portal-frontend/src/app/services/application-parcel/application-parcel.service.ts @@ -73,24 +73,13 @@ export class ApplicationParcelService { } async attachCertificateOfTitle(fileId: string, parcelUuid: string, file: File) { - try { - const document = await this.documentService.uploadFile( - fileId, - file, - DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, - DOCUMENT_SOURCE.APPLICANT, - `${environment.apiUrl}/application-parcel/${parcelUuid}/attachCertificateOfTitle` - ); - this.toastService.showSuccessToast('Document uploaded'); - return document; - } catch (e) { - if (e instanceof HttpErrorResponse && e.status === 403) { - throw e; - } - console.error(e); - this.toastService.showErrorToast('Failed to attach document to Parcel, please try again'); - } - return undefined; + return await this.documentService.uploadFile( + fileId, + file, + DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + DOCUMENT_SOURCE.APPLICANT, + `${environment.apiUrl}/application-parcel/${parcelUuid}/attachCertificateOfTitle`, + ); } async deleteMany(parcelUuids: string[]) { diff --git a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts index 40461754d6..b2593f56d0 100644 --- a/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts +++ b/portal-frontend/src/app/services/notice-of-intent-document/notice-of-intent-document.service.ts @@ -27,26 +27,13 @@ export class NoticeOfIntentDocumentService { documentType: DOCUMENT_TYPE | null, source = DOCUMENT_SOURCE.APPLICANT ) { - try { - const res = await this.documentService.uploadFile( - fileNumber, - file, - documentType, - source, - `${this.serviceUrl}/notice-of-intent/${fileNumber}/attachExternal` - ); - if (res) { - this.toastService.showSuccessToast('Document uploaded'); - } - return res; - } catch (e) { - if (e instanceof HttpErrorResponse && e.status === 403) { - throw e; - } - console.error(e); - this.toastService.showErrorToast('Failed to attach document, please try again'); - } - return undefined; + return await this.documentService.uploadFile( + fileNumber, + file, + documentType, + source, + `${this.serviceUrl}/notice-of-intent/${fileNumber}/attachExternal`, + ); } async openFile(fileUuid: string) { diff --git a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts index eee2f7b09c..fd8d5c6ccc 100644 --- a/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts +++ b/portal-frontend/src/app/services/notice-of-intent-owner/notice-of-intent-owner.service.ts @@ -127,21 +127,12 @@ export class NoticeOfIntentOwnerService { } async uploadCorporateSummary(noticeOfIntentFileId: string, file: File) { - try { - return await this.documentService.uploadFile<{ uuid: string }>( - noticeOfIntentFileId, - file, - DOCUMENT_TYPE.CORPORATE_SUMMARY, - DOCUMENT_SOURCE.APPLICANT, - `${this.serviceUrl}/attachCorporateSummary` - ); - } catch (e) { - if (e instanceof HttpErrorResponse && e.status === 403) { - throw e; - } - console.error(e); - this.toastService.showErrorToast('Failed to attach document to Owner, please try again'); - } - return undefined; + return await this.documentService.uploadFile<{ uuid: string }>( + noticeOfIntentFileId, + file, + DOCUMENT_TYPE.CORPORATE_SUMMARY, + DOCUMENT_SOURCE.APPLICANT, + `${this.serviceUrl}/attachCorporateSummary`, + ); } } diff --git a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts index e5dc086569..7d223c3f05 100644 --- a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts +++ b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.spec.ts @@ -95,35 +95,15 @@ describe('NoticeOfIntentParcelService', () => { await service.update([{ uuid: mockUuid }] as NoticeOfIntentParcelUpdateDto[]); expect(mockHttpClient.put).toHaveBeenCalledTimes(1); - expect(mockToastService.showSuccessToast).toHaveBeenCalledTimes(1); expect(mockHttpClient.put.mock.calls[0][0]).toContain('notice-of-intent-parcel'); }); - it('should show an error toast if updating a parcel fails', async () => { - mockHttpClient.put.mockReturnValue(throwError(() => ({}))); - - await service.update([{}] as NoticeOfIntentParcelUpdateDto[]); - - expect(mockHttpClient.put).toHaveBeenCalledTimes(1); - expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); - }); - it('should call document service for attaching certificate of title', async () => { mockDocumentService.uploadFile.mockResolvedValue({}); await service.attachCertificateOfTitle('fileId', 'parcelUuid', {} as File); expect(mockDocumentService.uploadFile).toHaveBeenCalledTimes(1); - expect(mockToastService.showSuccessToast).toHaveBeenCalledTimes(1); - }); - - it('should show an error toast if document service fails', async () => { - mockDocumentService.uploadFile.mockRejectedValue({}); - - await service.attachCertificateOfTitle('fileId', 'parcelUuid', {} as File); - - expect(mockDocumentService.uploadFile).toHaveBeenCalledTimes(1); - expect(mockToastService.showErrorToast).toHaveBeenCalledTimes(1); }); it('should make a delete request and show the overlay for removing all parcels', async () => { @@ -147,7 +127,7 @@ describe('NoticeOfIntentParcelService', () => { mockHttpClient.delete.mockReturnValue( throwError(() => { new Error(''); - }) + }), ); await service.deleteMany([]); diff --git a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts index d7ab1c5f42..2a00ea82f1 100644 --- a/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts +++ b/portal-frontend/src/app/services/notice-of-intent-parcel/notice-of-intent-parcel.service.ts @@ -73,24 +73,13 @@ export class NoticeOfIntentParcelService { } async attachCertificateOfTitle(fileId: string, parcelUuid: string, file: File) { - try { - const document = await this.documentService.uploadFile( - fileId, - file, - DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, - DOCUMENT_SOURCE.APPLICANT, - `${this.serviceUrl}/${parcelUuid}/attachCertificateOfTitle` - ); - this.toastService.showSuccessToast('Document uploaded'); - return document; - } catch (e) { - if (e instanceof HttpErrorResponse && e.status === 403) { - throw e; - } - console.error(e); - this.toastService.showErrorToast('Failed to attach document to Parcel, please try again'); - } - return undefined; + return await this.documentService.uploadFile( + fileId, + file, + DOCUMENT_TYPE.CERTIFICATE_OF_TITLE, + DOCUMENT_SOURCE.APPLICANT, + `${this.serviceUrl}/${parcelUuid}/attachCertificateOfTitle`, + ); } async deleteMany(parcelUuids: string[]) { diff --git a/portal-frontend/src/app/services/notification-document/notification-document.service.ts b/portal-frontend/src/app/services/notification-document/notification-document.service.ts index 698ecbae19..5a462832a6 100644 --- a/portal-frontend/src/app/services/notification-document/notification-document.service.ts +++ b/portal-frontend/src/app/services/notification-document/notification-document.service.ts @@ -27,26 +27,13 @@ export class NotificationDocumentService { documentType: DOCUMENT_TYPE | null, source = DOCUMENT_SOURCE.APPLICANT ) { - try { - const res = await this.documentService.uploadFile( - fileNumber, - file, - documentType, - source, - `${this.serviceUrl}/notification/${fileNumber}/attachExternal` - ); - if (res) { - this.toastService.showSuccessToast('Document uploaded'); - } - return res; - } catch (e) { - if (e instanceof HttpErrorResponse && e.status === 403) { - throw e; - } - console.error(e); - this.toastService.showErrorToast('Failed to attach document, please try again'); - } - return undefined; + return await this.documentService.uploadFile( + fileNumber, + file, + documentType, + source, + `${this.serviceUrl}/notification/${fileNumber}/attachExternal`, + ); } async openFile(fileUuid: string) { diff --git a/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.html b/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.html index 6a2efffad4..13dd3e1010 100644 --- a/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.html +++ b/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.html @@ -4,7 +4,7 @@
    {{ file.fileName }} @@ -27,8 +27,10 @@ *ngIf="!uploadedFiles.length || allowMultiple" [ngClass]="{ 'desktop-file-drag-drop': true, - 'error-outline': !disabled && ((isRequired && showErrors && !uploadedFiles.length) || showVirusError), - disabled: disabled + 'error-outline': + !disabled && + ((isRequired && showErrors && !uploadedFiles.length) || showHasVirusError || showVirusScanFailedError), + disabled: disabled, }" dragDropFile (files)="filesDropped($event)" @@ -55,7 +57,10 @@ This file upload is required - + A virus was detected in the file. Choose another file and try again. + + A problem occurred while scanning for viruses. Please try again. + Maximum file size: 100 MB diff --git a/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.ts b/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.ts index 0acc17688c..2dae680114 100644 --- a/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.ts +++ b/portal-frontend/src/app/shared/file-drag-drop/file-drag-drop.component.ts @@ -19,7 +19,8 @@ export class FileDragDropComponent implements OnInit { @Input() uploadedFiles: (ApplicationDocumentDto & { errorMessage?: string })[] = []; @Input() isRequired = false; @Input() showErrors = false; - @Input() showVirusError = false; + @Input() showHasVirusError = false; + @Input() showVirusScanFailedError = false; private uploadClicked = false; diff --git a/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.html b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.html index aec322f1b2..22671f2f9e 100644 --- a/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.html +++ b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.html @@ -46,7 +46,8 @@
    id="fileUpload" [showErrors]="showFileErrors" [isRequired]="true" - [showVirusError]="showVirusError" + [showHasVirusError]="showHasVirusError" + [showVirusScanFailedError]="showVirusScanFailedError" > diff --git a/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts index 5946c65b8e..237573d808 100644 --- a/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts +++ b/portal-frontend/src/app/shared/owner-dialogs/owner-dialog/owner-dialog.component.ts @@ -21,6 +21,8 @@ import { OWNER_TYPE } from '../../dto/owner.dto'; import { FileHandle } from '../../file-drag-drop/drag-drop.directive'; import { openFileInline } from '../../utils/file'; import { strictEmailValidator } from '../../validators/email-validator'; +import { ToastService } from '../../../services/toast/toast.service'; +import { HttpErrorResponse } from '@angular/common/http'; @Component({ selector: 'app-owner-dialog', @@ -38,7 +40,8 @@ export class OwnerDialogComponent { corporateSummary = new FormControl(null); isEdit = false; - showVirusError = false; + showHasVirusError = false; + showVirusScanFailedError = false; existingUuid: string | undefined; files: ApplicationDocumentDto[] = []; showFileErrors = false; @@ -71,6 +74,7 @@ export class OwnerDialogComponent { documentService: ApplicationDocumentService | NoticeOfIntentDocumentService; ownerService: ApplicationOwnerService | NoticeOfIntentOwnerService; }, + private toastService: ToastService, ) { if (data && data.existingOwner) { this.onChangeType({ @@ -98,7 +102,7 @@ export class OwnerDialogComponent { this.corporateSummary.setValidators([Validators.required]); } else { this.organizationName.setValidators([]); - this.corporateSummary.setValidators([]); + this.corporateSummary.setValidators([]); } this.corporateSummary.updateValueAndValidity(); this.organizationName.updateValueAndValidity(); @@ -181,7 +185,7 @@ export class OwnerDialogComponent { let document; if (this.pendingFile) { document = await this.uploadPendingFile(this.pendingFile); - } else { + } else { document = this.type.value === OWNER_TYPE.ORGANIZATION ? this.data.existingOwner?.corporateSummary : null; } @@ -214,7 +218,7 @@ export class OwnerDialogComponent { this.corporateSummary.setValue('pending'); const corporateSummaryType = this.documentCodes.find((code) => code.code === DOCUMENT_TYPE.CORPORATE_SUMMARY); if (corporateSummaryType) { - this.showVirusError = false; + this.showHasVirusError = false; this.files = [ { type: corporateSummaryType, @@ -267,19 +271,25 @@ export class OwnerDialogComponent { } private async uploadPendingFile(file?: File) { - let documentUuid; if (file) { try { - documentUuid = await this.data.ownerService.uploadCorporateSummary(this.data.fileId, file); - } catch (e) { - this.showVirusError = true; + const documentUuid = await this.data.ownerService.uploadCorporateSummary(this.data.fileId, file); + this.toastService.showSuccessToast('Document uploaded'); + this.showHasVirusError = false; + this.showVirusScanFailedError = false; + return documentUuid; + } catch (err) { + if (err instanceof HttpErrorResponse) { + this.showHasVirusError = err.status === 400 && err.error.name === 'VirusDetected'; + this.showVirusScanFailedError = err.status === 500 && err.error.name === 'VirusScanFailed'; + } this.pendingFile = undefined; this.corporateSummary.setValue(null); this.files = []; - return; + this.toastService.showErrorToast('Document upload failed'); } } - return documentUuid; + return; } private async loadDocumentCodes() { diff --git a/services/apps/alcs/src/clamav/clamav.service.ts b/services/apps/alcs/src/clamav/clamav.service.ts index 8d8ba4e348..e8210c66e2 100644 --- a/services/apps/alcs/src/clamav/clamav.service.ts +++ b/services/apps/alcs/src/clamav/clamav.service.ts @@ -1,4 +1,5 @@ import { CONFIG_TOKEN, IConfig } from '@app/common/config/config.module'; +import { BaseServiceException } from '@app/common/exceptions/base.exception'; import { HttpService } from '@nestjs/axios'; import { Inject, Injectable, Logger } from '@nestjs/common'; import * as NodeClam from 'clamscan'; diff --git a/services/apps/alcs/src/document/document.service.ts b/services/apps/alcs/src/document/document.service.ts index 33c028a2b5..70d3bfb741 100644 --- a/services/apps/alcs/src/document/document.service.ts +++ b/services/apps/alcs/src/document/document.service.ts @@ -1,11 +1,6 @@ import { CONFIG_TOKEN, IConfig } from '@app/common/config/config.module'; -import { BaseServiceException } from '@app/common/exceptions/base.exception'; -import { - DeleteObjectCommand, - GetObjectCommand, - PutObjectCommand, - S3Client, -} from '@aws-sdk/client-s3'; +import { BaseServiceException, ServiceValidationException } from '@app/common/exceptions/base.exception'; +import { DeleteObjectCommand, GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; import { MultipartFile } from '@fastify/multipart'; import { Inject, Injectable, Logger } from '@nestjs/common'; @@ -167,15 +162,23 @@ export class DocumentService { }); const isInfected = await this.clamAvService.scanFile(fileUrl); - if (isInfected) { + + if (isInfected === null || isInfected === undefined) { await this.deleteDocument(data.fileKey); - this.logger.warn(`Deleted malicious file ${data.fileKey}`); + this.logger.warn(`Deleted unscanned file ${data.fileKey}`); throw new BaseServiceException( - 'File may contain malicious data, upload blocked', - 403, + 'Virus scan failed, cannot determine if infected, upload blocked', + undefined, + 'VirusScanFailed', ); } + if (isInfected) { + await this.deleteDocument(data.fileKey); + this.logger.warn(`Deleted malicious file ${data.fileKey}`); + throw new ServiceValidationException('File may contain malicious data, upload blocked', 'VirusDetected'); + } + return this.documentRepository.save( new Document({ mimeType: data.mimeType, diff --git a/services/libs/common/src/exceptions/base.exception.ts b/services/libs/common/src/exceptions/base.exception.ts index 67eb664186..aecd69a9be 100644 --- a/services/libs/common/src/exceptions/base.exception.ts +++ b/services/libs/common/src/exceptions/base.exception.ts @@ -1,32 +1,37 @@ import { HttpException, HttpStatus } from '@nestjs/common'; export class BaseErrorResponseModel { - constructor(statusCode: number, message: string, path?: string) { + constructor(statusCode: number, name: string, message: string, path?: string) { this.statusCode = statusCode; + this.name = name; this.message = message; this.path = path; } statusCode: number; - path: string | undefined; + name: string; message: string; + path: string | undefined; } export class BaseServiceException extends HttpException { - constructor(error: string | Record, status?: number) { + constructor(error: string | Record, status?: number, name?: string) { super(error, status ?? HttpStatus.INTERNAL_SERVER_ERROR); + if (name) { + this.name = name; + } } } export class ServiceValidationException extends BaseServiceException { - constructor(error: string | Record) { - super(error, HttpStatus.BAD_REQUEST); + constructor(error: string | Record, name?: string) { + super(error, HttpStatus.BAD_REQUEST, name); } } export class ServiceNotFoundException extends BaseServiceException { - constructor(error: string | Record) { - super(error, HttpStatus.NOT_FOUND); + constructor(error: string | Record, name?: string) { + super(error, HttpStatus.NOT_FOUND, name); } } diff --git a/services/libs/common/src/exceptions/exception.filter.spec.ts b/services/libs/common/src/exceptions/exception.filter.spec.ts index 5642d588e5..8ce23bb451 100644 --- a/services/libs/common/src/exceptions/exception.filter.spec.ts +++ b/services/libs/common/src/exceptions/exception.filter.spec.ts @@ -35,10 +35,7 @@ describe('HttpExceptionFilter', () => { }); it('should call global HttpExceptionFilter', () => { - const mockHttpException = new HttpException( - { message: 'Sample Exception' }, - HttpStatus.BAD_REQUEST, - ); + const mockHttpException = new HttpException({ message: 'Sample Exception' }, HttpStatus.BAD_REQUEST); service.catch(mockHttpException, mockArgumentsHost); expect(mockHttpArgumentsHost).toBeCalledTimes(1); @@ -51,6 +48,7 @@ describe('HttpExceptionFilter', () => { expect(mockSend).toBeCalledWith( new BaseErrorResponseModel( mockHttpException.getStatus(), + mockHttpException.name, mockHttpException.message, mockGetRequest().url, ), diff --git a/services/libs/common/src/exceptions/exception.filter.ts b/services/libs/common/src/exceptions/exception.filter.ts index dd2f50a04c..02bf8d3577 100644 --- a/services/libs/common/src/exceptions/exception.filter.ts +++ b/services/libs/common/src/exceptions/exception.filter.ts @@ -14,8 +14,6 @@ export class HttpExceptionFilter implements ExceptionFilter { this.logger.error(exception); - response - .status(status) - .send(new BaseErrorResponseModel(status, exception.message, request.url)); + response.status(status).send(new BaseErrorResponseModel(status, exception.name, exception.message, request.url)); } }
    #{{ i + 1 }} + {{ i + 1 }} + Type + Additional Proposal Information Total Floor Area + Additional Proposal Information Action + @@ -165,12 +183,15 @@

    Additional Proposal Information

    - The proposed residential structure and its total floor area must be allowed under the ALC Act and/or ALR Use Regulation. - If not, you may require a 'Non-Adhering Residential Use' application instead. For more info, please see - Housing in the ALR + The proposed residential structure and its total floor area must be allowed under the ALC Act and/or + ALR Use Regulation. If not, you may require a 'Non-Adhering Residential Use' application instead. For + more info, please see + Housing in the ALR on the ALC website.
    -
    +