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 @@
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 @@
-
+
+
+
+ ⚠ {{ 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;