diff --git a/backend/src/components/supportingDocumentUpload.js b/backend/src/components/supportingDocumentUpload.js
index 1ea7b8f0..e441ca9a 100644
--- a/backend/src/components/supportingDocumentUpload.js
+++ b/backend/src/components/supportingDocumentUpload.js
@@ -1,8 +1,8 @@
'use strict';
-const {postApplicationDocument, getApplicationDocument, deleteDocument, patchOperationWithObjectId} = require('./utils');
+const { postApplicationDocument, getApplicationDocument, patchOperationWithObjectId } = require('./utils');
const HttpStatus = require('http-status-codes');
const log = require('./logger');
-const {getFileExtension, convertHeicDocumentToJpg} = require('../util/uploadFileUtils');
+const { getFileExtension, convertHeicDocumentToJpg } = require('../util/uploadFileUtils');
async function saveDocument(req, res) {
try {
@@ -11,7 +11,7 @@ async function saveDocument(req, res) {
let documentClone = document;
let changeRequestNewFacilityId = documentClone.changeRequestNewFacilityId;
delete documentClone.changeRequestNewFacilityId;
- if (getFileExtension(documentClone.filename) === 'heic' ) {
+ if (getFileExtension(documentClone.filename) === 'heic') {
log.verbose(`saveDocument :: heic detected for file name ${documentClone.filename} starting conversion`);
documentClone = await convertHeicDocumentToJpg(documentClone);
}
@@ -19,12 +19,13 @@ async function saveDocument(req, res) {
//if this is a new facility change request, link supporting documents to the New Facility Change Action
if (changeRequestNewFacilityId) {
await patchOperationWithObjectId('ccof_change_request_new_facilities', changeRequestNewFacilityId, {
- 'ccof_Attachments@odata.bind': `/ccof_application_facility_documents(${response?.applicationFacilityDocumentId})`
+ 'ccof_Attachments@odata.bind': `/ccof_application_facility_documents(${response?.applicationFacilityDocumentId})`,
});
}
}
return res.sendStatus(HttpStatus.OK);
} catch (e) {
+ log.error(e);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status);
}
}
@@ -39,7 +40,6 @@ function mapDocument(fileInfo) {
document.ccof_application_facility_documentId = fileInfo['ApplicationFacilityDocument.ccof_application_facility_documentid'];
document.description = fileInfo.notetext;
return document;
-
}
async function getUploadedDocuments(req, res) {
@@ -49,61 +49,23 @@ async function getUploadedDocuments(req, res) {
let documentFiles = [];
if (response?.value?.length > 0) {
for (let fileInfo of response?.value) {
- if(getAllFiles){
+ if (getAllFiles) {
documentFiles.push(mapDocument(fileInfo));
- }
- else{
- if(fileInfo.subject !== 'Facility License') {
+ } else {
+ if (fileInfo.subject !== 'Facility License') {
documentFiles.push(mapDocument(fileInfo));
}
}
-
}
}
return res.status(HttpStatus.OK).json(documentFiles);
} catch (e) {
+ log.error(e);
return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status);
}
}
-async function getAllUploadedDocuments(req, res) {
- try {
- let response = await getApplicationDocument(req.params.applicationId);
- let documentFiles = [];
- if (response?.value?.length > 0) {
- for (let fileInfo of response?.value) {
- const document = {};
- document.filename = fileInfo.filename;
- document.annotationid = fileInfo.annotationid;
- document.documentType = fileInfo.subject;
- document.ccof_facility = fileInfo['ApplicationFacilityDocument.ccof_facility'];
- document.ccof_facility_name = fileInfo['ApplicationFacilityDocument.ccof_facility@OData.Community.Display.V1.FormattedValue'];
- document.ccof_application_facility_documentId = fileInfo['ApplicationFacilityDocument.ccof_application_facility_documentid'];
- document.description=fileInfo.notetext;
- documentFiles.push(document);
- }
- }
- return res.status(HttpStatus.OK).json(documentFiles);
- } catch (e) {
- return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status);
- }
-}
-
-async function deleteUploadedDocuments(req, res) {
- try {
- let deletedDocuments = req.body;
- for (let annotationid of deletedDocuments) {
- await deleteDocument(annotationid);
- }
- return res.sendStatus(HttpStatus.OK);
- } catch (e) {
- return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status);
- }
-
-}
-
module.exports = {
saveDocument,
getUploadedDocuments,
- deleteUploadedDocuments
};
diff --git a/backend/src/routes/supportingDocuments.js b/backend/src/routes/supportingDocuments.js
index 0f2abdc2..d4c4b787 100644
--- a/backend/src/routes/supportingDocuments.js
+++ b/backend/src/routes/supportingDocuments.js
@@ -3,13 +3,10 @@ const passport = require('passport');
const router = express.Router();
const auth = require('../components/auth');
const isValidBackendToken = auth.isValidBackendToken();
-const {saveDocument, getUploadedDocuments, deleteUploadedDocuments} = require('../components/supportingDocumentUpload');
+const { saveDocument, getUploadedDocuments } = require('../components/supportingDocumentUpload');
module.exports = router;
-router.post('', passport.authenticate('jwt', {session: false}), isValidBackendToken, saveDocument);
-
-router.get('/:applicationId', passport.authenticate('jwt', {session: false}), isValidBackendToken, getUploadedDocuments);
-
-router.delete('', passport.authenticate('jwt', {session: false}), isValidBackendToken, deleteUploadedDocuments);
+router.post('', passport.authenticate('jwt', { session: false }), isValidBackendToken, saveDocument);
+router.get('/:applicationId', passport.authenticate('jwt', { session: false }), isValidBackendToken, getUploadedDocuments);
diff --git a/backend/src/util/mapping/Mappings.js b/backend/src/util/mapping/Mappings.js
index a73b4f14..25e7a41f 100644
--- a/backend/src/util/mapping/Mappings.js
+++ b/backend/src/util/mapping/Mappings.js
@@ -485,7 +485,11 @@ const DocumentsMappings = [
{ back: 'subject', front: 'documentType' },
];
-const ApplicationDocumentsMappings = [...DocumentsMappings, { back: 'ApplicationFacilityDocument.ccof_facility', front: 'facilityId' }];
+const ApplicationDocumentsMappings = [
+ ...DocumentsMappings,
+ { back: 'ApplicationFacilityDocument.ccof_facility', front: 'facilityId' },
+ { back: 'ApplicationFacilityDocument.ccof_facility@OData.Community.Display.V1.FormattedValue', front: 'facilityName' },
+];
module.exports = {
ApplicationDocumentsMappings,
diff --git a/frontend/src/components/RFI/RFIDocumentUpload.vue b/frontend/src/components/RFI/RFIDocumentUpload.vue
index 37a59313..2c7e23e0 100644
--- a/frontend/src/components/RFI/RFIDocumentUpload.vue
+++ b/frontend/src/components/RFI/RFIDocumentUpload.vue
@@ -9,8 +9,7 @@
Upload supporting documents (for example, receipts, quotes, invoices, and/or budget/finance documents)
- The maximum file size is 2MB for each document. Accepted file types are jpg, jpeg, heic, png, pdf, docx, doc,
- xls, and xlsx.
+ {{ FILE_REQUIREMENTS_TEXT }}
@@ -27,12 +26,16 @@
>
-
-
- mdi-plus
- Add
-
-
+
+ Add File
+
@@ -42,19 +45,18 @@
@@ -65,10 +67,10 @@
v-else
v-model="item.description"
placeholder="Enter a description (Optional)"
- density="compact"
clearable
:rules="[rules.maxLength(255)]"
max-length="255"
+ class="mt-4"
@change="descriptionChanged(item)"
/>
@@ -82,18 +84,21 @@
@@ -178,8 +147,12 @@ export default {
background-color: #f2f2f2;
}
->>> ::placeholder {
- color: #ff5252 !important;
- opacity: 1;
+:deep(::placeholder) {
+ color: red !important;
+ opacity: 1 !important;
+}
+
+:deep(.v-field__input) {
+ padding-left: 0px;
}
diff --git a/frontend/src/components/util/AppDocumentUpload.vue b/frontend/src/components/util/AppDocumentUpload.vue
index 5d6aac87..66f77bdb 100644
--- a/frontend/src/components/util/AppDocumentUpload.vue
+++ b/frontend/src/components/util/AppDocumentUpload.vue
@@ -6,8 +6,7 @@
(Required)
- The maximum file size is 2MB for each document. Accepted file types are jpg, jpeg, heic, png, pdf, docx, doc,
- xls, and xlsx.
+ {{ FILE_REQUIREMENTS_TEXT }}
@@ -28,8 +27,8 @@
v-model="item.file"
label="Select a file"
prepend-icon="mdi-file-upload"
- :rules="[...fileRules]"
- :accept="fileExtensionAccept"
+ :rules="rules.fileRules"
+ :accept="FILE_TYPES_ACCEPT"
:disabled="loading"
@update:model-value="validateFile(item.id)"
/>
@@ -76,8 +75,9 @@ import { uuid } from 'vue-uuid';
import AppButton from '@/components/guiComponents/AppButton.vue';
import alertMixin from '@/mixins/alertMixin.js';
-import { DOCUMENT_TYPES } from '@/utils/constants';
-import { humanFileSize, getFileExtensionWithDot, getFileNameWithMaxNameLength } from '@/utils/file';
+import rules from '@/utils/rules.js';
+import { DOCUMENT_TYPES, FILE_REQUIREMENTS_TEXT, FILE_TYPES_ACCEPT } from '@/utils/constants';
+import { isValidFile, readFile } from '@/utils/file';
export default {
components: { AppButton },
@@ -127,7 +127,11 @@ export default {
this.documents
?.filter((document) => document.isValidFile && document.file)
?.map(async (item) => {
- const convertedFile = await this.readFile(item.file);
+ const convertedFile = await readFile(item.file);
+ // XXX (vietle-cgi) - We need this part because the field names differ between the legacy code and AFS, and I want to avoid making too many changes to the legacy code.
+ convertedFile.fileName = convertedFile.filename;
+ convertedFile.fileSize = convertedFile.filesize;
+ convertedFile.documentBody = convertedFile.documentbody;
return { ...item, ...convertedFile };
}),
);
@@ -144,27 +148,9 @@ export default {
},
},
created() {
- this.MAX_FILE_SIZE = 2100000; // 2.18 MB is max size since after base64 encoding it might grow upto 3 MB.
- this.fileExtensionAccept = ['.pdf', '.png', '.jpg', '.jpeg', '.heic', '.doc', '.docx', '.xls', '.xlsx'];
- this.fileFormats = 'PDF, JPEG, JPG, PNG, HEIC, DOC, DOCX, XLS, and XLSX';
- this.fileRules = [
- (value) => {
- return (
- !value ||
- !value.length ||
- value[0].size < this.MAX_FILE_SIZE ||
- `The maximum file size is ${humanFileSize(this.MAX_FILE_SIZE)} for each document.`
- );
- },
- (value) => {
- return (
- !value ||
- !value.length ||
- this.fileExtensionAccept.includes(getFileExtensionWithDot(value[0].name)?.toLowerCase()) ||
- `Accepted file types are ${this.fileFormats}.`
- );
- },
- ];
+ this.FILE_REQUIREMENTS_TEXT = FILE_REQUIREMENTS_TEXT;
+ this.FILE_TYPES_ACCEPT = FILE_TYPES_ACCEPT;
+ this.rules = rules;
this.headersUploadedDocuments = [
{ title: 'File Name', key: 'fileName', width: '34%' },
{ title: 'Description', key: 'description', width: '60%' },
@@ -198,48 +184,13 @@ export default {
validateFile(updatedItemId) {
const document = this.documents.find((item) => item.id === updatedItemId);
- const file = document?.file;
- if (file) {
- const isLessThanMaxSize = file.size < this.MAX_FILE_SIZE;
- const isFileExtensionAccepted = this.fileExtensionAccept.includes(
- getFileExtensionWithDot(file.name)?.toLowerCase(),
- );
- document.isValidFile = isLessThanMaxSize && isFileExtensionAccepted;
- } else {
- document.isValidFile = true;
- }
+ document.isValidFile = isValidFile(document?.file);
},
resetDocuments() {
this.documents = [];
},
- readFile(file) {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.readAsArrayBuffer(file);
- reader.onload = () => {
- const arrayBuffer = reader.result;
- const binaryString = new Uint8Array(arrayBuffer).reduce((acc, byte) => acc + String.fromCharCode(byte), '');
- const base64String = window.btoa(binaryString); // Convert to Base64
- const doc = {
- fileName: getFileNameWithMaxNameLength(file.name),
- fileSize: file.size,
- documentBody: base64String,
- };
- resolve(doc);
- };
- reader.onabort = () => {
- this.setErrorAlert('Sorry, an unexpected error seems to have occurred. Try uploading your files later.');
- reject();
- };
- reader.onerror = () => {
- this.setErrorAlert('Sorry, an unexpected error seems to have occurred. Try uploading your files later.');
- reject();
- };
- });
- },
-
isDeletable(document) {
return !this.loading && !this.readonly && document?.documentType !== DOCUMENT_TYPES.APPLICATION_AFS_SUBMITTED;
},
diff --git a/frontend/src/store/application.js b/frontend/src/store/application.js
index 880ec135..05cae96f 100644
--- a/frontend/src/store/application.js
+++ b/frontend/src/store/application.js
@@ -31,6 +31,7 @@ export const useApplicationStore = defineStore('application', {
applicationMap: new Map(),
applicationUploadedDocuments: [],
+ isApplicationDocumentsLoading: false,
}),
actions: {
setApplicationId(value) {
@@ -148,10 +149,13 @@ export const useApplicationStore = defineStore('application', {
},
async getApplicationUploadedDocuments() {
try {
+ this.isApplicationDocumentsLoading = true;
this.applicationUploadedDocuments = await DocumentService.getApplicationUploadedDocuments(this.applicationId);
} catch (error) {
console.log(error);
throw error;
+ } finally {
+ this.isApplicationDocumentsLoading = false;
}
},
},
diff --git a/frontend/src/store/summaryDeclaration.js b/frontend/src/store/summaryDeclaration.js
index 19035b07..629c0759 100644
--- a/frontend/src/store/summaryDeclaration.js
+++ b/frontend/src/store/summaryDeclaration.js
@@ -219,20 +219,6 @@ export const useSummaryDeclarationStore = defineStore('summaryDeclaration', {
this.setSummaryModel(summaryModel);
}
- //new app only (i think this if block could be part of the one above?)
- if (payload.application?.organizationId) {
- const config = {
- params: {
- allFiles: true,
- },
- };
- summaryModel['allDocuments'] = (
- await ApiService.apiAxios.get(
- ApiRoutes.SUPPORTING_DOCUMENT_UPLOAD + '/' + payload.application.applicationId,
- config,
- )
- ).data;
- }
for (const facility of summaryModel.facilities) {
const index = summaryModel.facilities.indexOf(facility);
@@ -288,19 +274,10 @@ export const useSummaryDeclarationStore = defineStore('summaryDeclaration', {
).data;
this.setSummaryModel(summaryModel);
- if (summaryModel.allDocuments && summaryModel.allDocuments.length > 0) {
- const allDocuments = summaryModel.allDocuments;
- summaryModel.facilities[index].documents = allDocuments.filter(
- (document) => document.ccof_facility === facility.facilityId,
- );
- this.setSummaryModel(summaryModel);
- }
-
isSummaryLoading.splice(index, 1, false);
this.setIsSummaryLoading(isSummaryLoading);
} // end FOR loop. FIXME: make loop brief enough to read in one view
- summaryModel.allDocuments = null;
if (!changeRecGuid) this.setIsLoadingComplete(true);
} catch (error) {
console.log(`Failed to load Summary - ${error}`);
diff --git a/frontend/src/store/supportingDocumentUpload.js b/frontend/src/store/supportingDocumentUpload.js
index 96f5dc18..5e9a0fe9 100644
--- a/frontend/src/store/supportingDocumentUpload.js
+++ b/frontend/src/store/supportingDocumentUpload.js
@@ -30,13 +30,5 @@ export const useSupportingDocumentUploadStore = defineStore('supportingDocumentU
throw error;
}
},
- async deleteDocuments(deletedFiles) {
- try {
- await ApiService.apiAxios.delete(ApiRoutes.SUPPORTING_DOCUMENT_UPLOAD, { data: deletedFiles });
- } catch (error) {
- console.error(error);
- throw error;
- }
- },
},
});
diff --git a/frontend/src/utils/common.js b/frontend/src/utils/common.js
index 57b155e3..45cbe55d 100644
--- a/frontend/src/utils/common.js
+++ b/frontend/src/utils/common.js
@@ -32,10 +32,6 @@ export function deepCloneObject(objectToBeCloned) {
return clone(objectToBeCloned);
}
-export function getFileExtension(fileName) {
- if (fileName) return fileName.slice(fileName.lastIndexOf('.') + 1);
- return '';
-}
export function isNullOrBlank(value) {
return value === null || value === undefined || value === '';
}
diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js
index 7c96f722..a0ee77bb 100644
--- a/frontend/src/utils/constants.js
+++ b/frontend/src/utils/constants.js
@@ -331,6 +331,42 @@ export const PARENT_FEE_FREQUENCIES = Object.freeze({
export const DOCUMENT_TYPES = Object.freeze({
APPLICATION_AFS: 'AFS - Supporting Documents',
APPLICATION_AFS_SUBMITTED: 'AFS - Supporting Documents - Submitted',
+ APPLICATION_LICENCE: 'Facility License',
+ APPLICATION_SUPPORTING: 'SUPPORTING',
CR_NOTIFICATION_FORM: 'NOTIFICATION_FORM',
CR_NOTIFICATION_FORM_SUPPORTING: 'SUPPORTING_DOC',
});
+
+export const MAX_FILE_SIZE = 2100000; // 2.18 MB is max size since after base64 encoding it might grow upto 3 MB.
+
+export const FILE_REQUIREMENTS_TEXT =
+ 'The maximum file size is 2MB for each document. Accepted file types are jpg, jpeg, heic, png, pdf, docx, doc, xls, and xlsx.';
+
+export const FILE_EXTENSIONS_ACCEPT_TEXT = 'PDF, JPEG, JPG, PNG, HEIC, DOC, DOCX, XLS and XLSX';
+
+export const FILE_EXTENSIONS_ACCEPT = Object.freeze([
+ 'pdf',
+ 'png',
+ 'jpg',
+ 'jpeg',
+ 'heic',
+ 'doc',
+ 'docx',
+ 'xls',
+ 'xlsx',
+]);
+
+export const FILE_TYPES_ACCEPT = Object.freeze([
+ 'image/png',
+ 'image/jpeg',
+ 'image/jpg',
+ '.pdf',
+ '.png',
+ '.jpg',
+ '.jpeg',
+ '.heic',
+ '.doc',
+ '.docx',
+ '.xls',
+ '.xlsx',
+]);
diff --git a/frontend/src/utils/file.js b/frontend/src/utils/file.js
index a8f8ffb1..192237c0 100644
--- a/frontend/src/utils/file.js
+++ b/frontend/src/utils/file.js
@@ -1,3 +1,5 @@
+import { FILE_EXTENSIONS_ACCEPT, MAX_FILE_SIZE } from '@/utils/constants.js';
+
/**
* Converting bytes to human readable values (KB, MB, GB, TB, PB, EB, ZB, YB)
* @param {*} bytes
@@ -13,18 +15,9 @@ export function humanFileSize(bytes, decimals = 2) {
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
-/**
- * Get extension from file name
- * https://stackoverflow.com/a/12900504
- * "" --> ""
- * "name" --> ""
- * "name.txt" --> "txt"
- * ".htpasswd" --> ""
- * "name.with.many.dots.myext" --> "myext"
- * @param {*} fileName
- */
export function getFileExtension(fileName) {
- return fileName.slice(((fileName.lastIndexOf('.') - 1) >>> 0) + 2);
+ if (fileName) return fileName.slice(fileName.lastIndexOf('.') + 1);
+ return '';
}
export function getFileExtensionWithDot(fileName) {
@@ -39,3 +32,33 @@ export function getFileNameWithMaxNameLength(fileName, nameLength = 30, extensio
return name + extension;
}
+
+export function readFile(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsArrayBuffer(file);
+ reader.onload = () => {
+ const arrayBuffer = reader.result;
+ const binaryString = new Uint8Array(arrayBuffer).reduce((acc, byte) => acc + String.fromCharCode(byte), '');
+ const base64String = window.btoa(binaryString); // Convert to Base64
+ const doc = {
+ filename: getFileNameWithMaxNameLength(file.name),
+ filesize: file.size,
+ documentbody: base64String,
+ };
+ resolve(doc);
+ };
+ reader.onabort = () => {
+ reject(new Error(`Error reading file: ${reader.error?.message || 'Unknown error'}`));
+ };
+ reader.onerror = () => {
+ reject(new Error(`Error reading file: ${reader.error?.message || 'Unknown error'}`));
+ };
+ });
+}
+
+export function isValidFile(file) {
+ const isLessThanMaxSize = file?.size < MAX_FILE_SIZE;
+ const isFileExtensionAccepted = FILE_EXTENSIONS_ACCEPT.includes(getFileExtension(file?.name)?.toLowerCase());
+ return isLessThanMaxSize && isFileExtensionAccepted;
+}
diff --git a/frontend/src/utils/rules.js b/frontend/src/utils/rules.js
index 04bc3e31..1c6d4c6b 100644
--- a/frontend/src/utils/rules.js
+++ b/frontend/src/utils/rules.js
@@ -1,3 +1,6 @@
+import { FILE_EXTENSIONS_ACCEPT, FILE_EXTENSIONS_ACCEPT_TEXT, MAX_FILE_SIZE } from '@/utils/constants';
+import { getFileExtension, humanFileSize } from '@/utils/file';
+
const rules = {
email: [(v) => /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(v) || 'A valid email is required'],
required: [
@@ -34,6 +37,28 @@ const rules = {
},
wholeNumber: (v) => !v || /^\d+$/.test(v) || 'A valid whole number is required',
phone: (v) => /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(v) || 'A valid phone number is required', // https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch04s02.html
+ fileRules: [
+ (v) => !!v || 'This is required',
+ (value) => {
+ return !value || !value.length || value[0]?.name?.length < 255 || 'File name can be max 255 characters.';
+ },
+ (value) => {
+ return (
+ !value ||
+ !value.length ||
+ value[0].size < MAX_FILE_SIZE ||
+ `The maximum file size is ${humanFileSize(MAX_FILE_SIZE)} for each document.`
+ );
+ },
+ (value) => {
+ return (
+ !value ||
+ !value.length ||
+ FILE_EXTENSIONS_ACCEPT.includes(getFileExtension(value[0].name)?.toLowerCase()) ||
+ `Accepted file types are ${FILE_EXTENSIONS_ACCEPT_TEXT}.`
+ );
+ },
+ ],
};
export default rules;