diff --git a/cllc-public-app/ClientApp/src/app/components/applications/application/application.component.html b/cllc-public-app/ClientApp/src/app/components/applications/application/application.component.html index ead9e49a1f..5893b2fe86 100644 --- a/cllc-public-app/ClientApp/src/app/components/applications/application/application.component.html +++ b/cllc-public-app/ClientApp/src/app/components/applications/application/application.component.html @@ -1838,13 +1838,20 @@

ESTABLISHMENT TYPE

The below floor plan is on file.

- +

diff --git a/cllc-public-app/ClientApp/src/app/components/applications/application/application.component.ts b/cllc-public-app/ClientApp/src/app/components/applications/application/application.component.ts index c4ba22b560..d8c00a71c8 100644 --- a/cllc-public-app/ClientApp/src/app/components/applications/application/application.component.ts +++ b/cllc-public-app/ClientApp/src/app/components/applications/application/application.component.ts @@ -1428,6 +1428,10 @@ export class ApplicationComponent extends FormBase implements OnInit { } } + onOccupantLoadUpdated(load: number) { + this.form.patchValue({ totalOccupantLoad: load }); + } + relocateWinaryLicenceChanged() { if (this.application.relocateWinaryLicence == undefined || this.application.relocateWinaryLicence == false) { this.application.relocateWinaryLicence = true; diff --git a/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.html b/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.html index 2aea793f21..7d3068d182 100644 --- a/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.html +++ b/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.html @@ -22,7 +22,7 @@ - +
{{ item.name }} @@ -32,4 +32,14 @@
+ +
+ ⚠ {{ validationMessage }} +
+
+ ⚠ {{ validationMessage }} +
+
+ ✔ Your floor plan contains a valid occupant load stamp. +
diff --git a/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.scss b/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.scss index ec545c4103..c7c74bafdc 100644 --- a/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.scss +++ b/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.scss @@ -31,4 +31,27 @@ .red-snackbar { background: #F44336; -} \ No newline at end of file +} + +.validation-message { + margin-top: 10px; + font-size: 14px; + color: inherit; + border: none !important; + background: none; + box-shadow: none; + padding: 0; + } + + .validation-message.invalid { + color: red; + } + + .validation-message.expired { + color: red; + } + + .validation-message.valid { + color: green; + } + \ No newline at end of file diff --git a/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.ts b/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.ts index 48354c0cdc..fa1aa8dfd9 100644 --- a/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.ts +++ b/cllc-public-app/ClientApp/src/app/shared/components/file-uploader/file-uploader.component.ts @@ -5,7 +5,6 @@ import { Subscription } from "rxjs"; import { ApplicationDataService } from "@services/application-data.service"; import { HttpClient, HttpHeaders } from "@angular/common/http"; import { MatSnackBar } from "@angular/material/snack-bar"; - export interface DropdownOption { id: string; name: string; @@ -43,16 +42,24 @@ export class FileUploaderComponent implements OnInit, OnDestroy { useDocumentTypeForName = false; @Input() publicAccess = false; + @Input() + isFloorPlanUploader = false; + @Output() + occupantLoadUpdated = new EventEmitter(); @Output() numberOfUploadedFiles = new EventEmitter(); busy: Subscription; attachmentURL: string; + aiScanServiceUrl: string; actionPrefix: string; Math = Math; files: FileSystemItem[] = []; dataLoaded: boolean; fileReqOngoing: boolean; subscriptionList: Subscription[] = []; + isLoading: boolean = false; // Loading spinner state + apiValidationStatus: 'valid' | 'invalid' | 'expired' | null = null; // API result status + validationMessage: string | null = null; // Error messages // TODO: move http call to a service constructor( @@ -68,10 +75,15 @@ export class FileUploaderComponent implements OnInit, OnDestroy { this.actionPrefix = ""; } this.attachmentURL = `api/file/${this.entityId}/${this.actionPrefix}attachments/${this.entityName}`; + this.aiScanServiceUrl = ""; + console.log('aiScanServiceUrl', this.aiScanServiceUrl); this.getUploadedFileData(); } dropped(event: NgxFileDropEntry[]) { + // Clear validation state + this.apiValidationStatus = null; + this.validationMessage = null; const files = event; let newFileCount = 0; for (const droppedFile of files) { @@ -103,18 +115,54 @@ export class FileUploaderComponent implements OnInit, OnDestroy { } } + validateOccupantLoadStamp(file: File) { + const formData = new FormData(); + formData.append('file', file); + this.isLoading = true; // Show the existing loading bar + + this.http.post(this.aiScanServiceUrl, formData).subscribe({ + next: (response: any) => { + this.isLoading = false; // Hide the loading bar + const { date, occupancy_load, stamp_detected } = response; + + // Check response validity + const currentDate = new Date(); + const stampDate = new Date(date); + const isDateValid = currentDate.getTime() - stampDate.getTime() <= 365 * 24 * 60 * 60 * 1000; + + if (!stamp_detected) { + this.apiValidationStatus = 'invalid'; + this.validationMessage = "The uploaded document did not contain an occupant load stamp."; + } else if (!isDateValid) { + this.apiValidationStatus = 'expired'; + this.validationMessage = "The uploaded document's occupancy load stamp was issued more than one year ago. A new stamp is required before you can proceed with your application."; + } else { + this.apiValidationStatus = 'valid'; + this.validationMessage = null; + this.occupantLoadUpdated.emit(occupancy_load); // Emit occupant load to parent + } + }, + error: () => { + this.isLoading = false; // Hide the loading bar + this.apiValidationStatus = 'invalid'; + this.validationMessage = "Failed to validate the uploaded file."; + }, + }); + } + + getCurrentLastFileCounter(): number { let lastCount = 0; if (this.files.length) { const counts = this.files.map(file => { - const match = file.name.match(/_(\d+)\.([^\.]+)$/); - if (match) { - return parseInt(match[1], 10); - } else { - return 0; - } + const match = file.name.match(/_(\d+)\.([^\.]+)$/); + if (match) { + return parseInt(match[1], 10); + } else { + return 0; + } - }).sort() + }).sort() .reverse(); lastCount = counts[0]; } @@ -146,17 +194,24 @@ export class FileUploaderComponent implements OnInit, OnDestroy { input.value = ""; } - uploadFile(file, count) { + uploadFile(file: File, count: number) { + // Clear validation state + this.apiValidationStatus = null; + this.validationMessage = null; + if (this.isFloorPlanUploader) { + this.isLoading = true; // Show the loading bar + } + const validExt = this.extensions.filter(ex => file.name.toLowerCase().endsWith(ex)).length > 0; if (!validExt) { - this.snackBar.open("File type not supported.", "Fail", { duration: 3500, panelClass: ["red-snackbar"] }); + this.isLoading = false; // Hide the loading bar if the file type is invalid + this.snackBar.open("File type not supported.", "Fail", { duration: 3000, panelClass: ["red-snackbar"] }); return; } - if (file && file.name && file.name.length > 128) { - this.snackBar.open("File name must be 128 characters or less.", - "Fail", - { duration: 3500, panelClass: ["red-snackbar"] }); + if (file.name.length > 128) { + this.isLoading = false; // Hide the loading bar if the file name is invalid + this.snackBar.open("File name must be 128 characters or less.", "Fail", { duration: 3000, panelClass: ["red-snackbar"] }); return; } @@ -164,60 +219,68 @@ export class FileUploaderComponent implements OnInit, OnDestroy { let fileName = file.name; const extension = file.name.match(/\.([^\.])+$/)[0]; if (this.useDocumentTypeForName) { - fileName = (count) + extension; + fileName = `${count}${extension}`; } formData.append("file", file, fileName); formData.append("documentType", this.documentType); + + // Validate occupant load stamp if it's a floor plan + if (this.isFloorPlanUploader && this.aiScanServiceUrl && this.aiScanServiceUrl !== "") { + this.validateOccupantLoadStamp(file); + } + const headers = new HttpHeaders(); this.fileReqOngoing = true; - const sub = this.http.post(this.attachmentURL, formData, { headers: headers }).subscribe(result => { + + this.http.post(this.attachmentURL, formData, { headers }).subscribe({ + next: () => { this.getUploadedFileData(); }, - err => { - this.snackBar.open("Failed to upload file", "Fail", { duration: 3500, panelClass: ["red-snackbar"] }); + error: () => { + this.snackBar.open("Failed to upload file.", "Fail", { duration: 3000, panelClass: ["red-snackbar"] }); this.fileReqOngoing = false; - }); - // this.busy = sub; + }, + }); } getUploadedFileData() { this.fileReqOngoing = true; const headers = new HttpHeaders({ // 'Content-Type': 'application/json' - + }); const getFileURL = this.attachmentURL + "/" + this.documentType; const sub = this.http.get(getFileURL, { headers: headers }) .subscribe((data) => { - data.forEach(file => { - if (this.useDocumentTypeForName) { - file.name = this.documentType + "_" + file.name; - } - }); - // sort by filename - data = data.sort((fileA, fileB) => { - if (fileA.name > fileB.name) { - return 1; - } else { - return -1; - } - }); - this.subscriptionList.push(sub); - // this.busy = sub; + data.forEach(file => { + if (this.useDocumentTypeForName) { + file.name = this.documentType + "_" + file.name; + } + }); + // sort by filename + data = data.sort((fileA, fileB) => { + if (fileA.name > fileB.name) { + return 1; + } else { + return -1; + } + }); + this.subscriptionList.push(sub); + // this.busy = sub; - // convert bytes to KB - data.forEach((entry) => { - entry.size = Math.ceil(entry.size / 1024); - entry.downloadUrl = - `api/file/${this.entityId}/${this.actionPrefix}download-file/${this.entityName}/${entry.name}`; - entry.downloadUrl += `?serverRelativeUrl=${encodeURIComponent(entry.serverrelativeurl)}&documentType=${this - .documentType}`; - }); - this.files = data; - this.numberOfUploadedFiles.emit(this.files.length); - this.dataLoaded = true; - this.fileReqOngoing = false; - }, + // convert bytes to KB + data.forEach((entry) => { + entry.size = Math.ceil(entry.size / 1024); + entry.downloadUrl = + `api/file/${this.entityId}/${this.actionPrefix}download-file/${this.entityName}/${entry.name}`; + entry.downloadUrl += `?serverRelativeUrl=${encodeURIComponent(entry.serverrelativeurl)}&documentType=${this + .documentType}`; + }); + this.files = data; + this.numberOfUploadedFiles.emit(this.files.length); + this.dataLoaded = true; + this.fileReqOngoing = false; + }, err => { this.snackBar.open("Failed to get files", "Fail", { duration: 3500, panelClass: ["red-snackbar"] }); this.fileReqOngoing = false; @@ -226,14 +289,17 @@ export class FileUploaderComponent implements OnInit, OnDestroy { } deleteFile(relativeUrl: string) { + // Clear validation state + this.apiValidationStatus = null; + this.validationMessage = null; this.fileReqOngoing = true; const headers = new HttpHeaders({ 'Content-Type': "application/json" }); const queryParams = `?serverRelativeUrl=${encodeURIComponent(relativeUrl)}&documentType=${this.documentType}`; const sub = this.http.delete(this.attachmentURL + queryParams, { headers: headers }).subscribe(result => { - this.getUploadedFileData(); - }, + this.getUploadedFileData(); + }, err => { this.snackBar.open("Failed to delete file", "Fail", { duration: 3500, panelClass: ["red-snackbar"] }); this.fileReqOngoing = false;