Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LCSD-7482: POC for AI integration with LCRB #4325

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1838,13 +1838,20 @@ <h3 class="blue-header" *ngIf="this.isLP()">ESTABLISHMENT TYPE</h3>
The below floor plan is on file.
</p>
<section>
<app-file-uploader *ngIf="application?.id" documentType="Floor Plan"
fileTypes="FILE MUST BE IN PDF FORMAT."
[disableUploads]="isOpenedByLGForApproval || lGHasApproved()"
[enableFileDeletion]="!isOpenedByLGForApproval && !lGHasApproved()" [multipleFiles]="true"
entityName="application" [useDocumentTypeForName]="true"
(numberOfUploadedFiles)="uploadedFloorPlanDocuments = $event" [entityId]="application?.id"
[uploadHeader]="'TO UPLOAD FLOOR PLANS, DRAG FILES HERE OR'">
<app-file-uploader
*ngIf="application?.id"
documentType="Floor Plan"
fileTypes="FILE MUST BE IN PDF FORMAT."
[disableUploads]="isOpenedByLGForApproval || lGHasApproved()"
[enableFileDeletion]="!isOpenedByLGForApproval && !lGHasApproved()"
[multipleFiles]="true"
entityName="application"
[useDocumentTypeForName]="true"
[isFloorPlanUploader]="true"
(numberOfUploadedFiles)="uploadedFloorPlanDocuments = $event"
(occupantLoadUpdated)="onOccupantLoadUpdated($event)"
[entityId]="application?.id"
[uploadHeader]="'TO UPLOAD FLOOR PLANS, DRAG FILES HERE OR'">
</app-file-uploader>
</section>
<br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</div>
</ng-template>
</ngx-file-drop>
<mat-progress-bar class="mt-1 mb-1" *ngIf="!dataLoaded || fileReqOngoing" mode="indeterminate"></mat-progress-bar>
<mat-progress-bar class="mt-1 mb-1" *ngIf="!dataLoaded || fileReqOngoing || isLoading" mode="indeterminate"></mat-progress-bar>
<div style="color: #2C679E; margin-top: 5px;">
<section *ngFor="let item of files; let i=index" class="file-list">
<a name="{{item.name}}" href="{{item.downloadUrl}}">{{ item.name }}</a>
Expand All @@ -32,4 +32,14 @@
</span>
</section>
</div>
<!-- Validation Messages -->
<div *ngIf="apiValidationStatus === 'invalid'" class="validation-message invalid">
<span>&#9888; {{ validationMessage }}</span>
</div>
<div *ngIf="apiValidationStatus === 'expired'" class="validation-message expired">
<span>&#9888; {{ validationMessage }}</span>
</div>
<div *ngIf="apiValidationStatus === 'valid'" class="validation-message valid">
<span>&#10004; Your floor plan contains a valid occupant load stamp.</span>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,27 @@

.red-snackbar {
background: #F44336;
}
}

.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;
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -43,16 +42,24 @@ export class FileUploaderComponent implements OnInit, OnDestroy {
useDocumentTypeForName = false;
@Input()
publicAccess = false;
@Input()
isFloorPlanUploader = false;
@Output()
occupantLoadUpdated = new EventEmitter<number>();
@Output()
numberOfUploadedFiles = new EventEmitter<number>();
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(
Expand All @@ -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) {
Expand Down Expand Up @@ -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];
}
Expand Down Expand Up @@ -146,78 +194,93 @@ 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;
}

const formData = new FormData();
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<FileSystemItem[]>(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;
Expand All @@ -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;
Expand Down