From 8701e97c78bbf668503e91a4a9a74962a308a4ba Mon Sep 17 00:00:00 2001 From: Imran Issa Date: Fri, 10 May 2024 12:03:21 +0200 Subject: [PATCH 01/31] added infringements permission check in profile controller --- src/controllers/userProfileController.js | 6 +++++- src/test/createTestPermissions.js | 8 ++++++++ src/utilities/createInitialPermissions.js | 8 ++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 385801f2f..d9e086c25 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -538,8 +538,12 @@ const userProfileController = function (UserProfile) { } if ( req.body.infringements !== undefined && - (await hasPermission(req.body.requestor, 'infringementAuthorizer')) + ((await hasPermission(req.body.requestor, 'infringementAuthorizer')) || + (await hasPermission(req.body.requestor, 'addInfringements')) || + (await hasPermission(req.body.requestor, 'deleteInfringements')) || + (await hasPermission(req.body.requestor, 'editInfringements'))) ) { + console.log('in here'); record.infringements = req.body.infringements; } diff --git a/src/test/createTestPermissions.js b/src/test/createTestPermissions.js index 691f2cd5d..8723e550f 100644 --- a/src/test/createTestPermissions.js +++ b/src/test/createTestPermissions.js @@ -48,6 +48,8 @@ const permissionsRoles = [ 'updatePassword', 'deleteUserProfile', 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', // WBS 'postWbs', 'deleteWbs', @@ -108,6 +110,8 @@ const permissionsRoles = [ 'getProjectMembers', 'putUserProfile', 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', 'getReporteesLimitRoles', 'updateTask', 'putTeam', @@ -136,6 +140,8 @@ const permissionsRoles = [ 'getProjectMembers', 'putUserProfile', 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', 'getReporteesLimitRoles', 'getAllInvInProjectWBS', 'postInvInProjectWBS', @@ -194,6 +200,8 @@ const permissionsRoles = [ 'putUserProfileImportantInfo', 'deleteUserProfile', 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', 'postWbs', 'deleteWbs', 'getAllInvInProjectWBS', diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 23856d03a..41666b5a5 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -50,6 +50,8 @@ const permissionsRoles = [ 'updatePassword', 'deleteUserProfile', 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', // WBS 'postWbs', 'deleteWbs', @@ -110,6 +112,8 @@ const permissionsRoles = [ 'getProjectMembers', 'putUserProfile', 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', 'getReporteesLimitRoles', 'updateTask', 'putTeam', @@ -138,6 +142,8 @@ const permissionsRoles = [ 'getProjectMembers', 'putUserProfile', 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', 'getReporteesLimitRoles', 'getAllInvInProjectWBS', 'postInvInProjectWBS', @@ -196,6 +202,8 @@ const permissionsRoles = [ 'putUserProfileImportantInfo', 'deleteUserProfile', 'infringementAuthorizer', + 'addInfringements', + 'editInfringements', 'postWbs', 'deleteWbs', 'getAllInvInProjectWBS', From 47355488b78ac230d3f9a1002377ab7dc92d3997 Mon Sep 17 00:00:00 2001 From: Imran Issa Date: Sat, 11 May 2024 21:32:41 +0200 Subject: [PATCH 02/31] added infringements permission check in profile controller --- src/controllers/userProfileController.js | 4 +--- src/test/createTestPermissions.js | 8 ++++---- src/utilities/createInitialPermissions.js | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index d9e086c25..942ee660d 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -538,12 +538,10 @@ const userProfileController = function (UserProfile) { } if ( req.body.infringements !== undefined && - ((await hasPermission(req.body.requestor, 'infringementAuthorizer')) || - (await hasPermission(req.body.requestor, 'addInfringements')) || + ((await hasPermission(req.body.requestor, 'addInfringements')) || (await hasPermission(req.body.requestor, 'deleteInfringements')) || (await hasPermission(req.body.requestor, 'editInfringements'))) ) { - console.log('in here'); record.infringements = req.body.infringements; } diff --git a/src/test/createTestPermissions.js b/src/test/createTestPermissions.js index 8723e550f..879951b1f 100644 --- a/src/test/createTestPermissions.js +++ b/src/test/createTestPermissions.js @@ -47,9 +47,9 @@ const permissionsRoles = [ 'changeUserStatus', 'updatePassword', 'deleteUserProfile', - 'infringementAuthorizer', 'addInfringements', 'editInfringements', + 'deleteInfringements', // WBS 'postWbs', 'deleteWbs', @@ -109,9 +109,9 @@ const permissionsRoles = [ 'getUserProfiles', 'getProjectMembers', 'putUserProfile', - 'infringementAuthorizer', 'addInfringements', 'editInfringements', + 'deleteInfringements', 'getReporteesLimitRoles', 'updateTask', 'putTeam', @@ -139,9 +139,9 @@ const permissionsRoles = [ 'getUserProfiles', 'getProjectMembers', 'putUserProfile', - 'infringementAuthorizer', 'addInfringements', 'editInfringements', + 'deleteInfringements', 'getReporteesLimitRoles', 'getAllInvInProjectWBS', 'postInvInProjectWBS', @@ -199,9 +199,9 @@ const permissionsRoles = [ 'putUserProfile', 'putUserProfileImportantInfo', 'deleteUserProfile', - 'infringementAuthorizer', 'addInfringements', 'editInfringements', + 'deleteInfringements', 'postWbs', 'deleteWbs', 'getAllInvInProjectWBS', diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 41666b5a5..25eb72506 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -49,9 +49,9 @@ const permissionsRoles = [ 'changeUserRehireableStatus', 'updatePassword', 'deleteUserProfile', - 'infringementAuthorizer', 'addInfringements', 'editInfringements', + 'deleteInfringements', // WBS 'postWbs', 'deleteWbs', @@ -111,9 +111,9 @@ const permissionsRoles = [ 'getUserProfiles', 'getProjectMembers', 'putUserProfile', - 'infringementAuthorizer', 'addInfringements', 'editInfringements', + 'deleteInfringements', 'getReporteesLimitRoles', 'updateTask', 'putTeam', @@ -141,9 +141,9 @@ const permissionsRoles = [ 'getUserProfiles', 'getProjectMembers', 'putUserProfile', - 'infringementAuthorizer', 'addInfringements', 'editInfringements', + 'deleteInfringements', 'getReporteesLimitRoles', 'getAllInvInProjectWBS', 'postInvInProjectWBS', @@ -201,9 +201,9 @@ const permissionsRoles = [ 'putUserProfile', 'putUserProfileImportantInfo', 'deleteUserProfile', - 'infringementAuthorizer', 'addInfringements', 'editInfringements', + 'deleteInfringements', 'postWbs', 'deleteWbs', 'getAllInvInProjectWBS', From 5da634ada46e12ef43eea02fae77eb3cf28cef45 Mon Sep 17 00:00:00 2001 From: Imran Issa Date: Sat, 1 Jun 2024 18:34:50 +0200 Subject: [PATCH 03/31] add endpoints for each bluesquare modification --- src/controllers/userProfileController.js | 121 +++++++++++++++++++++++ src/routes/userProfileRouter.js | 7 ++ 2 files changed, 128 insertions(+) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 942ee660d..3a204b7cd 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1197,6 +1197,124 @@ const userProfileController = function (UserProfile) { } }; + const addInfringements = async function (req, res) { + if (!(await hasPermission(req.body.requestor, 'addInfringements'))) { + res.status(403).send('You are not authorized to add blue square'); + return; + } + const userid = req.params.userId; + + cache.removeCache(`user-${userid}`); + + if (req.body.blueSquare === undefined) { + res.status(400).send('Invalid Data'); + return; + } + + UserProfile.findById(userid, async (err, record) => { + if (err || !record) { + res.status(404).send('No valid records found'); + return; + } + // find userData in cache + const isUserInCache = cache.hasCache('allusers'); + let allUserData; + let userData; + let userIdx; + if (isUserInCache) { + allUserData = JSON.parse(cache.getCache('allusers')); + userIdx = allUserData.findIndex((users) => users._id === userid); + userData = allUserData[userIdx]; + } + + const originalinfringements = record?.infringements ?? []; + record.infringements = originalinfringements.concat(req.body.blueSquare); + + record + .save() + .then((results) => { + userHelper.notifyInfringements(originalinfringements, results.infringements); + res.status(200).json({ + _id: record._id, + }); + + // update alluser cache if we have cache + if (isUserInCache) { + allUserData.splice(userIdx, 1, userData); + cache.setCache('allusers', JSON.stringify(allUserData)); + } + }) + .catch((error) => res.status(400).send(error)); + }); + }; + + const editInfringements = async function (req, res) { + if (!(await hasPermission(req.body.requestor, 'editInfringements'))) { + res.status(403).send('You are not authorized to edit blue square'); + return; + } + const { userId, blueSquareId } = req.params; + const { dateStamp, summary } = req.body; + + UserProfile.findById(userId, async (err, record) => { + if (err || !record) { + res.status(404).send('No valid records found'); + return; + } + + const originalinfringements = record?.infringements ?? []; + + record.infringements = originalinfringements.map((blueSquare) => { + if (blueSquare._id.equals(blueSquareId)) { + blueSquare.date = dateStamp ?? blueSquare.date; + blueSquare.description = summary ?? blueSquare.description; + } + return blueSquare; + }); + + record + .save() + .then((results) => { + userHelper.notifyInfringements(originalinfringements, results.infringements); + res.status(200).json({ + _id: record._id, + }); + }) + .catch((error) => res.status(400).send(error)); + }); + }; + + const deleteInfringements = async function (req, res) { + if (!(await hasPermission(req.body.requestor, 'deleteInfringements'))) { + res.status(403).send('You are not authorized to delete blue square'); + return; + } + const { userId, blueSquareId } = req.params; + + UserProfile.findById(userId, async (err, record) => { + if (err || !record) { + res.status(404).send('No valid records found'); + return; + } + + const originalinfringements = record?.infringements ?? []; + + record.infringements = originalinfringements.filter( + (infringement) => !infringement._id.equals(blueSquareId), + ); + + record + .save() + .then((results) => { + userHelper.notifyInfringements(originalinfringements, results.infringements); + res.status(200).json({ + _id: record._id, + }); + }) + .catch((error) => res.status(400).send(error)); + }); + }; + return { postUserProfile, getUserProfiles, @@ -1218,6 +1336,9 @@ const userProfileController = function (UserProfile) { getUserByFullName, changeUserRehireableStatus, authorizeUser, + addInfringements, + editInfringements, + deleteInfringements, }; }; diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index 0f27abe33..2976b37f9 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -87,6 +87,13 @@ const routes = function (userProfile) { .route('/userProfile/authorizeUser/weeeklySummaries') .post(controller.authorizeUser); + userProfileRouter.route('/userProfile/:userId/addInfringement').post(controller.addInfringements); + + userProfileRouter + .route('/userProfile/:userId/infringements/:blueSquareId') + .put(controller.editInfringements) + .delete(controller.deleteInfringements); + return userProfileRouter; }; From ae99069c1a89789247ac2db17b0389ec152cad16 Mon Sep 17 00:00:00 2001 From: Jingyii800 Date: Fri, 7 Jun 2024 13:48:22 -0400 Subject: [PATCH 04/31] edit header message permission --- src/controllers/ownerMessageController.js | 10 ++++++++-- src/utilities/createInitialPermissions.js | 2 ++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/controllers/ownerMessageController.js b/src/controllers/ownerMessageController.js index 1b2c30205..886c15e93 100644 --- a/src/controllers/ownerMessageController.js +++ b/src/controllers/ownerMessageController.js @@ -1,8 +1,11 @@ +const helper = require('../utilities/permissions'); + const ownerMessageController = function (OwnerMessage) { const getOwnerMessage = async function (req, res) { try { const results = await OwnerMessage.find({}); - if (results.length === 0) { // first time initialization + if (results.length === 0) { + // first time initialization const ownerMessage = new OwnerMessage(); await ownerMessage.save(); res.status(200).send({ ownerMessage }); @@ -15,7 +18,10 @@ const ownerMessageController = function (OwnerMessage) { }; const updateOwnerMessage = async function (req, res) { - if (req.body.requestor.role !== 'Owner') { + if ( + req.body.requestor.role !== 'Owner' && + !(await helper.hasPermission(req.body.requestor, 'editHeaderMessage')) + ) { res.status(403).send('You are not authorized to create messages!'); } const { isStandard, newMessage } = req.body; diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index f04e12063..4202fd679 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -244,6 +244,8 @@ const permissionsRoles = [ 'changeUserRehireableStatus', 'manageAdminLinks', + + 'editHeaderMessage', ], }, ]; From 9345aaf9daad93022e33ecc046a57fcfd00aecf4 Mon Sep 17 00:00:00 2001 From: jingyij Date: Tue, 11 Jun 2024 19:54:16 -0400 Subject: [PATCH 05/31] logic with delete header message permission --- src/controllers/ownerMessageController.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/controllers/ownerMessageController.js b/src/controllers/ownerMessageController.js index 886c15e93..c817169b8 100644 --- a/src/controllers/ownerMessageController.js +++ b/src/controllers/ownerMessageController.js @@ -46,7 +46,10 @@ const ownerMessageController = function (OwnerMessage) { }; const deleteOwnerMessage = async function (req, res) { - if (req.body.requestor.role !== 'Owner') { + if ( + req.body.requestor.role !== 'Owner' && + !(await helper.hasPermission(req.body.requestor, 'editHeaderMessage')) + ) { res.status(403).send('You are not authorized to delete messages!'); } try { From 0942dbd0ca328bca0ca3075acb2c6605da0ebc30 Mon Sep 17 00:00:00 2001 From: Jingyii800 <112589476+Jingyii800@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:59:14 -0400 Subject: [PATCH 06/31] Update src/controllers/ownerMessageController.js Co-authored-by: Nathan Hoffman --- src/controllers/ownerMessageController.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/ownerMessageController.js b/src/controllers/ownerMessageController.js index c817169b8..139a2c28c 100644 --- a/src/controllers/ownerMessageController.js +++ b/src/controllers/ownerMessageController.js @@ -19,7 +19,6 @@ const ownerMessageController = function (OwnerMessage) { const updateOwnerMessage = async function (req, res) { if ( - req.body.requestor.role !== 'Owner' && !(await helper.hasPermission(req.body.requestor, 'editHeaderMessage')) ) { res.status(403).send('You are not authorized to create messages!'); From de33c6616ad45059d1ce6afb1a16f8a3efcb140c Mon Sep 17 00:00:00 2001 From: Jingyii800 <112589476+Jingyii800@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:18:34 -0400 Subject: [PATCH 07/31] Update ownerMessageController.js --- src/controllers/ownerMessageController.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/ownerMessageController.js b/src/controllers/ownerMessageController.js index 139a2c28c..3f74cb112 100644 --- a/src/controllers/ownerMessageController.js +++ b/src/controllers/ownerMessageController.js @@ -46,7 +46,6 @@ const ownerMessageController = function (OwnerMessage) { const deleteOwnerMessage = async function (req, res) { if ( - req.body.requestor.role !== 'Owner' && !(await helper.hasPermission(req.body.requestor, 'editHeaderMessage')) ) { res.status(403).send('You are not authorized to delete messages!'); From d636a378a25ec765ab3c3badfbc6f71ca4efb66f Mon Sep 17 00:00:00 2001 From: Diya Wadhwani Date: Wed, 3 Jul 2024 03:33:54 -0700 Subject: [PATCH 08/31] Changes for Blue Squares --- src/controllers/userProfileController.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 158ed106f..bf58ba3c1 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -666,11 +666,11 @@ const userProfileController = function (UserProfile) { const getUserById = function (req, res) { const userid = req.params.userId; - if (cache.getCache(`user-${userid}`)) { - const getData = JSON.parse(cache.getCache(`user-${userid}`)); - res.status(200).send(getData); - return; - } + // if (cache.getCache(`user-${userid}`)) { + // const getData = JSON.parse(cache.getCache(`user-${userid}`)); + // res.status(200).send(getData); + // return; + // } UserProfile.findById(userid, '-password -refreshTokens -lastModifiedDate -__v') .populate([ @@ -700,6 +700,15 @@ const userProfileController = function (UserProfile) { select: '_id badgeName type imageUrl description ranking showReport', }, }, + { + path: 'infringements', // Populate infringements field + select: 'date description', + options: { + sort: { + date: -1, // Sort by date descending if needed + }, + }, + }, ]) .exec() .then((results) => { From ed81b703ec95b9a8a9ff637ae40de9f54a5de416 Mon Sep 17 00:00:00 2001 From: Logeshwari-Renu Date: Wed, 10 Jul 2024 19:08:20 -0500 Subject: [PATCH 09/31] Update profileInitialSetupController.js --- src/controllers/profileInitialSetupController.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index f6086d02f..f6a09df98 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -448,7 +448,9 @@ const profileInitialSetupController = function ( */ const getSetupInvitation = (req, res) => { const { role } = req.body.requestor; - if (role === 'Administrator' || role === 'Owner') { + const { permissions } = req.body.requestor; + let user_permissions = ['getUserProfiles','postUserProfile','putUserProfile','changeUserStatus'] + if ((role === 'Administrator') || (role === 'Owner') || (role === 'Manager') || user_permissions.some(e=>permissions.frontPermissions.includes(e))) { try{ ProfileInitialSetupToken .find({ isSetupCompleted: false }) From ccedf07d9daa87105f2941970d0bf0403f129f18 Mon Sep 17 00:00:00 2001 From: Logeshwari-Renu Date: Wed, 10 Jul 2024 19:23:52 -0500 Subject: [PATCH 10/31] Update profileInitialSetupController.js --- src/controllers/profileInitialSetupController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index f6a09df98..2303b615c 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -450,7 +450,7 @@ const profileInitialSetupController = function ( const { role } = req.body.requestor; const { permissions } = req.body.requestor; let user_permissions = ['getUserProfiles','postUserProfile','putUserProfile','changeUserStatus'] - if ((role === 'Administrator') || (role === 'Owner') || (role === 'Manager') || user_permissions.some(e=>permissions.frontPermissions.includes(e))) { + if ((role === 'Administrator') || (role === 'Owner') || (role === 'Manager') || (role === 'Mentor') || user_permissions.some(e=>permissions.frontPermissions.includes(e))) { try{ ProfileInitialSetupToken .find({ isSetupCompleted: false }) From 21b2887cf697d6a4d160d66179d7ca86269489d5 Mon Sep 17 00:00:00 2001 From: Jatin Agrawal Date: Sat, 17 Aug 2024 18:31:12 -0700 Subject: [PATCH 11/31] new route added to update existing quick action functions --- .DS_Store | Bin 8196 -> 8196 bytes src/controllers/titleController.js | 85 ++++++++++++++++++++++- src/controllers/userProfileController.js | 4 +- src/routes/titleRouter.js | 6 +- 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/.DS_Store b/.DS_Store index 8d8d400ad748d55ea99c549e9cf8a44bed35dab6..1ad1b253c333add150c535537f2e0a63bceeb963 100644 GIT binary patch delta 223 zcmZp1XmOa}C7U^hRb%4Qw`HAYp&as~zlHU>S042DvMVuoyn%G`Vxm!zEhB%nA) zXXyDyjV#9|hYQNG!`Rh=Q|eh5QW!FU%1d(64TF>Oa|?iq85sDSE&xfm9I`=7ABwA+ zSdKZO%BR3B!X{skfn>I50#GXh4?`Y9K10f8F`<5@jmfW=HnU57W0~A4qOdXfITHX& CeLL&` delta 99 zcmZp1XmOa}¥U^hRb!e$-;HAYpYa|{d&Yz%q~84RTi#SGaDmAUyYE=f80NkDOq x>WuJ-8y_E?94;u!4r5mfPMN$}NOJQ6p<1TN>ty^lW(YHHW|#QJvN405834B>APfKi diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index 3bb268143..bf1fe0410 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -1,7 +1,11 @@ const Team = require('../models/team'); const Project = require('../models/project'); const cacheClosure = require('../utilities/nodeCache'); -const { getAllTeamCodeHelper } = require("./userProfileController"); +const userProfileController = require("./userProfileController"); +const userProfile = require('../models/userProfile'); +const project = require('../models/project'); +const controller = userProfileController(userProfile, project); +const getAllTeamCodeHelper = controller.getAllTeamCodeHelper; const titlecontroller = function (Title) { const cache = cacheClosure(); @@ -22,7 +26,6 @@ const titlecontroller = function (Title) { const postTitle = async function (req, res) { const title = new Title(); - title.titleName = req.body.titleName; title.teamCode = req.body.teamCode; title.projectAssigned = req.body.projectAssigned; @@ -81,6 +84,81 @@ const titlecontroller = function (Title) { .catch((error) => res.status(404).send(error)); }; + const putTitle = async function (req, res) { + const title = new Title(); + const filter=req.body.id; + title.titleName = req.body.titleName; + title.teamCode = req.body.teamCode; + title.projectAssigned = req.body.projectAssigned; + title.mediaFolder = req.body.mediaFolder; + title.teamAssiged = req.body.teamAssiged; + + // valid title name + if (!title.titleName.trim()) { + res.status(400).send({ message: 'Title cannot be empty.' }); + return; + } + + // if media is empty + if (!title.mediaFolder.trim()) { + res.status(400).send({ message: 'Media folder cannot be empty.' }); + return; + } + + const shortnames = title.titleName.trim().split(' '); + let shortname; + if (shortnames.length > 1) { + shortname = (shortnames[0][0] + shortnames[1][0]).toUpperCase(); + } else if (shortnames.length === 1) { + shortname = shortnames[0][0].toUpperCase(); + } + title.shortName = shortname; + + // Validate team code by checking if it exists in the database + if (!title.teamCode) { + res.status(400).send({ message: 'Please provide a team code.' }); + return; + } + + const teamCodeExists = await checkTeamCodeExists(title.teamCode); + if (!teamCodeExists) { + res.status(400).send({ message: 'Invalid team code. Please provide a valid team code.' }); + return; + } + + // validate if project exist + const projectExist = await checkProjectExists(title.projectAssigned._id); + if (!projectExist) { + res.status(400).send({ message: 'Project is empty or not exist.' }); + return; + } + + // validate if team exist + if (title.teamAssiged && title.teamAssiged._id === 'N/A') { + res.status(400).send({ message: 'Team not exists.' }); + return; + } + + title + .updateOne({ _id: filter }, title) + .then((result) => { + if (result.modifiedCount === 0) { + // No documents were modified, handle this case if necessary + console.log("No docs") + return res.status(404).send({ message: 'No documents were updated' }); + } + // Send a success response + console.log("Success") + res.status(200).send({ message: 'Update successful', result }); + }) + .catch((error) => { + // Send a server error response + console.log(error) + res.status(500).send({ message: 'An error occurred', error }); + }); + + }; + const deleteTitleById = async function (req, res) { const { titleId } = req.params; Title.deleteOne({ _id: titleId }) @@ -126,12 +204,15 @@ const titlecontroller = function (Title) { } } + + return { getAllTitles, getTitleById, postTitle, deleteTitleById, deleteAllTitles, + putTitle }; }; diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 6572635f3..0df8d4853 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1588,7 +1588,7 @@ const userProfileController = function (UserProfile, Project) { } catch (error) { throw new Error('Encountered an error to get all team codes, please try again!'); } - } + }; const getAllTeamCode = async function (req, res) { try { @@ -1597,7 +1597,7 @@ const userProfileController = function (UserProfile, Project) { } catch (error) { return res.status(500).send({ message: 'Encountered an error to get all team codes, please try again!' }); } - } + } return { postUserProfile, diff --git a/src/routes/titleRouter.js b/src/routes/titleRouter.js index f12cb5ec7..6d892284a 100644 --- a/src/routes/titleRouter.js +++ b/src/routes/titleRouter.js @@ -2,12 +2,14 @@ const express = require('express'); const router = function (title) { const controller = require('../controllers/titleController')(title); - const titleRouter = express.Router(); titleRouter.route('/title') .get(controller.getAllTitles) - .post(controller.postTitle); + .post(controller.postTitle) + // .put(controller.putTitle); + + titleRouter.route('/title/update').put(controller.putTitle); titleRouter.route('/title/:titleId') .get(controller.getTitleById) From d0a6e245a7f455d3b40eb6242142e057e5c73e3b Mon Sep 17 00:00:00 2001 From: Jatin Agrawal Date: Sat, 17 Aug 2024 23:58:18 -0700 Subject: [PATCH 12/31] update route added --- src/controllers/titleController.js | 6 ++---- src/routes/titleRouter.js | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index bf1fe0410..0d0b58ecd 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -84,7 +84,7 @@ const titlecontroller = function (Title) { .catch((error) => res.status(404).send(error)); }; - const putTitle = async function (req, res) { + const updateTitle = async function (req, res) { const title = new Title(); const filter=req.body.id; title.titleName = req.body.titleName; @@ -144,11 +144,9 @@ const titlecontroller = function (Title) { .then((result) => { if (result.modifiedCount === 0) { // No documents were modified, handle this case if necessary - console.log("No docs") return res.status(404).send({ message: 'No documents were updated' }); } // Send a success response - console.log("Success") res.status(200).send({ message: 'Update successful', result }); }) .catch((error) => { @@ -212,7 +210,7 @@ const titlecontroller = function (Title) { postTitle, deleteTitleById, deleteAllTitles, - putTitle + updateTitle }; }; diff --git a/src/routes/titleRouter.js b/src/routes/titleRouter.js index 6d892284a..ce00e2279 100644 --- a/src/routes/titleRouter.js +++ b/src/routes/titleRouter.js @@ -9,7 +9,7 @@ const router = function (title) { .post(controller.postTitle) // .put(controller.putTitle); - titleRouter.route('/title/update').put(controller.putTitle); + titleRouter.route('/title/update').post(controller.updateTitle); titleRouter.route('/title/:titleId') .get(controller.getTitleById) From 0397aa08f27cff8c5aa50afd7a2e509edd4ac069 Mon Sep 17 00:00:00 2001 From: 20chen-7 Date: Mon, 19 Aug 2024 13:28:40 -0600 Subject: [PATCH 13/31] chore: update invalid pwd status code --- src/controllers/logincontroller.js | 2 +- src/controllers/logincontroller.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/logincontroller.js b/src/controllers/logincontroller.js index 3ba0203aa..794d00d70 100644 --- a/src/controllers/logincontroller.js +++ b/src/controllers/logincontroller.js @@ -63,7 +63,7 @@ const logincontroller = function () { res.status(200).send({ token }); } else { - res.status(403).send({ + res.status(404).send({ message: 'Invalid password.', }); } diff --git a/src/controllers/logincontroller.spec.js b/src/controllers/logincontroller.spec.js index 595bfe77b..995be69de 100644 --- a/src/controllers/logincontroller.spec.js +++ b/src/controllers/logincontroller.spec.js @@ -110,7 +110,7 @@ describe('logincontroller module', () => { expect(findOneSpy).toHaveBeenCalledWith({ email: mockReqModified.body.email }); assertResMock( - 403, + 404, { message: 'Invalid password.', }, From f2fbe13a0c5b979c909b0aa282e171c653840e85 Mon Sep 17 00:00:00 2001 From: "Xiaoyu(Ivy) Chen" <102696148+20chen-7@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:24:51 -0600 Subject: [PATCH 14/31] Delete src/helpers/userHelper.js --- src/helpers/userHelper.js | 2178 ------------------------------------- 1 file changed, 2178 deletions(-) delete mode 100644 src/helpers/userHelper.js diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js deleted file mode 100644 index ed9c52131..000000000 --- a/src/helpers/userHelper.js +++ /dev/null @@ -1,2178 +0,0 @@ -/* eslint-disable quotes */ -/* eslint-disable no-continue */ -/* eslint-disable no-await-in-loop */ -/* eslint-disable no-console */ -/* eslint-disable consistent-return */ -/* eslint-disable no-unused-vars */ -/* eslint-disable no-shadow */ -/* eslint-disable prefer-destructuring */ -/* eslint-disable no-use-before-define */ -/* eslint-disable no-unsafe-optional-chaining */ -/* eslint-disable no-restricted-syntax */ - -const mongoose = require('mongoose'); -const moment = require('moment-timezone'); -const _ = require('lodash'); -const userProfile = require('../models/userProfile'); -const timeEntries = require('../models/timeentry'); -const badge = require('../models/badge'); -const myTeam = require('./helperModels/myTeam'); -const dashboardHelper = require('./dashboardhelper')(); -const reportHelper = require('./reporthelper')(); -const emailSender = require('../utilities/emailSender'); -const logger = require('../startup/logger'); -const token = require('../models/profileInitialSetupToken'); -const BlueSquareEmailAssignment = require('../models/BlueSquareEmailAssignment'); -const cache = require('../utilities/nodeCache')(); -const timeOffRequest = require('../models/timeOffRequest'); -const notificationService = require('../services/notificationService'); -const { NEW_USER_BLUE_SQUARE_NOTIFICATION_MESSAGE } = require('../constants/message'); -const timeUtils = require('../utilities/timeUtils'); - -const userHelper = function () { - // Update format to "MMM-DD-YY" from "YYYY-MMM-DD" (Confirmed with Jae) - const earnedDateBadge = () => { - const currentDate = new Date(Date.now()); - return moment(currentDate).tz('America/Los_Angeles').format('MMM-DD-YY'); - }; - - const getTeamMembers = function (user) { - const userId = mongoose.Types.ObjectId(user._id); - // var teamid = userdetails.teamId; - return myTeam.findById(userId).select({ - 'myTeam._id': 0, - 'myTeam.role': 0, - 'myTeam.fullName': 0, - _id: 0, - }); - }; - - const getTeamManagementEmail = function (teamId) { - const parsedTeamId = mongoose.Types.ObjectId(teamId); - return userProfile - .find( - { - isActive: true, - teams: { - $in: [parsedTeamId], - }, - role: { - $in: ['Manager', 'Administrator'], - }, - }, - 'email role', - ) - .exec(); - }; - - const getUserName = async function (userId) { - const userid = mongoose.Types.ObjectId(userId); - return userProfile.findById(userid, 'firstName lastName'); - }; - - const validateProfilePic = function (profilePic) { - // if it is a url - if (typeof profilePic !== 'string') { - return { - result: false, - errors: 'Invalid image', - }; - } - if (profilePic.startsWith('http') || profilePic.startsWith('https')) { - return { - result: true, - errors: 'Valid image', - }; - } - - const picParts = profilePic.split(','); - let result = true; - const errors = []; - - if (picParts.length < 2) { - return { - result: false, - errors: 'Invalid image', - }; - } - - // validate size - const imageSize = picParts[1].length; - const sizeInBytes = (Math.ceil(imageSize / 4) * 3) / 1024; - if (sizeInBytes > 50) { - errors.push('Image size should not exceed 50KB'); - result = false; - } - - const imageType = picParts[0].split('/')[1].split(';')[0]; - if (imageType !== 'jpeg' && imageType !== 'png') { - errors.push('Image type shoud be either jpeg or png.'); - result = false; - } - - return { - result, - errors, - }; - }; - - const getInfringementEmailBody = function ( - firstName, - lastName, - infringement, - totalInfringements, - timeRemaining, - coreTeamExtraHour, - requestForTimeOffEmailBody, - administrativeContent, - weeklycommittedHours, - ) { - let finalParagraph = ''; - let descrInfringement = ''; - if (timeRemaining === undefined) { - finalParagraph = - '

Life happens and we understand that. That’s why we allow 5 of them before taking action. This action usually includes removal from our team though, so please let your direct supervisor know what happened and do your best to avoid future blue squares if you are getting close to 5 and wish to avoid termination. Each blue square drops off after a year.

'; - descrInfringement = `

Total Infringements: This is your ${moment - .localeData() - .ordinal(totalInfringements)} blue square of 5.

`; - } else { - let hrThisweek = weeklycommittedHours || 0 + coreTeamExtraHour; - const remainHr = timeRemaining || 0; - hrThisweek += remainHr; - finalParagraph = `Please complete ALL owed time this week (${ - hrThisweek + totalInfringements - 5 - } hours) to avoid receiving another blue square. If you have any questions about any of this, please see the "One Community Core Team Policies and Procedures" page.`; - descrInfringement = `

Total Infringements: This is your ${moment - .localeData() - .ordinal( - totalInfringements, - )} blue square of 5 and that means you have ${totalInfringements - 5} hour(s) added to your - requirement this week. This is in addition to any hours missed for last week: - ${weeklycommittedHours} hours commitment + ${remainHr} hours owed for last week + ${totalInfringements - 5} hours - owed for this being your ${moment - .localeData() - .ordinal( - totalInfringements, - )} blue square = ${hrThisweek + totalInfringements - 5} hours required for this week. - .

`; - } - // bold description for 'System auto-assigned infringement for two reasons ....' and 'not submitting a weekly summary' and logged hrs - let emailDescription = requestForTimeOffEmailBody; - if (!requestForTimeOffEmailBody && infringement.description) { - const sentences = infringement.description.split('.'); - if (sentences[0].includes('System auto-assigned infringement for two reasons')) { - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, - '$1', - ); - emailDescription = sentences.join('.'); - emailDescription = emailDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else if ( - sentences[0].includes('System auto-assigned infringement for editing your time entries') - ) { - sentences[0] = sentences[0].replace( - /time entries <(\d+)>\s*times/i, - 'time entries $1 times', - ); - emailDescription = sentences.join('.'); - } else if (sentences[0].includes('System auto-assigned infringement')) { - sentences[0] = sentences[0].replace(/(not submitting a weekly summary)/gi, '$1'); - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment)/gi, - '$1', - ); - emailDescription = sentences.join('.'); - emailDescription = emailDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else { - emailDescription = `${infringement.description}`; - } - } - // add administrative content - const text = `Dear ${firstName} ${lastName}, -

Oops, it looks like something happened and you’ve managed to get a blue square.

-

Date Assigned: ${moment(infringement.date).format('M-D-YYYY')}

\ -

Description: ${emailDescription}

- ${descrInfringement} - ${finalParagraph} -

Thank you,

-

One Community

- -         -
-

ADMINISTRATIVE DETAILS:

-

Start Date: ${administrativeContent.startDate}

-

Role: ${administrativeContent.role}

-

Title: ${administrativeContent.userTitle || 'Volunteer'}

-

Previous Blue Square Reasons:

- ${administrativeContent.historyInfringements}`; - - return text; - }; - - /** - * This function will send out an email listing all users that have a summary provided for a specific week. - * A week is represented by an weekIndex: 0, 1, 2 or 3, where 0 is the most recent and 3 the oldest. - * It relies on the function weeklySummaries(startWeekIndex, endWeekIndex) to get the weekly summaries for the specific week. - * In this case both the startWeekIndex and endWeekIndex are set to 1 to get the last weeks' summaries for all users. - * - * @param {int} [weekIndex=1] Numbered representation of a week where 0 is the most recent and 3 the oldest. - * - * @return {void} - */ - const emailWeeklySummariesForAllUsers = async (weekIndex = 1) => { - const currentFormattedDate = moment().tz('America/Los_Angeles').format(); - /* eslint-disable no-undef */ - logger.logInfo( - `Job for emailing all users' weekly summaries starting at ${currentFormattedDate}`, - ); - - const emails = []; - let mappedResults; // this contains the emails - - try { - const results = await reportHelper.weeklySummaries(weekIndex, weekIndex); - // checks for userProfiles who are eligible to receive the weeklySummary Reports - await userProfile - .find({ getWeeklyReport: true }, { email: 1, teamCode: 1, _id: 0 }) - // eslint-disable-next-line no-shadow - .then((results) => { - mappedResults = results.map((ele) => ele.email); - mappedResults.push('onecommunityglobal@gmail.com', 'onecommunityhospitality@gmail.com'); - mappedResults = mappedResults.toString(); - }); - - let emailBody = '

Weekly Summaries for all active users:

'; - - const weeklySummaryNotProvidedMessage = - '
Weekly Summary: Not provided!
'; - - const weeklySummaryNotRequiredMessage = - '
Weekly Summary: Not required for this user
'; - - results.sort((a, b) => - `${a.firstName} ${a.lastName}`.localeCompare(`${b.firstName} ${b.lastname}`), - ); - - for (let i = 0; i < results.length; i += 1) { - const result = results[i]; - const { - firstName, - lastName, - email, - weeklySummaries, - mediaUrl, - adminLinks, - weeklySummariesCount, - weeklycommittedHours, - weeklySummaryOption, - teamCode, - } = result; - - if (email !== undefined && email !== null) { - emails.push(email); - } - - // weeklySummaries array will have only one item fetched (if present), - // consequently totalSeconds array will also have only one item in the array (if present) - // hence totalSeconds[0] should be used - const hoursLogged = result.totalSeconds[0] / 3600 || 0; - - const mediaUrlLink = mediaUrl ? `${mediaUrl}` : 'Not provided!'; - const teamCodeStr = teamCode ? `${teamCode}` : 'X-XXX'; - const googleDocLinkValue = - adminLinks?.length > 0 - ? adminLinks.find((link) => link.Name === 'Google Doc' && link.Link) - : null; - - const googleDocLink = googleDocLinkValue - ? `${googleDocLinkValue.Link}` - : null; - - let weeklySummaryMessage = weeklySummaryNotProvidedMessage; - const colorStyle = (() => { - switch (weeklySummaryOption) { - case 'Team': - return 'style="color: magenta;"'; - case 'Not Required': - return 'style="color: green"'; - case 'Required': - return ''; - default: - return result.weeklySummaryNotReq ? 'style="color: green"' : ''; - } - })(); - // weeklySummaries array should only have one item if any, hence weeklySummaries[0] needs be used to access it. - if (Array.isArray(weeklySummaries) && weeklySummaries[0]) { - const { dueDate, summary } = weeklySummaries[0]; - if (summary) { - weeklySummaryMessage = ` -
- Weekly Summary - (for the week ending on ${moment(dueDate) - .tz('America/Los_Angeles') - .format('YYYY-MMM-DD')}): -
-
- ${summary} -
- `; - } else if ( - weeklySummaryOption === 'Not Required' || - (!weeklySummaryOption && result.weeklySummaryNotReq) - ) { - weeklySummaryMessage = weeklySummaryNotRequiredMessage; - } - } - - emailBody += ` - \n -
- Name: ${firstName} ${lastName} -

- Team Code: ${teamCodeStr || 'X-XXX'} -

-

- - - Media URL: ${mediaUrlLink || 'Not provided!'} - -

-

- - Google Doc Link: ${ - googleDocLink || 'Not provided!' - } - -

- ${ - weeklySummariesCount === 8 - ? `

Total Valid Weekly Summaries: ${weeklySummariesCount}

` - : `

Total Valid Weekly Summaries: ${ - weeklySummariesCount || 'No valid submissions yet!' - }

` - } - ${ - hoursLogged >= weeklycommittedHours - ? `

Hours logged: ${hoursLogged.toFixed(2)} / ${weeklycommittedHours}

` - : `

Hours logged: ${hoursLogged.toFixed( - 2, - )} / ${weeklycommittedHours}

` - } - ${weeklySummaryMessage} -
`; - } - - // Necessary because our version of node is outdated - // and doesn't have String.prototype.replaceAll - let emailString = [...new Set(emails)].toString(); - while (emailString.includes(',')) { - emailString = emailString.replace(',', '\n'); - } - while (emailString.includes('\n')) { - emailString = emailString.replace('\n', ', '); - } - - emailBody += `\n -
-

Emails

-

- ${emailString} -

-
- `; - - const mailList = mappedResults; - - emailSender( - mailList, - 'Weekly Summaries for all active users...', - emailBody, - null, - null, - emailString, - ); - } catch (err) { - logger.logException(err); - } - }; - - /** - * This function will process the weeklySummaries array in the following way: - * 1 ) Push a new (blank) summary at the beginning of the array. - * 2 ) Always maintains 4 items in the array where each item represents a summary for a given week. - * - * @param {ObjectId} personId This is mongoose.Types.ObjectId object. - */ - const processWeeklySummariesByUserId = function (personId) { - userProfile - .findByIdAndUpdate(personId, { - $push: { - weeklySummaries: { - $each: [ - { - dueDate: moment().tz('America/Los_Angeles').endOf('week'), - summary: '', - }, - ], - $position: 0, - $slice: 4, - }, - }, - }) - .catch((error) => logger.logException(error)); - }; - - /** - * This function is called by a cron job to do 3 things to all active users: - * 1 ) Determine whether there's been an infringement for the weekly summary for last week. - * 2 ) Determine whether there's been an infringement for the time not met for last week. - * 3 ) Call the processWeeklySummariesByUserId(personId) to process the weeklySummaries array. - */ - const assignBlueSquareForTimeNotMet = async () => { - try { - console.log('run'); - const currentFormattedDate = moment().tz('America/Los_Angeles').format(); - moment.tz('America/Los_Angeles').startOf('day').toISOString(); - - logger.logInfo( - `Job for assigning blue square for commitment not met starting at ${currentFormattedDate}`, - ); - - const pdtStartOfLastWeek = moment() - .tz('America/Los_Angeles') - .startOf('week') - .subtract(1, 'week'); - - const pdtEndOfLastWeek = moment().tz('America/Los_Angeles').endOf('week').subtract(1, 'week'); - - const users = await userProfile.find( - { isActive: true }, - '_id weeklycommittedHours weeklySummaries missedHours', - ); - const usersRequiringBlueSqNotification = []; - // this part is supposed to be a for, so it'll be slower when sending emails, so the emails will not be - // targeted as spam - // There's no need to put Promise.all here - - /* - Note from Shengwei (3/11/24) Potential enhancement: - 1. I think we could remove the for loop to update find user profile by batch to reduce db roundtrips. - Otherwise, each record checking and update require at least 1 db roundtrip. Then, we could use for loop to do email sending. - - Do something like: - do while (batch != lastBatch) - const lsOfResult = await userProfile.find({ _id: { $in: arrayOfIds } } - for item in lsOfResult: - // do the update and checking - // save updated records in batch (mongoose updateMany) and do asyc email sending - 2. Wrap the operation in one transaction to ensure the atomicity of the operation. - */ - for (let i = 0; i < users.length; i += 1) { - const user = users[i]; - - const person = await userProfile.findById(user._id); - - const personId = mongoose.Types.ObjectId(user._id); - - let hasWeeklySummary = false; - - if (Array.isArray(user.weeklySummaries) && user.weeklySummaries.length) { - const { summary } = user.weeklySummaries[0]; - if (summary) { - hasWeeklySummary = true; - } - } - - // This needs to run AFTER the check for weekly summary above because the summaries array will be updated/shifted after this function runs. - await processWeeklySummariesByUserId(personId); - - const results = await dashboardHelper.laborthisweek( - personId, - pdtStartOfLastWeek, - pdtEndOfLastWeek, - ); - - const { timeSpent_hrs: timeSpent } = results[0]; - - const weeklycommittedHours = user.weeklycommittedHours + (user.missedHours ?? 0); - - const timeNotMet = timeSpent < weeklycommittedHours; - - let description; - - const timeRemaining = weeklycommittedHours - timeSpent; - - /** Check if the user is new user to prevent blue square assignment - * Condition: - * 1. Not Started: Start Date > end date of last week && totalTangibleHrs === 0 && totalIntangibleHrs === 0 - * 2. Short Week: Start Date (First time entrie) is after Monday && totalTangibleHrs === 0 && totalIntangibleHrs === 0 - * 3. No hours logged, and the account was after the start of last week. - * - * Notes: - * 1. Start date is automatically updated upon first time-log. - * 2. User meet above condition but meet minimum hours without submitting weekly summary - * should get a blue square as reminder. - * */ - let isNewUser = false; - const userStartDate = moment(person.startDate); - if ( - person.totalTangibleHrs === 0 && - person.totalIntangibleHrs === 0 && - timeSpent === 0 && - userStartDate.isAfter(pdtStartOfLastWeek) - ) { - console.log('1'); - isNewUser = true; - } - - if ( - userStartDate.isAfter(pdtEndOfLastWeek) || - (userStartDate.isAfter(pdtStartOfLastWeek) && - userStartDate.isBefore(pdtEndOfLastWeek) && - timeUtils.getDayOfWeekStringFromUTC(person.startDate) > 1) - ) { - console.log('2'); - isNewUser = true; - } - - const updateResult = await userProfile.findByIdAndUpdate( - personId, - { - $inc: { - totalTangibleHrs: timeSpent || 0, - }, - $max: { - personalBestMaxHrs: timeSpent || 0, - }, - $push: { - savedTangibleHrs: { $each: [timeSpent || 0], $slice: -200 }, - }, - $set: { - lastWeekTangibleHrs: timeSpent || 0, - }, - }, - { new: true }, - ); - - if ( - updateResult?.weeklySummaryOption === 'Not Required' || - updateResult?.weeklySummaryNotReq - ) { - hasWeeklySummary = true; - } - - const cutOffDate = moment().subtract(1, 'year'); - - const oldInfringements = []; - for (let k = 0; k < updateResult?.infringements.length; k += 1) { - if ( - updateResult?.infringements && - moment(updateResult?.infringements[k].date).diff(cutOffDate) >= 0 - ) { - oldInfringements.push(updateResult.infringements[k]); - } else { - break; - } - } - // use histroy Infringements to align the highlight requirements - let historyInfringements = 'No Previous Infringements.'; - if (oldInfringements.length) { - userProfile.findByIdAndUpdate( - personId, - { - $push: { - oldInfringements: { $each: oldInfringements, $slice: -10 }, - }, - }, - { new: true }, - ); - historyInfringements = oldInfringements - .map((item, index) => { - let enhancedDescription; - if (item.description) { - let sentences = item.description.split('.'); - const dateRegex = - /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; - sentences = sentences.map((sentence) => - sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { - const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; - }), - ); - if (sentences[0].includes('System auto-assigned infringement for two reasons')) { - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else if ( - sentences[0].includes( - 'System auto-assigned infringement for editing your time entries', - ) - ) { - sentences[0] = sentences[0].replace( - /time entries <(\d+)>\s*times/i, - 'time entries $1 times', - ); - enhancedDescription = sentences.join('.'); - } else if (sentences[0].includes('System auto-assigned infringement')) { - sentences[0] = sentences[0].replace( - /(not submitting a weekly summary)/gi, - '$1', - ); - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else { - enhancedDescription = `${item.description}`; - } - } - return `

${index + 1}. Date: ${moment( - item.date, - ).format('M-D-YYYY')}, Description: ${enhancedDescription}

`; - }) - .join(''); - } - // No extra hours is needed if blue squares isn't over 5. - // length +1 is because new infringement hasn't been created at this stage. - const coreTeamExtraHour = Math.max(0, oldInfringements.length - 5); - const utcStartMoment = moment(pdtStartOfLastWeek).add(1, 'second'); - const utcEndMoment = moment(pdtEndOfLastWeek).subtract(1, 'day').subtract(1, 'second'); - - const requestsForTimeOff = await timeOffRequest.find({ - requestFor: personId, - startingDate: { $lte: utcStartMoment }, - endingDate: { $gte: utcEndMoment }, - }); - - const hasTimeOffRequest = requestsForTimeOff.length > 0; - let requestForTimeOff; - let requestForTimeOffStartingDate; - let requestForTimeOffEndingDate; - let requestForTimeOffreason; - let requestForTimeOffEmailBody; - - if (hasTimeOffRequest) { - // eslint-disable-next-line prefer-destructuring - requestForTimeOff = requestsForTimeOff[0]; - requestForTimeOffStartingDate = moment(requestForTimeOff.startingDate).format( - 'dddd M-D-YYYY', - ); - requestForTimeOffEndingDate = moment(requestForTimeOff.endingDate).format( - 'dddd M-D-YYYY', - ); - requestForTimeOffreason = requestForTimeOff.reason; - requestForTimeOffEmailBody = `You had scheduled time off From ${requestForTimeOffStartingDate}, To ${requestForTimeOffEndingDate}, due to: ${requestForTimeOffreason}`; - } - - if (timeNotMet || !hasWeeklySummary) { - if (hasTimeOffRequest) { - description = requestForTimeOffreason; - } else if (timeNotMet && !hasWeeklySummary) { - if (person.role === 'Core Team') { - description = `System auto-assigned infringement for two reasons: not meeting weekly volunteer time commitment as well as not submitting a weekly summary. In the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', - )} and ending ${pdtEndOfLastWeek.format( - 'dddd M-D-YYYY', - )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ - person.weeklycommittedHours - } hours + ${ - person.missedHours ?? 0 - } hours owed for last week + ${coreTeamExtraHour} hours owed for this being your ${moment - .localeData() - .ordinal( - oldInfringements.length + 1, - )} blue square. So you should have completed ${weeklycommittedHours} hours and you completed ${timeSpent.toFixed( - 2, - )} hours.`; - } else { - description = `System auto-assigned infringement for two reasons: not meeting weekly volunteer time commitment as well as not submitting a weekly summary. For the hours portion, you logged ${timeSpent.toFixed( - 2, - )} hours against a committed effort of ${weeklycommittedHours} hours in the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', - )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; - } - } else if (timeNotMet) { - if (person.role === 'Core Team') { - description = `System auto-assigned infringement for not meeting weekly volunteer time commitment. In the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', - )} and ending ${pdtEndOfLastWeek.format( - 'dddd M-D-YYYY', - )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ - user.weeklycommittedHours - } hours + ${ - person.missedHours ?? 0 - } hours owed for last week + ${coreTeamExtraHour} hours owed for this being your ${moment - .localeData() - .ordinal( - oldInfringements.length + 1, - )} blue square. So you should have completed ${weeklycommittedHours} hours and you completed ${timeSpent.toFixed( - 2, - )} hours.`; - } else { - description = `System auto-assigned infringement for not meeting weekly volunteer time commitment. You logged ${timeSpent.toFixed( - 2, - )} hours against a committed effort of ${weeklycommittedHours} hours in the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', - )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; - } - } else { - description = `System auto-assigned infringement for not submitting a weekly summary for the week starting ${pdtStartOfLastWeek.format( - 'dddd M-D-YYYY', - )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; - } - - const infringement = { - date: moment().utc().format('YYYY-MM-DD'), - description, - createdDate: hasTimeOffRequest - ? moment(requestForTimeOff.createdAt).format('YYYY-MM-DD') - : null, - }; - // Only assign blue square and send email if the user IS NOT a new user - // Otherwise, display notification to users if new user && met the time requirement && weekly summary not submitted - // All other new users will not receive a blue square or notification - let emailBody = ''; - if (!isNewUser) { - const status = await userProfile.findByIdAndUpdate( - personId, - { - $push: { - infringements: infringement, - }, - }, - { new: true }, - ); - const administrativeContent = { - startDate: moment(person.startDate).utc().format('M-D-YYYY'), - role: person.role, - userTitle: person.jobTitle[0], - historyInfringements, - }; - if (person.role === 'Core Team' && timeRemaining > 0) { - emailBody = getInfringementEmailBody( - status.firstName, - status.lastName, - infringement, - status.infringements.length, - timeRemaining, - coreTeamExtraHour, - requestForTimeOffEmailBody, - administrativeContent, - weeklycommittedHours, - ); - } else { - emailBody = getInfringementEmailBody( - status.firstName, - status.lastName, - infringement, - status.infringements.length, - undefined, - null, - requestForTimeOffEmailBody, - administrativeContent, - ); - } - - let emailsBCCs; - /* eslint-disable array-callback-return */ - const blueSquareBCCs = await BlueSquareEmailAssignment.find() - .populate('assignedTo') - .exec(); - if (blueSquareBCCs.length > 0) { - emailsBCCs = blueSquareBCCs.map((assignment) => { - if (assignment.assignedTo.isActive === true) { - return assignment.email; - } - }); - } else { - emailsBCCs = null; - } - - emailSender( - status.email, - 'New Infringement Assigned', - emailBody, - emailsBCCs, - 'onecommunityglobal@gmail.com', - status.email, - null, - ); - } else if (isNewUser && !timeNotMet && !hasWeeklySummary) { - usersRequiringBlueSqNotification.push(personId); - } - - const categories = await dashboardHelper.laborThisWeekByCategory( - personId, - pdtStartOfLastWeek, - pdtEndOfLastWeek, - ); - - if (Array.isArray(categories) && categories.length > 0) { - await userProfile.findOneAndUpdate( - { _id: personId, categoryTangibleHrs: { $exists: false } }, - { $set: { categoryTangibleHrs: [] } }, - ); - } else { - continue; - } - - for (let j = 0; j < categories.length; j += 1) { - const elem = categories[j]; - - if (elem._id == null) { - elem._id = 'Other'; - } - - const updateResult2 = await userProfile.findOneAndUpdate( - { _id: personId, 'categoryTangibleHrs.category': elem._id }, - { $inc: { 'categoryTangibleHrs.$.hrs': elem.timeSpent_hrs } }, - { new: true }, - ); - - if (!updateResult2) { - await userProfile.findOneAndUpdate( - { - _id: personId, - 'categoryTangibleHrs.category': { $ne: elem._id }, - }, - { - $addToSet: { - categoryTangibleHrs: { - category: elem._id, - hrs: elem.timeSpent_hrs, - }, - }, - }, - ); - } - } - } - if (cache.hasCache(`user-${personId}`)) { - cache.removeCache(`user-${personId}`); - } - } - // eslint-disable-next-line no-use-before-define - await deleteOldTimeOffRequests(); - // Create notification for users who are new and met the time requirement but weekly summary not submitted - // Since the notification is required a sender, we fetch an owner user as the sender for the system generated notification - if (usersRequiringBlueSqNotification.length > 0) { - const senderId = await userProfile.findOne({ role: 'Owner', isActive: true }, '_id'); - await notificationService.createNotification( - senderId._id, - usersRequiringBlueSqNotification, - NEW_USER_BLUE_SQUARE_NOTIFICATION_MESSAGE, - true, - false, - ); - } - } catch (err) { - logger.logException(err); - } - - // processWeeklySummaries for nonActive users - try { - const inactiveUsers = await userProfile.find({ isActive: false }, '_id'); - for (let i = 0; i < inactiveUsers.length; i += 1) { - const user = inactiveUsers[i]; - - await processWeeklySummariesByUserId(mongoose.Types.ObjectId(user._id), false); - } - } catch (err) { - logger.logException(err); - } - }; - - const applyMissedHourForCoreTeam = async () => { - try { - const currentDate = moment().tz('America/Los_Angeles').format(); - - logger.logInfo( - `Job for applying missed hours for Core Team members starting at ${currentDate}`, - ); - - const startOfLastWeek = moment() - .tz('America/Los_Angeles') - .startOf('week') - .subtract(1, 'week') - .format('YYYY-MM-DD'); - - const endOfLastWeek = moment() - .tz('America/Los_Angeles') - .endOf('week') - .subtract(1, 'week') - .format('YYYY-MM-DD'); - - const missedHours = await userProfile.aggregate([ - { - $match: { - role: 'Core Team', - isActive: true, - }, - }, - { - $lookup: { - from: 'timeEntries', - localField: '_id', - foreignField: 'personId', - pipeline: [ - { - $match: { - $expr: { - $and: [ - { $eq: ['$isTangible', true] }, - { $gte: ['$dateOfWork', startOfLastWeek] }, - { $lte: ['$dateOfWork', endOfLastWeek] }, - ], - }, - }, - }, - ], - as: 'timeEntries', - }, - }, - { - $project: { - _id: 1, - missedHours: { - $max: [ - { - $subtract: [ - { - $sum: [{ $ifNull: ['$missedHours', 0] }, '$weeklycommittedHours'], - }, - { - $divide: [ - { - $sum: { - $map: { - input: '$timeEntries', - in: '$$this.totalSeconds', - }, - }, - }, - 3600, - ], - }, - ], - }, - 0, - ], - }, - }, - }, - ]); - - const bulkOps = []; - - missedHours.forEach((obj) => { - bulkOps.push({ - updateOne: { - filter: { _id: obj._id }, - update: { missedHours: obj.missedHours }, - }, - }); - }); - - await userProfile.bulkWrite(bulkOps); - } catch (err) { - logger.logException(err); - } - }; - - const deleteBlueSquareAfterYear = async () => { - const currentFormattedDate = moment().tz('America/Los_Angeles').format(); - - logger.logInfo( - `Job for deleting blue squares older than 1 year starting at ${currentFormattedDate}`, - ); - - const cutOffDate = moment().subtract(1, 'year').format('YYYY-MM-DD'); - - try { - const results = await userProfile.updateMany( - {}, - { - $pull: { - infringements: { - date: { - $lte: cutOffDate, - }, - }, - }, - }, - ); - - logger.logInfo(`Job deleting blue squares older than 1 year finished - at ${moment().tz('America/Los_Angeles').format()} \nReulst: ${JSON.stringify(results)}`); - } catch (err) { - logger.logException(err); - } - }; - - const reActivateUser = async () => { - const currentFormattedDate = moment().tz('America/Los_Angeles').format(); - - logger.logInfo( - `Job for activating users based on scheduled re-activation date starting at ${currentFormattedDate}`, - ); - - try { - const users = await userProfile.find( - { isActive: false, reactivationDate: { $exists: true } }, - '_id isActive reactivationDate', - ); - for (let i = 0; i < users.length; i += 1) { - const user = users[i]; - if (moment().isSameOrAfter(moment(user.reactivationDate))) { - await userProfile.findByIdAndUpdate( - user._id, - { - $set: { - isActive: true, - }, - $unset: { - endDate: user.endDate, - }, - }, - { new: true }, - ); - logger.logInfo( - `User with id: ${user._id} was re-acticated at ${moment() - .tz('America/Los_Angeles') - .format()}.`, - ); - const id = user._id; - const person = await userProfile.findById(id); - - const endDate = moment(person.endDate).format('YYYY-MM-DD'); - logger.logInfo(`User with id: ${user._id} was re-acticated at ${moment().format()}.`); - - const subject = `IMPORTANT:${person.firstName} ${person.lastName} has been RE-activated in the Highest Good Network`; - - const emailBody = `

Hi Admin!

- -

This email is to let you know that ${person.firstName} ${person.lastName} has been made active again in the Highest Good Network application after being paused on ${endDate}.

- -

If you need to communicate anything with them, this is their email from the system: ${person.email}.

- -

Thanks!

- -

The HGN A.I. (and One Community)

`; - - emailSender('onecommunityglobal@gmail.com', subject, emailBody, null, null, person.email); - } - } - } catch (err) { - logger.logException(err); - } - }; - - const notifyInfringements = function ( - original, - current, - firstName, - lastName, - emailAddress, - role, - startDate, - jobTitle, - ) { - if (!current) return; - const newOriginal = original.toObject(); - const newCurrent = current.toObject(); - const totalInfringements = newCurrent.length; - let newInfringements = []; - let historyInfringements = 'No Previous Infringements.'; - if (original.length) { - historyInfringements = original - .map((item, index) => { - let enhancedDescription; - if (item.description) { - let sentences = item.description.split('.'); - const dateRegex = - /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; - sentences = sentences.map((sentence) => - sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { - const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( - 'M-D-YYYY', - ); - return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; - }), - ); - if (sentences[0].includes('System auto-assigned infringement for two reasons')) { - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else if ( - sentences[0].includes( - 'System auto-assigned infringement for editing your time entries', - ) - ) { - sentences[0] = sentences[0].replace( - /time entries <(\d+)>\s*times/i, - 'time entries $1 times', - ); - enhancedDescription = sentences.join('.'); - } else if (sentences[0].includes('System auto-assigned infringement')) { - sentences[0] = sentences[0].replace( - /(not submitting a weekly summary)/gi, - '$1', - ); - sentences[0] = sentences[0].replace( - /(not meeting weekly volunteer time commitment)/gi, - '$1', - ); - enhancedDescription = sentences.join('.'); - enhancedDescription = enhancedDescription.replace( - /logged (\d+(\.\d+)?\s*hours)/i, - 'logged $1', - ); - } else { - enhancedDescription = `${item.description}`; - } - } - return `

${index + 1}. Date: ${moment(item.date).format('M-D-YYYY')}, Description: ${enhancedDescription}

`; - }) - .join(''); - } - const administrativeContent = { - startDate: moment(startDate).utc().format('M-D-YYYY'), - role, - userTitle: jobTitle, - historyInfringements, - }; - newInfringements = _.differenceWith(newCurrent, newOriginal, (arrVal, othVal) => - arrVal._id.equals(othVal._id), - ); - newInfringements.forEach((element) => { - emailSender( - emailAddress, - 'New Infringement Assigned', - getInfringementEmailBody( - firstName, - lastName, - element, - totalInfringements, - undefined, - undefined, - undefined, - administrativeContent, - ), - null, - 'onecommunityglobal@gmail.com', - emailAddress, - ); - }); - }; - - const replaceBadge = async function (personId, oldBadgeId, newBadgeId) { - userProfile.updateOne( - { _id: personId, 'badgeCollection.badge': oldBadgeId }, - { - $set: { - 'badgeCollection.$.badge': newBadgeId, - 'badgeCollection.$.lastModified': Date.now().toString(), - 'badgeCollection.$.count': 1, - 'badgeCollection.$.earnedDate': [earnedDateBadge()], - }, - }, - (err) => { - if (err) { - throw new Error(err); - } - }, - ); - }; - - const increaseBadgeCount = async function (personId, badgeId) { - userProfile.updateOne( - { _id: personId, 'badgeCollection.badge': badgeId }, - { - $inc: { 'badgeCollection.$.count': 1 }, - $set: { 'badgeCollection.$.lastModified': Date.now().toString() }, - $push: { 'badgeCollection.$.earnedDate': earnedDateBadge() }, - }, - (err) => { - if (err) { - console.log(err); - } - }, - ); - }; - - const addBadge = async function (personId, badgeId, count = 1, featured = false) { - userProfile.findByIdAndUpdate( - personId, - { - $push: { - badgeCollection: { - badge: badgeId, - count, - earnedDate: [earnedDateBadge()], - featured, - lastModified: Date.now().toString(), - }, - }, - }, - (err) => { - if (err) { - throw new Error(err); - } - }, - ); - }; - - const removeDupBadge = async function (personId, badgeId) { - userProfile.findByIdAndUpdate( - personId, - { - $pull: { - badgeCollection: { badge: mongoose.Types.ObjectId(badgeId) }, - }, - }, - { new: true }, - (err) => { - if (err) { - throw new Error(err); - } - }, - ); - }; - - const changeBadgeCount = async function (personId, badgeId, count) { - if (count === 0) { - removeDupBadge(personId, badgeId); - } else if (count) { - // Process exisiting earned date to match the new count - try { - const userInfo = await userProfile.findById(personId); - let newEarnedDate = []; - const recordToUpdate = userInfo.badgeCollection.find( - (item) => item.badge._id.toString() === badgeId.toString(), - ); - if (!recordToUpdate) { - throw new Error( - `Failed to update badge for ${personId}. Badge not found ${badgeId.toString()}`, - ); - } - // If the count is the same, do nothing - if (recordToUpdate.count === count) { - return; - } - const copyOfEarnedDate = recordToUpdate.earnedDate; - // Update: We refrain from automatically correcting the mismatch problem as we intend to preserve the original - // earned date even when a badge is deleted. This approach ensures that a record of badges earned is maintained, - // preventing oversight of any mismatches caused by bugs. - if (recordToUpdate.count < count) { - let dateToAdd = count - recordToUpdate.count; - // if the EarnedDate count is less than the new count, add a earned date to the end of the collection - while (dateToAdd > 0) { - copyOfEarnedDate.push(earnedDateBadge()); - dateToAdd -= 1; - } - } - newEarnedDate = [...copyOfEarnedDate]; - userProfile.updateOne( - { _id: personId, 'badgeCollection.badge': badgeId }, - { - $set: { - 'badgeCollection.$.count': count, - 'badgeCollection.$.lastModified': Date.now().toString(), - 'badgeCollection.$.earnedDate': newEarnedDate, - 'badgeCollection.$.hasBadgeDeletionImpact': recordToUpdate.count > count, // badge deletion impact set to true if the new count is less than the old count - }, - }, - (err) => { - if (err) { - throw new Error(err); - } - }, - ); - } catch (err) { - logger.logException(err); - } - } - }; - - // remove the last badge you earned on this streak(not including 1) - - const removePrevHrBadge = async function (personId, user, badgeCollection, hrs, weeks) { - // Check each Streak Greater than One to check if it works - if (weeks < 2) { - return; - } - let removed = false; - await badge - .aggregate([ - { - $match: { - type: 'X Hours for X Week Streak', - weeks: { $gt: 0, $lt: weeks }, - totalHrs: hrs, - }, - }, - { $sort: { weeks: -1, totalHrs: -1 } }, - { - $group: { - _id: '$weeks', - badges: { - $push: { _id: '$_id', hrs: '$totalHrs', weeks: '$weeks' }, - }, - }, - }, - ]) - .then((results) => { - results.forEach((streak) => { - streak.badges.every((bdge) => { - for (let i = 0; i < badgeCollection.length; i += 1) { - if ( - badgeCollection[i].badge?.type === 'X Hours for X Week Streak' && - badgeCollection[i].badge?.weeks === bdge.weeks && - badgeCollection[i].badge?.totalHrs === hrs && - !removed - ) { - changeBadgeCount( - personId, - badgeCollection[i].badge._id, - badgeCollection[i].count - 1, - ); - removed = true; - return false; - } - } - return true; - }); - }); - }); - }; - - // 'No Infringement Streak', - const checkNoInfringementStreak = async function (personId, user, badgeCollection) { - let badgeOfType; - for (let i = 0; i < badgeCollection.length; i += 1) { - if (badgeCollection[i].badge?.type === 'No Infringement Streak') { - if (badgeOfType && badgeOfType.months <= badgeCollection[i].badge.months) { - removeDupBadge(personId, badgeOfType._id); - badgeOfType = badgeCollection[i].badge; - } else if (badgeOfType && badgeOfType.months > badgeCollection[i].badge.months) { - removeDupBadge(personId, badgeCollection[i].badge._id); - } else if (!badgeOfType) { - badgeOfType = badgeCollection[i].badge; - } - } - } - await badge - .find({ type: 'No Infringement Streak' }) - .sort({ months: -1 }) - .then((results) => { - if (!Array.isArray(results) || !results.length) { - return; - } - - results.every((elem) => { - // Cannot account for time paused yet - - if (elem.months <= 12) { - if (moment().diff(moment(user.createdDate), 'months', true) >= elem.months) { - if ( - user.infringements.length === 0 || - Math.abs( - moment().diff( - moment( - // eslint-disable-next-line no-unsafe-optional-chaining - user.infringements[user.infringements?.length - 1].date, - ), - 'months', - true, - ), - ) >= elem.months - ) { - if (badgeOfType) { - if (badgeOfType._id.toString() !== elem._id.toString()) { - replaceBadge( - personId, - mongoose.Types.ObjectId(badgeOfType._id), - mongoose.Types.ObjectId(elem._id), - ); - } - return false; - } - addBadge(personId, mongoose.Types.ObjectId(elem._id)); - return false; - } - } - } else if (user?.infringements?.length === 0) { - if (moment().diff(moment(user.createdDate), 'months', true) >= elem.months) { - if ( - user.oldInfringements.length === 0 || - Math.abs( - moment().diff( - moment( - // eslint-disable-next-line no-unsafe-optional-chaining - user.oldInfringements[user.oldInfringements?.length - 1].date, - ), - 'months', - true, - ), - ) >= - elem.months - 12 - ) { - if (badgeOfType) { - if (badgeOfType._id.toString() !== elem._id.toString()) { - replaceBadge( - personId, - mongoose.Types.ObjectId(badgeOfType._id), - mongoose.Types.ObjectId(elem._id), - ); - } - return false; - } - addBadge(personId, mongoose.Types.ObjectId(elem._id)); - return false; - } - } - } - return true; - }); - }); - }; - - // 'Minimum Hours Multiple', - const checkMinHoursMultiple = async function (personId, user, badgeCollection) { - const badgesOfType = badgeCollection - .map((obj) => obj.badge) - .filter((badgeItem) => badgeItem.type === 'Minimum Hours Multiple'); - await badge - .find({ type: 'Minimum Hours Multiple' }) - .sort({ multiple: -1 }) - .then((results) => { - if (!Array.isArray(results) || !results.length) { - return; - } - for (let i = 0; i < results.length; i += 1) { - // this needs to be a for loop so that the returns break before assigning badges for lower multiples - const elem = results[i]; // making variable elem accessible for below code - - if (user.lastWeekTangibleHrs / user.weeklycommittedHours >= elem.multiple) { - const theBadge = badgesOfType.find( - (badgeItem) => badgeItem._id.toString() === elem._id.toString(), - ); - return theBadge - ? increaseBadgeCount(personId, mongoose.Types.ObjectId(theBadge._id)) - : addBadge(personId, mongoose.Types.ObjectId(elem._id)); - } - } - }); - }; - - const getAllWeeksData = async (personId, user) => { - const userId = mongoose.Types.ObjectId(personId); - const weeksData = []; - const currentDate = moment().tz('America/Los_Angeles'); - const startDate = moment(user.createdDate).tz('America/Los_Angeles'); - const numWeeks = Math.ceil(currentDate.diff(startDate, 'days') / 7); - - // iterate through weeks to get hours of each week - for (let week = 1; week <= numWeeks; week += 1) { - const pdtstart = startDate - .clone() - .add(week - 1, 'weeks') - .startOf('week') - .format('YYYY-MM-DD'); - const pdtend = startDate.clone().add(week, 'weeks').subtract(1, 'days').format('YYYY-MM-DD'); - try { - const results = await dashboardHelper.laborthisweek(userId, pdtstart, pdtend); - const { timeSpent_hrs: timeSpent } = results[0]; - weeksData.push(timeSpent); - } catch (error) { - console.error(error); - throw error; - } - } - return weeksData; - }; - - const getMaxHrs = async (personId, user) => { - const weeksdata = await getAllWeeksData(personId, user); - return Math.max(...weeksdata); - }; - - const updatePersonalMax = async (personId, user) => { - try { - const MaxHrs = await getMaxHrs(personId, user); - user.personalBestMaxHrs = MaxHrs; - await user.save(); - } catch (error) { - console.error(error); - } - }; - - // 'Personal Max', - const checkPersonalMax = async function (personId, user, badgeCollection) { - let badgeOfType; - const duplicateBadges = []; - - for (let i = 0; i < badgeCollection.length; i += 1) { - if (badgeCollection[i].badge?.type === 'Personal Max') { - if (!badgeOfType) { - badgeOfType = badgeCollection[i]; - } else { - duplicateBadges.push(badgeCollection[i]); - } - } - // eslint-disable-next-line no-restricted-syntax - for (const b of duplicateBadges) { - await removeDupBadge(personId, b._id); - } - } - await badge.findOne({ type: 'Personal Max' }).then((results) => { - const currentDate = moment(moment().format('MM-DD-YYYY'), 'MM-DD-YYYY') - .tz('America/Los_Angeles') - .format('MMM-DD-YY'); - if ( - user.lastWeekTangibleHrs && - user.lastWeekTangibleHrs >= user.personalBestMaxHrs && - !badgeOfType.earnedDate.includes(currentDate) - ) { - if (badgeOfType) { - increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType.badge._id)); - // Update the earnedDate array with the new date - badgeOfType.earnedDate.unshift(moment().format('MMM-DD-YYYY')); - } else { - addBadge(personId, mongoose.Types.ObjectId(results._id), user.personalBestMaxHrs); - } - } - }); - }; - - // 'Most Hrs in Week' - - const checkMostHrsWeek = async function (personId, user, badgeCollection) { - if (user.weeklycommittedHours > 0 && user.lastWeekTangibleHrs > user.weeklycommittedHours) { - const badgeOfType = badgeCollection - .filter((object) => object.badge.type === 'Most Hrs in Week') - .map((object) => object.badge); - await badge.findOne({ type: 'Most Hrs in Week' }).then((results) => { - userProfile - .aggregate([ - { $match: { isActive: true } }, - { $group: { _id: 1, maxHours: { $max: '$lastWeekTangibleHrs' } } }, - ]) - .then((userResults) => { - if (badgeOfType.length > 1) { - removeDupBadge(user._id, badgeOfType[0]._id); - } - - if (user.lastWeekTangibleHrs && user.lastWeekTangibleHrs >= userResults[0].maxHours) { - if (badgeOfType.length) { - increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType[0]._id)); - } else { - addBadge(personId, mongoose.Types.ObjectId(results._id)); - } - } - }); - }); - } - }; - - // 'X Hours in one week', - const checkXHrsInOneWeek = async function (personId, user, badgeCollection) { - const badgesOfType = []; - for (let i = 0; i < badgeCollection.length; i += 1) { - if (badgeCollection[i].badge?.type === 'X Hours for X Week Streak') { - badgesOfType.push(badgeCollection[i].badge); - } - } - await badge - .find({ type: 'X Hours for X Week Streak', weeks: 1 }) - .sort({ totalHrs: -1 }) - .then((results) => { - results.every((elem) => { - if (elem.totalHrs <= user.lastWeekTangibleHrs) { - let theBadge; - for (let i = 0; i < badgesOfType.length; i += 1) { - if (badgesOfType[i]._id.toString() === elem._id.toString()) { - theBadge = badgesOfType[i]._id; - break; - } - } - if (theBadge) { - increaseBadgeCount(personId, mongoose.Types.ObjectId(theBadge)); - return false; - } - addBadge(personId, mongoose.Types.ObjectId(elem._id)); - return false; - } - return true; - }); - }); - }; - - // 'X Hours for X Week Streak', - const checkXHrsForXWeeks = async function (personId, user, badgeCollection) { - let higherBadge = false; - // Check each Streak Greater than One to check if it works - await badge - .aggregate([ - { $match: { type: 'X Hours for X Week Streak', weeks: { $gt: 1 } } }, - // Group by 'week' property and sorting groups in descending order by 'week', then sorting badges within groups by 'totalHrs' in descending order. - { - $group: { - _id: '$weeks', - badges: { - $push: { _id: '$_id', hrs: '$totalHrs', weeks: '$weeks' }, - }, - }, - }, - { - $project: { - _id: 1, - badges: { - $slice: [ - { - $map: { - input: '$badges', - in: { - _id: '$$this._id', - hrs: '$$this.hrs', - weeks: '$$this.weeks', - }, - }, - }, - { $size: '$badges' }, - ], - }, - }, - }, - { $unwind: '$badges' }, - { $sort: { _id: -1, 'badges.hrs': -1 } }, // Primary sort on _id, secondary sort on badges.hrs - { - $group: { - _id: '$_id', - badges: { - $push: { - _id: '$badges._id', - hrs: '$badges.hrs', - weeks: '$badges.weeks', - }, - }, - }, - }, - { $sort: { _id: -1 } }, // Add this $sort stage for the final sorting by _id - ]) - .then((results) => { - let lastHr = -1; - results.forEach((streak) => { - streak.badges.every((bdge) => { - let badgeOfType; - for (let i = 0; i < badgeCollection.length; i += 1) { - if ( - badgeCollection[i].badge?.type === 'X Hours for X Week Streak' && - badgeCollection[i].badge?.weeks === bdge.weeks - ) { - if (badgeOfType && badgeOfType.totalHrs <= badgeCollection[i].badge.totalHrs) { - removeDupBadge(personId, badgeOfType._id); - badgeOfType = badgeCollection[i].badge; - } else if ( - badgeOfType && - badgeOfType.totalHrs > badgeCollection[i].badge.totalHrs - ) { - removeDupBadge(personId, badgeCollection[i].badge._id); - } else if (!badgeOfType) { - badgeOfType = badgeCollection[i].badge; - } - } - } - // check if it is possible to earn this streak - if (user.savedTangibleHrs.length >= bdge.weeks) { - let awardBadge = true; - const endOfArr = user.savedTangibleHrs.length - 1; - for (let i = endOfArr; i >= endOfArr - bdge.weeks + 1; i -= 1) { - if (user.savedTangibleHrs[i] < bdge.hrs) { - awardBadge = false; - return true; - } - } - // if all checks for award badge are green double check that we havent already awarded a higher streak for the same number of hours - if (awardBadge && bdge.hrs > lastHr) { - higherBadge = true; - lastHr = bdge.hrs; - if (badgeOfType && badgeOfType.totalHrs < bdge.hrs) { - replaceBadge( - personId, - mongoose.Types.ObjectId(badgeOfType._id), - mongoose.Types.ObjectId(bdge._id), - ); - - removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); - } else if (!badgeOfType) { - addBadge(personId, mongoose.Types.ObjectId(bdge._id)); - removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); - } else if (badgeOfType && badgeOfType.totalHrs === bdge.hrs) { - const lowerBound = badgeOfType.weeks; - let upperBound; - streak = 0; - - switch (bdge.weeks) { - case 2: - // In between 2Wk and 3Wk - upperBound = 3; - break; - case 3: - // In between 3Wk and 4Wk - upperBound = 4; - break; - case 4: - // In between 4Wk and 6Wk - upperBound = 6; - break; - case 6: - // In between 6Wk and 10Wk - upperBound = 10; - break; - case 10: - // In between 10Wk and 15Wk - upperBound = 15; - break; - case 15: - // In between 50Wk and 20Wk - upperBound = 20; - break; - case 20: - // In between 20Wk and 40Wk - upperBound = 40; - break; - case 40: - // In between 40Wk and 60Wk - upperBound = 60; - break; - case 60: - // In between 60Wk and 80Wk - upperBound = 80; - break; - case 80: - // In between 80Wk and 100Wk - upperBound = 100; - break; - case 100: - // In between 100Wk and 150Wk - upperBound = 150; - break; - case 150: - // In between 150Wk and 200Wk - upperBound = 200; - break; - default: - // Default case. Exiting function. - return; - } - for (let i = endOfArr; i >= endOfArr - upperBound + 1; i -= 1) { - if (user.savedTangibleHrs[i] >= bdge.hrs) { - streak += 1; - } - } - if (streak > lowerBound && streak < upperBound) { - higherBadge = false; - console.log('You are currently building an existing streak, no badge awarded.'); - } else { - console.log('You are currently building a new streak, new badge awarded'); - increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType._id)); - removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); - } - } - return false; - } - } - return true; - }); - }); - }); - - // Handle Increasing the 1 week streak badges - if (!higherBadge) await checkXHrsInOneWeek(personId, user, badgeCollection); - }; - - // 'Lead a team of X+' - - const checkLeadTeamOfXplus = async function (personId, user, badgeCollection) { - const leaderRoles = ['Mentor', 'Manager', 'Administrator', 'Owner', 'Core Team']; - const approvedRoles = ['Mentor', 'Manager']; - if (!approvedRoles.includes(user.role)) return; - - let teamMembers; - await getTeamMembers({ - _id: personId, - }).then((results) => { - if (results) { - teamMembers = results.myteam; - } else { - teamMembers = []; - } - }); - - const objIds = {}; - - teamMembers = teamMembers.filter((member) => { - if (leaderRoles.includes(member.role)) return false; - if (objIds[member._id]) return false; - objIds[member._id] = true; - - return true; - }); - let badgeOfType; - for (let i = 0; i < badgeCollection.length; i += 1) { - if (badgeCollection[i].badge?.type === 'Lead a team of X+') { - if (badgeOfType && badgeOfType.people <= badgeCollection[i].badge.people) { - removeDupBadge(personId, badgeOfType._id); - badgeOfType = badgeCollection[i].badge; - } else if (badgeOfType && badgeOfType.people > badgeCollection[i].badge.people) { - removeDupBadge(personId, badgeCollection[i].badge._id); - } else if (!badgeOfType) { - badgeOfType = badgeCollection[i].badge; - } - } - } - await badge - .find({ type: 'Lead a team of X+' }) - .sort({ people: -1 }) - .then((results) => { - if (!Array.isArray(results) || !results.length) { - return; - } - results.every((bg) => { - if (teamMembers && teamMembers.length >= bg.people) { - if (badgeOfType) { - if ( - badgeOfType._id.toString() !== bg._id.toString() && - badgeOfType.people < bg.people - ) { - replaceBadge( - personId, - mongoose.Types.ObjectId(badgeOfType._id), - - mongoose.Types.ObjectId(bg._id), - ); - } - return false; - } - addBadge(personId, mongoose.Types.ObjectId(bg._id)); - return false; - } - return true; - }); - }); - }; - - // 'Total Hrs in Category' - const checkTotalHrsInCat = async function (personId, user, badgeCollection) { - const hoursByCategory = user.hoursByCategory || {}; - const categories = [ - 'food', - 'energy', - 'housing', - 'education', - 'society', - 'economics', - 'stewardship', - ]; - - const badgesOfType = badgeCollection - .filter((object) => object.badge.type === 'Total Hrs in Category') - .map((object) => object.badge); - - categories.forEach(async (category) => { - const categoryHrs = Object.keys(hoursByCategory).find((elem) => elem === category); - - let badgeOfType; - for (let i = 0; i < badgeCollection.length; i += 1) { - if ( - badgeCollection[i].badge?.type === 'Total Hrs in Category' && - badgeCollection[i].badge?.category === category - ) { - if (badgeOfType && badgeOfType.totalHrs <= badgeCollection[i].badge.totalHrs) { - removeDupBadge(personId, badgeOfType._id); - badgeOfType = badgeCollection[i].badge; - } else if (badgeOfType && badgeOfType.totalHrs > badgeCollection[i].badge.totalHrs) { - removeDupBadge(personId, badgeCollection[i].badge._id); - } else if (!badgeOfType) { - badgeOfType = badgeCollection[i].badge; - } - } - } - - const newCatg = category.charAt(0).toUpperCase() + category.slice(1); - - await badge - .find({ type: 'Total Hrs in Category', category: newCatg }) - - .sort({ totalHrs: -1 }) - .then((results) => { - if (!Array.isArray(results) || !results.length || !categoryHrs) { - return; - } - - results.every((elem) => { - if ( - hoursByCategory[categoryHrs] >= 100 && - hoursByCategory[categoryHrs] >= elem.totalHrs - ) { - let theBadge; - for (let i = 0; i < badgesOfType.length; i += 1) { - if (badgesOfType[i]._id.toString() === elem._id.toString()) { - theBadge = badgesOfType[i]._id; - break; - } - } - if (theBadge) { - increaseBadgeCount(personId, mongoose.Types.ObjectId(theBadge)); - return false; - } - if (badgeOfType) { - if ( - badgeOfType._id.toString() !== elem._id.toString() && - badgeOfType.totalHrs < elem.totalHrs - ) { - replaceBadge( - personId, - mongoose.Types.ObjectId(badgeOfType._id), - mongoose.Types.ObjectId(elem._id), - ); - } - return false; - } - addBadge(personId, mongoose.Types.ObjectId(elem._id)); - return false; - } - return true; - }); - }); - }); - }; - - const awardNewBadges = async () => { - try { - const users = await userProfile.find({ isActive: true }).populate('badgeCollection.badge'); - for (let i = 0; i < users.length; i += 1) { - const user = users[i]; - const { _id, badgeCollection } = user; - const personId = mongoose.Types.ObjectId(_id); - - await updatePersonalMax(personId, user); - await checkPersonalMax(personId, user, badgeCollection); - await checkMostHrsWeek(personId, user, badgeCollection); - await checkMinHoursMultiple(personId, user, badgeCollection); - await checkTotalHrsInCat(personId, user, badgeCollection); - await checkLeadTeamOfXplus(personId, user, badgeCollection); - await checkXHrsForXWeeks(personId, user, badgeCollection); - await checkNoInfringementStreak(personId, user, badgeCollection); - // remove cache after badge asssignment. - if (cache.hasCache(`user-${_id}`)) { - cache.removeCache(`user-${_id}`); - } - } - } catch (err) { - logger.logException(err); - } - }; - - const getTangibleHoursReportedThisWeekByUserId = function (personId) { - const userId = mongoose.Types.ObjectId(personId); - - const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); - const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); - - return timeEntries - .find( - { - personId: userId, - dateOfWork: { $gte: pdtstart, $lte: pdtend }, - isTangible: true, - }, - 'totalSeconds', - ) - .then((results) => { - const totalTangibleWeeklySeconds = results.reduce( - (acc, { totalSeconds }) => acc + totalSeconds, - 0, - ); - return (totalTangibleWeeklySeconds / 3600).toFixed(2); - }); - }; - - const sendDeactivateEmailBody = function ( - firstName, - lastName, - endDate, - email, - recipients, - isSet, - ) { - if (endDate && !isSet) { - const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; - const emailBody = `

Management,

- -

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${endDate}. - Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

- -

With Gratitude,

- -

One Community

`; - recipients.push('onecommunityglobal@gmail.com'); - recipients = recipients.toString(); - emailSender(recipients, subject, emailBody, null, null, email); - } else if (isSet) { - const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; - const emailBody = `

Management,

- -

Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network ${endDate}. - For a smooth transition, please confirm all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.

- -

With Gratitude,

- -

One Community

`; - recipients.push('onecommunityglobal@gmail.com'); - recipients = recipients.toString(); - emailSender(recipients, subject, emailBody, null, null, email); - } - }; - - const deActivateUser = async () => { - try { - const emailReceivers = await userProfile.find( - { isActive: true, role: { $in: ['Owner'] } }, - '_id isActive role email', - ); - const recipients = emailReceivers.map((receiver) => receiver.email); - const users = await userProfile.find( - { isActive: true, endDate: { $exists: true } }, - '_id isActive endDate isSet', - ); - for (let i = 0; i < users.length; i += 1) { - const user = users[i]; - const { endDate } = user; - endDate.setHours(endDate.getHours() + 7); - if (moment().isAfter(moment(endDate).add(1, 'days'))) { - try { - await userProfile.findByIdAndUpdate( - user._id, - user.set({ - isActive: false, - }), - { new: true }, - ); - } catch (err) { - // Log the error and continue to the next user - logger.logException(err, `Error in deActivateUser. Failed to update User ${user._id}`); - continue; - } - const id = user._id; - const person = await userProfile.findById(id); - const lastDay = moment(person.endDate).format('YYYY-MM-DD'); - logger.logInfo(`User with id: ${user._id} was de-activated at ${moment().format()}.`); - person.teams.map(async (teamId) => { - const managementEmails = await userHelper.getTeamManagementEmail(teamId); - if (Array.isArray(managementEmails) && managementEmails.length > 0) { - managementEmails.forEach((management) => { - recipients.push(management.email); - }); - } - }); - sendDeactivateEmailBody( - person.firstName, - person.lastName, - lastDay, - person.email, - recipients, - person.isSet, - ); - } - } - } catch (err) { - logger.logException(err, 'Unexpected error in deActivateUser'); - } - }; - - // Update by Shengwei/Peter PR767: - /** - * Delete all tokens used in new user setup from database that in cancelled, expired, or used status. - * Data retention: 90 days - */ - const deleteExpiredTokens = async () => { - const ninetyDaysAgo = moment().subtract(90, 'days').toDate(); - try { - await token.deleteMany({ isCancelled: true, expiration: { $lt: ninetyDaysAgo } }); - } catch (error) { - /* eslint-disable no-undef */ - logger.logException(error, `Error in deleteExpiredTokens. Date ${currentDate}`); - } - }; - - const deleteOldTimeOffRequests = async () => { - const endOfLastWeek = moment().tz('America/Los_Angeles').endOf('week').subtract(1, 'week'); - - const utcEndMoment = moment(endOfLastWeek).subtract(1, 'day').add(1, 'second'); - try { - await timeOffRequest.deleteMany({ endingDate: { $lte: utcEndMoment } }); - } catch (error) { - logger.logException( - error, - `Error deleting expired time-off requests: utcEndMoment ${utcEndMoment}`, - ); - } - }; - - return { - changeBadgeCount, - getUserName, - getTeamMembers, - getTeamManagementEmail, - validateProfilePic, - assignBlueSquareForTimeNotMet, - applyMissedHourForCoreTeam, - deleteBlueSquareAfterYear, - reActivateUser, - sendDeactivateEmailBody, - deActivateUser, - notifyInfringements, - getInfringementEmailBody, - emailWeeklySummariesForAllUsers, - awardNewBadges, - checkXHrsForXWeeks, - getTangibleHoursReportedThisWeekByUserId, - deleteExpiredTokens, - deleteOldTimeOffRequests, - }; -}; - -module.exports = userHelper; From c6d8ed7e82a84b1e39f08a6eaccf746f85495f8a Mon Sep 17 00:00:00 2001 From: "Xiaoyu(Ivy) Chen" <102696148+20chen-7@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:25:42 -0600 Subject: [PATCH 15/31] Update userProfileController.js --- src/controllers/userProfileController.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index fff2df579..7ed5b17c1 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1184,7 +1184,6 @@ const userProfileController = function (UserProfile, Project) { isActive: status, reactivationDate: activationDate, endDate, - isSet, }); user .save() From e1c413ba6dca42811fa8235464bcc666ea743971 Mon Sep 17 00:00:00 2001 From: "Xiaoyu(Ivy) Chen" <102696148+20chen-7@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:39:46 -0600 Subject: [PATCH 16/31] Update userProfileController.js --- src/controllers/userProfileController.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 7ed5b17c1..e3b24884b 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1206,7 +1206,6 @@ const userProfileController = function (UserProfile, Project) { endDate, user.email, recipients, - isSet, ); auditIfProtectedAccountUpdated( req.body.requestor.requestorId, From ea3dadca18b32b1c3fac56666fa7fe7c58b724b5 Mon Sep 17 00:00:00 2001 From: "Xiaoyu(Ivy) Chen" <102696148+20chen-7@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:40:25 -0600 Subject: [PATCH 17/31] Update userProfileController.js --- src/controllers/userProfileController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index e3b24884b..a61cf3c43 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1184,6 +1184,7 @@ const userProfileController = function (UserProfile, Project) { isActive: status, reactivationDate: activationDate, endDate, + isSet, }); user .save() From 5c9cf30b2f2a00457955c2cd4f8c02bc351e531c Mon Sep 17 00:00:00 2001 From: Jatin Agrawal Date: Mon, 19 Aug 2024 19:17:39 -0700 Subject: [PATCH 18/31] update function for editing the quick setup modal working --- src/controllers/titleController.js | 130 ++++++++++++++--------------- src/startup/db.js | 2 +- 2 files changed, 63 insertions(+), 69 deletions(-) diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index 0d0b58ecd..fea79a98d 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -84,77 +84,70 @@ const titlecontroller = function (Title) { .catch((error) => res.status(404).send(error)); }; + // update title function. const updateTitle = async function (req, res) { - const title = new Title(); - const filter=req.body.id; - title.titleName = req.body.titleName; - title.teamCode = req.body.teamCode; - title.projectAssigned = req.body.projectAssigned; - title.mediaFolder = req.body.mediaFolder; - title.teamAssiged = req.body.teamAssiged; - - // valid title name - if (!title.titleName.trim()) { - res.status(400).send({ message: 'Title cannot be empty.' }); - return; - } - - // if media is empty - if (!title.mediaFolder.trim()) { - res.status(400).send({ message: 'Media folder cannot be empty.' }); - return; - } - - const shortnames = title.titleName.trim().split(' '); - let shortname; - if (shortnames.length > 1) { + try{ + + const filter=req.body.id; + + // valid title name + if (!req.body.titleName.trim()) { + res.status(400).send({ message: 'Title cannot be empty.' }); + return; + } + + // if media is empty + if (!req.body.mediaFolder.trim()) { + res.status(400).send({ message: 'Media folder cannot be empty.' }); + return; + } + const shortnames = req.body.titleName.trim().split(' '); + let shortname; + if (shortnames.length > 1) { shortname = (shortnames[0][0] + shortnames[1][0]).toUpperCase(); - } else if (shortnames.length === 1) { + } else if (shortnames.length === 1) { shortname = shortnames[0][0].toUpperCase(); - } - title.shortName = shortname; - - // Validate team code by checking if it exists in the database - if (!title.teamCode) { - res.status(400).send({ message: 'Please provide a team code.' }); - return; - } - - const teamCodeExists = await checkTeamCodeExists(title.teamCode); - if (!teamCodeExists) { - res.status(400).send({ message: 'Invalid team code. Please provide a valid team code.' }); - return; - } - - // validate if project exist - const projectExist = await checkProjectExists(title.projectAssigned._id); - if (!projectExist) { - res.status(400).send({ message: 'Project is empty or not exist.' }); - return; - } - - // validate if team exist - if (title.teamAssiged && title.teamAssiged._id === 'N/A') { - res.status(400).send({ message: 'Team not exists.' }); - return; - } - - title - .updateOne({ _id: filter }, title) - .then((result) => { - if (result.modifiedCount === 0) { - // No documents were modified, handle this case if necessary - return res.status(404).send({ message: 'No documents were updated' }); - } - // Send a success response - res.status(200).send({ message: 'Update successful', result }); - }) - .catch((error) => { - // Send a server error response - console.log(error) - res.status(500).send({ message: 'An error occurred', error }); - }); - + } + req.body.shortName = shortname; + + // Validate team code by checking if it exists in the database + if (!req.body.teamCode) { + res.status(400).send({ message: 'Please provide a team code.' }); + return; + } + + const teamCodeExists = await checkTeamCodeExists(req.body.teamCode); + if (!teamCodeExists) { + res.status(400).send({ message: 'Invalid team code. Please provide a valid team code.' }); + return; + } + + // validate if project exist + const projectExist = await checkProjectExists(req.body.projectAssigned._id); + if (!projectExist) { + res.status(400).send({ message: 'Project is empty or not exist.' }); + return; + } + + // validate if team exist + if (req.body.teamAssiged && req.body.teamAssiged._id === 'N/A') { + res.status(400).send({ message: 'Team not exists.' }); + return; + } + const result = await Title.findById(filter); + result.titleName = req.body.titleName; + result.teamCode = req.body.teamCode; + result.projectAssigned = req.body.projectAssigned; + result.mediaFolder = req.body.mediaFolder; + result.teamAssiged = req.body.teamAssiged; + const updatedTitle = await result.save(); + res.status(200).send({ message: 'Update successful', updatedTitle }); + + }catch(error){ + console.log(error); + res.status(500).send({ message: 'An error occurred', error }); + } + }; const deleteTitleById = async function (req, res) { @@ -174,6 +167,7 @@ const titlecontroller = function (Title) { } }) .catch((error) => { + console.log(error) res.status(500).send(error); }); }; diff --git a/src/startup/db.js b/src/startup/db.js index c3c61807c..719c17f94 100644 --- a/src/startup/db.js +++ b/src/startup/db.js @@ -33,7 +33,7 @@ const afterConnect = async () => { module.exports = function () { const uri = `mongodb://${process.env.user}:${encodeURIComponent(process.env.password)}@${process.env.cluster}/${process.env.dbName}?ssl=true&replicaSet=${process.env.replicaSetName}&authSource=admin`; - + mongoose.connect(uri, { useNewUrlParser: true, useUnifiedTopology: true, From 325ddf9576b56630a591cc165d150e92bee61647 Mon Sep 17 00:00:00 2001 From: 20chen-7 Date: Mon, 2 Sep 2024 11:48:11 -0600 Subject: [PATCH 19/31] chore: conflict --- src/helpers/userHelper.js | 2157 +++++++++++++++++++++++++++++++++++++ 1 file changed, 2157 insertions(+) create mode 100644 src/helpers/userHelper.js diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js new file mode 100644 index 000000000..4fbe3376e --- /dev/null +++ b/src/helpers/userHelper.js @@ -0,0 +1,2157 @@ +/* eslint-disable quotes */ +/* eslint-disable no-continue */ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-console */ +/* eslint-disable consistent-return */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-shadow */ +/* eslint-disable prefer-destructuring */ +/* eslint-disable no-use-before-define */ +/* eslint-disable no-unsafe-optional-chaining */ +/* eslint-disable no-restricted-syntax */ + +const mongoose = require('mongoose'); +const moment = require('moment-timezone'); +const _ = require('lodash'); +const userProfile = require('../models/userProfile'); +const timeEntries = require('../models/timeentry'); +const badge = require('../models/badge'); +const myTeam = require('./helperModels/myTeam'); +const dashboardHelper = require('./dashboardhelper')(); +const reportHelper = require('./reporthelper')(); +const emailSender = require('../utilities/emailSender'); +const logger = require('../startup/logger'); +const token = require('../models/profileInitialSetupToken'); +const BlueSquareEmailAssignment = require('../models/BlueSquareEmailAssignment'); +const cache = require('../utilities/nodeCache')(); +const timeOffRequest = require('../models/timeOffRequest'); +const notificationService = require('../services/notificationService'); +const { NEW_USER_BLUE_SQUARE_NOTIFICATION_MESSAGE } = require('../constants/message'); +const timeUtils = require('../utilities/timeUtils'); + +const userHelper = function () { + // Update format to "MMM-DD-YY" from "YYYY-MMM-DD" (Confirmed with Jae) + const earnedDateBadge = () => { + const currentDate = new Date(Date.now()); + return moment(currentDate).tz('America/Los_Angeles').format('MMM-DD-YY'); + }; + + const getTeamMembers = function (user) { + const userId = mongoose.Types.ObjectId(user._id); + // var teamid = userdetails.teamId; + return myTeam.findById(userId).select({ + 'myTeam._id': 0, + 'myTeam.role': 0, + 'myTeam.fullName': 0, + _id: 0, + }); + }; + + const getTeamManagementEmail = function (teamId) { + const parsedTeamId = mongoose.Types.ObjectId(teamId); + return userProfile + .find( + { + isActive: true, + teams: { + $in: [parsedTeamId], + }, + role: { + $in: ['Manager', 'Administrator'], + }, + }, + 'email role', + ) + .exec(); + }; + + const getUserName = async function (userId) { + const userid = mongoose.Types.ObjectId(userId); + return userProfile.findById(userid, 'firstName lastName'); + }; + + const validateProfilePic = function (profilePic) { + // if it is a url + if (typeof profilePic !== 'string') { + return { + result: false, + errors: 'Invalid image', + }; + } + if (profilePic.startsWith('http') || profilePic.startsWith('https')) { + return { + result: true, + errors: 'Valid image', + }; + } + + const picParts = profilePic.split(','); + let result = true; + const errors = []; + + if (picParts.length < 2) { + return { + result: false, + errors: 'Invalid image', + }; + } + + // validate size + const imageSize = picParts[1].length; + const sizeInBytes = (Math.ceil(imageSize / 4) * 3) / 1024; + if (sizeInBytes > 50) { + errors.push('Image size should not exceed 50KB'); + result = false; + } + + const imageType = picParts[0].split('/')[1].split(';')[0]; + if (imageType !== 'jpeg' && imageType !== 'png') { + errors.push('Image type shoud be either jpeg or png.'); + result = false; + } + + return { + result, + errors, + }; + }; + + const getInfringementEmailBody = function ( + firstName, + lastName, + infringement, + totalInfringements, + timeRemaining, + coreTeamExtraHour, + requestForTimeOffEmailBody, + administrativeContent, + weeklycommittedHours, + ) { + let finalParagraph = ''; + let descrInfringement = ''; + if (timeRemaining === undefined) { + finalParagraph = + '

Life happens and we understand that. That’s why we allow 5 of them before taking action. This action usually includes removal from our team though, so please let your direct supervisor know what happened and do your best to avoid future blue squares if you are getting close to 5 and wish to avoid termination. Each blue square drops off after a year.

'; + descrInfringement = `

Total Infringements: This is your ${moment + .localeData() + .ordinal(totalInfringements)} blue square of 5.

`; + } else { + let hrThisweek = weeklycommittedHours || 0 + coreTeamExtraHour; + const remainHr = timeRemaining || 0; + hrThisweek += remainHr; + finalParagraph = `Please complete ALL owed time this week (${ + hrThisweek + totalInfringements - 5 + } hours) to avoid receiving another blue square. If you have any questions about any of this, please see the "One Community Core Team Policies and Procedures" page.`; + descrInfringement = `

Total Infringements: This is your ${moment + .localeData() + .ordinal( + totalInfringements, + )} blue square of 5 and that means you have ${totalInfringements - 5} hour(s) added to your + requirement this week. This is in addition to any hours missed for last week: + ${weeklycommittedHours} hours commitment + ${remainHr} hours owed for last week + ${totalInfringements - 5} hours + owed for this being your ${moment + .localeData() + .ordinal( + totalInfringements, + )} blue square = ${hrThisweek + totalInfringements - 5} hours required for this week. + .

`; + } + // bold description for 'System auto-assigned infringement for two reasons ....' and 'not submitting a weekly summary' and logged hrs + let emailDescription = requestForTimeOffEmailBody; + if (!requestForTimeOffEmailBody && infringement.description) { + const sentences = infringement.description.split('.'); + if (sentences[0].includes('System auto-assigned infringement for two reasons')) { + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, + '$1', + ); + emailDescription = sentences.join('.'); + emailDescription = emailDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else if ( + sentences[0].includes('System auto-assigned infringement for editing your time entries') + ) { + sentences[0] = sentences[0].replace( + /time entries <(\d+)>\s*times/i, + 'time entries $1 times', + ); + emailDescription = sentences.join('.'); + } else if (sentences[0].includes('System auto-assigned infringement')) { + sentences[0] = sentences[0].replace(/(not submitting a weekly summary)/gi, '$1'); + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment)/gi, + '$1', + ); + emailDescription = sentences.join('.'); + emailDescription = emailDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else { + emailDescription = `${infringement.description}`; + } + } + // add administrative content + const text = `Dear ${firstName} ${lastName}, +

Oops, it looks like something happened and you’ve managed to get a blue square.

+

Date Assigned: ${moment(infringement.date).format('M-D-YYYY')}

\ +

Description: ${emailDescription}

+ ${descrInfringement} + ${finalParagraph} +

Thank you,

+

One Community

+ +         +
+

ADMINISTRATIVE DETAILS:

+

Start Date: ${administrativeContent.startDate}

+

Role: ${administrativeContent.role}

+

Title: ${administrativeContent.userTitle || 'Volunteer'}

+

Previous Blue Square Reasons:

+ ${administrativeContent.historyInfringements}`; + + return text; + }; + + /** + * This function will send out an email listing all users that have a summary provided for a specific week. + * A week is represented by an weekIndex: 0, 1, 2 or 3, where 0 is the most recent and 3 the oldest. + * It relies on the function weeklySummaries(startWeekIndex, endWeekIndex) to get the weekly summaries for the specific week. + * In this case both the startWeekIndex and endWeekIndex are set to 1 to get the last weeks' summaries for all users. + * + * @param {int} [weekIndex=1] Numbered representation of a week where 0 is the most recent and 3 the oldest. + * + * @return {void} + */ + const emailWeeklySummariesForAllUsers = async (weekIndex = 1) => { + const currentFormattedDate = moment().tz('America/Los_Angeles').format(); + /* eslint-disable no-undef */ + logger.logInfo( + `Job for emailing all users' weekly summaries starting at ${currentFormattedDate}`, + ); + + const emails = []; + let mappedResults; // this contains the emails + + try { + const results = await reportHelper.weeklySummaries(weekIndex, weekIndex); + // checks for userProfiles who are eligible to receive the weeklySummary Reports + await userProfile + .find({ getWeeklyReport: true }, { email: 1, teamCode: 1, _id: 0 }) + // eslint-disable-next-line no-shadow + .then((results) => { + mappedResults = results.map((ele) => ele.email); + mappedResults.push('onecommunityglobal@gmail.com', 'onecommunityhospitality@gmail.com'); + mappedResults = mappedResults.toString(); + }); + + let emailBody = '

Weekly Summaries for all active users:

'; + + const weeklySummaryNotProvidedMessage = + '
Weekly Summary: Not provided!
'; + + const weeklySummaryNotRequiredMessage = + '
Weekly Summary: Not required for this user
'; + + results.sort((a, b) => + `${a.firstName} ${a.lastName}`.localeCompare(`${b.firstName} ${b.lastname}`), + ); + + for (let i = 0; i < results.length; i += 1) { + const result = results[i]; + const { + firstName, + lastName, + email, + weeklySummaries, + mediaUrl, + adminLinks, + weeklySummariesCount, + weeklycommittedHours, + weeklySummaryOption, + teamCode, + } = result; + + if (email !== undefined && email !== null) { + emails.push(email); + } + + // weeklySummaries array will have only one item fetched (if present), + // consequently totalSeconds array will also have only one item in the array (if present) + // hence totalSeconds[0] should be used + const hoursLogged = result.totalSeconds[0] / 3600 || 0; + + const mediaUrlLink = mediaUrl ? `${mediaUrl}` : 'Not provided!'; + const teamCodeStr = teamCode ? `${teamCode}` : 'X-XXX'; + const googleDocLinkValue = + adminLinks?.length > 0 + ? adminLinks.find((link) => link.Name === 'Google Doc' && link.Link) + : null; + + const googleDocLink = googleDocLinkValue + ? `${googleDocLinkValue.Link}` + : null; + + let weeklySummaryMessage = weeklySummaryNotProvidedMessage; + const colorStyle = (() => { + switch (weeklySummaryOption) { + case 'Team': + return 'style="color: magenta;"'; + case 'Not Required': + return 'style="color: green"'; + case 'Required': + return ''; + default: + return result.weeklySummaryNotReq ? 'style="color: green"' : ''; + } + })(); + // weeklySummaries array should only have one item if any, hence weeklySummaries[0] needs be used to access it. + if (Array.isArray(weeklySummaries) && weeklySummaries[0]) { + const { dueDate, summary } = weeklySummaries[0]; + if (summary) { + weeklySummaryMessage = ` +
+ Weekly Summary + (for the week ending on ${moment(dueDate) + .tz('America/Los_Angeles') + .format('YYYY-MMM-DD')}): +
+
+ ${summary} +
+ `; + } else if ( + weeklySummaryOption === 'Not Required' || + (!weeklySummaryOption && result.weeklySummaryNotReq) + ) { + weeklySummaryMessage = weeklySummaryNotRequiredMessage; + } + } + + emailBody += ` + \n +
+ Name: ${firstName} ${lastName} +

+ Team Code: ${teamCodeStr || 'X-XXX'} +

+

+ + + Media URL: ${mediaUrlLink || 'Not provided!'} + +

+

+ + Google Doc Link: ${ + googleDocLink || 'Not provided!' + } + +

+ ${ + weeklySummariesCount === 8 + ? `

Total Valid Weekly Summaries: ${weeklySummariesCount}

` + : `

Total Valid Weekly Summaries: ${ + weeklySummariesCount || 'No valid submissions yet!' + }

` + } + ${ + hoursLogged >= weeklycommittedHours + ? `

Hours logged: ${hoursLogged.toFixed(2)} / ${weeklycommittedHours}

` + : `

Hours logged: ${hoursLogged.toFixed( + 2, + )} / ${weeklycommittedHours}

` + } + ${weeklySummaryMessage} +
`; + } + + // Necessary because our version of node is outdated + // and doesn't have String.prototype.replaceAll + let emailString = [...new Set(emails)].toString(); + while (emailString.includes(',')) { + emailString = emailString.replace(',', '\n'); + } + while (emailString.includes('\n')) { + emailString = emailString.replace('\n', ', '); + } + + emailBody += `\n +
+

Emails

+

+ ${emailString} +

+
+ `; + + const mailList = mappedResults; + + emailSender( + mailList, + 'Weekly Summaries for all active users...', + emailBody, + null, + null, + emailString, + ); + } catch (err) { + logger.logException(err); + } + }; + + /** + * This function will process the weeklySummaries array in the following way: + * 1 ) Push a new (blank) summary at the beginning of the array. + * 2 ) Always maintains 4 items in the array where each item represents a summary for a given week. + * + * @param {ObjectId} personId This is mongoose.Types.ObjectId object. + */ + const processWeeklySummariesByUserId = function (personId) { + userProfile + .findByIdAndUpdate(personId, { + $push: { + weeklySummaries: { + $each: [ + { + dueDate: moment().tz('America/Los_Angeles').endOf('week'), + summary: '', + }, + ], + $position: 0, + $slice: 4, + }, + }, + }) + .catch((error) => logger.logException(error)); + }; + + /** + * This function is called by a cron job to do 3 things to all active users: + * 1 ) Determine whether there's been an infringement for the weekly summary for last week. + * 2 ) Determine whether there's been an infringement for the time not met for last week. + * 3 ) Call the processWeeklySummariesByUserId(personId) to process the weeklySummaries array. + */ + const assignBlueSquareForTimeNotMet = async () => { + try { + console.log('run'); + const currentFormattedDate = moment().tz('America/Los_Angeles').format(); + moment.tz('America/Los_Angeles').startOf('day').toISOString(); + + logger.logInfo( + `Job for assigning blue square for commitment not met starting at ${currentFormattedDate}`, + ); + + const pdtStartOfLastWeek = moment() + .tz('America/Los_Angeles') + .startOf('week') + .subtract(1, 'week'); + + const pdtEndOfLastWeek = moment().tz('America/Los_Angeles').endOf('week').subtract(1, 'week'); + + const users = await userProfile.find( + { isActive: true }, + '_id weeklycommittedHours weeklySummaries missedHours', + ); + const usersRequiringBlueSqNotification = []; + // this part is supposed to be a for, so it'll be slower when sending emails, so the emails will not be + // targeted as spam + // There's no need to put Promise.all here + + /* + Note from Shengwei (3/11/24) Potential enhancement: + 1. I think we could remove the for loop to update find user profile by batch to reduce db roundtrips. + Otherwise, each record checking and update require at least 1 db roundtrip. Then, we could use for loop to do email sending. + + Do something like: + do while (batch != lastBatch) + const lsOfResult = await userProfile.find({ _id: { $in: arrayOfIds } } + for item in lsOfResult: + // do the update and checking + // save updated records in batch (mongoose updateMany) and do asyc email sending + 2. Wrap the operation in one transaction to ensure the atomicity of the operation. + */ + for (let i = 0; i < users.length; i += 1) { + const user = users[i]; + + const person = await userProfile.findById(user._id); + + const personId = mongoose.Types.ObjectId(user._id); + + let hasWeeklySummary = false; + + if (Array.isArray(user.weeklySummaries) && user.weeklySummaries.length) { + const { summary } = user.weeklySummaries[0]; + if (summary) { + hasWeeklySummary = true; + } + } + + // This needs to run AFTER the check for weekly summary above because the summaries array will be updated/shifted after this function runs. + await processWeeklySummariesByUserId(personId); + + const results = await dashboardHelper.laborthisweek( + personId, + pdtStartOfLastWeek, + pdtEndOfLastWeek, + ); + + const { timeSpent_hrs: timeSpent } = results[0]; + + const weeklycommittedHours = user.weeklycommittedHours + (user.missedHours ?? 0); + + const timeNotMet = timeSpent < weeklycommittedHours; + + let description; + + const timeRemaining = weeklycommittedHours - timeSpent; + + /** Check if the user is new user to prevent blue square assignment + * Condition: + * 1. Not Started: Start Date > end date of last week && totalTangibleHrs === 0 && totalIntangibleHrs === 0 + * 2. Short Week: Start Date (First time entrie) is after Monday && totalTangibleHrs === 0 && totalIntangibleHrs === 0 + * 3. No hours logged, and the account was after the start of last week. + * + * Notes: + * 1. Start date is automatically updated upon first time-log. + * 2. User meet above condition but meet minimum hours without submitting weekly summary + * should get a blue square as reminder. + * */ + let isNewUser = false; + const userStartDate = moment(person.startDate); + if ( + person.totalTangibleHrs === 0 && + person.totalIntangibleHrs === 0 && + timeSpent === 0 && + userStartDate.isAfter(pdtStartOfLastWeek) + ) { + console.log('1'); + isNewUser = true; + } + + if ( + userStartDate.isAfter(pdtEndOfLastWeek) || + (userStartDate.isAfter(pdtStartOfLastWeek) && + userStartDate.isBefore(pdtEndOfLastWeek) && + timeUtils.getDayOfWeekStringFromUTC(person.startDate) > 1) + ) { + console.log('2'); + isNewUser = true; + } + + const updateResult = await userProfile.findByIdAndUpdate( + personId, + { + $inc: { + totalTangibleHrs: timeSpent || 0, + }, + $max: { + personalBestMaxHrs: timeSpent || 0, + }, + $push: { + savedTangibleHrs: { $each: [timeSpent || 0], $slice: -200 }, + }, + $set: { + lastWeekTangibleHrs: timeSpent || 0, + }, + }, + { new: true }, + ); + + if ( + updateResult?.weeklySummaryOption === 'Not Required' || + updateResult?.weeklySummaryNotReq + ) { + hasWeeklySummary = true; + } + + const cutOffDate = moment().subtract(1, 'year'); + + const oldInfringements = []; + for (let k = 0; k < updateResult?.infringements.length; k += 1) { + if ( + updateResult?.infringements && + moment(updateResult?.infringements[k].date).diff(cutOffDate) >= 0 + ) { + oldInfringements.push(updateResult.infringements[k]); + } else { + break; + } + } + // use histroy Infringements to align the highlight requirements + let historyInfringements = 'No Previous Infringements.'; + if (oldInfringements.length) { + userProfile.findByIdAndUpdate( + personId, + { + $push: { + oldInfringements: { $each: oldInfringements, $slice: -10 }, + }, + }, + { new: true }, + ); + historyInfringements = oldInfringements + .map((item, index) => { + let enhancedDescription; + if (item.description) { + let sentences = item.description.split('.'); + const dateRegex = + /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; + sentences = sentences.map((sentence) => + sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { + const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; + }), + ); + if (sentences[0].includes('System auto-assigned infringement for two reasons')) { + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else if ( + sentences[0].includes( + 'System auto-assigned infringement for editing your time entries', + ) + ) { + sentences[0] = sentences[0].replace( + /time entries <(\d+)>\s*times/i, + 'time entries $1 times', + ); + enhancedDescription = sentences.join('.'); + } else if (sentences[0].includes('System auto-assigned infringement')) { + sentences[0] = sentences[0].replace( + /(not submitting a weekly summary)/gi, + '$1', + ); + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else { + enhancedDescription = `${item.description}`; + } + } + return `

${index + 1}. Date: ${moment( + item.date, + ).format('M-D-YYYY')}, Description: ${enhancedDescription}

`; + }) + .join(''); + } + // No extra hours is needed if blue squares isn't over 5. + // length +1 is because new infringement hasn't been created at this stage. + const coreTeamExtraHour = Math.max(0, oldInfringements.length - 5); + const utcStartMoment = moment(pdtStartOfLastWeek).add(1, 'second'); + const utcEndMoment = moment(pdtEndOfLastWeek).subtract(1, 'day').subtract(1, 'second'); + + const requestsForTimeOff = await timeOffRequest.find({ + requestFor: personId, + startingDate: { $lte: utcStartMoment }, + endingDate: { $gte: utcEndMoment }, + }); + + const hasTimeOffRequest = requestsForTimeOff.length > 0; + let requestForTimeOff; + let requestForTimeOffStartingDate; + let requestForTimeOffEndingDate; + let requestForTimeOffreason; + let requestForTimeOffEmailBody; + + if (hasTimeOffRequest) { + // eslint-disable-next-line prefer-destructuring + requestForTimeOff = requestsForTimeOff[0]; + requestForTimeOffStartingDate = moment(requestForTimeOff.startingDate).format( + 'dddd M-D-YYYY', + ); + requestForTimeOffEndingDate = moment(requestForTimeOff.endingDate).format( + 'dddd M-D-YYYY', + ); + requestForTimeOffreason = requestForTimeOff.reason; + requestForTimeOffEmailBody = `You had scheduled time off From ${requestForTimeOffStartingDate}, To ${requestForTimeOffEndingDate}, due to: ${requestForTimeOffreason}`; + } + + if (timeNotMet || !hasWeeklySummary) { + if (hasTimeOffRequest) { + description = requestForTimeOffreason; + } else if (timeNotMet && !hasWeeklySummary) { + if (person.role === 'Core Team') { + description = `System auto-assigned infringement for two reasons: not meeting weekly volunteer time commitment as well as not submitting a weekly summary. In the week starting ${pdtStartOfLastWeek.format( + 'dddd M-D-YYYY', + )} and ending ${pdtEndOfLastWeek.format( + 'dddd M-D-YYYY', + )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ + person.weeklycommittedHours + } hours + ${ + person.missedHours ?? 0 + } hours owed for last week + ${coreTeamExtraHour} hours owed for this being your ${moment + .localeData() + .ordinal( + oldInfringements.length + 1, + )} blue square. So you should have completed ${weeklycommittedHours} hours and you completed ${timeSpent.toFixed( + 2, + )} hours.`; + } else { + description = `System auto-assigned infringement for two reasons: not meeting weekly volunteer time commitment as well as not submitting a weekly summary. For the hours portion, you logged ${timeSpent.toFixed( + 2, + )} hours against a committed effort of ${weeklycommittedHours} hours in the week starting ${pdtStartOfLastWeek.format( + 'dddd M-D-YYYY', + )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; + } + } else if (timeNotMet) { + if (person.role === 'Core Team') { + description = `System auto-assigned infringement for not meeting weekly volunteer time commitment. In the week starting ${pdtStartOfLastWeek.format( + 'dddd M-D-YYYY', + )} and ending ${pdtEndOfLastWeek.format( + 'dddd M-D-YYYY', + )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ + user.weeklycommittedHours + } hours + ${ + person.missedHours ?? 0 + } hours owed for last week + ${coreTeamExtraHour} hours owed for this being your ${moment + .localeData() + .ordinal( + oldInfringements.length + 1, + )} blue square. So you should have completed ${weeklycommittedHours} hours and you completed ${timeSpent.toFixed( + 2, + )} hours.`; + } else { + description = `System auto-assigned infringement for not meeting weekly volunteer time commitment. You logged ${timeSpent.toFixed( + 2, + )} hours against a committed effort of ${weeklycommittedHours} hours in the week starting ${pdtStartOfLastWeek.format( + 'dddd M-D-YYYY', + )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; + } + } else { + description = `System auto-assigned infringement for not submitting a weekly summary for the week starting ${pdtStartOfLastWeek.format( + 'dddd M-D-YYYY', + )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; + } + + const infringement = { + date: moment().utc().format('YYYY-MM-DD'), + description, + createdDate: hasTimeOffRequest + ? moment(requestForTimeOff.createdAt).format('YYYY-MM-DD') + : null, + }; + // Only assign blue square and send email if the user IS NOT a new user + // Otherwise, display notification to users if new user && met the time requirement && weekly summary not submitted + // All other new users will not receive a blue square or notification + let emailBody = ''; + if (!isNewUser) { + const status = await userProfile.findByIdAndUpdate( + personId, + { + $push: { + infringements: infringement, + }, + }, + { new: true }, + ); + const administrativeContent = { + startDate: moment(person.startDate).utc().format('M-D-YYYY'), + role: person.role, + userTitle: person.jobTitle[0], + historyInfringements, + }; + if (person.role === 'Core Team' && timeRemaining > 0) { + emailBody = getInfringementEmailBody( + status.firstName, + status.lastName, + infringement, + status.infringements.length, + timeRemaining, + coreTeamExtraHour, + requestForTimeOffEmailBody, + administrativeContent, + weeklycommittedHours, + ); + } else { + emailBody = getInfringementEmailBody( + status.firstName, + status.lastName, + infringement, + status.infringements.length, + undefined, + null, + requestForTimeOffEmailBody, + administrativeContent, + ); + } + + let emailsBCCs; + /* eslint-disable array-callback-return */ + const blueSquareBCCs = await BlueSquareEmailAssignment.find() + .populate('assignedTo') + .exec(); + if (blueSquareBCCs.length > 0) { + emailsBCCs = blueSquareBCCs.map((assignment) => { + if (assignment.assignedTo.isActive === true) { + return assignment.email; + } + }); + } else { + emailsBCCs = null; + } + + emailSender( + status.email, + 'New Infringement Assigned', + emailBody, + emailsBCCs, + 'onecommunityglobal@gmail.com', + status.email, + null, + ); + } else if (isNewUser && !timeNotMet && !hasWeeklySummary) { + usersRequiringBlueSqNotification.push(personId); + } + + const categories = await dashboardHelper.laborThisWeekByCategory( + personId, + pdtStartOfLastWeek, + pdtEndOfLastWeek, + ); + + if (Array.isArray(categories) && categories.length > 0) { + await userProfile.findOneAndUpdate( + { _id: personId, categoryTangibleHrs: { $exists: false } }, + { $set: { categoryTangibleHrs: [] } }, + ); + } else { + continue; + } + + for (let j = 0; j < categories.length; j += 1) { + const elem = categories[j]; + + if (elem._id == null) { + elem._id = 'Other'; + } + + const updateResult2 = await userProfile.findOneAndUpdate( + { _id: personId, 'categoryTangibleHrs.category': elem._id }, + { $inc: { 'categoryTangibleHrs.$.hrs': elem.timeSpent_hrs } }, + { new: true }, + ); + + if (!updateResult2) { + await userProfile.findOneAndUpdate( + { + _id: personId, + 'categoryTangibleHrs.category': { $ne: elem._id }, + }, + { + $addToSet: { + categoryTangibleHrs: { + category: elem._id, + hrs: elem.timeSpent_hrs, + }, + }, + }, + ); + } + } + } + if (cache.hasCache(`user-${personId}`)) { + cache.removeCache(`user-${personId}`); + } + } + // eslint-disable-next-line no-use-before-define + await deleteOldTimeOffRequests(); + // Create notification for users who are new and met the time requirement but weekly summary not submitted + // Since the notification is required a sender, we fetch an owner user as the sender for the system generated notification + if (usersRequiringBlueSqNotification.length > 0) { + const senderId = await userProfile.findOne({ role: 'Owner', isActive: true }, '_id'); + await notificationService.createNotification( + senderId._id, + usersRequiringBlueSqNotification, + NEW_USER_BLUE_SQUARE_NOTIFICATION_MESSAGE, + true, + false, + ); + } + } catch (err) { + logger.logException(err); + } + + // processWeeklySummaries for nonActive users + try { + const inactiveUsers = await userProfile.find({ isActive: false }, '_id'); + for (let i = 0; i < inactiveUsers.length; i += 1) { + const user = inactiveUsers[i]; + + await processWeeklySummariesByUserId(mongoose.Types.ObjectId(user._id), false); + } + } catch (err) { + logger.logException(err); + } + }; + + const applyMissedHourForCoreTeam = async () => { + try { + const currentDate = moment().tz('America/Los_Angeles').format(); + + logger.logInfo( + `Job for applying missed hours for Core Team members starting at ${currentDate}`, + ); + + const startOfLastWeek = moment() + .tz('America/Los_Angeles') + .startOf('week') + .subtract(1, 'week') + .format('YYYY-MM-DD'); + + const endOfLastWeek = moment() + .tz('America/Los_Angeles') + .endOf('week') + .subtract(1, 'week') + .format('YYYY-MM-DD'); + + const missedHours = await userProfile.aggregate([ + { + $match: { + role: 'Core Team', + isActive: true, + }, + }, + { + $lookup: { + from: 'timeEntries', + localField: '_id', + foreignField: 'personId', + pipeline: [ + { + $match: { + $expr: { + $and: [ + { $eq: ['$isTangible', true] }, + { $gte: ['$dateOfWork', startOfLastWeek] }, + { $lte: ['$dateOfWork', endOfLastWeek] }, + ], + }, + }, + }, + ], + as: 'timeEntries', + }, + }, + { + $project: { + _id: 1, + missedHours: { + $max: [ + { + $subtract: [ + { + $sum: [{ $ifNull: ['$missedHours', 0] }, '$weeklycommittedHours'], + }, + { + $divide: [ + { + $sum: { + $map: { + input: '$timeEntries', + in: '$$this.totalSeconds', + }, + }, + }, + 3600, + ], + }, + ], + }, + 0, + ], + }, + }, + }, + ]); + + const bulkOps = []; + + missedHours.forEach((obj) => { + bulkOps.push({ + updateOne: { + filter: { _id: obj._id }, + update: { missedHours: obj.missedHours }, + }, + }); + }); + + await userProfile.bulkWrite(bulkOps); + } catch (err) { + logger.logException(err); + } + }; + + const deleteBlueSquareAfterYear = async () => { + const currentFormattedDate = moment().tz('America/Los_Angeles').format(); + + logger.logInfo( + `Job for deleting blue squares older than 1 year starting at ${currentFormattedDate}`, + ); + + const cutOffDate = moment().subtract(1, 'year').format('YYYY-MM-DD'); + + try { + const results = await userProfile.updateMany( + {}, + { + $pull: { + infringements: { + date: { + $lte: cutOffDate, + }, + }, + }, + }, + ); + + logger.logInfo(`Job deleting blue squares older than 1 year finished + at ${moment().tz('America/Los_Angeles').format()} \nReulst: ${JSON.stringify(results)}`); + } catch (err) { + logger.logException(err); + } + }; + + const reActivateUser = async () => { + const currentFormattedDate = moment().tz('America/Los_Angeles').format(); + + logger.logInfo( + `Job for activating users based on scheduled re-activation date starting at ${currentFormattedDate}`, + ); + + try { + const users = await userProfile.find( + { isActive: false, reactivationDate: { $exists: true } }, + '_id isActive reactivationDate', + ); + for (let i = 0; i < users.length; i += 1) { + const user = users[i]; + if (moment().isSameOrAfter(moment(user.reactivationDate))) { + await userProfile.findByIdAndUpdate( + user._id, + { + $set: { + isActive: true, + }, + $unset: { + endDate: user.endDate, + }, + }, + { new: true }, + ); + logger.logInfo( + `User with id: ${user._id} was re-acticated at ${moment() + .tz('America/Los_Angeles') + .format()}.`, + ); + const id = user._id; + const person = await userProfile.findById(id); + + const endDate = moment(person.endDate).format('YYYY-MM-DD'); + logger.logInfo(`User with id: ${user._id} was re-acticated at ${moment().format()}.`); + + const subject = `IMPORTANT:${person.firstName} ${person.lastName} has been RE-activated in the Highest Good Network`; + + const emailBody = `

Hi Admin!

+ +

This email is to let you know that ${person.firstName} ${person.lastName} has been made active again in the Highest Good Network application after being paused on ${endDate}.

+ +

If you need to communicate anything with them, this is their email from the system: ${person.email}.

+ +

Thanks!

+ +

The HGN A.I. (and One Community)

`; + + emailSender('onecommunityglobal@gmail.com', subject, emailBody, null, null, person.email); + } + } + } catch (err) { + logger.logException(err); + } + }; + + const notifyInfringements = function ( + original, + current, + firstName, + lastName, + emailAddress, + role, + startDate, + jobTitle, + ) { + if (!current) return; + const newOriginal = original.toObject(); + const newCurrent = current.toObject(); + const totalInfringements = newCurrent.length; + let newInfringements = []; + let historyInfringements = 'No Previous Infringements.'; + if (original.length) { + historyInfringements = original + .map((item, index) => { + let enhancedDescription; + if (item.description) { + let sentences = item.description.split('.'); + const dateRegex = + /in the week starting Sunday (\d{4})-(\d{2})-(\d{2}) and ending Saturday (\d{4})-(\d{2})-(\d{2})/g; + sentences = sentences.map((sentence) => + sentence.replace(dateRegex, (match, year1, month1, day1, year2, month2, day2) => { + const startDate = moment(`${year1}-${month1}-${day1}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + const endDate = moment(`${year2}-${month2}-${day2}`, 'YYYY-MM-DD').format( + 'M-D-YYYY', + ); + return `in the week starting Sunday ${startDate} and ending Saturday ${endDate}`; + }), + ); + if (sentences[0].includes('System auto-assigned infringement for two reasons')) { + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment as well as not submitting a weekly summary)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else if ( + sentences[0].includes( + 'System auto-assigned infringement for editing your time entries', + ) + ) { + sentences[0] = sentences[0].replace( + /time entries <(\d+)>\s*times/i, + 'time entries $1 times', + ); + enhancedDescription = sentences.join('.'); + } else if (sentences[0].includes('System auto-assigned infringement')) { + sentences[0] = sentences[0].replace( + /(not submitting a weekly summary)/gi, + '$1', + ); + sentences[0] = sentences[0].replace( + /(not meeting weekly volunteer time commitment)/gi, + '$1', + ); + enhancedDescription = sentences.join('.'); + enhancedDescription = enhancedDescription.replace( + /logged (\d+(\.\d+)?\s*hours)/i, + 'logged $1', + ); + } else { + enhancedDescription = `${item.description}`; + } + } + return `

${index + 1}. Date: ${moment(item.date).format('M-D-YYYY')}, Description: ${enhancedDescription}

`; + }) + .join(''); + } + const administrativeContent = { + startDate: moment(startDate).utc().format('M-D-YYYY'), + role, + userTitle: jobTitle, + historyInfringements, + }; + newInfringements = _.differenceWith(newCurrent, newOriginal, (arrVal, othVal) => + arrVal._id.equals(othVal._id), + ); + newInfringements.forEach((element) => { + emailSender( + emailAddress, + 'New Infringement Assigned', + getInfringementEmailBody( + firstName, + lastName, + element, + totalInfringements, + undefined, + undefined, + undefined, + administrativeContent, + ), + null, + 'onecommunityglobal@gmail.com', + emailAddress, + ); + }); + }; + + const replaceBadge = async function (personId, oldBadgeId, newBadgeId) { + userProfile.updateOne( + { _id: personId, 'badgeCollection.badge': oldBadgeId }, + { + $set: { + 'badgeCollection.$.badge': newBadgeId, + 'badgeCollection.$.lastModified': Date.now().toString(), + 'badgeCollection.$.count': 1, + 'badgeCollection.$.earnedDate': [earnedDateBadge()], + }, + }, + (err) => { + if (err) { + throw new Error(err); + } + }, + ); + }; + + const increaseBadgeCount = async function (personId, badgeId) { + userProfile.updateOne( + { _id: personId, 'badgeCollection.badge': badgeId }, + { + $inc: { 'badgeCollection.$.count': 1 }, + $set: { 'badgeCollection.$.lastModified': Date.now().toString() }, + $push: { 'badgeCollection.$.earnedDate': earnedDateBadge() }, + }, + (err) => { + if (err) { + console.log(err); + } + }, + ); + }; + + const addBadge = async function (personId, badgeId, count = 1, featured = false) { + userProfile.findByIdAndUpdate( + personId, + { + $push: { + badgeCollection: { + badge: badgeId, + count, + earnedDate: [earnedDateBadge()], + featured, + lastModified: Date.now().toString(), + }, + }, + }, + (err) => { + if (err) { + throw new Error(err); + } + }, + ); + }; + + const removeDupBadge = async function (personId, badgeId) { + userProfile.findByIdAndUpdate( + personId, + { + $pull: { + badgeCollection: { badge: mongoose.Types.ObjectId(badgeId) }, + }, + }, + { new: true }, + (err) => { + if (err) { + throw new Error(err); + } + }, + ); + }; + + const changeBadgeCount = async function (personId, badgeId, count) { + if (count === 0) { + removeDupBadge(personId, badgeId); + } else if (count) { + // Process exisiting earned date to match the new count + try { + const userInfo = await userProfile.findById(personId); + let newEarnedDate = []; + const recordToUpdate = userInfo.badgeCollection.find( + (item) => item.badge._id.toString() === badgeId.toString(), + ); + if (!recordToUpdate) { + throw new Error( + `Failed to update badge for ${personId}. Badge not found ${badgeId.toString()}`, + ); + } + // If the count is the same, do nothing + if (recordToUpdate.count === count) { + return; + } + const copyOfEarnedDate = recordToUpdate.earnedDate; + // Update: We refrain from automatically correcting the mismatch problem as we intend to preserve the original + // earned date even when a badge is deleted. This approach ensures that a record of badges earned is maintained, + // preventing oversight of any mismatches caused by bugs. + if (recordToUpdate.count < count) { + let dateToAdd = count - recordToUpdate.count; + // if the EarnedDate count is less than the new count, add a earned date to the end of the collection + while (dateToAdd > 0) { + copyOfEarnedDate.push(earnedDateBadge()); + dateToAdd -= 1; + } + } + newEarnedDate = [...copyOfEarnedDate]; + userProfile.updateOne( + { _id: personId, 'badgeCollection.badge': badgeId }, + { + $set: { + 'badgeCollection.$.count': count, + 'badgeCollection.$.lastModified': Date.now().toString(), + 'badgeCollection.$.earnedDate': newEarnedDate, + 'badgeCollection.$.hasBadgeDeletionImpact': recordToUpdate.count > count, // badge deletion impact set to true if the new count is less than the old count + }, + }, + (err) => { + if (err) { + throw new Error(err); + } + }, + ); + } catch (err) { + logger.logException(err); + } + } + }; + + // remove the last badge you earned on this streak(not including 1) + + const removePrevHrBadge = async function (personId, user, badgeCollection, hrs, weeks) { + // Check each Streak Greater than One to check if it works + if (weeks < 2) { + return; + } + let removed = false; + await badge + .aggregate([ + { + $match: { + type: 'X Hours for X Week Streak', + weeks: { $gt: 0, $lt: weeks }, + totalHrs: hrs, + }, + }, + { $sort: { weeks: -1, totalHrs: -1 } }, + { + $group: { + _id: '$weeks', + badges: { + $push: { _id: '$_id', hrs: '$totalHrs', weeks: '$weeks' }, + }, + }, + }, + ]) + .then((results) => { + results.forEach((streak) => { + streak.badges.every((bdge) => { + for (let i = 0; i < badgeCollection.length; i += 1) { + if ( + badgeCollection[i].badge?.type === 'X Hours for X Week Streak' && + badgeCollection[i].badge?.weeks === bdge.weeks && + badgeCollection[i].badge?.totalHrs === hrs && + !removed + ) { + changeBadgeCount( + personId, + badgeCollection[i].badge._id, + badgeCollection[i].count - 1, + ); + removed = true; + return false; + } + } + return true; + }); + }); + }); + }; + + // 'No Infringement Streak', + const checkNoInfringementStreak = async function (personId, user, badgeCollection) { + let badgeOfType; + for (let i = 0; i < badgeCollection.length; i += 1) { + if (badgeCollection[i].badge?.type === 'No Infringement Streak') { + if (badgeOfType && badgeOfType.months <= badgeCollection[i].badge.months) { + removeDupBadge(personId, badgeOfType._id); + badgeOfType = badgeCollection[i].badge; + } else if (badgeOfType && badgeOfType.months > badgeCollection[i].badge.months) { + removeDupBadge(personId, badgeCollection[i].badge._id); + } else if (!badgeOfType) { + badgeOfType = badgeCollection[i].badge; + } + } + } + await badge + .find({ type: 'No Infringement Streak' }) + .sort({ months: -1 }) + .then((results) => { + if (!Array.isArray(results) || !results.length) { + return; + } + + results.every((elem) => { + // Cannot account for time paused yet + + if (elem.months <= 12) { + if (moment().diff(moment(user.createdDate), 'months', true) >= elem.months) { + if ( + user.infringements.length === 0 || + Math.abs( + moment().diff( + moment( + // eslint-disable-next-line no-unsafe-optional-chaining + user.infringements[user.infringements?.length - 1].date, + ), + 'months', + true, + ), + ) >= elem.months + ) { + if (badgeOfType) { + if (badgeOfType._id.toString() !== elem._id.toString()) { + replaceBadge( + personId, + mongoose.Types.ObjectId(badgeOfType._id), + mongoose.Types.ObjectId(elem._id), + ); + } + return false; + } + addBadge(personId, mongoose.Types.ObjectId(elem._id)); + return false; + } + } + } else if (user?.infringements?.length === 0) { + if (moment().diff(moment(user.createdDate), 'months', true) >= elem.months) { + if ( + user.oldInfringements.length === 0 || + Math.abs( + moment().diff( + moment( + // eslint-disable-next-line no-unsafe-optional-chaining + user.oldInfringements[user.oldInfringements?.length - 1].date, + ), + 'months', + true, + ), + ) >= + elem.months - 12 + ) { + if (badgeOfType) { + if (badgeOfType._id.toString() !== elem._id.toString()) { + replaceBadge( + personId, + mongoose.Types.ObjectId(badgeOfType._id), + mongoose.Types.ObjectId(elem._id), + ); + } + return false; + } + addBadge(personId, mongoose.Types.ObjectId(elem._id)); + return false; + } + } + } + return true; + }); + }); + }; + + // 'Minimum Hours Multiple', + const checkMinHoursMultiple = async function (personId, user, badgeCollection) { + const badgesOfType = badgeCollection + .map((obj) => obj.badge) + .filter((badgeItem) => badgeItem.type === 'Minimum Hours Multiple'); + await badge + .find({ type: 'Minimum Hours Multiple' }) + .sort({ multiple: -1 }) + .then((results) => { + if (!Array.isArray(results) || !results.length) { + return; + } + for (let i = 0; i < results.length; i += 1) { + // this needs to be a for loop so that the returns break before assigning badges for lower multiples + const elem = results[i]; // making variable elem accessible for below code + + if (user.lastWeekTangibleHrs / user.weeklycommittedHours >= elem.multiple) { + const theBadge = badgesOfType.find( + (badgeItem) => badgeItem._id.toString() === elem._id.toString(), + ); + return theBadge + ? increaseBadgeCount(personId, mongoose.Types.ObjectId(theBadge._id)) + : addBadge(personId, mongoose.Types.ObjectId(elem._id)); + } + } + }); + }; + + const getAllWeeksData = async (personId, user) => { + const userId = mongoose.Types.ObjectId(personId); + const weeksData = []; + const currentDate = moment().tz('America/Los_Angeles'); + const startDate = moment(user.createdDate).tz('America/Los_Angeles'); + const numWeeks = Math.ceil(currentDate.diff(startDate, 'days') / 7); + + // iterate through weeks to get hours of each week + for (let week = 1; week <= numWeeks; week += 1) { + const pdtstart = startDate + .clone() + .add(week - 1, 'weeks') + .startOf('week') + .format('YYYY-MM-DD'); + const pdtend = startDate.clone().add(week, 'weeks').subtract(1, 'days').format('YYYY-MM-DD'); + try { + const results = await dashboardHelper.laborthisweek(userId, pdtstart, pdtend); + const { timeSpent_hrs: timeSpent } = results[0]; + weeksData.push(timeSpent); + } catch (error) { + console.error(error); + throw error; + } + } + return weeksData; + }; + + const getMaxHrs = async (personId, user) => { + const weeksdata = await getAllWeeksData(personId, user); + return Math.max(...weeksdata); + }; + + const updatePersonalMax = async (personId, user) => { + try { + const MaxHrs = await getMaxHrs(personId, user); + user.personalBestMaxHrs = MaxHrs; + await user.save(); + } catch (error) { + console.error(error); + } + }; + + // 'Personal Max', + const checkPersonalMax = async function (personId, user, badgeCollection) { + let badgeOfType; + const duplicateBadges = []; + + for (let i = 0; i < badgeCollection.length; i += 1) { + if (badgeCollection[i].badge?.type === 'Personal Max') { + if (!badgeOfType) { + badgeOfType = badgeCollection[i]; + } else { + duplicateBadges.push(badgeCollection[i]); + } + } + // eslint-disable-next-line no-restricted-syntax + for (const b of duplicateBadges) { + await removeDupBadge(personId, b._id); + } + } + await badge.findOne({ type: 'Personal Max' }).then((results) => { + const currentDate = moment(moment().format('MM-DD-YYYY'), 'MM-DD-YYYY') + .tz('America/Los_Angeles') + .format('MMM-DD-YY'); + if ( + user.lastWeekTangibleHrs && + user.lastWeekTangibleHrs >= user.personalBestMaxHrs && + !badgeOfType.earnedDate.includes(currentDate) + ) { + if (badgeOfType) { + increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType.badge._id)); + // Update the earnedDate array with the new date + badgeOfType.earnedDate.unshift(moment().format('MMM-DD-YYYY')); + } else { + addBadge(personId, mongoose.Types.ObjectId(results._id), user.personalBestMaxHrs); + } + } + }); + }; + + // 'Most Hrs in Week' + + const checkMostHrsWeek = async function (personId, user, badgeCollection) { + if (user.weeklycommittedHours > 0 && user.lastWeekTangibleHrs > user.weeklycommittedHours) { + const badgeOfType = badgeCollection + .filter((object) => object.badge.type === 'Most Hrs in Week') + .map((object) => object.badge); + await badge.findOne({ type: 'Most Hrs in Week' }).then((results) => { + userProfile + .aggregate([ + { $match: { isActive: true } }, + { $group: { _id: 1, maxHours: { $max: '$lastWeekTangibleHrs' } } }, + ]) + .then((userResults) => { + if (badgeOfType.length > 1) { + removeDupBadge(user._id, badgeOfType[0]._id); + } + + if (user.lastWeekTangibleHrs && user.lastWeekTangibleHrs >= userResults[0].maxHours) { + if (badgeOfType.length) { + increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType[0]._id)); + } else { + addBadge(personId, mongoose.Types.ObjectId(results._id)); + } + } + }); + }); + } + }; + + // 'X Hours in one week', + const checkXHrsInOneWeek = async function (personId, user, badgeCollection) { + const badgesOfType = []; + for (let i = 0; i < badgeCollection.length; i += 1) { + if (badgeCollection[i].badge?.type === 'X Hours for X Week Streak') { + badgesOfType.push(badgeCollection[i].badge); + } + } + await badge + .find({ type: 'X Hours for X Week Streak', weeks: 1 }) + .sort({ totalHrs: -1 }) + .then((results) => { + results.every((elem) => { + if (elem.totalHrs <= user.lastWeekTangibleHrs) { + let theBadge; + for (let i = 0; i < badgesOfType.length; i += 1) { + if (badgesOfType[i]._id.toString() === elem._id.toString()) { + theBadge = badgesOfType[i]._id; + break; + } + } + if (theBadge) { + increaseBadgeCount(personId, mongoose.Types.ObjectId(theBadge)); + return false; + } + addBadge(personId, mongoose.Types.ObjectId(elem._id)); + return false; + } + return true; + }); + }); + }; + + // 'X Hours for X Week Streak', + const checkXHrsForXWeeks = async function (personId, user, badgeCollection) { + let higherBadge = false; + // Check each Streak Greater than One to check if it works + await badge + .aggregate([ + { $match: { type: 'X Hours for X Week Streak', weeks: { $gt: 1 } } }, + // Group by 'week' property and sorting groups in descending order by 'week', then sorting badges within groups by 'totalHrs' in descending order. + { + $group: { + _id: '$weeks', + badges: { + $push: { _id: '$_id', hrs: '$totalHrs', weeks: '$weeks' }, + }, + }, + }, + { + $project: { + _id: 1, + badges: { + $slice: [ + { + $map: { + input: '$badges', + in: { + _id: '$$this._id', + hrs: '$$this.hrs', + weeks: '$$this.weeks', + }, + }, + }, + { $size: '$badges' }, + ], + }, + }, + }, + { $unwind: '$badges' }, + { $sort: { _id: -1, 'badges.hrs': -1 } }, // Primary sort on _id, secondary sort on badges.hrs + { + $group: { + _id: '$_id', + badges: { + $push: { + _id: '$badges._id', + hrs: '$badges.hrs', + weeks: '$badges.weeks', + }, + }, + }, + }, + { $sort: { _id: -1 } }, // Add this $sort stage for the final sorting by _id + ]) + .then((results) => { + let lastHr = -1; + results.forEach((streak) => { + streak.badges.every((bdge) => { + let badgeOfType; + for (let i = 0; i < badgeCollection.length; i += 1) { + if ( + badgeCollection[i].badge?.type === 'X Hours for X Week Streak' && + badgeCollection[i].badge?.weeks === bdge.weeks + ) { + if (badgeOfType && badgeOfType.totalHrs <= badgeCollection[i].badge.totalHrs) { + removeDupBadge(personId, badgeOfType._id); + badgeOfType = badgeCollection[i].badge; + } else if ( + badgeOfType && + badgeOfType.totalHrs > badgeCollection[i].badge.totalHrs + ) { + removeDupBadge(personId, badgeCollection[i].badge._id); + } else if (!badgeOfType) { + badgeOfType = badgeCollection[i].badge; + } + } + } + // check if it is possible to earn this streak + if (user.savedTangibleHrs.length >= bdge.weeks) { + let awardBadge = true; + const endOfArr = user.savedTangibleHrs.length - 1; + for (let i = endOfArr; i >= endOfArr - bdge.weeks + 1; i -= 1) { + if (user.savedTangibleHrs[i] < bdge.hrs) { + awardBadge = false; + return true; + } + } + // if all checks for award badge are green double check that we havent already awarded a higher streak for the same number of hours + if (awardBadge && bdge.hrs > lastHr) { + higherBadge = true; + lastHr = bdge.hrs; + if (badgeOfType && badgeOfType.totalHrs < bdge.hrs) { + replaceBadge( + personId, + mongoose.Types.ObjectId(badgeOfType._id), + mongoose.Types.ObjectId(bdge._id), + ); + + removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); + } else if (!badgeOfType) { + addBadge(personId, mongoose.Types.ObjectId(bdge._id)); + removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); + } else if (badgeOfType && badgeOfType.totalHrs === bdge.hrs) { + const lowerBound = badgeOfType.weeks; + let upperBound; + streak = 0; + + switch (bdge.weeks) { + case 2: + // In between 2Wk and 3Wk + upperBound = 3; + break; + case 3: + // In between 3Wk and 4Wk + upperBound = 4; + break; + case 4: + // In between 4Wk and 6Wk + upperBound = 6; + break; + case 6: + // In between 6Wk and 10Wk + upperBound = 10; + break; + case 10: + // In between 10Wk and 15Wk + upperBound = 15; + break; + case 15: + // In between 50Wk and 20Wk + upperBound = 20; + break; + case 20: + // In between 20Wk and 40Wk + upperBound = 40; + break; + case 40: + // In between 40Wk and 60Wk + upperBound = 60; + break; + case 60: + // In between 60Wk and 80Wk + upperBound = 80; + break; + case 80: + // In between 80Wk and 100Wk + upperBound = 100; + break; + case 100: + // In between 100Wk and 150Wk + upperBound = 150; + break; + case 150: + // In between 150Wk and 200Wk + upperBound = 200; + break; + default: + // Default case. Exiting function. + return; + } + for (let i = endOfArr; i >= endOfArr - upperBound + 1; i -= 1) { + if (user.savedTangibleHrs[i] >= bdge.hrs) { + streak += 1; + } + } + if (streak > lowerBound && streak < upperBound) { + higherBadge = false; + console.log('You are currently building an existing streak, no badge awarded.'); + } else { + console.log('You are currently building a new streak, new badge awarded'); + increaseBadgeCount(personId, mongoose.Types.ObjectId(badgeOfType._id)); + removePrevHrBadge(personId, user, badgeCollection, bdge.hrs, bdge.weeks); + } + } + return false; + } + } + return true; + }); + }); + }); + + // Handle Increasing the 1 week streak badges + if (!higherBadge) await checkXHrsInOneWeek(personId, user, badgeCollection); + }; + + // 'Lead a team of X+' + + const checkLeadTeamOfXplus = async function (personId, user, badgeCollection) { + const leaderRoles = ['Mentor', 'Manager', 'Administrator', 'Owner', 'Core Team']; + const approvedRoles = ['Mentor', 'Manager']; + if (!approvedRoles.includes(user.role)) return; + + let teamMembers; + await getTeamMembers({ + _id: personId, + }).then((results) => { + if (results) { + teamMembers = results.myteam; + } else { + teamMembers = []; + } + }); + + const objIds = {}; + + teamMembers = teamMembers.filter((member) => { + if (leaderRoles.includes(member.role)) return false; + if (objIds[member._id]) return false; + objIds[member._id] = true; + + return true; + }); + let badgeOfType; + for (let i = 0; i < badgeCollection.length; i += 1) { + if (badgeCollection[i].badge?.type === 'Lead a team of X+') { + if (badgeOfType && badgeOfType.people <= badgeCollection[i].badge.people) { + removeDupBadge(personId, badgeOfType._id); + badgeOfType = badgeCollection[i].badge; + } else if (badgeOfType && badgeOfType.people > badgeCollection[i].badge.people) { + removeDupBadge(personId, badgeCollection[i].badge._id); + } else if (!badgeOfType) { + badgeOfType = badgeCollection[i].badge; + } + } + } + await badge + .find({ type: 'Lead a team of X+' }) + .sort({ people: -1 }) + .then((results) => { + if (!Array.isArray(results) || !results.length) { + return; + } + results.every((bg) => { + if (teamMembers && teamMembers.length >= bg.people) { + if (badgeOfType) { + if ( + badgeOfType._id.toString() !== bg._id.toString() && + badgeOfType.people < bg.people + ) { + replaceBadge( + personId, + mongoose.Types.ObjectId(badgeOfType._id), + + mongoose.Types.ObjectId(bg._id), + ); + } + return false; + } + addBadge(personId, mongoose.Types.ObjectId(bg._id)); + return false; + } + return true; + }); + }); + }; + + // 'Total Hrs in Category' + const checkTotalHrsInCat = async function (personId, user, badgeCollection) { + const hoursByCategory = user.hoursByCategory || {}; + const categories = [ + 'food', + 'energy', + 'housing', + 'education', + 'society', + 'economics', + 'stewardship', + ]; + + const badgesOfType = badgeCollection + .filter((object) => object.badge.type === 'Total Hrs in Category') + .map((object) => object.badge); + + categories.forEach(async (category) => { + const categoryHrs = Object.keys(hoursByCategory).find((elem) => elem === category); + + let badgeOfType; + for (let i = 0; i < badgeCollection.length; i += 1) { + if ( + badgeCollection[i].badge?.type === 'Total Hrs in Category' && + badgeCollection[i].badge?.category === category + ) { + if (badgeOfType && badgeOfType.totalHrs <= badgeCollection[i].badge.totalHrs) { + removeDupBadge(personId, badgeOfType._id); + badgeOfType = badgeCollection[i].badge; + } else if (badgeOfType && badgeOfType.totalHrs > badgeCollection[i].badge.totalHrs) { + removeDupBadge(personId, badgeCollection[i].badge._id); + } else if (!badgeOfType) { + badgeOfType = badgeCollection[i].badge; + } + } + } + + const newCatg = category.charAt(0).toUpperCase() + category.slice(1); + + await badge + .find({ type: 'Total Hrs in Category', category: newCatg }) + + .sort({ totalHrs: -1 }) + .then((results) => { + if (!Array.isArray(results) || !results.length || !categoryHrs) { + return; + } + + results.every((elem) => { + if ( + hoursByCategory[categoryHrs] >= 100 && + hoursByCategory[categoryHrs] >= elem.totalHrs + ) { + let theBadge; + for (let i = 0; i < badgesOfType.length; i += 1) { + if (badgesOfType[i]._id.toString() === elem._id.toString()) { + theBadge = badgesOfType[i]._id; + break; + } + } + if (theBadge) { + increaseBadgeCount(personId, mongoose.Types.ObjectId(theBadge)); + return false; + } + if (badgeOfType) { + if ( + badgeOfType._id.toString() !== elem._id.toString() && + badgeOfType.totalHrs < elem.totalHrs + ) { + replaceBadge( + personId, + mongoose.Types.ObjectId(badgeOfType._id), + mongoose.Types.ObjectId(elem._id), + ); + } + return false; + } + addBadge(personId, mongoose.Types.ObjectId(elem._id)); + return false; + } + return true; + }); + }); + }); + }; + + const awardNewBadges = async () => { + try { + const users = await userProfile.find({ isActive: true }).populate('badgeCollection.badge'); + for (let i = 0; i < users.length; i += 1) { + const user = users[i]; + const { _id, badgeCollection } = user; + const personId = mongoose.Types.ObjectId(_id); + + await updatePersonalMax(personId, user); + await checkPersonalMax(personId, user, badgeCollection); + await checkMostHrsWeek(personId, user, badgeCollection); + await checkMinHoursMultiple(personId, user, badgeCollection); + await checkTotalHrsInCat(personId, user, badgeCollection); + await checkLeadTeamOfXplus(personId, user, badgeCollection); + await checkXHrsForXWeeks(personId, user, badgeCollection); + await checkNoInfringementStreak(personId, user, badgeCollection); + // remove cache after badge asssignment. + if (cache.hasCache(`user-${_id}`)) { + cache.removeCache(`user-${_id}`); + } + } + } catch (err) { + logger.logException(err); + } + }; + + const getTangibleHoursReportedThisWeekByUserId = function (personId) { + const userId = mongoose.Types.ObjectId(personId); + + const pdtstart = moment().tz('America/Los_Angeles').startOf('week').format('YYYY-MM-DD'); + const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); + + return timeEntries + .find( + { + personId: userId, + dateOfWork: { $gte: pdtstart, $lte: pdtend }, + isTangible: true, + }, + 'totalSeconds', + ) + .then((results) => { + const totalTangibleWeeklySeconds = results.reduce( + (acc, { totalSeconds }) => acc + totalSeconds, + 0, + ); + return (totalTangibleWeeklySeconds / 3600).toFixed(2); + }); + }; + + const sendDeactivateEmailBody = function (firstName, lastName, endDate, email, recipients) { + if (endDate) { + const subject = `IMPORTANT:${firstName} ${lastName} has been deactivated in the Highest Good Network`; + const emailBody = `

Management,

+ +

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${endDate}. + Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

+ +

With Gratitude,

+ +

One Community

`; + recipients.push('onecommunityglobal@gmail.com'); + recipients = recipients.toString(); + emailSender(recipients, subject, emailBody, null, null, email); + } + }; + + const deActivateUser = async () => { + try { + const emailReceivers = await userProfile.find( + { isActive: true, role: { $in: ['Owner'] } }, + '_id isActive role email', + ); + const recipients = emailReceivers.map((receiver) => receiver.email); + const users = await userProfile.find( + { isActive: true, endDate: { $exists: true } }, + '_id isActive endDate', + ); + for (let i = 0; i < users.length; i += 1) { + const user = users[i]; + const { endDate } = user; + endDate.setHours(endDate.getHours() + 7); + if (moment().isAfter(moment(endDate).add(1, 'days'))) { + try { + await userProfile.findByIdAndUpdate( + user._id, + user.set({ + isActive: false, + }), + { new: true }, + ); + } catch (err) { + // Log the error and continue to the next user + logger.logException(err, `Error in deActivateUser. Failed to update User ${user._id}`); + continue; + } + const id = user._id; + const person = await userProfile.findById(id); + const lastDay = moment(person.endDate).format('YYYY-MM-DD'); + logger.logInfo(`User with id: ${user._id} was de-acticated at ${moment().format()}.`); + person.teams.map(async (teamId) => { + const managementEmails = await userHelper.getTeamManagementEmail(teamId); + if (Array.isArray(managementEmails) && managementEmails.length > 0) { + managementEmails.forEach((management) => { + recipients.push(management.email); + }); + } + }); + sendDeactivateEmailBody( + person.firstName, + person.lastName, + lastDay, + person.email, + recipients, + ); + } + } + } catch (err) { + logger.logException(err, 'Unexpected error in deActivateUser'); + } + }; + + // Update by Shengwei/Peter PR767: + /** + * Delete all tokens used in new user setup from database that in cancelled, expired, or used status. + * Data retention: 90 days + */ + const deleteExpiredTokens = async () => { + const ninetyDaysAgo = moment().subtract(90, 'days').toDate(); + try { + await token.deleteMany({ isCancelled: true, expiration: { $lt: ninetyDaysAgo } }); + } catch (error) { + /* eslint-disable no-undef */ + logger.logException(error, `Error in deleteExpiredTokens. Date ${currentDate}`); + } + }; + + const deleteOldTimeOffRequests = async () => { + const endOfLastWeek = moment().tz('America/Los_Angeles').endOf('week').subtract(1, 'week'); + + const utcEndMoment = moment(endOfLastWeek).subtract(1, 'day').add(1, 'second'); + try { + await timeOffRequest.deleteMany({ endingDate: { $lte: utcEndMoment } }); + } catch (error) { + logger.logException( + error, + `Error deleting expired time-off requests: utcEndMoment ${utcEndMoment}`, + ); + } + }; + + return { + changeBadgeCount, + getUserName, + getTeamMembers, + getTeamManagementEmail, + validateProfilePic, + assignBlueSquareForTimeNotMet, + applyMissedHourForCoreTeam, + deleteBlueSquareAfterYear, + reActivateUser, + sendDeactivateEmailBody, + deActivateUser, + notifyInfringements, + getInfringementEmailBody, + emailWeeklySummariesForAllUsers, + awardNewBadges, + checkXHrsForXWeeks, + getTangibleHoursReportedThisWeekByUserId, + deleteExpiredTokens, + deleteOldTimeOffRequests, + }; +}; + +module.exports = userHelper; From e341c2ad687b48137e6cd0ab6d9417e06a3000aa Mon Sep 17 00:00:00 2001 From: 20chen-7 Date: Mon, 2 Sep 2024 12:15:42 -0600 Subject: [PATCH 20/31] chore: conflict --- src/helpers/userHelper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 7e69f9c7d..2b6aa4645 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -2099,7 +2099,7 @@ const userHelper = function () { const id = user._id; const person = await userProfile.findById(id); const lastDay = moment(person.endDate).format('YYYY-MM-DD'); - logger.logInfo(`User with id: ${user._id} was de-acticated at ${moment().format()}.`); + logger.logInfo(`User with id: ${user._id} was de-activated at ${moment().format()}.`); person.teams.map(async (teamId) => { const managementEmails = await userHelper.getTeamManagementEmail(teamId); if (Array.isArray(managementEmails) && managementEmails.length > 0) { From f7d7c075a6a63c1649537e27df34564d34ebcec4 Mon Sep 17 00:00:00 2001 From: 20chen-7 Date: Mon, 2 Sep 2024 12:17:43 -0600 Subject: [PATCH 21/31] chore: conflict --- src/helpers/userHelper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 2b6aa4645..6afcde243 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -2076,7 +2076,7 @@ const userHelper = function () { const recipients = emailReceivers.map((receiver) => receiver.email); const users = await userProfile.find( { isActive: true, endDate: { $exists: true } }, - '_id isActive endDate', + '_id isActive endDate isSet', ); for (let i = 0; i < users.length; i += 1) { const user = users[i]; From 6c33cd122d9601eb6880e3dc25d8233e18559c72 Mon Sep 17 00:00:00 2001 From: 20chen-7 Date: Mon, 2 Sep 2024 12:39:42 -0600 Subject: [PATCH 22/31] chore: conflict --- src/controllers/userProfileController.js | 19 +++++++++++-------- src/helpers/userHelper.js | 1 + 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index c3925b4c4..620f11ef6 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -505,7 +505,7 @@ const userProfileController = function (UserProfile, Project) { } }); - // Since we leverage cache for all team code retrival (refer func getAllTeamCode()), + // Since we leverage cache for all team code retrival (refer func getAllTeamCode()), // we need to remove the cache when team code is updated in case of new team code generation if (req.body.teamCode) { // remove teamCode cache when new team assigned @@ -729,14 +729,14 @@ const userProfileController = function (UserProfile, Project) { }) .catch((error) => { if (error.name === 'ValidationError' && error.errors.lastName) { - const errors = Object.values(error.errors).map(er => er.message); + const errors = Object.values(error.errors).map((er) => er.message); return res.status(400).json({ message: 'Validation Error', error: errors, }); } - console.error('Failed to save record:', error); - return res.status(400).json({ error: 'Failed to save record.' }); + console.error('Failed to save record:', error); + return res.status(400).json({ error: 'Failed to save record.' }); }); }); }; @@ -1241,6 +1241,7 @@ const userProfileController = function (UserProfile, Project) { endDate, user.email, recipients, + isSet, ); auditIfProtectedAccountUpdated( req.body.requestor.requestorId, @@ -1606,23 +1607,25 @@ const userProfileController = function (UserProfile, Project) { return teamCodes; } const distinctTeamCodes = await UserProfile.distinct('teamCode', { - teamCode: { $ne: null } + teamCode: { $ne: null }, }); cache.setCache('teamCodes', JSON.stringify(distinctTeamCodes)); return distinctTeamCodes; } catch (error) { throw new Error('Encountered an error to get all team codes, please try again!'); } - } + }; const getAllTeamCode = async function (req, res) { try { const distinctTeamCodes = await getAllTeamCodeHelper(); return res.status(200).send({ message: 'Found', distinctTeamCodes }); } catch (error) { - return res.status(500).send({ message: 'Encountered an error to get all team codes, please try again!' }); + return res + .status(500) + .send({ message: 'Encountered an error to get all team codes, please try again!' }); } - } + }; return { postUserProfile, diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 6afcde243..428051a0e 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -2114,6 +2114,7 @@ const userHelper = function () { lastDay, person.email, recipients, + person.isSet, ); } } From 0f177f9020f4319597909315853e297a1d82d4a3 Mon Sep 17 00:00:00 2001 From: Imran Issa Date: Tue, 3 Sep 2024 19:40:10 +0200 Subject: [PATCH 23/31] remove infringement duplicate code in userProfile --- src/controllers/userProfileController.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index b48b3d55d..d5cc6edae 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -684,14 +684,7 @@ const userProfileController = function (UserProfile, Project) { userData.startDate = record.startDate.toISOString(); } } - if ( - req.body.infringements !== undefined && - ((await hasPermission(req.body.requestor, 'addInfringements')) || - (await hasPermission(req.body.requestor, 'deleteInfringements')) || - (await hasPermission(req.body.requestor, 'editInfringements'))) - ) { - record.infringements = req.body.infringements; - } + let updatedDiff = null; if (PROTECTED_EMAIL_ACCOUNT.includes(record.email)) { updatedDiff = record.modifiedPaths(); From 9191e9d440dd44cd71ae53172eb843b16a584b0d Mon Sep 17 00:00:00 2001 From: huijieliu8 <91745551+metaphor987@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:13:42 -0400 Subject: [PATCH 24/31] fix: send keep-alive messages at regular intervals --- src/controllers/timeEntryController.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 6e8d36596..37a0f362c 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -1373,8 +1373,15 @@ const timeEntrycontroller = function (TimeEntry) { const recalculateHoursByCategoryAllUsers = async function (req, res) { const session = await mongoose.startSession(); session.startTransaction(); + let keepAliveInterval; try { + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Transfer-Encoding', 'chunked'); + keepAliveInterval = setInterval(() => { + res.write('Processing... keep connection alive\n'); + }, 100 * 1000); // interval of 100 seconds + const userprofiles = await UserProfile.find({}, '_id').lean(); const recalculationPromises = userprofiles.map(async (userprofile) => { @@ -1385,13 +1392,21 @@ const timeEntrycontroller = function (TimeEntry) { await Promise.all(recalculationPromises); await session.commitTransaction(); - return res.status(200).send({ - message: 'finished the recalculation for hoursByCategory for all users', - }); + // return res.status(200).send({ + // message: 'finished the recalculation for hoursByCategory for all users', + // }); + clearInterval(keepAliveInterval); + res.write('finished the recalculation for hoursByCategory for all users\n'); + return res.end(); } catch (err) { await session.abortTransaction(); + if (keepAliveInterval) { + clearInterval(keepAliveInterval); + } logger.logException(err); - return res.status(500).send({ error: err.toString() }); + res.write(`error: ${err.toString()}\n`); + // return res.status(500).send({ error: err.toString() }); + return res.end(); } finally { session.endSession(); } From 50e6f193fb88e5f0447681c150e6cfb475c606ee Mon Sep 17 00:00:00 2001 From: huijieliu8 <91745551+metaphor987@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:16:18 -0400 Subject: [PATCH 25/31] fix: update interval --- src/controllers/timeEntryController.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 37a0f362c..b8114633c 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -1380,7 +1380,7 @@ const timeEntrycontroller = function (TimeEntry) { res.setHeader('Transfer-Encoding', 'chunked'); keepAliveInterval = setInterval(() => { res.write('Processing... keep connection alive\n'); - }, 100 * 1000); // interval of 100 seconds + }, 150 * 1000); // interval of 150 seconds const userprofiles = await UserProfile.find({}, '_id').lean(); @@ -1392,9 +1392,6 @@ const timeEntrycontroller = function (TimeEntry) { await Promise.all(recalculationPromises); await session.commitTransaction(); - // return res.status(200).send({ - // message: 'finished the recalculation for hoursByCategory for all users', - // }); clearInterval(keepAliveInterval); res.write('finished the recalculation for hoursByCategory for all users\n'); return res.end(); @@ -1405,7 +1402,6 @@ const timeEntrycontroller = function (TimeEntry) { } logger.logException(err); res.write(`error: ${err.toString()}\n`); - // return res.status(500).send({ error: err.toString() }); return res.end(); } finally { session.endSession(); From 866db56c2c1be15c9041a74f4de7d9e7ca5f5ce2 Mon Sep 17 00:00:00 2001 From: 20chen-7 Date: Tue, 10 Sep 2024 13:38:42 -0600 Subject: [PATCH 26/31] chore: hotfix-template --- src/helpers/userHelper.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index ed9c52131..b9bd429d5 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -2042,7 +2042,7 @@ const userHelper = function () { const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; const emailBody = `

Management,

-

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${endDate}. +

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}. Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

With Gratitude,

@@ -2052,11 +2052,11 @@ const userHelper = function () { recipients = recipients.toString(); emailSender(recipients, subject, emailBody, null, null, email); } else if (isSet) { - const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; + const subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; const emailBody = `

Management,

-

Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network ${endDate}. - For a smooth transition, please confirm all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.

+

Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.

+

For a smooth transition, please confirm all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.

With Gratitude,

From ecb486324a58f8a5a1813210c1874469f15c1ba2 Mon Sep 17 00:00:00 2001 From: 20chen-7 Date: Tue, 10 Sep 2024 13:40:28 -0600 Subject: [PATCH 27/31] chore: hotfix-template --- src/helpers/userHelper.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index b9bd429d5..eaf63a39a 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -2042,8 +2042,8 @@ const userHelper = function () { const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; const emailBody = `

Management,

-

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}. - Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

+

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

+

Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

With Gratitude,

From e91fd72bb4ae7b67f41ee3a4d333bed26cb29d78 Mon Sep 17 00:00:00 2001 From: 20chen-7 Date: Wed, 11 Sep 2024 14:46:05 -0600 Subject: [PATCH 28/31] chore: pause --- src/controllers/userProfileController.js | 13 ++++++++----- src/helpers/userHelper.js | 19 +++++++++++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 8226aee65..c15f970b2 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -505,7 +505,7 @@ const userProfileController = function (UserProfile, Project) { } }); - // Since we leverage cache for all team code retrival (refer func getAllTeamCode()), + // Since we leverage cache for all team code retrival (refer func getAllTeamCode()), // we need to remove the cache when team code is updated in case of new team code generation if (req.body.teamCode) { // remove teamCode cache when new team assigned @@ -1232,6 +1232,7 @@ const userProfileController = function (UserProfile, Project) { user.email, recipients, isSet, + activationDate, ); auditIfProtectedAccountUpdated( req.body.requestor.requestorId, @@ -1597,23 +1598,25 @@ const userProfileController = function (UserProfile, Project) { return teamCodes; } const distinctTeamCodes = await UserProfile.distinct('teamCode', { - teamCode: { $ne: null } + teamCode: { $ne: null }, }); cache.setCache('teamCodes', JSON.stringify(distinctTeamCodes)); return distinctTeamCodes; } catch (error) { throw new Error('Encountered an error to get all team codes, please try again!'); } - } + }; const getAllTeamCode = async function (req, res) { try { const distinctTeamCodes = await getAllTeamCodeHelper(); return res.status(200).send({ message: 'Found', distinctTeamCodes }); } catch (error) { - return res.status(500).send({ message: 'Encountered an error to get all team codes, please try again!' }); + return res + .status(500) + .send({ message: 'Encountered an error to get all team codes, please try again!' }); } - } + }; return { postUserProfile, diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index ed9c52131..978a6b2e5 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -2037,8 +2037,22 @@ const userHelper = function () { email, recipients, isSet, + reactivationDate, ) { - if (endDate && !isSet) { + if (reactivationDate) { + const subject = `IMPORTANT: ${firstName} ${lastName} has been PAUSED in the Highest Good Network`; + const emailBody = `

Management,

+ +

Please note that ${firstName} ${lastName} has been PAUSED in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

+

Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part until they return on ${moment(reactivationDate).format('M-D-YYYY')}.

+ +

With Gratitude,

+ +

One Community

`; + recipients.push('onecommunityglobal@gmail.com'); + recipients = recipients.toString(); + emailSender(recipients, subject, emailBody, null, null, email); + } else if (endDate && !isSet) { const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; const emailBody = `

Management,

@@ -2076,7 +2090,7 @@ const userHelper = function () { const recipients = emailReceivers.map((receiver) => receiver.email); const users = await userProfile.find( { isActive: true, endDate: { $exists: true } }, - '_id isActive endDate isSet', + '_id isActive endDate isSet reactivationDate', ); for (let i = 0; i < users.length; i += 1) { const user = users[i]; @@ -2115,6 +2129,7 @@ const userHelper = function () { person.email, recipients, person.isSet, + person.reactivationDate, ); } } From 7cc97a3252738d89a31a6ea7c9cea8b8aca34188 Mon Sep 17 00:00:00 2001 From: "Xiaoyu(Ivy) Chen" <102696148+20chen-7@users.noreply.github.com> Date: Wed, 11 Sep 2024 18:50:15 -0600 Subject: [PATCH 29/31] Update userHelper.js --- src/helpers/userHelper.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 8c1ff8481..307c484be 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -2038,12 +2038,12 @@ const userHelper = function () { recipients, isSet, ) { - if (endDate && isSet) { - const subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; + if (endDate && !isSet) { + const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; const emailBody = `

Management,

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

-

For a smooth transition, please confirm all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.

+

Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

With Gratitude,

@@ -2052,11 +2052,10 @@ const userHelper = function () { recipients = recipients.toString(); emailSender(recipients, subject, emailBody, null, null, email); } else if (isSet) { - const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; + const subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; const emailBody = `

Management,

- -

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

. -

Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

+

Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

+

For a smooth transition, please confirm all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.

With Gratitude,

From 55c67e3f26d25758a5cf03668a10b9cbaeaafc97 Mon Sep 17 00:00:00 2001 From: "Xiaoyu(Ivy) Chen" <102696148+20chen-7@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:45:39 -0600 Subject: [PATCH 30/31] Update userHelper.js --- src/helpers/userHelper.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index ca9d75331..ba7d64bc5 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -2044,7 +2044,7 @@ const userHelper = function () { const emailBody = `

Management,

Please note that ${firstName} ${lastName} has been PAUSED in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

-

Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part until they return on ${moment(reactivationDate).format('M-D-YYYY')}.

+

For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part until they return on ${moment(reactivationDate).format('M-D-YYYY')}.

With Gratitude,

@@ -2052,12 +2052,12 @@ const userHelper = function () { recipients.push('onecommunityglobal@gmail.com'); recipients = recipients.toString(); emailSender(recipients, subject, emailBody, null, null, email); - } else if (endDate && !isSet) { + } else if (endDate && isSet) { const subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; const emailBody = `

Management,

- -

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

-

Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

+ +

Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

+

For a smooth transition, please confirm all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.

With Gratitude,

@@ -2065,11 +2065,12 @@ const userHelper = function () { recipients.push('onecommunityglobal@gmail.com'); recipients = recipients.toString(); emailSender(recipients, subject, emailBody, null, null, email); - } else if (isSet) { - const subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; + } else { + const subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; const emailBody = `

Management,

-

Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

-

For a smooth transition, please confirm all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.

+ +

Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as of ${moment(endDate).format('M-D-YYYY')}.

+

For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.

With Gratitude,

From 2f0d9495dd56bc4ac5c24851a1da25d09f128c2b Mon Sep 17 00:00:00 2001 From: 20chen-7 Date: Thu, 12 Sep 2024 15:49:25 -0600 Subject: [PATCH 31/31] chore: check endDate & inactive --- src/controllers/userProfileController.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index c15f970b2..ba66976c9 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1156,7 +1156,15 @@ const userProfileController = function (UserProfile, Project) { const activationDate = req.body.reactivationDate; const { endDate } = req.body; const isSet = req.body.isSet === 'FinalDay'; - + let activeStatus = status; + if (endDate && status) { + const dateObject = new Date(endDate); + dateObject.setHours(dateObject.getHours() + 7); + const setEndDate = dateObject; + if (moment().isAfter(moment(setEndDate).add(1, 'days'))) { + activeStatus = false; + } + } if (!mongoose.Types.ObjectId.isValid(userId)) { res.status(400).send({ error: 'Bad Request', @@ -1205,7 +1213,7 @@ const userProfileController = function (UserProfile, Project) { UserProfile.findById(userId, 'isActive email firstName lastName') .then((user) => { user.set({ - isActive: status, + isActive: activeStatus, reactivationDate: activationDate, endDate, isSet,