diff --git a/backend/src/app.js b/backend/src/app.js index b8176583b..341a671bb 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -27,6 +27,7 @@ const organizationRouter = require('./routes/organization'); const publicRouter = require('./routes/public'); const configRouter = require('./routes/config'); const applicationRouter = require('./routes/application'); +const documentRouter = require('./routes/document'); const fundingRouter = require('./routes/funding'); const messageRouter = require('./routes/message'); const licenseUploadRouter = require('./routes/licenseUpload'); @@ -44,42 +45,48 @@ app.set('trust proxy', 1); app.use(cors()); app.use(helmet()); app.use(noCache()); -app.use(promMid({ - metricsPath: '/api/prometheus', - collectDefaultMetrics: true, - requestDurationBuckets: [0.1, 0.5, 1, 1.5] -})); +app.use( + promMid({ + metricsPath: '/api/prometheus', + collectDefaultMetrics: true, + requestDurationBuckets: [0.1, 0.5, 1, 1.5], + }), +); //tells the app to use json as means of transporting data app.use(bodyParser.json({ limit: '50mb', extended: true })); -app.use(bodyParser.urlencoded({ - extended: true, - limit: '50mb' -})); +app.use( + bodyParser.urlencoded({ + extended: true, + limit: '50mb', + }), +); const logStream = { write: (message) => { log.info(message); - } + }, }; const dbSession = getRedisDbSession(); const cookie = { secure: true, httpOnly: true, - maxAge: 1800000 //30 minutes in ms. this is same as session time. DO NOT MODIFY, IF MODIFIED, MAKE SURE SAME AS SESSION TIME OUT VALUE. + maxAge: 1800000, //30 minutes in ms. this is same as session time. DO NOT MODIFY, IF MODIFIED, MAKE SURE SAME AS SESSION TIME OUT VALUE. }; if ('local' === config.get('environment')) { cookie.secure = false; } //sets cookies for security purposes (prevent cookie access, allow secure connections only, etc) -app.use(session({ - name: 'ccof_cookie', - secret: config.get('oidc:clientSecret'), - resave: false, - saveUninitialized: true, - cookie: cookie, - store: dbSession -})); +app.use( + session({ + name: 'ccof_cookie', + secret: config.get('oidc:clientSecret'), + resave: false, + saveUninitialized: true, + cookie: cookie, + store: dbSession, + }), +); app.use(require('./routes/health-check').router); //initialize routing and session. Cookies are now only reachable via requests (not js) @@ -101,29 +108,34 @@ function getRedisDbSession() { } function addLoginPassportUse(discovery, strategyName, callbackURI, kc_idp_hint, clientId, clientSecret) { - passport.use(strategyName, new OidcStrategy({ - issuer: discovery.issuer, - authorizationURL: discovery.authorization_endpoint, - tokenURL: discovery.token_endpoint, - userInfoURL: discovery.userinfo_endpoint, - clientID: config.get(clientId), - clientSecret: config.get(clientSecret), - callbackURL: callbackURI, - scope: 'openid', - kc_idp_hint: kc_idp_hint - }, (_issuer, profile, _context, _idToken, accessToken, refreshToken, done) => { - if ((typeof (accessToken) === 'undefined') || (accessToken === null) || - (typeof (refreshToken) === 'undefined') || (refreshToken === null)) { - return done('No access token', null); - } - - //set access and refresh tokens - profile.jwtFrontend = auth.generateUiToken(); - profile.jwt = accessToken; - profile._json = parseJwt(accessToken); - profile.refreshToken = refreshToken; - return done(null, profile); - })); + passport.use( + strategyName, + new OidcStrategy( + { + issuer: discovery.issuer, + authorizationURL: discovery.authorization_endpoint, + tokenURL: discovery.token_endpoint, + userInfoURL: discovery.userinfo_endpoint, + clientID: config.get(clientId), + clientSecret: config.get(clientSecret), + callbackURL: callbackURI, + scope: 'openid', + kc_idp_hint: kc_idp_hint, + }, + (_issuer, profile, _context, _idToken, accessToken, refreshToken, done) => { + if (typeof accessToken === 'undefined' || accessToken === null || typeof refreshToken === 'undefined' || refreshToken === null) { + return done('No access token', null); + } + + //set access and refresh tokens + profile.jwtFrontend = auth.generateUiToken(); + profile.jwt = accessToken; + profile._json = parseJwt(accessToken); + profile.refreshToken = refreshToken; + return done(null, profile); + }, + ), + ); } const parseJwt = (token) => { @@ -134,44 +146,49 @@ const parseJwt = (token) => { } }; //initialize our authentication strategy -utils.getOidcDiscovery().then(discovery => { +utils.getOidcDiscovery().then((discovery) => { //OIDC Strategy is used for authorization addLoginPassportUse(discovery, 'oidcIdir', config.get('server:frontend') + '/api/auth/callback_idir', 'keycloak_bcdevexchange_idir', 'oidc:clientIdIDIR', 'oidc:clientSecretIDIR'); addLoginPassportUse(discovery, 'oidcBceid', config.get('server:frontend') + '/api/auth/callback', 'keycloak_bcdevexchange_bceid', 'oidc:clientId', 'oidc:clientSecret'); //JWT strategy is used for authorization keycloak_bcdevexchange_idir - passport.use('jwt', new JWTStrategy({ - algorithms: ['RS256'], - // Keycloak 7.3.0 no longer automatically supplies matching client_id audience. - // If audience checking is needed, check the following SO to update Keycloak first. - // Ref: https://stackoverflow.com/a/53627747 - audience: config.get('server:frontend'), - issuer: config.get('tokenGenerate:issuer'), - jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), - secretOrKey: config.get('tokenGenerate:publicKey'), - ignoreExpiration: true - }, (jwtPayload, done) => { - if ((typeof (jwtPayload) === 'undefined') || (jwtPayload === null)) { - return done('No JWT token', null); - } - - done(null, { - email: jwtPayload.email, - familyName: jwtPayload.family_name, - givenName: jwtPayload.given_name, - jwt: jwtPayload, - name: jwtPayload.name, - user_guid: jwtPayload.user_guid, - realmRole: jwtPayload.realm_role - }); - })); + passport.use( + 'jwt', + new JWTStrategy( + { + algorithms: ['RS256'], + // Keycloak 7.3.0 no longer automatically supplies matching client_id audience. + // If audience checking is needed, check the following SO to update Keycloak first. + // Ref: https://stackoverflow.com/a/53627747 + audience: config.get('server:frontend'), + issuer: config.get('tokenGenerate:issuer'), + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: config.get('tokenGenerate:publicKey'), + ignoreExpiration: true, + }, + (jwtPayload, done) => { + if (typeof jwtPayload === 'undefined' || jwtPayload === null) { + return done('No JWT token', null); + } + + done(null, { + email: jwtPayload.email, + familyName: jwtPayload.family_name, + givenName: jwtPayload.given_name, + jwt: jwtPayload, + name: jwtPayload.name, + user_guid: jwtPayload.user_guid, + realmRole: jwtPayload.realm_role, + }); + }, + ), + ); }); //functions for serializing/deserializing users passport.serializeUser((user, next) => next(null, user)); passport.deserializeUser((obj, next) => next(null, obj)); - -app.use(morgan(config.get('server:morganFormat'), { 'stream': logStream })); +app.use(morgan(config.get('server:morganFormat'), { stream: logStream })); //set up routing to auth and main API app.use(/(\/api)?/, apiRouter); @@ -180,8 +197,9 @@ apiRouter.use('/user', userRouter); apiRouter.use('/facility', facilityRouter); apiRouter.use('/organization', organizationRouter); apiRouter.use('/public', publicRouter); -apiRouter.use('/config',configRouter); +apiRouter.use('/config', configRouter); apiRouter.use('/application', applicationRouter); +apiRouter.use('/document', documentRouter); apiRouter.use('/group/funding', fundingRouter); apiRouter.use('/messages', messageRouter); apiRouter.use('/licenseUpload', licenseUploadRouter); @@ -194,7 +212,7 @@ app.use((err, _req, res, next) => { //This is from the ResultValidation if (err.errors && err.mapped) { return res.status(400).json({ - errors: err.mapped() + errors: err.mapped(), }); } log.error(err?.stack); diff --git a/backend/src/components/application.js b/backend/src/components/application.js index 9a3c6b853..7e53318bb 100644 --- a/backend/src/components/application.js +++ b/backend/src/components/application.js @@ -57,17 +57,14 @@ async function renewCCOFApplication(req, res) { } async function patchCCFRIApplication(req, res) { - let payload = req.body; - payload = new MappableObjectForBack(payload, CCFRIFacilityMappings); - payload = payload.toJSON(); - try { - await patchOperationWithObjectId('ccof_applicationccfris', req.params.ccfriId, payload); + const payload = new MappableObjectForBack(req.body, CCFRIFacilityMappings).toJSON(); + const response = await patchOperationWithObjectId('ccof_applicationccfris', req.params.ccfriId, payload); + return res.status(HttpStatus.OK).json(response); } catch (e) { log.error(e); return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status); } - return res.status(HttpStatus.OK).json(payload); } async function deleteCCFRIApplication(req, res) { diff --git a/backend/src/components/document.js b/backend/src/components/document.js new file mode 100644 index 000000000..5e3a37ecf --- /dev/null +++ b/backend/src/components/document.js @@ -0,0 +1,73 @@ +'use strict'; +const { MappableObjectForFront, MappableObjectForBack } = require('../util/mapping/MappableObject'); +const { ApplicationDocumentsMappings, DocumentsMappings } = require('../util/mapping/Mappings'); +const { postApplicationDocument, getApplicationDocument, deleteDocument, patchOperationWithObjectId } = require('./utils'); +const HttpStatus = require('http-status-codes'); +const log = require('./logger'); + +const { getFileExtension, convertHeicDocumentToJpg } = require('../util/uploadFileUtils'); + +async function createApplicationDocuments(req, res) { + try { + const documents = req.body; + for (let document of documents) { + let payload = new MappableObjectForBack(document, ApplicationDocumentsMappings).toJSON(); + payload.ccof_applicationid = document.ccof_applicationid; + payload.ccof_facility = document.ccof_facility; + if (getFileExtension(payload.filename) === 'heic') { + log.verbose(`createApplicationDocuments :: heic detected for file name ${payload?.filename} starting conversion`); + payload = await convertHeicDocumentToJpg(payload); + } + await postApplicationDocument(payload); + } + return res.sendStatus(HttpStatus.OK); + } catch (e) { + log.error(e); + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status); + } +} + +async function getApplicationDocuments(req, res) { + try { + const documents = []; + const response = await getApplicationDocument(req.params.applicationId); + response?.value?.forEach((document) => documents.push(new MappableObjectForFront(document, ApplicationDocumentsMappings).toJSON())); + return res.status(HttpStatus.OK).json(documents); + } catch (e) { + log.error(e); + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status); + } +} + +async function updateDocument(req, res) { + try { + const payload = new MappableObjectForBack(req.body, DocumentsMappings).toJSON(); + const response = await patchOperationWithObjectId('annotations', req.params.annotationId, payload); + return res.status(HttpStatus.OK).json(response); + } catch (e) { + log.error(e); + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status); + } +} + +async function deleteDocuments(req, res) { + try { + const deletedDocuments = req.body; + await Promise.all( + deletedDocuments.map(async (annotationId) => { + await deleteDocument(annotationId); + }), + ); + return res.sendStatus(HttpStatus.OK); + } catch (e) { + log.error(e); + return res.status(HttpStatus.INTERNAL_SERVER_ERROR).json(e.data ? e.data : e?.status); + } +} + +module.exports = { + createApplicationDocuments, + getApplicationDocuments, + updateDocument, + deleteDocuments, +}; diff --git a/backend/src/routes/application.js b/backend/src/routes/application.js index 0b9d3d599..a3c8edb1f 100644 --- a/backend/src/routes/application.js +++ b/backend/src/routes/application.js @@ -11,7 +11,6 @@ const { updateECEWEApplication, updateECEWEFacilityApplication, getApprovableFeeSchedules, - getCCFRIApplication, getDeclaration, submitApplication, updateStatusForApplicationComponents, @@ -26,10 +25,6 @@ router.post('/renew-ccof', passport.authenticate('jwt', { session: false }), isV /* CREATE or UPDATE an existing CCFRI application for opt-in and out CCOF application guid and facility guid are defined in the payload */ -router.get('/ccfri/:ccfriId', passport.authenticate('jwt', { session: false }), isValidBackendToken, [param('ccfriId', 'URL param: [ccfriId] is required').notEmpty().isUUID()], (req, res) => { - validationResult(req).throw(); - return getCCFRIApplication(req, res); -}); router.get('/ccfri/:ccfriId/afs', passport.authenticate('jwt', { session: false }), isValidBackendToken, [param('ccfriId', 'URL param: [ccfriId] is required').notEmpty().isUUID()], (req, res) => { validationResult(req).throw(); @@ -67,7 +62,7 @@ router.patch('/ccfri', passport.authenticate('jwt', { session: false }), isValid }); router.patch('/ccfri/:ccfriId/', passport.authenticate('jwt', { session: false }), isValidBackendToken, [param('ccfriId', 'URL param: [ccfriId] is required').notEmpty().isUUID()], (req, res) => { - //validationResult(req).throw(); + validationResult(req).throw(); return patchCCFRIApplication(req, res); }); diff --git a/backend/src/routes/document.js b/backend/src/routes/document.js new file mode 100644 index 000000000..05e5f3ac2 --- /dev/null +++ b/backend/src/routes/document.js @@ -0,0 +1,35 @@ +const express = require('express'); +const passport = require('passport'); +const router = express.Router(); +const auth = require('../components/auth'); +const { param, validationResult } = require('express-validator'); +const isValidBackendToken = auth.isValidBackendToken(); +const { createApplicationDocuments, getApplicationDocuments, deleteDocuments, updateDocument } = require('../components/document'); + +module.exports = router; + +router.post('/application/', passport.authenticate('jwt', { session: false }), isValidBackendToken, createApplicationDocuments); + +router.get( + '/application/:applicationId', + passport.authenticate('jwt', { session: false }), + isValidBackendToken, + [param('applicationId', 'URL param: [applicationId] is required').notEmpty().isUUID()], + (req, res) => { + validationResult(req).throw(); + return getApplicationDocuments(req, res); + }, +); + +router.delete('/', passport.authenticate('jwt', { session: false }), isValidBackendToken, deleteDocuments); + +router.patch( + '/:annotationId', + passport.authenticate('jwt', { session: false }), + isValidBackendToken, + [param('annotationId', 'URL param: [annotationId] is required').notEmpty().isUUID()], + (req, res) => { + validationResult(req).throw(); + return updateDocument(req, res); + }, +); diff --git a/backend/src/util/mapping/Mappings.js b/backend/src/util/mapping/Mappings.js index c73c74a37..3ab059660 100644 --- a/backend/src/util/mapping/Mappings.js +++ b/backend/src/util/mapping/Mappings.js @@ -57,9 +57,9 @@ const CCFRIFacilityMappings = [ { back: 'ccof_chargefeeccfri', front: 'hasClosureFees' }, { back: 'ccof_applicationccfriid', front: 'ccfriApplicationId' }, { back: 'ccof_unlock_rfi', front: 'unlockRfi' }, - - // XXXXXXXXXXXXX: 'licenseEffectiveDate', - // XXXXXXXXXXXXX: 'hasReceivedFunding', + { back: 'ccof_unlock_afs', front: 'unlockAfs' }, + { back: 'ccof_unlock_afsenable', front: 'enableAfs' }, + { back: 'ccof_afs_status', front: 'afsStatus' }, ]; const RFIApplicationMappings = [ @@ -319,6 +319,7 @@ const UserProfileCCFRIMappings = [ { back: 'ccof_unlock_nmf_rfi', front: 'unlockNmf' }, { back: 'ccof_unlock_afs', front: 'unlockAfs' }, { back: 'ccof_unlock_afsenable', front: 'enableAfs' }, + { back: 'ccof_afs_status', front: 'afsStatus' }, ...UserProfileBaseCCFRIMappings, ]; const UserProfileECEWEMappings = [ @@ -429,7 +430,6 @@ const ApplicationSummaryCcfriMappings = [ { back: 'ccof_has_rfi', front: 'hasRfi' }, // false, { back: 'ccof_unlock_rfi', front: 'unlockRfi' }, // null, { back: 'ccof_rfi_formcomplete', front: 'isRfiComplete' }, // false, - { back: 'ccof_afs_status', front: 'afsStatus' }, ]; const CCFRIApprovableFeeSchedulesMappings = [ @@ -473,7 +473,20 @@ const fundingAgreementMappings = [ { back: 'ccof_name', front: 'fundingAgreementNumber' }, // null, ]; +const DocumentsMappings = [ + { back: 'annotationid', front: 'annotationId' }, + { back: 'filesize', front: 'fileSize' }, + { back: 'filename', front: 'fileName' }, + { back: 'documentbody', front: 'documentBody' }, + { back: 'notetext', front: 'description' }, + { back: 'subject', front: 'documentType' }, +]; + +const ApplicationDocumentsMappings = [...DocumentsMappings, { back: 'ApplicationFacilityDocument.ccof_facility', front: 'facilityId' }]; + module.exports = { + ApplicationDocumentsMappings, + DocumentsMappings, OrganizationMappings, FacilityMappings, CCOFApplicationMappings, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 201d48c5c..88e4bcf8e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -30,6 +30,7 @@ "vue-pdf-app": "^2.1.0", "vue-quick-chat": "^1.2.8", "vue-router": "^4.4.3", + "vue-uuid": "^3.0.0", "vuetify": "^3.7.1", "vuex-map-fields": "^1.4.1" }, @@ -927,6 +928,11 @@ "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==" }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==" + }, "node_modules/@vitejs/plugin-vue": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.3.tgz", @@ -2978,6 +2984,14 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", @@ -3220,6 +3234,18 @@ "vue": "^3.2.0" } }, + "node_modules/vue-uuid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vue-uuid/-/vue-uuid-3.0.0.tgz", + "integrity": "sha512-+5DP857xVmTHYd00dMC1c1gVg/nxG6+K4Lepojv9ckHt8w0fDpGc5gQCCttS9D+AkSkTJgb0cekidKjTWu5OQQ==", + "dependencies": { + "@types/uuid": "^8.3.4", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "vue": ">= 3.0.0" + } + }, "node_modules/vuetify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0be001dba..3ee3a3ef2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,6 +44,7 @@ "vue-pdf-app": "^2.1.0", "vue-quick-chat": "^1.2.8", "vue-router": "^4.4.3", + "vue-uuid": "^3.0.0", "vuetify": "^3.7.1", "vuex-map-fields": "^1.4.1" }, diff --git a/frontend/public/styles/common.css b/frontend/public/styles/common.css index 4480de859..2823d5b26 100644 --- a/frontend/public/styles/common.css +++ b/frontend/public/styles/common.css @@ -108,3 +108,7 @@ input[type="number"] { .v-table .v-table__wrapper { font-size: 1rem; } + +.error-message { + color: #ff5252; +} diff --git a/frontend/src/components/SummaryDeclaration.vue b/frontend/src/components/SummaryDeclaration.vue index aa04afb85..316a49427 100644 --- a/frontend/src/components/SummaryDeclaration.vue +++ b/frontend/src/components/SummaryDeclaration.vue @@ -172,6 +172,14 @@ @is-summary-valid="isFormComplete" /> + + + v == 1)); // Update payload unlock properties from 1 to 0. Object.keys(unlockPayload).forEach((key) => { unlockPayload[key] = '0'; }); + + const afs = this.approvableFeeSchedules?.find( + (item) => item.ccfriApplicationId === facility.ccfriApplicationId, + ); + if (afs?.afsStatus === AFS_STATUSES.UPLOAD_DOCUMENTS) { + unlockPayload.enableAfs = '0'; + } + if (Object.keys(unlockPayload).length > 0) { ccrfiRelockPayload.push({ ...applicationIdPayload, ...unlockPayload }); } @@ -830,6 +855,7 @@ export default { 'ccfri-summary', 'rfi-summary', 'nmf-summary', + 'afs-summary', 'ecewe-summary-a', 'ecewe-summary-b', 'uploaded-documents-summary', @@ -947,6 +973,22 @@ export default { } this.forceNavBarRefresh(); }, + + // CCFRI-3808 - This function ensures that submitted AFS documents from previous submissions cannot be deleted from the Portal when the Ministry Adjudicators re-enable/re-unlock the AFS section. + // i.e.: Documents with documentType = APPLICATION_AFS_SUBMITTED are not deletable. + async updateAfsSupportingDocuments() { + const afsDocuments = this.applicationUploadedDocuments?.filter( + (document) => document.documentType === DOCUMENT_TYPES.APPLICATION_AFS, + ); + await Promise.all( + afsDocuments?.map(async (document) => { + const payload = { + documentType: DOCUMENT_TYPES.APPLICATION_AFS_SUBMITTED, + }; + await DocumentService.updateDocument(document.annotationId, payload); + }), + ); + }, }, }; diff --git a/frontend/src/components/ccfriApplication/AFS/AfsDecisionCard.vue b/frontend/src/components/ccfriApplication/AFS/AfsDecisionCard.vue new file mode 100644 index 000000000..52ea301a5 --- /dev/null +++ b/frontend/src/components/ccfriApplication/AFS/AfsDecisionCard.vue @@ -0,0 +1,63 @@ + + + + diff --git a/frontend/src/components/ccfriApplication/AFS/ApprovableFeeSchedule.vue b/frontend/src/components/ccfriApplication/AFS/ApprovableFeeSchedule.vue index fa278ff28..ce3577725 100644 --- a/frontend/src/components/ccfriApplication/AFS/ApprovableFeeSchedule.vue +++ b/frontend/src/components/ccfriApplication/AFS/ApprovableFeeSchedule.vue @@ -1,5 +1,5 @@ diff --git a/frontend/src/services/documentService.js b/frontend/src/services/documentService.js new file mode 100644 index 000000000..742ab633d --- /dev/null +++ b/frontend/src/services/documentService.js @@ -0,0 +1,49 @@ +import { isEmpty } from 'lodash'; + +import ApiService from '@/common/apiService'; +import { ApiRoutes } from '@/utils/constants'; + +export default { + async getApplicationUploadedDocuments(applicationId) { + try { + if (!applicationId) return; + const response = await ApiService.apiAxios.get(`${ApiRoutes.DOCUMENT_APPLICATION}/${applicationId}`); + return response?.data; + } catch (error) { + console.log(`Failed to get application's documents - ${error}`); + throw error; + } + }, + + async createApplicationDocuments(payload) { + try { + if (isEmpty(payload)) return; + await ApiService.apiAxios.post(ApiRoutes.DOCUMENT_APPLICATION, payload); + } catch (error) { + console.log(`Failed to create application's documents - ${error}`); + throw error; + } + }, + + async updateDocument(annotationId, payload) { + try { + if (isEmpty(annotationId) || isEmpty(payload)) return; + console.log('updateDocument'); + console.log(annotationId); + await ApiService.apiAxios.patch(`${ApiRoutes.DOCUMENT}/${annotationId}`, payload); + } catch (error) { + console.log(`Failed to update document - ${error}`); + throw error; + } + }, + + async deleteDocuments(deletedFiles) { + try { + if (isEmpty(deletedFiles)) return; + await ApiService.apiAxios.delete(ApiRoutes.DOCUMENT, { data: deletedFiles }); + } catch (error) { + console.log(`Failed to delete documents - ${error}`); + throw error; + } + }, +}; diff --git a/frontend/src/store/application.js b/frontend/src/store/application.js index febe8a8e1..880ec135f 100644 --- a/frontend/src/store/application.js +++ b/frontend/src/store/application.js @@ -1,6 +1,7 @@ import { defineStore } from 'pinia'; import ApiService from '@/common/apiService.js'; +import DocumentService from '@/services/documentService'; import { useAppStore } from '@/store/app.js'; import { useNavBarStore } from '@/store/navBar.js'; import { checkApplicationUnlocked, filterFacilityListForPCF } from '@/utils/common.js'; @@ -28,6 +29,8 @@ export const useApplicationStore = defineStore('application', { ccofConfirmationEnabled: false, applicationMap: new Map(), + + applicationUploadedDocuments: [], }), actions: { setApplicationId(value) { @@ -143,8 +146,17 @@ export const useApplicationStore = defineStore('application', { throw error; } }, + async getApplicationUploadedDocuments() { + try { + this.applicationUploadedDocuments = await DocumentService.getApplicationUploadedDocuments(this.applicationId); + } catch (error) { + console.log(error); + throw error; + } + }, }, getters: { + isApplicationSubmitted: (state) => state.applicationStatus !== 'INCOMPLETE', formattedProgramYear: (state) => formatFiscalYearName(state.programYearLabel), fiscalStartAndEndDates: (state) => { //set fiscal year dates to prevent user from choosing dates outside the current FY diff --git a/frontend/src/store/ccfriApp.js b/frontend/src/store/ccfriApp.js index e5d087e25..0d8afc0b0 100644 --- a/frontend/src/store/ccfriApp.js +++ b/frontend/src/store/ccfriApp.js @@ -1,4 +1,4 @@ -import { isEqual } from 'lodash'; +import { isEmpty, isEqual } from 'lodash'; import { defineStore } from 'pinia'; import ApiService from '@/common/apiService.js'; @@ -112,6 +112,7 @@ export const useCcfriAppStore = defineStore('ccfriApp', { ccfriStore: {}, ccfriMedianStore: {}, previousFeeStore: {}, + approvableFeeSchedules: null, }), getters: { getCCFRIById: (state) => (ccfriId) => { @@ -571,12 +572,43 @@ export const useCcfriAppStore = defineStore('ccfriApp', { } }, - async getApprovableFeeSchedules(ccfriId) { + async getApprovableFeeSchedulesForFacilities(facilities) { try { - const response = await ApiService.apiAxios.get(`${ApiRoutes.APPLICATION_CCFRI}/${ccfriId}/afs`); - return response?.data; + if (isEmpty(facilities)) return; + const appStore = useAppStore(); + this.approvableFeeSchedules = []; + const enabledAfsFacilities = facilities?.filter((facility) => facility.enableAfs); + if (isEmpty(enabledAfsFacilities)) return; + await Promise.all( + enabledAfsFacilities?.map(async (facility) => { + const response = await ApiService.apiAxios.get( + `${ApiRoutes.APPLICATION_CCFRI}/${facility.ccfriApplicationId}/afs`, + ); + const afs = response?.data; + afs?.approvableFeeSchedules?.forEach((item) => { + item.programYearOrder = appStore.getProgramYearOrderById(item.programYearId); + item.childCareCategoryNumber = appStore.getChildCareCategoryNumberById(item.childCareCategoryId); + }); + afs?.approvableFeeSchedules?.sort( + (a, b) => + a.programYearOrder - b.programYearOrder || a.childCareCategoryNumber - b.childCareCategoryNumber, + ); + this.approvableFeeSchedules = this.approvableFeeSchedules?.concat(afs); + }), + ); + } catch (e) { + console.log(`Failed to Approvable Fee Schedules for facilities with error - ${e}`); + throw e; + } + }, + + async updateApplicationCCFRI(ccfriId, payload) { + try { + if (!ccfriId || isEmpty(payload)) return; + const response = await ApiService.apiAxios.patch(`${ApiRoutes.APPLICATION_CCFRI}/${ccfriId}`, payload); + return response; } catch (e) { - console.log(`Failed to get existing approvable parent fees with error - ${e}`); + console.log(`Failed to update Application CCFRI with error - ${e}`); throw e; } }, diff --git a/frontend/src/store/navBar.js b/frontend/src/store/navBar.js index 0de0398a7..209ae0dfd 100644 --- a/frontend/src/store/navBar.js +++ b/frontend/src/store/navBar.js @@ -1,3 +1,4 @@ +import { isEmpty } from 'lodash'; import { defineStore } from 'pinia'; import { useAppStore } from '@/store/app.js'; @@ -258,6 +259,12 @@ export const useNavBarStore = defineStore('navBar', { this.refreshNavBar++; } }, + setNavBarAfsComplete({ ccfriId, complete }) { + const userProfileItem = this.userProfileList?.find((item) => item.ccfriApplicationId === ccfriId); + if (isEmpty(userProfileItem)) return; + userProfileItem.isAFSComplete = complete; + this.refreshNavBarList(); + }, addToNavBar(payload) { this.userProfileList.push(payload); this.filterNavBar(); diff --git a/frontend/src/store/summaryDeclaration.js b/frontend/src/store/summaryDeclaration.js index 40b9596e4..4ce2362c9 100644 --- a/frontend/src/store/summaryDeclaration.js +++ b/frontend/src/store/summaryDeclaration.js @@ -4,6 +4,7 @@ import ApiService from '@/common/apiService.js'; import { useAppStore } from '@/store/app.js'; import { useApplicationStore } from '@/store/application.js'; import { useAuthStore } from '@/store/auth.js'; +import { useCcfriAppStore } from '@/store/ccfriApp.js'; import { useNavBarStore } from '@/store/navBar.js'; import { useReportChangesStore } from '@/store/reportChanges.js'; import { ApiRoutes, CHANGE_REQUEST_TYPES } from '@/utils/constants.js'; @@ -178,6 +179,7 @@ export const useSummaryDeclarationStore = defineStore('summaryDeclaration', { checkSession(); const appStore = useAppStore(); const applicationStore = useApplicationStore(); + const ccfriAppStore = useCcfriAppStore(); const navBarStore = useNavBarStore(); let appID = applicationStore?.applicationMap?.get(applicationStore?.programYearId)?.applicationId; @@ -206,6 +208,10 @@ export const useSummaryDeclarationStore = defineStore('summaryDeclaration', { let isSummaryLoading = new Array(summaryModel.facilities.length).fill(true); this.setIsSummaryLoading(isSummaryLoading); + await Promise.all([ + ccfriAppStore.getApprovableFeeSchedulesForFacilities(navBarStore.userProfileList), + applicationStore.getApplicationUploadedDocuments(), + ]); //new app only? if (!applicationStore.isRenewal && payload.application?.organizationId) { @@ -231,7 +237,6 @@ export const useSummaryDeclarationStore = defineStore('summaryDeclaration', { config, ) ).data; - console.info('allDocuments', summaryModel['allDocuments'].length); } for (const facility of summaryModel.facilities) { diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index cd8b8c0ba..7965a0a26 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -50,6 +50,8 @@ export const ApiRoutes = Object.freeze({ CHANGE_REQUEST: baseRoot + '/changeRequest/', PDFS: baseRoot + '/pdf', PDF: baseRoot + '/pdf/getDocument/', + DOCUMENT: baseRoot + '/document', + DOCUMENT_APPLICATION: baseRoot + '/document/application', }); export const PAGE_TITLES = Object.freeze({ @@ -293,3 +295,8 @@ export const PARENT_FEE_FREQUENCIES = Object.freeze({ WEEKLY: 100000001, DAILY: 100000002, }); + +export const DOCUMENT_TYPES = Object.freeze({ + APPLICATION_AFS: 'AFS - Supporting Documents', + APPLICATION_AFS_SUBMITTED: 'AFS - Supporting Documents - Submitted', +});