From b60e77ff3ba46c258df4d061ba5958f403436ba8 Mon Sep 17 00:00:00 2001 From: One Community Date: Fri, 28 Jun 2024 18:07:01 -0700 Subject: [PATCH] Revert "Shereen purchase equipment" --- package-lock.json | 8 +- requirements/badgeController/deleteBadge.md | 20 + requirements/badgeController/putBadge.md | 19 + .../createMouseoverText.md | 11 + .../getMouseoverText.md | 11 + .../updateMouseoverText.md | 12 + .../rolePresetsController/createNewPresets.md | 18 + .../rolePresetsController/deletePresetById.md | 17 + .../rolePresetsController/getPresetsByRole.md | 15 + .../rolePresetsController/updatePresetById.md | 17 + .../userProfileController/deleteWBS.md | 18 + .../getUserProfiles-usecase.md | 20 - requirements/userProfileController/getWBS.md | 16 + .../userProfileController/getWBSByUserId.md | 16 + .../postUserProfile-usecase.md | 31 - requirements/wbsController/getAllWBS.md | 16 + requirements/wbsController/postWBS.md | 18 + src/constants/eventTypes.js | 17 +- src/constants/message.js | 25 +- src/controllers/actionItemController.js | 254 ++++---- src/controllers/badgeController.js | 5 +- src/controllers/badgeController.spec.js | 604 +++++++++++------- .../bmdashboard/bmConsumableController.js | 55 +- .../bmdashboard/bmEquipmentController.js | 202 +++--- .../bmdashboard/bmInventoryTypeController.js | 47 +- .../bmdashboard/bmMaterialsController.js | 119 ++-- .../bmdashboard/bmNewLessonController.js | 10 +- .../bmdashboard/bmProjectController.js | 6 +- .../bmdashboard/bmToolController.js | 149 ++++- src/controllers/dashBoardController.js | 36 +- src/controllers/informationController.js | 10 +- src/controllers/inventoryController.js | 80 +-- src/controllers/mapLocationsController.js | 4 +- src/controllers/mouseoverTextController.js | 78 +-- .../mouseoverTextController.spec.js | 162 +++++ src/controllers/notificationController.js | 3 + .../popupEditorBackupController.js | 14 +- src/controllers/popupEditorController.js | 16 +- .../profileInitialSetupController.js | 551 +++++++++++----- src/controllers/projectController.js | 270 ++++---- src/controllers/reportsController.js | 2 +- src/controllers/rolePresetsController.js | 35 +- src/controllers/rolePresetsController.spec.js | 444 +++++++++++++ src/controllers/rolesController.js | 51 +- src/controllers/taskController.js | 68 +- src/controllers/taskNotificationController.js | 8 +- src/controllers/timeEntryController.js | 52 +- src/controllers/titleController.js | 130 ++++ src/controllers/userProfileController.js | 71 +- src/controllers/wbsController.js | 67 +- src/controllers/wbsController.spec.js | 323 ++++++++++ src/cronjobs/userProfileJobs.js | 2 +- src/helpers/dashboardhelper.js | 89 +-- src/helpers/helperModels/userProjects.js | 17 - src/helpers/notificationhelper.js | 95 +-- src/helpers/taskHelper.js | 182 +++--- src/helpers/userHelper.js | 357 ++++++++++- .../bmdashboard/buildingInventoryItem.js | 15 +- .../bmdashboard/buildingInventoryType.js | 11 +- src/models/notification.js | 40 +- src/models/profileInitialSetupToken.js | 19 + src/models/project.js | 17 +- src/models/timeentry.js | 1 + src/models/title.js | 21 + src/models/userProfile.js | 8 +- src/models/wbs.js | 2 - src/routes/actionItemRouter.js | 33 +- src/routes/badgeRouter.js | 2 +- src/routes/badgeRouter.test.js | 172 ++++- src/routes/bmdashboard/bmConsumablesRouter.js | 3 + src/routes/bmdashboard/bmEquipmentRouter.js | 4 +- .../bmdashboard/bmInventoryTypeRouter.js | 2 - src/routes/bmdashboard/bmToolRouter.js | 10 +- src/routes/mouseoverTextRouter.test.js | 98 +++ src/routes/notificationRouter.js | 26 +- src/routes/profileInitialSetupRouter.js | 32 +- src/routes/rolePresetRouter.test.js | 238 +++++++ src/routes/taskRouter.js | 50 +- src/routes/timeentryRouter.js | 2 + src/routes/titleRouter.js | 22 + src/routes/userProfileRouter.js | 12 +- src/routes/wbsRouter.js | 10 +- src/routes/wbsRouter.test.js | 353 ++++++++++ src/startup/db.js | 6 +- src/startup/routes.js | 19 +- src/utilities/addMembersToTeams.js | 10 +- src/utilities/addNewField.js | 2 +- src/utilities/createInitialPermissions.js | 52 +- src/utilities/timeUtils.js | 21 +- src/utilities/yearMonthDayDateValidator.js | 2 +- src/websockets/TimerService/clientsHandler.js | 31 +- .../TimerService/connectionsHandler.js | 4 +- 92 files changed, 4809 insertions(+), 1534 deletions(-) create mode 100644 requirements/badgeController/deleteBadge.md create mode 100644 requirements/badgeController/putBadge.md create mode 100644 requirements/mouseoverTextController/createMouseoverText.md create mode 100644 requirements/mouseoverTextController/getMouseoverText.md create mode 100644 requirements/mouseoverTextController/updateMouseoverText.md create mode 100644 requirements/rolePresetsController/createNewPresets.md create mode 100644 requirements/rolePresetsController/deletePresetById.md create mode 100644 requirements/rolePresetsController/getPresetsByRole.md create mode 100644 requirements/rolePresetsController/updatePresetById.md create mode 100644 requirements/userProfileController/deleteWBS.md delete mode 100644 requirements/userProfileController/getUserProfiles-usecase.md create mode 100644 requirements/userProfileController/getWBS.md create mode 100644 requirements/userProfileController/getWBSByUserId.md delete mode 100644 requirements/userProfileController/postUserProfile-usecase.md create mode 100644 requirements/wbsController/getAllWBS.md create mode 100644 requirements/wbsController/postWBS.md create mode 100644 src/controllers/mouseoverTextController.spec.js create mode 100644 src/controllers/rolePresetsController.spec.js create mode 100644 src/controllers/titleController.js create mode 100644 src/controllers/wbsController.spec.js delete mode 100644 src/helpers/helperModels/userProjects.js create mode 100644 src/models/title.js create mode 100644 src/routes/mouseoverTextRouter.test.js create mode 100644 src/routes/rolePresetRouter.test.js create mode 100644 src/routes/titleRouter.js create mode 100644 src/routes/wbsRouter.test.js diff --git a/package-lock.json b/package-lock.json index 6c87d4a77..c5b9d5fb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3762,7 +3762,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "colorette": { "version": "2.0.20", @@ -5890,7 +5890,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" }, "has-property-descriptors": { "version": "1.0.2", @@ -9050,7 +9050,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "lodash.includes": { "version": "4.3.0", @@ -10043,7 +10043,7 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" }, "path-is-absolute": { "version": "1.0.1", diff --git a/requirements/badgeController/deleteBadge.md b/requirements/badgeController/deleteBadge.md new file mode 100644 index 000000000..9b226852b --- /dev/null +++ b/requirements/badgeController/deleteBadge.md @@ -0,0 +1,20 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200 if the badge is successfully removed and all instances of the badge are removed from user profiles. +3. ✅ Clears cache if cache exists. + +> ## Negative case + +1. ❌ Returns error 404 if the API does not exist +2. ✅ Returns 403 if the user does not have permission to delete badges +3. ✅ Returns 400 if an no badge is found +4. ✅ Returns 500 if the removeBadgeFromProfile fails. +5. ✅ Returns 500 if the remove method fails. + +> ## Edge case diff --git a/requirements/badgeController/putBadge.md b/requirements/badgeController/putBadge.md new file mode 100644 index 000000000..bfeaa043f --- /dev/null +++ b/requirements/badgeController/putBadge.md @@ -0,0 +1,19 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Post Badge + +> ## Positive case + +1. ❌ Receives a POST request in the **/api/userProfile** route +2. ✅ Returns 200 if all is successful +3. ✅ Removes `allBadges` from cache if all is successful and the cache is not empty + +> ## Negative case + +1. ❌ Returns error 404 if the API does not exist +2. ✅ Returns 403 if the user is not authorized +3. ✅ Returns 400 if an error occurs in `findById` +4. ✅ Returns 400 if no badge is found + +> ## Edge case diff --git a/requirements/mouseoverTextController/createMouseoverText.md b/requirements/mouseoverTextController/createMouseoverText.md new file mode 100644 index 000000000..118803ae8 --- /dev/null +++ b/requirements/mouseoverTextController/createMouseoverText.md @@ -0,0 +1,11 @@ +Check mark: ✅ +Cross Mark: ❌ + +# createMouseoverText + +> ## Positive case +1. ✅ Return 201 if create new mouseoverText successfully. + +> ## Negative case +1. ✅ Returns error 500 if any error when saving the new mouseoverText +> ## Edge case \ No newline at end of file diff --git a/requirements/mouseoverTextController/getMouseoverText.md b/requirements/mouseoverTextController/getMouseoverText.md new file mode 100644 index 000000000..aee52b346 --- /dev/null +++ b/requirements/mouseoverTextController/getMouseoverText.md @@ -0,0 +1,11 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getMouseoverText + +> ## Positive case +1. ✅ Return 200 if find mouseoverText successfully. + +> ## Negative case +1. ✅ Returns error 404 if any error when finding the mouseoverText +> ## Edge case \ No newline at end of file diff --git a/requirements/mouseoverTextController/updateMouseoverText.md b/requirements/mouseoverTextController/updateMouseoverText.md new file mode 100644 index 000000000..84f786c26 --- /dev/null +++ b/requirements/mouseoverTextController/updateMouseoverText.md @@ -0,0 +1,12 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updateMouseoverText + +> ## Positive case +1. ✅ Return 201 if updating mouseoverText successfully. + +> ## Negative case +1. ✅ Returns error 500 if any error when finding the mouseoverText by Id +2. ✅ Returns error 400 if any error when saving the mouseoverText +> ## Edge case \ No newline at end of file diff --git a/requirements/rolePresetsController/createNewPresets.md b/requirements/rolePresetsController/createNewPresets.md new file mode 100644 index 000000000..7a6edc948 --- /dev/null +++ b/requirements/rolePresetsController/createNewPresets.md @@ -0,0 +1,18 @@ +Check mark: ✅ +Cross Mark: ❌ + +# createNewPreset + +> ## Positive case +1. ✅ Receives a POST request in the **/api/rolePreset** route +2. ✅ Return 201 if create New Presets successfully. + +> ## Negative case + +1. ✅ Returns error 403 if user doesn't have permissions for putRole +2. ✅ Returns 400 if missing presetName +3. ✅ Returns 400 if missing roleName +4. ✅ Returns 400 if missing premissions +5. ✅ Returns error 400 when saving new presets + +> ## Edge case diff --git a/requirements/rolePresetsController/deletePresetById.md b/requirements/rolePresetsController/deletePresetById.md new file mode 100644 index 000000000..7698663fb --- /dev/null +++ b/requirements/rolePresetsController/deletePresetById.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# deletePresetById + +> ## Positive case + +1. ✅ Return 200 if removing preset by id successfully. + +> ## Negative case + +1. ✅ Returns error 403 if user doesn't have permissions for putRole +2. ✅ Returns 400 if error in finding by id +3. ✅ Returns 400 if the route doesn't exist +4. ✅ Returns 400 if any error when removing results + +> ## Edge case \ No newline at end of file diff --git a/requirements/rolePresetsController/getPresetsByRole.md b/requirements/rolePresetsController/getPresetsByRole.md new file mode 100644 index 000000000..7f14b828e --- /dev/null +++ b/requirements/rolePresetsController/getPresetsByRole.md @@ -0,0 +1,15 @@ +Check mark: ✅ +Cross Mark: ❌ + +# getPresetsByRole + +> ## Positive case +1. ✅ Receives a GET request in the **/api/rolePreset** route +2. ✅ Return 200 if get Presets by roleName successfully. + +> ## Negative case + +1. ✅ Returns error 403 if user doesn't have permissions for putRole +2. ✅ Returns 400 when catching any error in finding roleName + +> ## Edge case \ No newline at end of file diff --git a/requirements/rolePresetsController/updatePresetById.md b/requirements/rolePresetsController/updatePresetById.md new file mode 100644 index 000000000..6ce963cf3 --- /dev/null +++ b/requirements/rolePresetsController/updatePresetById.md @@ -0,0 +1,17 @@ +Check mark: ✅ +Cross Mark: ❌ + +# updatePresetById + +> ## Positive case + +1. ✅ Return 200 if update preset by id successfully. + +> ## Negative case + +1. ✅ Returns error 403 if user doesn't have permissions for putRole +2. ✅ Returns 400 if the router doesn't exist +3. ✅ Returns 400 if error in finding by id +3. ✅ Returns 400 if any error when saving results + +> ## Edge case \ No newline at end of file diff --git a/requirements/userProfileController/deleteWBS.md b/requirements/userProfileController/deleteWBS.md new file mode 100644 index 000000000..105510d3e --- /dev/null +++ b/requirements/userProfileController/deleteWBS.md @@ -0,0 +1,18 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get User Profiles + +> ## Positive case + +1. ✅ Receives a GET request in the **/api/userProfile** route +2. ✅ Returns 200 if all is successful + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist +2. ✅ Returns 403 if the user does not have permission +3. ✅ Returns 400 if and error occurs when querying DB +4. ✅ returns 400 if an error occurs when removing the WBS + +> ## Edge case diff --git a/requirements/userProfileController/getUserProfiles-usecase.md b/requirements/userProfileController/getUserProfiles-usecase.md deleted file mode 100644 index ff4d8d392..000000000 --- a/requirements/userProfileController/getUserProfiles-usecase.md +++ /dev/null @@ -1,20 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Get User Profiles - -> ## Positive case - -1. ✅ Receives a GET request in the **/api/userProfile** route -2. ✅ Returns 200 if there are no users in the database and the allusers key exists in NodeCache. -3. ✅ Returns 200 if there are users in the database - -> ## Negative case - -1. ✅ Returns error 404 if the API does not exist -2. ✅ Returns 400 if the user doesn't have - getUserProfiles permission -3. ✅ Returns 500 if there are no users in the database and the allusers key doesn't exist in NodeCache -4. ✅ Returns 404 if any error occurs while getting all user profiles - -> ## Edge case diff --git a/requirements/userProfileController/getWBS.md b/requirements/userProfileController/getWBS.md new file mode 100644 index 000000000..b5cef8990 --- /dev/null +++ b/requirements/userProfileController/getWBS.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get User Profiles + +> ## Positive case + +1. ✅ Receives a GET request in the **/api/userProfile** route +2. ✅ Returns 200 if all is successful + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist +2. ✅ Returns 500 if any errors occur when finding all WBS + +> ## Edge case diff --git a/requirements/userProfileController/getWBSByUserId.md b/requirements/userProfileController/getWBSByUserId.md new file mode 100644 index 000000000..f4f87fa23 --- /dev/null +++ b/requirements/userProfileController/getWBSByUserId.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get User Profiles + +> ## Positive case + +1. ✅ Receives a GET request in the **/api/userProfile** route +2. ✅ Returns 200 if all is successful + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist +2. ✅ Returns 404 if an error occurs in the aggregation query + +> ## Edge case diff --git a/requirements/userProfileController/postUserProfile-usecase.md b/requirements/userProfileController/postUserProfile-usecase.md deleted file mode 100644 index b61d5fe59..000000000 --- a/requirements/userProfileController/postUserProfile-usecase.md +++ /dev/null @@ -1,31 +0,0 @@ -Check mark: ✅ -Cross Mark: ❌ - -# Post User Profile - -> ## Positive case - -1. ✅ Receives a POST request in the **/api/userProfile** route -2. ✅ check if user has permissions for **postUserProfile** -3. ✅ check if user has permissions for **addDeleteEditOwners** or if the user role is **owner** -4. ✅ verify if the email address is already in use -5. ✅ check if environment is not dev environment **hgnData_dev** -6. ✅ check if firstname and lastname exist -7. ✅ Save user profile -8. ✅ Returns **200**, with the id of userProfile created - -> ## Negative case - -1. ✅ Returns error 404 if the API does not exist -2. ✅ Returns error 403 if the user doesn't have permissions for **postUserProfile** -3. ✅ Returns error 403 if the user doesn't have permissions for **addDeleteEditOwners** and if the user role is an **owner** -4. ✅ Returns error 400 if the email address is already in use -5. ✅ Returns error 400 if in dev environment, the role is owner or administrator and the actual email or password are incorrect -6. ✅ Returns 400 if the firstname and lastname already exist and if no duplicate name is allowed -7. ✅ Returns error 501 if there is an error when trying to create the userProfile - -> ## Edge case - -1. ❌ Returns 400 if email is invalid -2. ❌ Returns 400 if password is invalid -3. ❌ Returns 400 if teamcode is invalid diff --git a/requirements/wbsController/getAllWBS.md b/requirements/wbsController/getAllWBS.md new file mode 100644 index 000000000..d4c464a13 --- /dev/null +++ b/requirements/wbsController/getAllWBS.md @@ -0,0 +1,16 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get User Profiles + +> ## Positive case + +1. ✅ Receives a GET request in the **/api/userProfile** route +2. ✅ Returns 200 if all is successful + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist +2. ✅ Returns 404 if an error occurs when querying the database. + +> ## Edge case diff --git a/requirements/wbsController/postWBS.md b/requirements/wbsController/postWBS.md new file mode 100644 index 000000000..5e4151662 --- /dev/null +++ b/requirements/wbsController/postWBS.md @@ -0,0 +1,18 @@ +Check mark: ✅ +Cross Mark: ❌ + +# Get User Profiles + +> ## Positive case + +1. ✅ Receives a GET request in the **/api/userProfile** route +2. ✅ Returns 201 if all is successful + +> ## Negative case + +1. ✅ Returns error 404 if the API does not exist +2. ✅ Returns 403 if the user does not have permission +3. ✅ Returns 400 if `req.body` does not contain `wbsName` or `isActive` +4. ✅ returns 500 if an error occurs when saving + +> ## Edge case diff --git a/src/constants/eventTypes.js b/src/constants/eventTypes.js index aeef27028..e3a6ff0cb 100644 --- a/src/constants/eventTypes.js +++ b/src/constants/eventTypes.js @@ -1,7 +1,12 @@ -const eventtypes = { - ActionCreated: 'Action Created', - ActionEdited: 'Action Edited', - ActionDeleted: 'Action Deleted', -}; +/** + * Unused legacy code. Commented out to avoid confusion. + * Commented by: Shengwei Peng + * Date: 2024-03-22 + */ +// const eventtypes = { +// ActionCreated: 'Action Created', +// ActionEdited: 'Action Edited', +// ActionDeleted: 'Action Deleted', +// }; -module.exports = eventtypes; +// module.exports = eventtypes; diff --git a/src/constants/message.js b/src/constants/message.js index 3b834f1ec..3ac3e54d3 100644 --- a/src/constants/message.js +++ b/src/constants/message.js @@ -1,19 +1,24 @@ -export const URL_TO_BLUE_SQUARE_PAGE = +const URL_TO_BLUE_SQUARE_PAGE = 'https://www.onecommunityglobal.org/hands-off-administration-policy'; // Since the notification banner is blue background, added white color for hyperlink style. -export const NEW_USER_BLUE_SQUARE_NOTIFICATION_MESSAGE = ` -

Welcome as one of our newest members to the One Community team and family! - Heads up we’ve removed a “blue square” that - was issued due to not completing your hours and/or summary this past week. The reason we removed - this blue square is because you didn’t have the full week available to complete your volunteer +const NEW_USER_BLUE_SQUARE_NOTIFICATION_MESSAGE = ` +

Welcome as one of our newest members to the One Community team and family! + Heads up we’ve removed a “blue square” that + was issued due to not completing your hours and/or summary this past week. The reason we removed + this blue square is because you didn’t have the full week available to complete your volunteer time with us.

- -

If you’d like to learn more about this policy and/or blue squares, click here: - “Blue Square FAQ” + +

If you’d like to learn more about this policy and/or blue squares, click here: + “Blue Square FAQ”

- +

Welcome again, we’re glad to have you joining us!

With Gratitude,
One Community

`; + +module.exports = { + NEW_USER_BLUE_SQUARE_NOTIFICATION_MESSAGE, + URL_TO_BLUE_SQUARE_PAGE, +}; diff --git a/src/controllers/actionItemController.js b/src/controllers/actionItemController.js index d0675c522..6d5864213 100644 --- a/src/controllers/actionItemController.js +++ b/src/controllers/actionItemController.js @@ -1,126 +1,128 @@ -const mongoose = require('mongoose'); -const closure = require('../helpers/notificationhelper'); - -const actionItemController = function (ActionItem) { - const notificationhelper = closure(); - - const getactionItem = async function (req, res) { - const userid = req.params.userId; - - try { - const actionItems = await ActionItem.find( - { - assignedTo: userid, - }, - '-createdDateTime -__v', - ).populate('createdBy', 'firstName lastName'); - const actionitems = []; - - actionItems.forEach((element) => { - const actionitem = {}; - - actionitem._id = element._id; - actionitem.description = element.description; - actionitem.createdBy = `${element.createdBy.firstName} ${element.createdBy.lastName}`; - actionitem.assignedTo = element.assignedTo; - - actionitems.push(actionitem); - }); - - res.status(200).send(actionitems); - } catch (error) { - res.status(400).send(error); - } - }; - const postactionItem = async function (req, res) { - const { requestorId, assignedTo } = req.body.requestor; - const _actionItem = new ActionItem(); - - _actionItem.description = req.body.description; - _actionItem.assignedTo = req.body.assignedTo; - _actionItem.createdBy = req.body.requestor.requestorId; - - try { - const savedItem = await _actionItem.save(); - notificationhelper.notificationcreated(requestorId, assignedTo, _actionItem.description); - - const actionitem = {}; - - actionitem.createdBy = 'You'; - actionitem.description = _actionItem.description; - actionitem._id = savedItem._id; - actionitem.assignedTo = _actionItem.assignedTo; - - res.status(200).send(actionitem); - } catch (err) { - res.status(400).send(err); - } - }; - - const deleteactionItem = async function (req, res) { - const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); - - try { - const _actionItem = await ActionItem.findById(actionItemId); - - if (!_actionItem) { - res.status(400).send({ - message: 'No valid records found', - }); - return; - } - - const { requestorId, assignedTo } = req.body.requestor; - - notificationhelper.notificationdeleted(requestorId, assignedTo, _actionItem.description); - - await _actionItem.remove(); - res.status(200).send({ - message: 'removed', - }); - } catch (error) { - res.status(400).send(error); - } - }; - - const editactionItem = async function (req, res) { - const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); - - const { requestorId, assignedTo } = req.body.requestor; - - try { - const _actionItem = await ActionItem.findById(actionItemId); - - if (!_actionItem) { - res.status(400).send({ - message: 'No valid records found', - }); - return; - } - - notificationhelper.notificationedited( - requestorId, - assignedTo, - _actionItem.description, - req.body.description, - ); - - _actionItem.description = req.body.description; - _actionItem.assignedTo = req.body.assignedTo; - - await _actionItem.save(); - res.status(200).send({ message: 'Saved' }); - } catch (error) { - res.status(400).send(error); - } - }; - - return { - getactionItem, - postactionItem, - deleteactionItem, - editactionItem, - }; -}; - -module.exports = actionItemController; +/** + * Unused legacy code. Commented out to avoid confusion. Will delete in the next cycle. + * Commented by: Shengwei Peng + * Date: 2024-03-22 + */ + +// const mongoose = require('mongoose'); +// const notificationhelper = require('../helpers/notificationhelper')(); + +// const actionItemController = function (ActionItem) { +// const getactionItem = function (req, res) { +// const userid = req.params.userId; +// ActionItem.find({ +// assignedTo: userid, +// }, ('-createdDateTime -__v')) +// .populate('createdBy', 'firstName lastName') +// .then((results) => { +// const actionitems = []; + +// results.forEach((element) => { +// const actionitem = {}; + +// actionitem._id = element._id; +// actionitem.description = element.description; +// actionitem.createdBy = `${element.createdBy.firstName} ${element.createdBy.lastName}`; +// actionitem.assignedTo = element.assignedTo; + +// actionitems.push(actionitem); +// }); + +// res.status(200).send(actionitems); +// }) +// .catch((error) => { +// res.status(400).send(error); +// }); +// }; +// const postactionItem = function (req, res) { +// const { requestorId, assignedTo } = req.body.requestor; +// const _actionItem = new ActionItem(); + +// _actionItem.description = req.body.description; +// _actionItem.assignedTo = req.body.assignedTo; +// _actionItem.createdBy = req.body.requestor.requestorId; + +// _actionItem.save() +// .then((result) => { +// notificationhelper.notificationcreated(requestorId, assignedTo, _actionItem.description); + +// const actionitem = {}; + +// actionitem.createdBy = 'You'; +// actionitem.description = _actionItem.description; +// actionitem._id = result._id; +// actionitem.assignedTo = _actionItem.assignedTo; + +// res.status(200).send(actionitem); +// }) +// .catch((error) => { +// res.status(400).send(error); +// }); +// }; + +// const deleteactionItem = async function (req, res) { +// const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); + +// const _actionItem = await ActionItem.findById(actionItemId) +// .catch((error) => { +// res.status(400).send(error); +// }); + +// if (!_actionItem) { +// res.status(400).send({ +// message: 'No valid records found', +// }); +// return; +// } + +// const { requestorId, assignedTo } = req.body.requestor; + +// notificationhelper.notificationdeleted(requestorId, assignedTo, _actionItem.description); + +// _actionItem.remove() +// .then(() => { +// res.status(200).send({ +// message: 'removed', +// }); +// }) +// .catch((error) => { +// res.status(400).send(error); +// }); +// }; + +// const editactionItem = async function (req, res) { +// const actionItemId = mongoose.Types.ObjectId(req.params.actionItemId); + +// const { requestorId, assignedTo } = req.body.requestor; + +// const _actionItem = await ActionItem.findById(actionItemId) +// .catch((error) => { +// res.status(400).send(error); +// }); + +// if (!_actionItem) { +// res.status(400).send({ +// message: 'No valid records found', +// }); +// return; +// } +// notificationhelper.notificationedited(requestorId, assignedTo, _actionItem.description, req.body.description); + +// _actionItem.description = req.body.description; +// _actionItem.assignedTo = req.body.assignedTo; + +// _actionItem.save() +// .then(res.status(200).send('Saved')) +// .catch((error) => res.status(400).send(error)); +// }; + +// return { +// getactionItem, +// postactionItem, +// deleteactionItem, +// editactionItem, + +// }; +// }; + +// module.exports = actionItemController; diff --git a/src/controllers/badgeController.js b/src/controllers/badgeController.js index 7735ac736..5793f0204 100644 --- a/src/controllers/badgeController.js +++ b/src/controllers/badgeController.js @@ -224,9 +224,10 @@ const badgeController = function (Badge) { .catch((errors) => { res.status(500).send(errors); }); - }).catch((error) => { - res.status(500).send(error); }); + // .catch((error) => { + // res.status(500).send(error); + // }); }; const putBadge = async function (req, res) { diff --git a/src/controllers/badgeController.spec.js b/src/controllers/badgeController.spec.js index 6159b688e..3c0c92b7f 100644 --- a/src/controllers/badgeController.spec.js +++ b/src/controllers/badgeController.spec.js @@ -1,6 +1,7 @@ -// const mongoose = require('mongoose'); -// const UserProfile = require('../models/userProfile'); const mongoose = require('mongoose'); +// mock the cache function before importing so we can manipulate the implementation +jest.mock('../utilities/nodeCache'); +const cache = require('../utilities/nodeCache'); const Badge = require('../models/badge'); const helper = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); @@ -8,14 +9,10 @@ const badgeController = require('./badgeController'); const { mockReq, mockRes, assertResMock } = require('../test'); const UserProfile = require('../models/userProfile'); -// mock the cache function before importing so we can manipulate the implementation -jest.mock('../utilities/nodeCache'); -const cache = require('../utilities/nodeCache'); - const makeSut = () => { - const { postBadge, getAllBadges, assignBadges } = badgeController(Badge); + const { postBadge, getAllBadges, assignBadges, deleteBadge } = badgeController(Badge); - return { postBadge, getAllBadges, assignBadges }; + return { postBadge, getAllBadges, assignBadges, deleteBadge }; }; const flushPromises = () => new Promise(setImmediate); @@ -23,7 +20,6 @@ const flushPromises = () => new Promise(setImmediate); const mockHasPermission = (value) => jest.spyOn(helper, 'hasPermission').mockImplementationOnce(() => Promise.resolve(value)); -// eslint-disable-next-line no-unused-vars const makeMockCache = (method, value) => { const cacheObject = { getCache: jest.fn(), @@ -41,6 +37,7 @@ const makeMockCache = (method, value) => { describe('badeController module', () => { beforeEach(() => { + mockReq.params.badgeId = '601acda376045c7879d13a75'; mockReq.body.badgeName = 'random badge'; mockReq.body.category = 'Food'; mockReq.body.type = 'No Infringement Streak'; @@ -168,83 +165,81 @@ describe('badeController module', () => { assertResMock(500, new Error(errorMsg), response, mockRes); }); - // test('Returns 201 if a badge is succesfully created and no badges in cache.', async () => { - // const { mockCache: getCacheMock } = makeMockCache('getCache', ''); - // const { postBadge } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - - // const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); - - // const newBadge = { - // badgeName: mockReq.body.badgeName, - // category: mockReq.body.category, - // multiple: mockReq.body.multiple, - // totalHrs: mockReq.body.totalHrs, - // weeks: mockReq.body.weeks, - // months: mockReq.body.months, - // people: mockReq.body.people, - // project: mockReq.body.project, - // imageUrl: mockReq.body.imageUrl, - // ranking: mockReq.body.ranking, - // description: mockReq.body.description, - // showReport: mockReq.body.showReport, - // }; - - // jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); - - // const response = await postBadge(mockReq, mockRes); - - // expect(getCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); - // expect(findSpy).toHaveBeenCalledWith({ - // badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, - // }); - // assertResMock(201, newBadge, response, mockRes); - // }); - - // test('Clears cache if all is successful and there is a badge cache', async () => { - // const { mockCache: getCacheMock, cacheObject } = makeMockCache('getCache', '[{_id: 1}]'); - // const removeCacheMock = jest - // .spyOn(cacheObject, 'removeCache') - // .mockImplementationOnce(() => null); - // const { postBadge } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - - // const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); - - // const newBadge = { - // badgeName: mockReq.body.badgeName, - // category: mockReq.body.category, - // multiple: mockReq.body.multiple, - // totalHrs: mockReq.body.totalHrs, - // weeks: mockReq.body.weeks, - // months: mockReq.body.months, - // people: mockReq.body.people, - // project: mockReq.body.project, - // imageUrl: mockReq.body.imageUrl, - // ranking: mockReq.body.ranking, - // description: mockReq.body.description, - // showReport: mockReq.body.showReport, - // }; - - // jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); - - // const response = await postBadge(mockReq, mockRes); - - // expect(getCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(removeCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); - // expect(findSpy).toHaveBeenCalledWith({ - // badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, - // }); - // assertResMock(201, newBadge, response, mockRes); - // }); + test('Returns 201 if a badge is succesfully created and no badges in cache.', async () => { + const { mockCache: getCacheMock } = makeMockCache('getCache', ''); + const { postBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); + + const newBadge = { + badgeName: mockReq.body.badgeName, + category: mockReq.body.category, + multiple: mockReq.body.multiple, + totalHrs: mockReq.body.totalHrs, + weeks: mockReq.body.weeks, + months: mockReq.body.months, + people: mockReq.body.people, + project: mockReq.body.project, + imageUrl: mockReq.body.imageUrl, + ranking: mockReq.body.ranking, + description: mockReq.body.description, + showReport: mockReq.body.showReport, + }; + + jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); + + const response = await postBadge(mockReq, mockRes); + + expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); + expect(findSpy).toHaveBeenCalledWith({ + badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, + }); + assertResMock(201, newBadge, response, mockRes); + }); + + test('Clears cache if all is successful and there is a badge cache', async () => { + const { mockCache: getCacheMock, cacheObject } = makeMockCache('getCache', '[{_id: 1}]'); + const removeCacheMock = jest + .spyOn(cacheObject, 'removeCache') + .mockImplementationOnce(() => null); + const { postBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + const findSpy = jest.spyOn(Badge, 'find').mockImplementationOnce(() => Promise.resolve([])); + + const newBadge = { + badgeName: mockReq.body.badgeName, + category: mockReq.body.category, + multiple: mockReq.body.multiple, + totalHrs: mockReq.body.totalHrs, + weeks: mockReq.body.weeks, + months: mockReq.body.months, + people: mockReq.body.people, + project: mockReq.body.project, + imageUrl: mockReq.body.imageUrl, + ranking: mockReq.body.ranking, + description: mockReq.body.description, + showReport: mockReq.body.showReport, + }; + + jest.spyOn(Badge.prototype, 'save').mockImplementationOnce(() => Promise.resolve(newBadge)); + + const response = await postBadge(mockReq, mockRes); + + expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + expect(removeCacheMock).toHaveBeenCalledWith('allBadges'); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'createBadges'); + expect(findSpy).toHaveBeenCalledWith({ + badgeName: { $regex: escapeRegex(mockReq.body.badgeName), $options: 'i' }, + }); + assertResMock(201, newBadge, response, mockRes); + }); }); describe('getAllBadges method', () => { - // eslint-disable-next-line no-unused-vars const findObject = { populate: () => {} }; - // eslint-disable-next-line no-unused-vars const populateObject = { sort: () => {} }; test('Returns 403 if the user is not authorized', async () => { const { getAllBadges } = makeSut(); @@ -258,93 +253,93 @@ describe('badeController module', () => { expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); }); - // test('Returns 500 if an error occurs when querying DB', async () => { - // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - // const { getAllBadges } = makeSut(); - // const mockPermission = mockHasPermission(true); - // const errorMsg = 'Error when finding badges'; - - // const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); - // const populateMock = jest - // .spyOn(findObject, 'populate') - // .mockImplementationOnce(() => populateObject); - // const sortMock = jest - // .spyOn(populateObject, 'sort') - // .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); - - // getAllBadges(mockReq, mockRes); - // await flushPromises(); - - // expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(mockRes.status).toHaveBeenCalledWith(500); - // expect(mockRes.send).toHaveBeenCalledWith(new Error(errorMsg)); - // expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); - // expect(findMock).toHaveBeenCalledWith( - // {}, - // 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', - // ); - // expect(populateMock).toHaveBeenCalledWith({ - // path: 'project', - // select: '_id projectName', - // }); - // expect(sortMock).toHaveBeenCalledWith({ - // ranking: 1, - // badgeName: 1, - // }); - // }); - - // test('Returns 200 if the badges are in cache', async () => { - // const badges = [{ badge: 'random badge' }]; - // const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); - // const getCacheMock = jest.spyOn(cacheObject, 'getCache').mockReturnValueOnce(badges); - - // const { getAllBadges } = makeSut(); - - // const mockPermission = mockHasPermission(true); - - // const response = await getAllBadges(mockReq, mockRes); - // await flushPromises(); - - // assertResMock(200, badges, response, mockRes); - // expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(getCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); - // }); - - // test('Returns 200 if not in cache, and all the async code succeeds.', async () => { - // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - // const { getAllBadges } = makeSut(); - // const mockPermission = mockHasPermission(true); - // const badges = [{ badge: 'random badge' }]; - - // const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); - // const populateMock = jest - // .spyOn(findObject, 'populate') - // .mockImplementationOnce(() => populateObject); - // const sortMock = jest - // .spyOn(populateObject, 'sort') - // .mockImplementationOnce(() => Promise.resolve(badges)); - - // getAllBadges(mockReq, mockRes); - // await flushPromises(); - - // expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); - // expect(mockRes.status).toHaveBeenCalledWith(200); - // expect(mockRes.send).toHaveBeenCalledWith(badges); - // expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); - // expect(findMock).toHaveBeenCalledWith( - // {}, - // 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', - // ); - // expect(populateMock).toHaveBeenCalledWith({ - // path: 'project', - // select: '_id projectName', - // }); - // expect(sortMock).toHaveBeenCalledWith({ - // ranking: 1, - // badgeName: 1, - // }); - // }); + test('Returns 500 if an error occurs when querying DB', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + const { getAllBadges } = makeSut(); + const mockPermission = mockHasPermission(true); + const errorMsg = 'Error when finding badges'; + + const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); + const populateMock = jest + .spyOn(findObject, 'populate') + .mockImplementationOnce(() => populateObject); + const sortMock = jest + .spyOn(populateObject, 'sort') + .mockImplementationOnce(() => Promise.reject(new Error(errorMsg))); + + getAllBadges(mockReq, mockRes); + await flushPromises(); + + expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); + expect(mockRes.status).toHaveBeenCalledWith(500); + expect(mockRes.send).toHaveBeenCalledWith(new Error(errorMsg)); + expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); + expect(findMock).toHaveBeenCalledWith( + {}, + 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', + ); + expect(populateMock).toHaveBeenCalledWith({ + path: 'project', + select: '_id projectName', + }); + expect(sortMock).toHaveBeenCalledWith({ + ranking: 1, + badgeName: 1, + }); + }); + + test('Returns 200 if the badges are in cache', async () => { + const badges = [{ badge: 'random badge' }]; + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); + const getCacheMock = jest.spyOn(cacheObject, 'getCache').mockReturnValueOnce(badges); + + const { getAllBadges } = makeSut(); + + const mockPermission = mockHasPermission(true); + + const response = await getAllBadges(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, badges, response, mockRes); + expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); + expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); + }); + + test('Returns 200 if not in cache, and all the async code succeeds.', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + const { getAllBadges } = makeSut(); + const mockPermission = mockHasPermission(true); + const badges = [{ badge: 'random badge' }]; + + const findMock = jest.spyOn(Badge, 'find').mockImplementationOnce(() => findObject); + const populateMock = jest + .spyOn(findObject, 'populate') + .mockImplementationOnce(() => populateObject); + const sortMock = jest + .spyOn(populateObject, 'sort') + .mockImplementationOnce(() => Promise.resolve(badges)); + + getAllBadges(mockReq, mockRes); + await flushPromises(); + + expect(hasCacheMock).toHaveBeenCalledWith('allBadges'); + expect(mockRes.status).toHaveBeenCalledWith(200); + expect(mockRes.send).toHaveBeenCalledWith(badges); + expect(mockPermission).toHaveBeenCalledWith(mockReq.body.requestor, 'seeBadges'); + expect(findMock).toHaveBeenCalledWith( + {}, + 'badgeName type multiple weeks months totalHrs people imageUrl category project ranking description showReport', + ); + expect(populateMock).toHaveBeenCalledWith({ + path: 'project', + select: '_id projectName', + }); + expect(sortMock).toHaveBeenCalledWith({ + ranking: 1, + badgeName: 1, + }); + }); }); describe('assignBadges method', () => { @@ -390,71 +385,242 @@ describe('badeController module', () => { expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); }); - // test('Returns 500 if an error occurs when saving edited user profile', async () => { - // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + test('Returns 500 if an error occurs when saving edited user profile', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); - // const { assignBadges } = makeSut(); + const { assignBadges } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - // const errMsg = 'Error when saving'; - // const findObj = { save: () => { } }; - // const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); - // jest.spyOn(findObj, 'save').mockRejectedValueOnce(new Error(errMsg)); + const hasPermissionSpy = mockHasPermission(true); + const errMsg = 'Error when saving'; + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockRejectedValueOnce(new Error(errMsg)); - // const response = await assignBadges(mockReq, mockRes); + const response = await assignBadges(mockReq, mockRes); - // assertResMock(500, `Internal Error: Badge Collection. ${errMsg}`, response, mockRes); - // expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); - // expect(hasCacheMock).toHaveBeenCalledWith( - // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - // ); + assertResMock(500, `Internal Error: Badge Collection. ${errMsg}`, response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); + expect(hasCacheMock).toHaveBeenCalledWith( + `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + ); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); - // }); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); + }); - // test('Returns 201 and removes appropriate user from cache if successful and user exists in cache', async () => { - // const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); - // const removeCacheMock = jest.spyOn(cacheObject, 'removeCache').mockReturnValueOnce(null); + test('Returns 201 and removes appropriate user from cache if successful and user exists in cache', async () => { + const { mockCache: hasCacheMock, cacheObject } = makeMockCache('hasCache', true); + const removeCacheMock = jest.spyOn(cacheObject, 'removeCache').mockReturnValueOnce(null); - // const { assignBadges } = makeSut(); + const { assignBadges } = makeSut(); - // const hasPermissionSpy = mockHasPermission(true); - // const findObj = { save: () => { } }; - // const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); - // jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); + const hasPermissionSpy = mockHasPermission(true); + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); - // const response = await assignBadges(mockReq, mockRes); + const response = await assignBadges(mockReq, mockRes); - // assertResMock(201, `randomId`, response, mockRes); - // expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); - // expect(hasCacheMock).toHaveBeenCalledWith( - // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - // ); - // expect(removeCacheMock).toHaveBeenCalledWith( - // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - // ); + assertResMock(201, `randomId`, response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); + expect(hasCacheMock).toHaveBeenCalledWith( + `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + ); + expect(removeCacheMock).toHaveBeenCalledWith( + `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + ); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); - // }); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); + }); + + test('Returns 201 and if successful and user does not exist in cache', async () => { + const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + + const { assignBadges } = makeSut(); + + const hasPermissionSpy = mockHasPermission(true); + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); + + const response = await assignBadges(mockReq, mockRes); + + assertResMock(201, `randomId`, response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); + expect(hasCacheMock).toHaveBeenCalledWith( + `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, + ); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); + }); + }); + + describe('deleteBadge method', () => { + test('Returns 403 if the user does not have permission to delete badges', async () => { + const { deleteBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(false); + + const response = await deleteBadge(mockReq, mockRes); + await flushPromises(); + + assertResMock(403, { error: 'You are not authorized to delete badges.' }, response, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteBadges'); + }); + + test('Returns 400 if an no badge is found', async () => { + const { deleteBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + const findByIdSpy = jest + .spyOn(Badge, 'findById') + .mockImplementationOnce((_, callback) => callback(null, null)); + + const response = await deleteBadge(mockReq, mockRes); + + assertResMock(400, { error: 'No valid records found' }, response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.badgeId, expect.anything()); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteBadges'); + }); + + test('Returns 500 if the removeBadgeFromProfile fails.', async () => { + const { deleteBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + const findByIdSpy = jest + .spyOn(Badge, 'findById') + .mockImplementationOnce((_, callback) => + callback(null, { _id: mockReq.params.badgeId, remove: () => Promise.resolve() }), + ); + + const errMsg = 'Update many failed'; + const updateManyObj = { exec: () => {} }; + const updateManySpy = jest + .spyOn(UserProfile, 'updateMany') + .mockImplementationOnce(() => updateManyObj); + + jest.spyOn(updateManyObj, 'exec').mockRejectedValueOnce(new Error(errMsg)); + + const response = await deleteBadge(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, new Error(errMsg), response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.badgeId, expect.anything()); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteBadges'); + expect(updateManySpy).toHaveBeenCalledWith( + {}, + { $pull: { badgeCollection: { badge: mockReq.params.badgeId } } }, + ); + }); + + test('Returns 500 if the remove method fails.', async () => { + const { deleteBadge } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); - // test('Returns 201 and if successful and user does not exist in cache', async () => { - // const { mockCache: hasCacheMock } = makeMockCache('hasCache', false); + const errMsg = 'Remove failed'; + const findByIdSpy = jest.spyOn(Badge, 'findById').mockImplementationOnce((_, callback) => + callback(null, { + _id: mockReq.params.badgeId, + remove: () => Promise.reject(new Error(errMsg)), + }), + ); + + const updateManyObj = { exec: () => {} }; + const updateManySpy = jest + .spyOn(UserProfile, 'updateMany') + .mockImplementationOnce(() => updateManyObj); + + jest.spyOn(updateManyObj, 'exec').mockResolvedValueOnce(true); + + const response = await deleteBadge(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, new Error(errMsg), response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.badgeId, expect.anything()); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteBadges'); + expect(updateManySpy).toHaveBeenCalledWith( + {}, + { $pull: { badgeCollection: { badge: mockReq.params.badgeId } } }, + ); + }); + + test('Returns 200 if the badge is successfully removed, all instances of the badge are removed from user profiles, and cache does not have badges.', async () => { + const { mockCache: getCacheMock } = makeMockCache('getCache', false); + const { deleteBadge } = makeSut(); + + const hasPermissionSpy = mockHasPermission(true); + + const findByIdSpy = jest.spyOn(Badge, 'findById').mockImplementationOnce((_, callback) => + callback(null, { + _id: mockReq.params.badgeId, + remove: () => Promise.resolve(true), + }), + ); + + const updateManyObj = { exec: () => {} }; + const updateManySpy = jest + .spyOn(UserProfile, 'updateMany') + .mockImplementationOnce(() => updateManyObj); + + jest.spyOn(updateManyObj, 'exec').mockResolvedValueOnce(true); + + const response = await deleteBadge(mockReq, mockRes); + await flushPromises(); + + assertResMock( + 200, + { + message: 'Badge successfully deleted and user profiles updated', + }, + response, + mockRes, + ); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.badgeId, expect.anything()); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteBadges'); + expect(updateManySpy).toHaveBeenCalledWith( + {}, + { $pull: { badgeCollection: { badge: mockReq.params.badgeId } } }, + ); + expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + }); + + test('Clears cache if cache exists.', async () => { + const { mockCache: getCacheMock, cacheObject } = makeMockCache('getCache', true); + const removeCacheSpy = jest.spyOn(cacheObject, 'removeCache').mockReturnValueOnce(null); + const { deleteBadge } = makeSut(); + + const hasPermissionSpy = mockHasPermission(true); + + const findByIdSpy = jest.spyOn(Badge, 'findById').mockImplementationOnce((_, callback) => + callback(null, { + _id: mockReq.params.badgeId, + remove: () => Promise.resolve(true), + }), + ); - // const { assignBadges } = makeSut(); + const updateManyObj = { exec: () => {} }; + const updateManySpy = jest + .spyOn(UserProfile, 'updateMany') + .mockImplementationOnce(() => updateManyObj); - // const hasPermissionSpy = mockHasPermission(true); - // const findObj = { save: () => { } }; - // const findByIdSpy = jest.spyOn(UserProfile, 'findById').mockResolvedValue(findObj); - // jest.spyOn(findObj, 'save').mockResolvedValueOnce({ _id: 'randomId' }); + jest.spyOn(updateManyObj, 'exec').mockResolvedValueOnce(true); - // const response = await assignBadges(mockReq, mockRes); + const response = await deleteBadge(mockReq, mockRes); + await flushPromises(); - // assertResMock(201, `randomId`, response, mockRes); - // expect(findByIdSpy).toHaveBeenCalledWith(mongoose.Types.ObjectId(mockReq.params.userId)); - // expect(hasCacheMock).toHaveBeenCalledWith( - // `user-${mongoose.Types.ObjectId(mockReq.params.userId)}`, - // ); - // expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'assignBadges'); - // }); + assertResMock( + 200, + { + message: 'Badge successfully deleted and user profiles updated', + }, + response, + mockRes, + ); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.badgeId, expect.anything()); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteBadges'); + expect(updateManySpy).toHaveBeenCalledWith( + {}, + { $pull: { badgeCollection: { badge: mockReq.params.badgeId } } }, + ); + expect(getCacheMock).toHaveBeenCalledWith('allBadges'); + expect(removeCacheSpy).toHaveBeenCalledWith('allBadges'); + }); }); }); diff --git a/src/controllers/bmdashboard/bmConsumableController.js b/src/controllers/bmdashboard/bmConsumableController.js index 2a2b6dbdc..d501c5a47 100644 --- a/src/controllers/bmdashboard/bmConsumableController.js +++ b/src/controllers/bmdashboard/bmConsumableController.js @@ -78,10 +78,63 @@ const bmConsumableController = function (BuildingConsumable) { } }; + const bmPostConsumableUpdateRecord = function (req, res) { + const { + quantityUsed, quantityWasted, qtyUsedLogUnit, qtyWastedLogUnit, stockAvailable, consumable, + } = req.body; + let unitsUsed = quantityUsed; + let unitsWasted = quantityWasted; + + if (quantityUsed >= 0 && qtyUsedLogUnit === 'percent') { + unitsUsed = (stockAvailable / 100) * quantityUsed; + } + if (quantityWasted >= 0 && qtyWastedLogUnit === 'percent') { + unitsWasted = (stockAvailable / 100) * quantityWasted; + } + if (unitsUsed > stockAvailable || unitsWasted > stockAvailable || (unitsUsed + unitsWasted) > stockAvailable) { + return res.status(500).send({ message: 'Please check the used and wasted stock values. Either individual values or their sum exceeds the total stock available.' }); + } if (unitsUsed < 0 || unitsWasted < 0) { + return res.status(500).send({ message: 'Please check the used and wasted stock values. Negative numbers are invalid.' }); + } + + + const newStockUsed = parseFloat((consumable.stockUsed + unitsUsed).toFixed(4)); + const newStockWasted = parseFloat((consumable.stockWasted + unitsWasted).toFixed(4)); + const newAvailable = parseFloat((stockAvailable - (unitsUsed + unitsWasted)).toFixed(4)); + + BuildingConsumable.updateOne( + { _id: consumable._id }, + { + $set: { + stockUsed: newStockUsed, + stockWasted: newStockWasted, + stockAvailable: newAvailable, + }, + $push: { + updateRecord: { + date: req.body.date, + createdBy: req.body.requestor.requestorId, + quantityUsed: unitsUsed, + quantityWasted: unitsWasted, + }, + }, + + }, + ) + .then((results) => { + res.status(200).send(results); + }) + .catch((error) => { + console.log('error: ', error); + res.status(500).send({ message: error }); + }); +}; + return { fetchBMConsumables, bmPurchaseConsumables, + bmPostConsumableUpdateRecord }; }; -module.exports = bmConsumableController; +module.exports = bmConsumableController; \ No newline at end of file diff --git a/src/controllers/bmdashboard/bmEquipmentController.js b/src/controllers/bmdashboard/bmEquipmentController.js index 07342b5e4..1255493ca 100644 --- a/src/controllers/bmdashboard/bmEquipmentController.js +++ b/src/controllers/bmdashboard/bmEquipmentController.js @@ -1,111 +1,109 @@ const mongoose = require('mongoose'); const bmEquipmentController = (BuildingEquipment) => { - const fetchSingleEquipment = async (req, res) => { - const { equipmentId } = req.params; - try { - BuildingEquipment - .findById(equipmentId) - .populate([ - { - path: 'itemType', - select: '_id name description unit imageUrl category', - }, - { - path: 'project', - select: 'name', - }, - { - path: 'userResponsible', - select: '_id firstName lastName', - }, - { - path: 'purchaseRecord', - populate: { - path: 'requestedBy', - select: '_id firstName lastName', - }, - }, - { - path: 'updateRecord', - populate: { - path: 'createdBy', - select: '_id firstName lastName', - }, - }, - { - path: 'logRecord', - populate: [{ - path: 'createdBy', - select: '_id firstName lastName', - }, - { - path: 'responsibleUser', - select: '_id firstName lastName', - }], - }, - ]) - .exec() - .then((equipment) => res.status(200).send(equipment)) - .catch((error) => res.status(500).send(error)); - } catch (err) { - res.json(err); - } - }; + const fetchSingleEquipment = async (req, res) => { + const { equipmentId } = req.params; + try { + BuildingEquipment.findById(equipmentId) + .populate([ + { + path: 'itemType', + select: '_id name description unit imageUrl category', + }, + { + path: 'project', + select: 'name', + }, + { + path: 'userResponsible', + select: '_id firstName lastName', + }, + { + path: 'purchaseRecord', + populate: { + path: 'requestedBy', + select: '_id firstName lastName', + }, + }, + { + path: 'updateRecord', + populate: { + path: 'createdBy', + select: '_id firstName lastName', + }, + }, + { + path: 'logRecord', + populate: [ + { + path: 'createdBy', + select: '_id firstName lastName', + }, + { + path: 'responsibleUser', + select: '_id firstName lastName', + }, + ], + }, + ]) + .exec() + .then((equipment) => res.status(200).send(equipment)) + .catch((error) => res.status(500).send(error)); + } catch (err) { + res.json(err); + } + }; - const bmPurchaseEquipments = async function (req, res) { - const { - projectId, - equipmentId, - quantity, - // isTracking, - priority, - estTime: estUsageTime, - desc: usageDesc, - makeModel: makeModelPref, - requestor: { requestorId }, - } = req.body; - try { - const newPurchaseRecord = { - quantity, - priority, - estUsageTime, - usageDesc, - makeModelPref, - requestedBy: requestorId, - }; - const doc = await BuildingEquipment.findOne({ project: projectId, itemType: equipmentId }); - if (!doc) { - const newDoc = { - itemType: equipmentId, - project: projectId, - // isTracked: isTracking, - purchaseRecord: [newPurchaseRecord], - }; - BuildingEquipment - .create(newDoc) - .then(() => res.status(201).send()) - .catch((error) => res.status(500).send(error)); - return; - } - - BuildingEquipment - .findOneAndUpdate( - { _id: mongoose.Types.ObjectId(doc._id) }, - { $push: { purchaseRecord: newPurchaseRecord } }, - ) - .exec() - .then(() => res.status(201).send()) - .catch((error) => res.status(500).send(error)); - } catch (error) { - res.status(500).send(error); - } + const bmPurchaseEquipments = async function (req, res) { + const { + projectId, + equipmentId, + quantity, + priority, + estTime: estUsageTime, + desc: usageDesc, + makeModel: makeModelPref, + requestor: { requestorId }, + } = req.body; + try { + const newPurchaseRecord = { + quantity, + priority, + estUsageTime, + usageDesc, + makeModelPref, + requestedBy: requestorId, }; + const doc = await BuildingEquipment.findOne({ project: projectId, itemType: equipmentId }); + if (!doc) { + const newDoc = { + itemType: equipmentId, + project: projectId, + purchaseRecord: [newPurchaseRecord], + }; - return { - fetchSingleEquipment, - bmPurchaseEquipments, - }; + BuildingEquipment.create(newDoc) + .then(() => res.status(201).send()) + .catch((error) => res.status(500).send(error)); + return; + } + + BuildingEquipment.findOneAndUpdate( + { _id: mongoose.Types.ObjectId(doc._id) }, + { $push: { purchaseRecord: newPurchaseRecord } }, + ) + .exec() + .then(() => res.status(201).send()) + .catch((error) => res.status(500).send(error)); + } catch (error) { + res.status(500).send(error); + } + }; + + return { + fetchSingleEquipment, + bmPurchaseEquipments, + }; }; module.exports = bmEquipmentController; diff --git a/src/controllers/bmdashboard/bmInventoryTypeController.js b/src/controllers/bmdashboard/bmInventoryTypeController.js index b2cd93a71..491b7a127 100644 --- a/src/controllers/bmdashboard/bmInventoryTypeController.js +++ b/src/controllers/bmdashboard/bmInventoryTypeController.js @@ -31,24 +31,40 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp } } - async function fetchEquipmentTypes(req, res) { - try { - EquipType.find() - .exec() - .then((result) => res.status(200).send(result)) - .catch((error) => res.status(500).send(error)); - } catch (err) { - res.json(err); - } - } - const fetchToolTypes = async (req, res) => { + try { - ToolType.find() + ToolType + .find() + .populate([ + { + path: 'available', + select: '_id code project', + populate: { + path: 'project', + select: '_id name' + } + }, + { + path: 'using', + select: '_id code project', + populate: { + path: 'project', + select: '_id name' + } + } + ]) .exec() - .then((result) => res.status(200).send(result)) - .catch((error) => res.status(500).send(error)); + .then(result => { + res.status(200).send(result); + }) + .catch(error => { + console.error("fetchToolTypes error: ", error); + res.status(500).send(error); + }); + } catch (err) { + console.log("error: ", err) res.json(err); } }; @@ -299,7 +315,6 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp fetchMaterialTypes, fetchConsumableTypes, fetchReusableTypes, - fetchEquipmentTypes, fetchToolTypes, addEquipmentType, fetchSingleInventoryType, @@ -311,4 +326,4 @@ function bmInventoryTypeController(InvType, MatType, ConsType, ReusType, ToolTyp }; } -module.exports = bmInventoryTypeController; +module.exports = bmInventoryTypeController; \ No newline at end of file diff --git a/src/controllers/bmdashboard/bmMaterialsController.js b/src/controllers/bmdashboard/bmMaterialsController.js index 42d25c3c6..ecce5a35e 100644 --- a/src/controllers/bmdashboard/bmMaterialsController.js +++ b/src/controllers/bmdashboard/bmMaterialsController.js @@ -1,4 +1,4 @@ -const mongoose = require('mongoose'); +const mongoose = require("mongoose"); const bmMaterialsController = function (BuildingMaterial) { const bmMaterialsList = async function _matsList(req, res) { @@ -6,25 +6,25 @@ const bmMaterialsController = function (BuildingMaterial) { BuildingMaterial.find() .populate([ { - path: 'project', - select: '_id name', + path: "project", + select: "_id name", }, { - path: 'itemType', - select: '_id name unit', + path: "itemType", + select: "_id name unit", }, { - path: 'updateRecord', + path: "updateRecord", populate: { - path: 'createdBy', - select: '_id firstName lastName', + path: "createdBy", + select: "_id firstName lastName", }, }, { - path: 'purchaseRecord', + path: "purchaseRecord", populate: { - path: 'requestedBy', - select: '_id firstName lastName', + path: "requestedBy", + select: "_id firstName lastName", }, }, ]) @@ -65,27 +65,28 @@ const bmMaterialsController = function (BuildingMaterial) { brandPref, requestedBy: requestorId, }; - const doc = await BuildingMaterial.findOne({ project: projectId, itemType: matTypeId }); + const doc = await BuildingMaterial.findOne({ + project: projectId, + itemType: matTypeId, + }); if (!doc) { const newDoc = { itemType: matTypeId, project: projectId, purchaseRecord: [newPurchaseRecord], }; - BuildingMaterial - .create(newDoc) + BuildingMaterial.create(newDoc) .then(() => res.status(201).send()) .catch((error) => res.status(500).send(error)); return; } - BuildingMaterial - .findOneAndUpdate( - { _id: mongoose.Types.ObjectId(doc._id) }, - { $push: { purchaseRecord: newPurchaseRecord } }, - ) + BuildingMaterial.findOneAndUpdate( + { _id: mongoose.Types.ObjectId(doc._id) }, + { $push: { purchaseRecord: newPurchaseRecord } } + ) .exec() .then(() => res.status(201).send()) - .catch((error) => res.status(500).send(error)); + .catch(error => res.status(500).send(error)); } catch (error) { res.status(500).send(error); } @@ -96,19 +97,35 @@ const bmMaterialsController = function (BuildingMaterial) { let quantityUsed = +req.body.quantityUsed; let quantityWasted = +req.body.quantityWasted; const { material } = req.body; - if (payload.QtyUsedLogUnit == 'percent' && quantityWasted >= 0) { - quantityUsed = +((+quantityUsed / 100) * material.stockAvailable).toFixed(4); + if (payload.QtyUsedLogUnit == "percent" && quantityWasted >= 0) { + quantityUsed = +((+quantityUsed / 100) * material.stockAvailable).toFixed( + 4 + ); } - if (payload.QtyWastedLogUnit == 'percent' && quantityUsed >= 0) { - quantityWasted = +((+quantityWasted / 100) * material.stockAvailable).toFixed(4); + if (payload.QtyWastedLogUnit == "percent" && quantityUsed >= 0) { + quantityWasted = +( + (+quantityWasted / 100) * + material.stockAvailable + ).toFixed(4); } - if (quantityUsed > material.stockAvailable || quantityWasted > material.stockAvailable || (quantityUsed + quantityWasted) > material.stockAvailable) { - res.status(500).send('Please check the used and wasted stock values. Either individual values or their sum exceeds the total stock available.'); + if ( + quantityUsed > material.stockAvailable || + quantityWasted > material.stockAvailable || + quantityUsed + quantityWasted > material.stockAvailable + ) { + res + .status(500) + .send( + "Please check the used and wasted stock values. Either individual values or their sum exceeds the total stock available." + ); } else { let newStockUsed = +material.stockUsed + parseFloat(quantityUsed); let newStockWasted = +material.stockWasted + parseFloat(quantityWasted); - let newAvailable = +material.stockAvailable - parseFloat(quantityUsed) - parseFloat(quantityWasted); + let newAvailable = + +material.stockAvailable - + parseFloat(quantityUsed) - + parseFloat(quantityWasted); newStockUsed = parseFloat(newStockUsed.toFixed(4)); newStockWasted = parseFloat(newStockWasted.toFixed(4)); newAvailable = parseFloat(newAvailable.toFixed(4)); @@ -129,10 +146,11 @@ const bmMaterialsController = function (BuildingMaterial) { quantityWasted, }, }, - }, - + } ) - .then((results) => { res.status(200).send(results); }) + .then((results) => { + res.status(200).send(results); + }) .catch((error) => res.status(500).send({ message: error })); } }; @@ -146,16 +164,25 @@ const bmMaterialsController = function (BuildingMaterial) { let quantityUsed = +payload.quantityUsed; let quantityWasted = +payload.quantityWasted; const { material } = payload; - if (payload.QtyUsedLogUnit == 'percent' && quantityWasted >= 0) { - quantityUsed = +((+quantityUsed / 100) * material.stockAvailable).toFixed(4); + if (payload.QtyUsedLogUnit == "percent" && quantityWasted >= 0) { + quantityUsed = +( + (+quantityUsed / 100) * + material.stockAvailable + ).toFixed(4); } - if (payload.QtyWastedLogUnit == 'percent' && quantityUsed >= 0) { - quantityWasted = +((+quantityWasted / 100) * material.stockAvailable).toFixed(4); + if (payload.QtyWastedLogUnit == "percent" && quantityUsed >= 0) { + quantityWasted = +( + (+quantityWasted / 100) * + material.stockAvailable + ).toFixed(4); } let newStockUsed = +material.stockUsed + parseFloat(quantityUsed); let newStockWasted = +material.stockWasted + parseFloat(quantityWasted); - let newAvailable = +material.stockAvailable - parseFloat(quantityUsed) - parseFloat(quantityWasted); + let newAvailable = + +material.stockAvailable - + parseFloat(quantityUsed) - + parseFloat(quantityWasted); newStockUsed = parseFloat(newStockUsed.toFixed(4)); newStockWasted = parseFloat(newStockWasted.toFixed(4)); newAvailable = parseFloat(newAvailable.toFixed(4)); @@ -181,19 +208,23 @@ const bmMaterialsController = function (BuildingMaterial) { try { if (errorFlag) { - res.status(500).send('Stock quantities submitted seems to be invalid'); + res.status(500).send("Stock quantities submitted seems to be invalid"); return; } - const updatePromises = updateRecordsToBeAdded.map((updateItem) => BuildingMaterial.updateOne( - { _id: updateItem.updateId }, - { - $set: updateItem.set, - $push: { updateRecord: updateItem.updateValue }, - }, - ).exec()); + const updatePromises = updateRecordsToBeAdded.map((updateItem) => + BuildingMaterial.updateOne( + { _id: updateItem.updateId }, + { + $set: updateItem.set, + $push: { updateRecord: updateItem.updateValue }, + } + ).exec() + ); Promise.all(updatePromises) .then((results) => { - res.status(200).send({ result: `Successfully posted log for ${results.length} Material records.` }); + res.status(200).send({ + result: `Successfully posted log for ${results.length} Material records.`, + }); }) .catch((error) => res.status(500).send(error)); } catch (err) { diff --git a/src/controllers/bmdashboard/bmNewLessonController.js b/src/controllers/bmdashboard/bmNewLessonController.js index c2e24f286..e0ffc0863 100644 --- a/src/controllers/bmdashboard/bmNewLessonController.js +++ b/src/controllers/bmdashboard/bmNewLessonController.js @@ -8,8 +8,8 @@ const bmNewLessonController = function (BuildingNewLesson) { BuildingNewLesson .find() .populate() - .then((result) => res.status(200).send(result)) - .catch((error) => res.status(500).send(error)); + .then(result => res.status(200).send(result)) + .catch(error => res.status(500).send(error)); } catch (err) { res.json(err); } @@ -17,8 +17,8 @@ const bmNewLessonController = function (BuildingNewLesson) { const bmPostLessonList = async (req, res) => { try { const newLesson = BuildingNewLesson.create(req.body) - .then((result) => res.status(201).send(result)) - .catch((error) => res.status(500).send(error)); + .then(result => res.status(201).send(result)) + .catch(error => res.status(500).send(error)); } catch (err) { res.json(err); } @@ -47,7 +47,7 @@ const bmNewLessonController = function (BuildingNewLesson) { // Extract only allowed fields (content, tag, relatedProject and title) const allowedFields = ['content', 'tags', 'relatedProject', 'title', 'allowedRoles', 'files']; const filteredUpdateData = Object.keys(updateData) - .filter((key) => allowedFields.includes(key)) + .filter(key => allowedFields.includes(key)) .reduce((obj, key) => { obj[key] = updateData[key]; return obj; diff --git a/src/controllers/bmdashboard/bmProjectController.js b/src/controllers/bmdashboard/bmProjectController.js index 1b9237c44..a4f6712e0 100644 --- a/src/controllers/bmdashboard/bmProjectController.js +++ b/src/controllers/bmdashboard/bmProjectController.js @@ -78,7 +78,7 @@ const bmMProjectController = function (BuildingProject) { }); res.status(200).send(results); }) - .catch((error) => res.status(500).send(error)); + .catch(error => res.status(500).send(error)); } catch (err) { res.status(500).send(err); } @@ -105,7 +105,7 @@ const bmMProjectController = function (BuildingProject) { }, ]) .exec() - .then((project) => res.status(200).send(project)) + .then(project => res.status(200).send(project)) // TODO: uncomment this block to execute the auth check // authenticate request by comparing userId param with buildingManager id field // Note: _id has type object and must be converted to string @@ -117,7 +117,7 @@ const bmMProjectController = function (BuildingProject) { // } // return res.status(200).send(project); // }) - .catch((error) => res.status(500).send(error)); + .catch(error => res.status(500).send(error)); } catch (err) { res.json(err); } diff --git a/src/controllers/bmdashboard/bmToolController.js b/src/controllers/bmdashboard/bmToolController.js index e62153875..be37639ac 100644 --- a/src/controllers/bmdashboard/bmToolController.js +++ b/src/controllers/bmdashboard/bmToolController.js @@ -1,6 +1,61 @@ const mongoose = require('mongoose'); -const bmToolController = (BuildingTool) => { +const bmToolController = (BuildingTool, ToolType) => { + + const fetchAllTools = (req, res) => { + const populateFields = [ + { + path: 'project', + select: '_id name', + }, + { + path: 'itemType', + select: '_id name description unit imageUrl category available using', + }, + { + path: 'updateRecord', + populate: { + path: 'createdBy', + select: '_id firstName lastName', + }, + }, + { + path: 'purchaseRecord', + populate: { + path: 'requestedBy', + select: '_id firstName lastName', + }, + }, + { + path: 'logRecord', + populate: [ + { + path: 'createdBy', + select: '_id firstName lastName', + }, + { + path: 'responsibleUser', + select: '_id firstName lastName', + }, + ], + }, + ]; + + BuildingTool.find() + .populate(populateFields) + .exec() + .then(results => { + res.status(200).send(results); + }) + .catch(error => { + const errorMessage = `Error occurred while fetching tools: ${error.message}`; + console.error(errorMessage); + res.status(500).send({ message: errorMessage }); + }); + }; + + + const fetchSingleTool = async (req, res) => { const { toolId } = req.params; try { @@ -46,8 +101,8 @@ const bmToolController = (BuildingTool) => { }, ]) .exec() - .then((tool) => res.status(200).send(tool)) - .catch((error) => res.status(500).send(error)); + .then(tool => res.status(200).send(tool)) + .catch(error => res.status(500).send(error)); } catch (err) { res.json(err); } @@ -84,7 +139,7 @@ const bmToolController = (BuildingTool) => { BuildingTool .create(newDoc) .then(() => res.status(201).send()) - .catch((error) => res.status(500).send(error)); + .catch(error => res.status(500).send(error)); return; } @@ -95,15 +150,99 @@ const bmToolController = (BuildingTool) => { ) .exec() .then(() => res.status(201).send()) - .catch((error) => res.status(500).send(error)); + .catch(error => res.status(500).send(error)); } catch (error) { res.status(500).send(error); } }; + const bmLogTools = async function (req, res) { + const requestor = req.body.requestor.requestorId; + const {typesArray, action, date} = req.body + const results = []; + const errors = []; + + if(typesArray.length === 0 || typesArray === undefined){ + errors.push({ message: 'Invalid request. No tools selected'}) + return res.status(500).send({errors, results}); + } + + for (const type of typesArray) { + const toolName = type.toolName; + const toolCodes = type.toolCodes; + const codeMap = {}; + toolCodes.forEach(obj => { + codeMap[obj.value] = obj.label; + }) + + try{ + const toolTypeDoc = await ToolType.findOne({ _id: mongoose.Types.ObjectId(type.toolType) }); + if(!toolTypeDoc) { + errors.push({ message: `Tool type ${toolName} with id ${type.toolType} was not found.`}); + continue; + } + const availableItems = toolTypeDoc.available; + const usingItems = toolTypeDoc.using; + + for(const toolItem of type.toolItems){ + const buildingToolDoc = await BuildingTool.findOne({ _id: mongoose.Types.ObjectId(toolItem)}); + if(!buildingToolDoc){ + errors.push({ message: `${toolName} with id ${toolItem} was not found.`}); + continue; + } + + if(action === "Check Out" && availableItems.length > 0){ + const foundIndex = availableItems.indexOf(toolItem); + if(foundIndex >= 0){ + availableItems.splice(foundIndex, 1); + usingItems.push(toolItem); + }else{ + errors.push({ message: `${toolName} with code ${codeMap[toolItem]} is not available for ${action}`}); + continue; + } + } + + if(action === "Check In" && usingItems.length > 0){ + const foundIndex = usingItems.indexOf(toolItem); + if(foundIndex >= 0){ + usingItems.splice(foundIndex, 1); + availableItems.push(toolItem); + }else{ + errors.push({ message: `${toolName} ${codeMap[toolItem]} is not available for ${action}`}); + continue; + } + } + + const newRecord = { + date: date, + createdBy: requestor, + responsibleUser: buildingToolDoc.userResponsible, + type: action + } + + buildingToolDoc.logRecord.push(newRecord); + buildingToolDoc.save(); + results.push({message: `${action} successful for ${toolName} ${codeMap[toolItem]}`}) + } + + await toolTypeDoc.save(); + }catch(error){ + errors.push({message: `Error for tool type ${type}: ${error.message}` }); + } + } + + if (errors.length > 0) { + return res.status(404).send({ errors, results }); + } else { + return res.status(200).send({ errors, results }); + } + } + return { + fetchAllTools, fetchSingleTool, bmPurchaseTools, + bmLogTools }; }; diff --git a/src/controllers/dashBoardController.js b/src/controllers/dashBoardController.js index 660922e07..b1dc150f4 100644 --- a/src/controllers/dashBoardController.js +++ b/src/controllers/dashBoardController.js @@ -23,7 +23,7 @@ const dashboardcontroller = function () { return User.findOneAndUpdate( { _id: req.params.userId }, { copiedAiPrompt: Date.now() }, - { new: true }, + { new: true } ) .then((user) => { if (user) { @@ -51,12 +51,12 @@ const dashboardcontroller = function () { ...req.body, aIPromptText: req.body.aIPromptText, modifiedDatetime: Date.now(), - }, + } ) .then(() => { res.status(200).send("Successfully saved AI prompt."); }) - .catch((error) => res.status(500).send(error)); + .catch(error => res.status(500).send(error)); } }; @@ -82,7 +82,7 @@ const dashboardcontroller = function () { }); } }) - .catch((error) => res.status(500).send(error)); + .catch(error => res.status(500).send(error)); }; const monthlydata = function (req, res) { @@ -90,7 +90,7 @@ const dashboardcontroller = function () { const laborthismonth = dashboardhelper.laborthismonth( userId, req.params.fromDate, - req.params.toDate, + req.params.toDate ); laborthismonth.then((results) => { if (!results || results.length === 0) { @@ -112,7 +112,7 @@ const dashboardcontroller = function () { const laborthisweek = dashboardhelper.laborthisweek( userId, req.params.fromDate, - req.params.toDate, + req.params.toDate ); laborthisweek.then((results) => { res.status(200).send(results); @@ -133,7 +133,7 @@ const dashboardcontroller = function () { }); } }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }; const orgData = function (req, res) { @@ -143,7 +143,7 @@ const dashboardcontroller = function () { .then((results) => { res.status(200).send(results[0]); }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }; const getBugReportEmailBody = function ( @@ -155,7 +155,7 @@ const dashboardcontroller = function () { expected, actual, visual, - severity, + severity ) { const text = `New Bug Report From ${firstName} ${lastName}:

[Feature Name] Bug Title:

@@ -200,7 +200,7 @@ const dashboardcontroller = function () { expected, actual, visual, - severity, + severity ); try { @@ -208,7 +208,7 @@ const dashboardcontroller = function () { "onecommunityglobal@gmail.com", `Bug Rport from ${firstName} ${lastName}`, emailBody, - email, + email ); res.status(200).send("Success"); } catch { @@ -234,7 +234,7 @@ const dashboardcontroller = function () { let fieldaaray = []; if (suggestionData.field.length) { fieldaaray = suggestionData.field.map( - (item) => `

${item}

+ item => `

${item}

${args[3][item]}

`, ); } @@ -263,15 +263,13 @@ const dashboardcontroller = function () { // send suggestion email const sendMakeSuggestion = async (req, res) => { - const { - suggestioncate, suggestion, confirm, email, ...rest -} = req.body; + const { suggestioncate, suggestion, confirm, email, ...rest } = req.body; const emailBody = await getsuggestionEmailBody( suggestioncate, suggestion, confirm, rest, - email, + email ); try { emailSender( @@ -281,7 +279,7 @@ const dashboardcontroller = function () { null, null, email, - null, + null ); res.status(200).send("Success"); } catch { @@ -310,7 +308,7 @@ const dashboardcontroller = function () { } if (req.body.action === "delete") { suggestionData.suggestion = suggestionData.suggestion.filter( - (item, index) => index + 1 !== +req.body.newField, + (item, index) => index + 1 !== +req.body.newField ); } } else { @@ -319,7 +317,7 @@ const dashboardcontroller = function () { } if (req.body.action === "delete") { suggestionData.field = suggestionData.field.filter( - (item) => item !== req.body.newField, + item => item !== req.body.newField, ); } } diff --git a/src/controllers/informationController.js b/src/controllers/informationController.js index 92c0a7855..792620995 100644 --- a/src/controllers/informationController.js +++ b/src/controllers/informationController.js @@ -18,7 +18,7 @@ const informationController = function (Information) { cache.setCache('informations', results); res.status(200).send(results); }) - .catch((error) => res.status(404).send(error)); + .catch(error => res.status(404).send(error)); }; const addInformation = function (req, res) { @@ -41,9 +41,9 @@ const informationController = function (Information) { } res.status(201).send(newInformation); }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }) - .catch((error) => res.status(500).send({ error })); + .catch(error => res.status(500).send({ error })); }; const deleteInformation = function (req, res) { @@ -56,7 +56,7 @@ const informationController = function (Information) { } res.status(200).send(deletedInformation); }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }; // Update existing information by id @@ -70,7 +70,7 @@ const informationController = function (Information) { } res.status(200).send(updatedInformation); }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }; return { diff --git a/src/controllers/inventoryController.js b/src/controllers/inventoryController.js index 02aedf578..125c594cc 100644 --- a/src/controllers/inventoryController.js +++ b/src/controllers/inventoryController.js @@ -35,8 +35,8 @@ const inventoryController = function (Item, ItemType) { .sort({ wasted: 1, }) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const postInvInProjectWBS = async function (req, res) { @@ -80,8 +80,8 @@ const inventoryController = function (Item, ItemType) { const inventoryItem = new Item(data); return inventoryItem.save() - .then((results) => res.status(201).send(results)) - .catch((errors) => res.status(500).send(errors)); + .then(results => res.status(201).send(results)) + .catch(errors => res.status(500).send(errors)); } return Item.findOneAndUpdate( { @@ -103,7 +103,7 @@ const inventoryController = function (Item, ItemType) { ) .then((results) => { Item.findByIdAndUpdate(results._id, { costPer: results.quantity !== 0 ? results.cost / results.quantity : 0 }, { new: true }) - .then((result) => res.status(201).send(result)); + .then(result => res.status(201).send(result)); }); } return res.status(400).send('Valid Project, Quantity and Type Id are necessary as well as valid wbs if sent in and not Unassigned'); @@ -137,8 +137,8 @@ const inventoryController = function (Item, ItemType) { .sort({ wasted: 1, }) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const postInvInProject = async function (req, res) { @@ -171,8 +171,8 @@ const inventoryController = function (Item, ItemType) { const inventoryItem = new Item(data); return inventoryItem.save() - .then((results) => res.status(201).send(results)) - .catch((errors) => res.status(500).send(errors)); + .then(results => res.status(201).send(results)) + .catch(errors => res.status(500).send(errors)); } // if item does exist we will update it return Item.findOneAndUpdate( @@ -191,8 +191,8 @@ const inventoryController = function (Item, ItemType) { .then((results) => { // new call to update the costPer using the new quantities and cost Item.findByIdAndUpdate(results._id, { costPer: results.quantity !== 0 ? results.cost / results.quantity : 0 }, { new: true }) - .then((result) => res.status(201).send(result)) - .catch((errors) => res.status(500).send(errors)); + .then(result => res.status(201).send(result)) + .catch(errors => res.status(500).send(errors)); }); } return res.status(400).send('Valid Project, Quantity and Type Id are necessary'); @@ -256,9 +256,9 @@ const inventoryController = function (Item, ItemType) { }, }, { new: true }).then((results) => { Item.findByIdAndUpdate(results._id, { costPer: results.quantity !== 0 ? results.cost / results.quantity : 0 }, { new: true }) - .then((result) => res.status(201).send(result)) - .catch((errors) => res.status(500).send(errors)); - }).catch((errors) => res.status(500).send(errors)); + .then(result => res.status(201).send(result)) + .catch(errors => res.status(500).send(errors)); + }).catch(errors => res.status(500).send(errors)); } const data = { quantity: req.body.quantity, @@ -276,12 +276,12 @@ const inventoryController = function (Item, ItemType) { const inventoryItem = new Item(data); return inventoryItem.save() - .then((results) => res.status(201).send({ from: prevResults, to: results })) - .catch((errors) => res.status(500).send(errors)); + .then(results => res.status(201).send({ from: prevResults, to: results })) + .catch(errors => res.status(500).send(errors)); }) - .catch((errors) => res.status(500).send(errors)); + .catch(errors => res.status(500).send(errors)); }) - .catch((errors) => res.status(500).send(errors)); + .catch(errors => res.status(500).send(errors)); } return res.status(400).send('Valid Project, Quantity and Type Id are necessary as well as valid wbs if sent in and not Unassigned'); }; @@ -345,9 +345,9 @@ const inventoryController = function (Item, ItemType) { }, }, { new: true }).then((results) => { Item.findByIdAndUpdate(results._id, { costPer: results.quantity !== 0 ? results.cost / results.quantity : 0 }, { new: true }) - .then((result) => res.status(201).send(result)) - .catch((errors) => res.status(500).send(errors)); - }).catch((errors) => res.status(500).send(errors)); + .then(result => res.status(201).send(result)) + .catch(errors => res.status(500).send(errors)); + }).catch(errors => res.status(500).send(errors)); } const data = { quantity: req.body.quantity, @@ -365,12 +365,12 @@ const inventoryController = function (Item, ItemType) { const inventoryItem = new Item(data); return inventoryItem.save() - .then((results) => res.status(201).send({ from: prevResults, to: results })) - .catch((errors) => res.status(500).send(errors)); + .then(results => res.status(201).send({ from: prevResults, to: results })) + .catch(errors => res.status(500).send(errors)); }) - .catch((errors) => res.status(500).send(errors)); + .catch(errors => res.status(500).send(errors)); }) - .catch((errors) => res.status(500).send(errors)); + .catch(errors => res.status(500).send(errors)); } return res.status(400).send('Valid Project, Quantity and Type Id are necessary as well as valid wbs if sent in and not Unassigned'); }; @@ -426,9 +426,9 @@ const inventoryController = function (Item, ItemType) { }, }, { new: true }).then((results) => { Item.findByIdAndUpdate(results._id, { costPer: results.quantity !== 0 ? results.cost / results.quantity : 0 }, { new: true }) - .then((result) => res.status(201).send(result)) - .catch((errors) => res.status(500).send(errors)); - }).catch((errors) => res.status(500).send(errors)); + .then(result => res.status(201).send(result)) + .catch(errors => res.status(500).send(errors)); + }).catch(errors => res.status(500).send(errors)); } const data = { quantity: req.body.quantity, @@ -446,12 +446,12 @@ const inventoryController = function (Item, ItemType) { const inventoryItem = new Item(data); return inventoryItem.save() - .then((results) => res.status(201).send({ from: prevResults, to: results })) - .catch((errors) => res.status(500).send(errors)); + .then(results => res.status(201).send({ from: prevResults, to: results })) + .catch(errors => res.status(500).send(errors)); }) - .catch((errors) => res.status(500).send(errors)); + .catch(errors => res.status(500).send(errors)); }) - .catch((errors) => res.status(500).send(errors)); + .catch(errors => res.status(500).send(errors)); } return res.status(400).send('Valid Project, Quantity and Type Id are necessary as well as valid wbs if sent in and not Unassigned'); }; @@ -464,8 +464,8 @@ const inventoryController = function (Item, ItemType) { // Look up an inventory item by id and send back the info as jsong // send result just sending something now to have it work and not break anything return Item.findById({ _id: req.params.invId }) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const putInvById = async function (req, res) { @@ -503,8 +503,8 @@ const inventoryController = function (Item, ItemType) { // send result just sending something now to have it work and not break anything // Use model ItemType and return the find by Id return ItemType.findById({ _id: req.params.typeId }) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const putInvType = async function (req, res) { @@ -536,8 +536,8 @@ const inventoryController = function (Item, ItemType) { } // send result just sending something now to have it work and not break anything return ItemType.find({}) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const postInvType = async function (req, res) { @@ -563,8 +563,8 @@ const inventoryController = function (Item, ItemType) { itemType.link = req.body.link; itemType.save() - .then((results) => res.status(201).send(results)) - .catch((errors) => res.status(500).send(errors)); + .then(results => res.status(201).send(results)) + .catch(errors => res.status(500).send(errors)); }); // send result just sending something now to have it work and not break anything // create an inventory type req.body.name, req.body.description, req.body.imageUrl, req.body.quantifier diff --git a/src/controllers/mapLocationsController.js b/src/controllers/mapLocationsController.js index dee7f2684..c7a359718 100644 --- a/src/controllers/mapLocationsController.js +++ b/src/controllers/mapLocationsController.js @@ -18,7 +18,7 @@ const mapLocationsController = function (MapLocation) { users.push(item); } }); - const modifiedUsers = users.map((item) => ({ + const modifiedUsers = users.map(item => ({ location: item.location, isActive: item.isActive, jobTitle: item.jobTitle[0], @@ -42,7 +42,7 @@ const mapLocationsController = function (MapLocation) { MapLocation.findOneAndDelete({ _id: locationId }) .then(() => res.status(200).send({ message: 'The location was successfully removed!' })) - .catch((error) => res.status(500).send({ message: error || "Couldn't remove the location" })); + .catch(error => res.status(500).send({ message: error || "Couldn't remove the location" })); }; const putUserLocation = async function (req, res) { if (!req.body.requestor.role === 'Owner') { diff --git a/src/controllers/mouseoverTextController.js b/src/controllers/mouseoverTextController.js index bb96aa3ca..636f8763a 100644 --- a/src/controllers/mouseoverTextController.js +++ b/src/controllers/mouseoverTextController.js @@ -1,43 +1,49 @@ -const mouseoverTextController = (function (MouseoverText) { - const createMouseoverText = function (req, res) { - const newMouseoverText = new MouseoverText(); - newMouseoverText.mouseoverText = req.body.newMouseoverText; - newMouseoverText.save().then(() => res.status(201).json({ - _serverMessage: 'MouseoverText succesfuly created!', - mouseoverText: newMouseoverText, - })).catch((err) => res.status(500).send({ err })); - }; +const mouseoverTextController = function (MouseoverText) { + const createMouseoverText = function (req, res) { + const newMouseoverText = new MouseoverText(); + newMouseoverText.mouseoverText = req.body.newMouseoverText; + newMouseoverText + .save() + .then(() => + res.status(201).json({ + _serverMessage: 'MouseoverText succesfuly created!', + mouseoverText: newMouseoverText, + }), + ) + .catch((err) => res.status(500).send({ err })); + }; - const getMouseoverText = function (req, res) { - MouseoverText.find() - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); - }; + const getMouseoverText = function (req, res) { + MouseoverText.find() + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); + }; - const updateMouseoverText = function (req, res) { - // if (req.body.requestor.role !== 'Owner') { - // res.status(403).send('You are not authorized to update mouseoverText!'); - // } - const { id } = req.params; + const updateMouseoverText = function (req, res) { + // if (req.body.requestor.role !== 'Owner') { + // res.status(403).send('You are not authorized to update mouseoverText!'); + // } + const { id } = req.params; - return MouseoverText.findById(id, (error, mouseoverText) => { - if (error || mouseoverText === null) { - res.status(500).send('MouseoverText not found with the given ID'); - return; - } + return MouseoverText.findById(id, (error, mouseoverText) => { + if (error || mouseoverText === null) { + res.status(500).send('MouseoverText not found with the given ID'); + return; + } - mouseoverText.mouseoverText = req.body.newMouseoverText; - mouseoverText.save() - .then((results) => res.status(201).send(results)) - .catch((errors) => res.status(400).send(errors)); - }); - }; + mouseoverText.mouseoverText = req.body.newMouseoverText; + mouseoverText + .save() + .then((results) => res.status(201).send(results)) + .catch((errors) => res.status(400).send(errors)); + }); + }; - return { - createMouseoverText, - getMouseoverText, - updateMouseoverText, - }; -}); + return { + createMouseoverText, + getMouseoverText, + updateMouseoverText, + }; +}; module.exports = mouseoverTextController; diff --git a/src/controllers/mouseoverTextController.spec.js b/src/controllers/mouseoverTextController.spec.js new file mode 100644 index 000000000..b4a5bd48b --- /dev/null +++ b/src/controllers/mouseoverTextController.spec.js @@ -0,0 +1,162 @@ +const mouseoverTextController = require('./mouseoverTextController'); +const { mockReq, mockRes, assertResMock } = require('../test'); +const MouseoverText = require('../models/mouseoverText'); + +const makeSut = () => { + const { createMouseoverText, getMouseoverText, updateMouseoverText } = + mouseoverTextController(MouseoverText); + return { createMouseoverText, getMouseoverText, updateMouseoverText }; +}; + +const flushPromises = () => new Promise(setImmediate); +describe('mouseoverText Controller', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockReq.params.id = '6237f9af9820a0134ca79c5g'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createMouseoverText method', () => { + test('Ensure createMouseoverText returns 500 if any error when saving new mouseoverText', async () => { + const { createMouseoverText } = makeSut(); + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + newMouseoverText: 'some mouseoverText', + }, + }; + jest + .spyOn(MouseoverText.prototype, 'save') + .mockImplementationOnce(() => Promise.reject(new Error('Error when saving'))); + + const response = createMouseoverText(newMockReq, mockRes); + await flushPromises(); + + assertResMock(500, { err: new Error('Error when saving') }, response, mockRes); + }); + test('Ensure createMouseoverText returns 201 if create new mouseoverText successfully', async () => { + const { createMouseoverText } = makeSut(); + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + newMouseoverText: 'new mouseoverText', + }, + }; + mockRes.json = jest.fn(); + jest.spyOn(MouseoverText.prototype, 'save').mockResolvedValue({ + mouseoverText: 'new mouseoverText', + }); + createMouseoverText(newMockReq, mockRes); + await flushPromises(); + + expect(mockRes.status).toHaveBeenCalledWith(201); + expect(mockRes.json).toHaveBeenCalledWith( + expect.objectContaining({ + mouseoverText: expect.objectContaining({ + mouseoverText: 'new mouseoverText', + }), + }), + ); + }); + }); + describe('getMouseoverText method', () => { + test('Ensure getMouseoverText returns 404 if any error when finding the mouseoverText', async () => { + const { getMouseoverText } = makeSut(); + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + mouseoverText: 'some mouseoverText', + }, + }; + jest + .spyOn(MouseoverText, 'find') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding'))); + + const response = getMouseoverText(newMockReq, mockRes); + await flushPromises(); + + assertResMock(404, new Error('Error when finding'), response, mockRes); + }); + test('Ensure getMouseoverText returns 200 if get the mouseoverText successfully', async () => { + const { getMouseoverText } = makeSut(); + const data = { + mouseoverText: 'some get mouseoverText', + }; + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + mouseoverText: 'get mouseoverText', + }, + }; + jest.spyOn(MouseoverText, 'find').mockImplementationOnce(() => Promise.resolve(data)); + const response = getMouseoverText(newMockReq, mockRes); + await flushPromises(); + + assertResMock(200, data, response, mockRes); + }); + }); + describe('updateMouseoverText method', () => { + test('Ensure updateMouseoverText returns 500 if any error when finding the mouseoverText by Id', async () => { + const { updateMouseoverText } = makeSut(); + const findByIdSpy = jest + .spyOn(MouseoverText, 'findById') + .mockImplementationOnce((_, cb) => cb(true, null)); + const response = updateMouseoverText(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, 'MouseoverText not found with the given ID', response, mockRes); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); + }); + test('Ensure updateMouseoverText returns 400 if any error when saving the mouseoverText', async () => { + const { updateMouseoverText } = makeSut(); + const data = { + mouseoverText: 'old mouseoverText', + save: () => {}, + }; + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + newMouseoverText: 'some new mouseoverText', + }, + }; + const findByIdSpy = jest + .spyOn(MouseoverText, 'findById') + .mockImplementationOnce((_, cb) => cb(false, data)); + jest.spyOn(data, 'save').mockRejectedValueOnce(new Error('Error when saving')); + const response = updateMouseoverText(newMockReq, mockRes); + await flushPromises(); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); + assertResMock(400, new Error('Error when saving'), response, mockRes); + }); + test('Ensure updateMouseoverText returns 201 if updating mouseoverText successfully', async () => { + const { updateMouseoverText } = makeSut(); + const data = { + mouseoverText: 'some get mouseoverText', + save: () => {}, + }; + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + newMouseoverText: 'some new mouseoverText', + }, + }; + const findByIdSpy = jest + .spyOn(MouseoverText, 'findById') + .mockImplementationOnce((_, cb) => cb(false, data)); + jest.spyOn(data, 'save').mockResolvedValueOnce(data); + const response = updateMouseoverText(newMockReq, mockRes); + await flushPromises(); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); + assertResMock(201, data, response, mockRes); + }); + }); +}); diff --git a/src/controllers/notificationController.js b/src/controllers/notificationController.js index fc0995abe..e15f0d989 100644 --- a/src/controllers/notificationController.js +++ b/src/controllers/notificationController.js @@ -172,8 +172,11 @@ const notificationController = function () { return { getUserNotifications, + getUnreadUserNotifications, + getSentNotifications, deleteUserNotification, createUserNotification, + markNotificationAsRead, }; }; diff --git a/src/controllers/popupEditorBackupController.js b/src/controllers/popupEditorBackupController.js index 9e12545cc..8c4a7349c 100644 --- a/src/controllers/popupEditorBackupController.js +++ b/src/controllers/popupEditorBackupController.js @@ -3,8 +3,8 @@ const { hasPermission } = require('../utilities/permissions'); const popupEditorBackupController = function (PopupEditorBackups) { const getAllPopupEditorBackups = function (req, res) { PopupEditorBackups.find() - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const getPopupEditorBackupById = function (req, res) { @@ -39,8 +39,8 @@ const popupEditorBackupController = function (PopupEditorBackups) { popup.popupContent = req.body.popupContent; popup.save() - .then((results) => res.status(201).send(results)) - .catch((error) => res.status(500).send({ error })); + .then(results => res.status(201).send(results)) + .catch(error => res.status(500).send({ error })); }; const updatePopupEditorBackup = async function (req, res) { @@ -64,15 +64,15 @@ const popupEditorBackupController = function (PopupEditorBackups) { PopupEditorBackups.find({ popupId: { $in: popupId } }, (error, popupBackup) => { if (popupBackup.length > 0) { popupBackup[0].popupContent = req.body.popupContent; - popupBackup[0].save().then((results) => res.status(201).send(results)); + popupBackup[0].save().then(results => res.status(201).send(results)); } else { const popup = new PopupEditorBackups(); popup.popupId = req.params.id; popup.popupContent = req.body.popupContent; popup.popupName = req.body.popupName; popup.save() - .then((results) => res.status(201).send(results)) - .catch((err) => res.status(500).send({ err })); + .then(results => res.status(201).send(results)) + .catch(err => res.status(500).send({ err })); } }); } catch (error) { diff --git a/src/controllers/popupEditorController.js b/src/controllers/popupEditorController.js index 1c62dd6b1..33afbb9c8 100644 --- a/src/controllers/popupEditorController.js +++ b/src/controllers/popupEditorController.js @@ -3,14 +3,14 @@ const { hasPermission } = require('../utilities/permissions'); const popupEditorController = function (PopupEditors) { const getAllPopupEditors = function (req, res) { PopupEditors.find() - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const getPopupEditorById = function (req, res) { PopupEditors.findById(req.params.id) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send(error)); }; const createPopupEditor = async function (req, res) { @@ -32,8 +32,8 @@ const popupEditorController = function (PopupEditors) { popup.popupContent = req.body.popupContent; popup.save() - .then((results) => res.status(201).send(results)) - .catch((error) => res.status(500).send({ error })); + .then(results => res.status(201).send(results)) + .catch(error => res.status(500).send({ error })); }; const updatePopupEditor = async function (req, res) { @@ -55,8 +55,8 @@ const popupEditorController = function (PopupEditors) { PopupEditors.findById(popupId, (error, popup) => { popup.popupContent = req.body.popupContent; - popup.save().then((results) => res.status(201).send(results)) - .catch((err) => res.status(500).send({ err })); + popup.save().then(results => res.status(201).send(results)) + .catch(err => res.status(500).send({ err })); }); }; diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index 7e1d5ed0a..f6086d02f 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -5,6 +5,15 @@ const jwt = require('jsonwebtoken'); const emailSender = require('../utilities/emailSender'); const config = require('../config'); const cache = require('../utilities/nodeCache')(); +const LOGGER = require('../startup/logger'); + + +const TOKEN_HAS_SETUP_MESSAGE = 'SETUP_ALREADY_COMPLETED'; +const TOKEN_CANCEL_MESSAGE = 'CANCELLED'; +const TOKEN_INVALID_MESSAGE = 'INVALID'; +const TOKEN_EXPIRED_MESSAGE = 'EXPIRED'; +const TOKEN_NOT_FOUND_MESSAGE = 'NOT_FOUND'; +const { startSession } = mongoose; // returns the email body that includes the setup link for the recipient. function sendLinkMessage(Link) { @@ -12,6 +21,20 @@ function sendLinkMessage(Link) {

Welcome to the One Community Highest Good Network! We’re excited to have you as a new member of our team.
To work as a member of our volunteer team, you need to complete the following profile setup:

Click to Complete Profile

+

Please complete the profile setup within 21 days of this invite.

+

Please complete all fields and be accurate. If you have any questions or need assistance during the profile setup process, please contact your manager.

+

Thank you and welcome!

+

With Gratitude,

+

One Community

`; + return message; +} + +function sendRefreshedLinkMessage(Link) { + const message = `

Hello,

+

You setup link is refreshed! Welcome to the One Community Highest Good Network! We’re excited to have you as a new member of our team.
+ To work as a member of our volunteer team, you need to complete the following profile setup by:

+

Click to Complete Profile

+

Please complete the profile setup within 21 days of this invite.

Please complete all fields and be accurate. If you have any questions or need assistance during the profile setup process, please contact your manager.

Thank you and welcome!

With Gratitude,

@@ -19,6 +42,16 @@ function sendLinkMessage(Link) { return message; } +function sendCancelLinkMessage() { + const message = `

Hello,

+

Your setup link has been deactivated by the administrator.

+

If you have any questions or need assistance during the profile setup process, please contact your manager.

+

Thank you and welcome!

+

With Gratitude,

+

One Community

`; + return message; +} + // returns the email body containing the details of the newly created user. function informManagerMessage(user) { const message = ` @@ -76,7 +109,8 @@ function informManagerMessage(user) { return message; } -const sendEmailWithAcknowledgment = (email, subject, message) => new Promise((resolve, reject) => { +const sendEmailWithAcknowledgment = (email, subject, message) => + new Promise((resolve, reject) => { emailSender(email, subject, message, null, null, null, (error, result) => { if (result) resolve(result); if (error) reject(result); @@ -87,7 +121,7 @@ const profileInitialSetupController = function ( ProfileInitialSetupToken, userProfile, Project, - MapLocation, + MapLocation ) { const { JWT_SECRET } = config; @@ -99,210 +133,247 @@ const profileInitialSetupController = function ( return response; } catch (err) { return { - type: 'Error', - message: err.message || 'An error occurred while saving the location', + type: "Error", + message: err.message || "An error occurred while saving the location", }; } }; - /* - Function to handle token generation and email process: - - Generates a new token and saves it to the database. - - If the email already has a token, the old one is deleted. - - Sets the token expiration to three weeks. + /** + * Function to handle token generation and email process: + - Generates a new token and saves it to the database. + - If the email already has a token, the old one is deleted. + - Sets the token expiration to three weeks. - Generates a link using the token and emails it to the recipient. + * @param {*} req payload include: email, baseUrl, weeklyCommittedHours + * @param {*} res */ const getSetupToken = async (req, res) => { - let { email, baseUrl, weeklyCommittedHours } = req.body; + let { email } = req.body; + const { baseUrl, weeklyCommittedHours } = req.body; email = email.toLowerCase(); const token = uuidv4(); - const expiration = moment().tz('America/Los_Angeles').add(3, 'week'); + const expiration = moment().add(3, 'week'); + // Wrap multiple db operations in a transaction + const session = await startSession(); + try { const existingEmail = await userProfile.findOne({ email, }); if (existingEmail) { - res.status(400).send('email already in use'); - } else { - await ProfileInitialSetupToken.findOneAndDelete({ email }); - - const newToken = new ProfileInitialSetupToken({ - token, - email, - weeklyCommittedHours, - expiration: expiration.toDate(), - }); + return res.status(400).send('email already in use'); + } + session.startTransaction(); + await ProfileInitialSetupToken.findOneAndDelete({ email }); - const savedToken = await newToken.save(); - const link = `${baseUrl}/ProfileInitialSetup/${savedToken.token}`; + const newToken = new ProfileInitialSetupToken({ + token, + email, + weeklyCommittedHours, + expiration: expiration.toDate(), + isSetupCompleted: false, + isCancelled: false, + createdDate: Date.now(), + }); - const acknowledgment = await sendEmailWithAcknowledgment( - email, - 'NEEDED: Complete your One Community profile setup', - sendLinkMessage(link), - ); + const savedToken = await newToken.save(); + const link = `${baseUrl}/ProfileInitialSetup/${savedToken.token}`; + await session.commitTransaction(); - res.status(200).send(acknowledgment); - } + const acknowledgment = await sendEmailWithAcknowledgment( + email, + 'NEEDED: Complete your One Community profile setup', + sendLinkMessage(link), + ); + + return res.status(200).send(acknowledgment); } catch (error) { - res.status(400).send(`Error: ${error}`); + await session.abortTransaction(); + LOGGER.logException(error, 'getSetupToken', JSON.stringify(req.body), null); + return res.status(400).send(`Error: ${error}`); + } finally { + session.endSession(); } }; - /* - Function to validate a token: - - Checks if the token exists in the database. - - Verifies that the token's expiration date has not passed yet. - */ + /** + * Function to validate a token: + - Checks if the token exists in the database. + - Verifies that the token's expiration date has not passed yet. + * @param {*} req + * @param {*} res + */ const validateSetupToken = async (req, res) => { const { token } = req.body; - const currentMoment = moment.tz('America/Los_Angeles'); + const currentMoment = moment.now(); try { const foundToken = await ProfileInitialSetupToken.findOne({ token }); if (foundToken) { const expirationMoment = moment(foundToken.expiration); - - if (expirationMoment.isAfter(currentMoment)) { - res.status(200).send(foundToken); - } else { - res.status(400).send('Invalid token'); + // Check if the token is already used + if (foundToken.isSetupCompleted) { + return res.status(400).send(TOKEN_HAS_SETUP_MESSAGE); + } + // Check if the token is cancelled + if (foundToken.isCancelled) { + return res.status(400).send(TOKEN_CANCEL_MESSAGE); } - } else { - res.status(404).send('Token not found'); + // Check if the token is expired + if (expirationMoment.isBefore(currentMoment)) { + return res.status(400).send(TOKEN_EXPIRED_MESSAGE); + } + return res.status(200).send(foundToken); } + // Token not found + return res.status(404).send(TOKEN_NOT_FOUND_MESSAGE); } catch (error) { + LOGGER.logException(error, 'validateSetupToken', JSON.stringify(req.body), null); res.status(500).send(`Error finding token: ${error}`); } }; /* - Function for creating and authenticating a new user: - - Validates the token used to submit the form. - - Creates a new user using the information received through req.body. - - Sends an email to the manager to inform them of the new user creation. - - Deletes the token used for user creation from the database. - - Generates a JWT token using the newly created user information. - - Sends the JWT as a response. + Function for creating and authenticating a new user: + - Validates the token used to submit the form. + - Creates a new user using the information received through req.body. + - Sends an email to the manager to inform them of the new user creation. + - Deletes the token used for user creation from the database. + - Generates a JWT token using the newly created user information. + - Sends the JWT as a response. */ const setUpNewUser = async (req, res) => { const { token } = req.body; - const currentMoment = moment.tz('America/Los_Angeles'); + const currentMoment = moment.now(); // use UTC for comparison try { const foundToken = await ProfileInitialSetupToken.findOne({ token }); + + if (!foundToken) { + res.status(400).send('Invalid token'); + return; + } + const existingEmail = await userProfile.findOne({ email: foundToken.email, }); + if (existingEmail) { res.status(400).send('email already in use'); - } else if (foundToken) { - const expirationMoment = moment(foundToken.expiration); + return; + } - if (expirationMoment.isAfter(currentMoment)) { - const defaultProject = await Project.findOne({ - projectName: 'Orientation and Initial Setup', - }); - - const newUser = new userProfile(); - newUser.password = req.body.password; - newUser.role = 'Volunteer'; - newUser.firstName = req.body.firstName; - newUser.lastName = req.body.lastName; - newUser.jobTitle = req.body.jobTitle; - newUser.phoneNumber = req.body.phoneNumber; - newUser.bio = ''; - newUser.weeklycommittedHours = foundToken.weeklyCommittedHours; - newUser.weeklycommittedHoursHistory = [ - { - hours: newUser.weeklycommittedHours, - dateChanged: Date.now(), - }, - ]; - newUser.personalLinks = []; - newUser.adminLinks = []; - newUser.teams = Array.from(new Set([])); - newUser.projects = Array.from(new Set([defaultProject])); - newUser.createdDate = Date.now(); - newUser.email = req.body.email; - newUser.weeklySummaries = [{ summary: '' }]; - newUser.weeklySummariesCount = 0; - newUser.weeklySummaryOption = 'Required'; - newUser.mediaUrl = ''; - newUser.collaborationPreference = req.body.collaborationPreference; - newUser.timeZone = req.body.timeZone || 'America/Los_Angeles'; - newUser.location = req.body.location; - newUser.profilePic = req.body.profilePicture; - newUser.permissions = { - frontPermissions: [], - backPermissions: [], - }; - newUser.bioPosted = 'default'; - newUser.privacySettings.email = req.body.privacySettings.email; - newUser.privacySettings.phoneNumber = req.body.privacySettings.phoneNumber; - newUser.teamCode = ''; - newUser.isFirstTimelog = true; - - const savedUser = await newUser.save(); - - emailSender( - process.env.MANAGER_EMAIL || 'jae@onecommunityglobal.org', // "jae@onecommunityglobal.org" - `NEW USER REGISTERED: ${savedUser.firstName} ${savedUser.lastName}`, - informManagerMessage(savedUser), - null, - null, - ); - await ProfileInitialSetupToken.findByIdAndDelete(foundToken._id); - - const jwtPayload = { - userid: savedUser._id, - role: savedUser.role, - permissions: savedUser.permissions, - expiryTimestamp: moment().add( - config.TOKEN.Lifetime, - config.TOKEN.Units, - ), - }; - - const token = jwt.sign(jwtPayload, JWT_SECRET); - - const locationData = { - title: '', - firstName: req.body.firstName, - lastName: req.body.lastName, - jobTitle: req.body.jobTitle, - location: req.body.homeCountry, - isActive: true, - }; - - res.send({ token }).status(200); - - const mapEntryResult = await setMapLocation(locationData); - if (mapEntryResult.type === 'Error') { - console.log(mapEntryResult.message); - } + const expirationMoment = moment(foundToken.expiration); + if (foundToken.isSetupCompleted) { + return res.status(400).send('User has been setup already.'); + } + if (foundToken.isCancelled) { + return res.status(400).send('Token is invalided by admin.'); + } + if (expirationMoment.isBefore(currentMoment)) { + return res.status(400).send('Token has expired.'); + } - const NewUserCache = { - permissions: savedUser.permissions, - isActive: true, - weeklycommittedHours: savedUser.weeklycommittedHours, - createdDate: savedUser.createdDate.toISOString(), - _id: savedUser._id, - role: savedUser.role, - firstName: savedUser.firstName, - lastName: savedUser.lastName, - email: savedUser.email, - }; - - const allUserCache = JSON.parse(cache.getCache('allusers')); - allUserCache.push(NewUserCache); - cache.setCache('allusers', JSON.stringify(allUserCache)); - } else { - res.status(400).send('Token is expired'); - } - } else { - res.status(400).send('Invalid token'); + const defaultProject = await Project.findOne({ + projectName: 'Orientation and Initial Setup', + }); + + const newUser = new userProfile(); + newUser.password = req.body.password; + newUser.role = 'Volunteer'; + newUser.firstName = req.body.firstName; + newUser.lastName = req.body.lastName; + newUser.jobTitle = req.body.jobTitle; + newUser.phoneNumber = req.body.phoneNumber; + newUser.bio = ''; + newUser.weeklycommittedHours = foundToken.weeklyCommittedHours; + newUser.weeklycommittedHoursHistory = [ + { + hours: newUser.weeklycommittedHours, + dateChanged: Date.now(), + }, + ]; + newUser.personalLinks = []; + newUser.adminLinks = []; + newUser.teams = Array.from(new Set([])); + newUser.projects = Array.from(new Set([defaultProject])); + newUser.createdDate = Date.now(); + newUser.email = req.body.email; + newUser.weeklySummaries = [{ summary: '' }]; + newUser.weeklySummariesCount = 0; + newUser.weeklySummaryOption = 'Required'; + newUser.mediaUrl = ''; + newUser.collaborationPreference = req.body.collaborationPreference; + newUser.timeZone = req.body.timeZone || 'America/Los_Angeles'; + newUser.location = req.body.location; + newUser.profilePic = req.body.profilePicture; + newUser.permissions = { + frontPermissions: [], + backPermissions: [], + }; + newUser.bioPosted = 'default'; + newUser.privacySettings.email = req.body.privacySettings.email; + newUser.privacySettings.phoneNumber = req.body.privacySettings.phoneNumber; + newUser.teamCode = ''; + newUser.isFirstTimelog = true; + + const savedUser = await newUser.save(); + + emailSender( + process.env.MANAGER_EMAIL || 'jae@onecommunityglobal.org', // "jae@onecommunityglobal.org" + `NEW USER REGISTERED: ${savedUser.firstName} ${savedUser.lastName}`, + informManagerMessage(savedUser), + null, + null, + ); + + const jwtPayload = { + userid: savedUser._id, + role: savedUser.role, + permissions: savedUser.permissions, + expiryTimestamp: moment().add(config.TOKEN.Lifetime, config.TOKEN.Units), + }; + + const jwtToken = jwt.sign(jwtPayload, JWT_SECRET); + + const locationData = { + title: '', + firstName: req.body.firstName, + lastName: req.body.lastName, + jobTitle: req.body.jobTitle, + location: req.body.homeCountry, + isActive: true, + }; + + res.status(200).send({ token: jwtToken }); + await ProfileInitialSetupToken.findOneAndUpdate( + { _id: foundToken._id }, + { isSetupCompleted: true }, + { new: true }, + ); + + const mapEntryResult = await setMapLocation(locationData); + if (mapEntryResult.type === 'Error') { + console.log(mapEntryResult.message); } + + const NewUserCache = { + permissions: savedUser.permissions, + isActive: true, + weeklycommittedHours: savedUser.weeklycommittedHours, + createdDate: savedUser.createdDate.toISOString(), + _id: savedUser._id, + role: savedUser.role, + firstName: savedUser.firstName, + lastName: savedUser.lastName, + email: savedUser.email, + }; + + const allUserCache = JSON.parse(cache.getCache('allusers')); + allUserCache.push(NewUserCache); + cache.setCache('allusers', JSON.stringify(allUserCache)); } catch (error) { + LOGGER.logException(error); res.status(500).send(`Error: ${error}`); } }; @@ -321,37 +392,45 @@ const profileInitialSetupController = function ( if (foundToken) { res.status(200).send({ userAPIKey: premiumKey }); } else { - res.status(403).send('Unauthorized Request'); + res.status(403).send("Unauthorized Request"); } }; + function calculateTotalHours(hoursByCategory) { + let hours = 0; + Object.keys(hoursByCategory).forEach((x) => { + hours += hoursByCategory[x]; + }); + return hours; + } + const getTotalCountryCount = async (req, res) => { try { const users = []; const results = await userProfile.find( {}, - 'location totalTangibleHrs hoursByCategory', + "location totalTangibleHrs hoursByCategory" ); results.forEach((item) => { if ( - (item.location?.coords.lat - && item.location?.coords.lng - && item.totalTangibleHrs >= 10) - || (item.location?.coords.lat - && item.location?.coords.lng - && calculateTotalHours(item.hoursByCategory) >= 10) + (item.location?.coords.lat && + item.location?.coords.lng && + item.totalTangibleHrs >= 10) || + (item.location?.coords.lat && + item.location?.coords.lng && + calculateTotalHours(item.hoursByCategory) >= 10) ) { users.push(item); } }); - const modifiedUsers = users.map((item) => ({ + const modifiedUsers = users.map(item => ({ location: item.location, })); const mapUsers = await MapLocation.find({}); const combined = [...modifiedUsers, ...mapUsers]; - const countries = combined.map((user) => user.location.country); + const countries = combined.map(user => user.location.country); const totalUniqueCountries = [...new Set(countries)].length; res.status(200).send({ CountryCount: totalUniqueCountries }); } catch (error) { @@ -359,13 +438,136 @@ const profileInitialSetupController = function ( } }; - function calculateTotalHours(hoursByCategory) { - let hours = 0; - Object.keys(hoursByCategory).forEach((x) => { - hours += hoursByCategory[x]; - }); - return hours; - } + + + /** + * Returns a list of setup token in not completed status + * @param {*} req HTTP request include requester role information + * @param {*} res HTTP response include setup invitation records response's body + * @returns a list of setup invitation records which setup is not complete + */ + const getSetupInvitation = (req, res) => { + const { role } = req.body.requestor; + if (role === 'Administrator' || role === 'Owner') { + try{ + ProfileInitialSetupToken + .find({ isSetupCompleted: false }) + .sort({ createdDate: -1 }) + .exec((err, result) => { + // Handle the result + if (err) { + LOGGER.logException(err); + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + } + return res.status(200).send(result); + }); + } catch (error) { + LOGGER.logException(error); + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + } + } else { + return res.status(403).send('You are not authorized to get setup history.'); + } + }; + + /** + * Cancel the setup token + * @param {*} req HTTP request include requester role information + * @param {*} res HTTP response include whether the setup invitation record is successfully cancelled + * @returns + */ + const cancelSetupInvitation = (req, res) => { + const { role } = req.body.requestor; + const { token } = req.body; + if (role === 'Administrator' || role === 'Owner') { + try { + ProfileInitialSetupToken + .findOneAndUpdate( + { token }, + { isCancelled: true }, + (err, result) => { + if (err) { + LOGGER.logException(err); + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + } + sendEmailWithAcknowledgment( + result.email, + 'One Community: Your Profile Setup Link Has Been Deactivated', + sendCancelLinkMessage(), + ); + return res.status(200).send(result); + }, + ); + } catch (error) { + LOGGER.logException(error); + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + } + } else { + res.status(403).send('You are not authorized to cancel setup invitation.'); + } + }; + /** + * Update the expired setup token to active status. After refreshing, the expiration date will be extended by 3 weeks. + * @param {*} req HTTP request include requester role information + * @param {*} res HTTP response include whether the setup invitation record is successfully refreshed + * @returns updated result of the setup invitation record. + */ + const refreshSetupInvitation = async (req, res) => { + const { role } = req.body.requestor; + const { token, baseUrl } = req.body; + + if (role === 'Administrator' || role === 'Owner') { + try { + ProfileInitialSetupToken + .findOneAndUpdate( + { token }, + { + expiration: moment().add(3, 'week'), + isCancelled: false, + }, + ) + .then((result) => { + const { email } = result; + const link = `${baseUrl}/ProfileInitialSetup/${result.token}`; + sendEmailWithAcknowledgment( + email, + 'Invitation Link Refreshed: Complete Your One Community Profile Setup', + sendRefreshedLinkMessage(link), + ); + return res.status(200).send(result); + }) + .catch((err) => { + LOGGER.logException(err); + res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + }); + } catch (error) { + return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + } + } else { + return res.status(403).send('You are not authorized to refresh setup invitation.'); + } + }; + + // const expiredSetupInvitation = (req, res) => { + // const { role } = req.body.requestor; + // const { token } = req.body; + // if (role === 'Administrator' || role === 'Owner') { + // ProfileInitialSetupToken + // .findOneAndUpdate( + // { token }, + // { + // expiration: moment().tz('America/Los_Angeles').subtract(1, 'minutes'), + // }, + // (err, result) => { + // if (err) { + // // LOGGER.logException(err); + // return res.status(500).send('Internal Error: Please retry. If the problem persists, please contact the administrator'); + // } + // return res.status(200).send(result); + // }, + // ); + // } + // }; return { getSetupToken, @@ -373,6 +575,9 @@ const profileInitialSetupController = function ( validateSetupToken, getTimeZoneAPIKeyByToken, getTotalCountryCount, + getSetupInvitation, + cancelSetupInvitation, + refreshSetupInvitation, }; }; diff --git a/src/controllers/projectController.js b/src/controllers/projectController.js index 139479c94..e724ff38f 100644 --- a/src/controllers/projectController.js +++ b/src/controllers/projectController.js @@ -1,43 +1,47 @@ /* eslint-disable quotes */ /* eslint-disable arrow-parens */ -const mongoose = require("mongoose"); -const timeentry = require("../models/timeentry"); -const userProfile = require("../models/userProfile"); -const userProject = require("../helpers/helperModels/userProjects"); -const { hasPermission } = require("../utilities/permissions"); -const escapeRegex = require("../utilities/escapeRegex"); +const mongoose = require('mongoose'); +const timeentry = require('../models/timeentry'); +const task = require('../models/task'); +const wbs = require('../models/wbs'); +const userProfile = require('../models/userProfile'); +const { hasPermission } = require('../utilities/permissions'); +const escapeRegex = require('../utilities/escapeRegex'); +const logger = require('../startup/logger'); +const cache = require('../utilities/nodeCache')(); const projectController = function (Project) { - const getAllProjects = function (req, res) { - Project.find({}, "projectName isActive category modifiedDatetime") - .sort({ modifiedDatetime: -1 }) - .then((results) => { - res.status(200).send(results); - }) - .catch((error) => res.status(404).send(error)); + const getAllProjects = async function (req, res) { + try { + const projects = await Project.find( + { isArchived: { $ne: true } }, + 'projectName isActive category modifiedDatetime', + ).sort({ modifiedDatetime: -1 }); + res.status(200).send(projects); + } catch (error) { + logger.logException(error); + res.status(404).send('Error fetching projects. Please try again.'); + } }; const deleteProject = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "deleteProject"))) { - res - .status(403) - .send({ error: "You are not authorized to delete projects." }); + if (!(await hasPermission(req.body.requestor, 'deleteProject'))) { + res.status(403).send({ error: 'You are not authorized to delete projects.' }); return; } const { projectId } = req.params; Project.findById(projectId, (error, record) => { if (error || !record || record === null || record.length === 0) { - res.status(400).send({ error: "No valid records found" }); + res.status(400).send({ error: 'No valid records found' }); return; } - // find if project has any time entries associated with it - timeentry.find({ projectId: record._id }, "_id").then((timeentries) => { + timeentry.find({ projectId: record._id }, '_id').then((timeentries) => { if (timeentries.length > 0) { res.status(400).send({ error: - "This project has associated time entries and cannot be deleted. Consider inactivaing it instead.", + 'This project has associated time entries and cannot be deleted. Consider inactivaing it instead.', }); } else { const removeprojectfromprofile = userProfile @@ -48,8 +52,7 @@ const projectController = function (Project) { Promise.all([removeprojectfromprofile, removeproject]) .then( res.status(200).send({ - message: - "Project successfully deleted and user profiles updated.", + message: 'Project successfully deleted and user profiles updated.', }), ) .catch((errors) => { @@ -63,117 +66,164 @@ const projectController = function (Project) { }; const postProject = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "postProject"))) { - res - .status(403) - .send({ error: "You are not authorized to create new projects." }); - return; + if (!(await hasPermission(req.body.requestor, 'postProject'))) { + return res.status(401).send('You are not authorized to create new projects.'); } - if (!req.body.projectName || !req.body.isActive) { - res.status(400).send({ - error: "Project Name and active status are mandatory fields.", - }); - return; + if (!req.body.projectName) { + return res.status(400).send('Project Name is mandatory fields.'); } - Project.find({ - projectName: { $regex: escapeRegex(req.body.projectName), $options: "i" }, - }).then((result) => { - if (result.length > 0) { - res.status(400).send({ - error: `Project Name must be unique. Another project with name ${result.projectName} already exists. Please note that project names are case insensitive.`, - }); + try { + const projectWithRepeatedName = await Project.find({ + projectName: { + $regex: escapeRegex(req.body.projectName), + $options: 'i', + }, + }); + if (projectWithRepeatedName.length > 0) { + res + .status(400) + .send( + `Project Name must be unique. Another project with name ${req.body.projectName} already exists. Please note that project names are case insensitive.`, + ); return; } const _project = new Project(); + const now = new Date(); _project.projectName = req.body.projectName; - _project.category = req.body.projectCategory || "Unspecified"; - _project.isActive = req.body.isActive; - _project.createdDatetime = Date.now(); - _project.modifiedDatetime = Date.now(); - - _project - .save() - .then((results) => res.status(201).send(results)) - .catch((error) => res.status(500).send({ error })); - }); + _project.category = req.body.projectCategory; + _project.isActive = true; + _project.createdDatetime = now; + _project.modifiedDatetime = now; + const savedProject = await _project.save(); + return res.status(200).send(savedProject); + } catch (error) { + logger.logException(error); + res.status(400).send('Error creating project. Please try again.'); + } }; const putProject = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "putProject"))) { - res - .status(403) - .send("You are not authorized to make changes in the projects."); + if (!(await hasPermission(req.body.requestor, 'putProject'))) { + res.status(403).send('You are not authorized to make changes in the projects.'); return; } - - const { projectId } = req.params; - Project.findById(projectId, (error, record) => { - if (error || record === null) { - res.status(400).send("No valid records found"); + const { projectName, category, isActive, _id: projectId, isArchived } = req.body; + const sameNameProejct = await Project.find({ + projectName, + _id: { $ne: projectId }, + }); + if (sameNameProejct.length > 0) { + res.status(400).send('This project name is already taken'); + return; + } + const session = await mongoose.startSession(); + session.startTransaction(); + try { + const targetProject = await Project.findById(projectId); + if (!targetProject) { + res.status(400).send('No valid records found'); return; } - - record.projectName = req.body.projectName; - record.category = req.body.category; - record.isActive = req.body.isActive; - record.modifiedDatetime = Date.now(); - - record - .save() - .then((results) => res.status(201).send(results._id)) - .catch((errors) => res.status(400).send(errors)); - }); + targetProject.projectName = projectName; + targetProject.category = category; + targetProject.isActive = isActive; + targetProject.modifiedDatetime = Date.now(); + if (isArchived) { + targetProject.isArchived = isArchived; + // deactivate wbs within target project + await wbs.updateMany({ projectId }, { isActive: false }, { session }); + // deactivate tasks within affected wbs + const deactivatedwbsIds = await wbs.find({ projectId }, '_id'); + await task.updateMany( + { wbsId: { $in: deactivatedwbsIds } }, + { isActive: false }, + { session }, + ); + // remove project from userprofiles.projects array + await userProfile.updateMany( + { projects: projectId }, + { $pull: { projects: projectId } }, + { session }, + ); + // deactivate timeentry for affected tasks + await timeentry.updateMany({ projectId }, { isActive: false }, { session }); + } + await targetProject.save({ session }); + await session.commitTransaction(); + res.status(200).send(targetProject); + } catch (error) { + await session.abortTransaction(); + logger.logException(error); + res.status(400).send('Error updating project. Please try again.'); + } finally { + session.endSession(); + } }; const getProjectById = function (req, res) { const { projectId } = req.params; - - Project.findById(projectId, "-__v -createdDatetime -modifiedDatetime") + Project.findById(projectId, '-__v -createdDatetime -modifiedDatetime') .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send(error)); + .catch((err) => { + logger.logException(err); + res.status(404).send('Error fetching project. Please try again.'); + }); }; - const getUserProjects = function (req, res) { - const { userId } = req.params; - - userProject - .findById(userId) - .then((results) => { - res.status(200).send(results.projects); - }) - .catch((error) => { - res.status(400).send(error); - }); + const getUserProjects = async function (req, res) { + try { + const { userId } = req.params; + const user = await userProfile.findById(userId, 'projects'); + if (!user) { + res.status(400).send('Invalid user'); + return; + } + const { projects } = user; + const projectList = await Project.find( + { _id: { $in: projects }, isActive: { $ne: false } }, + '_id projectName category', + ); + const result = projectList + .map((p) => { + p = p.toObject(); + p.projectId = p._id; + delete p._id; + return p; + }) + .sort((p1, p2) => { + if (p1.projectName.toLowerCase() < p2.projectName.toLowerCase()) return -1; + if (p1.projectName.toLowerCase() > p2.projectName.toLowerCase()) return 1; + return 0; + }); + res.status(200).send(result); + } catch (error) { + logger.logException(error); + res.status(400).send('Error fetching projects. Please try again.'); + } }; const assignProjectToUsers = async function (req, res) { // verify requestor is administrator, projectId is passed in request params and is valid mongoose objectid, and request body contains an array of users - - if (!(await hasPermission(req.body.requestor, "assignProjectToUsers"))) { - res - .status(403) - .send({ error: "You are not authorized to perform this operation" }); + if (!(await hasPermission(req.body.requestor, 'assignProjectToUsers'))) { + res.status(403).send('You are not authorized to perform this operation'); return; } - if ( - !req.params.projectId - || !mongoose.Types.ObjectId.isValid(req.params.projectId) - || !req.body.users - || req.body.users.length === 0 + !req.params.projectId || + !mongoose.Types.ObjectId.isValid(req.params.projectId) || + !req.body.users || + req.body.users.length === 0 ) { - res.status(400).send({ error: "Invalid request" }); + res.status(400).send('Invalid request'); return; } - // verify project exists - Project.findById(req.params.projectId) .then((project) => { if (!project || project.length === 0) { - res.status(400).send({ error: "Invalid project" }); + res.status(400).send('Invalid project'); return; } const { users } = req.body; @@ -182,7 +232,10 @@ const projectController = function (Project) { users.forEach((element) => { const { userId, operation } = element; - if (operation === "Assign") { + if (cache.hasCache(`user-${userId}`)) { + cache.removeCache(`user-${userId}`); + } + if (operation === 'Assign') { assignlist.push(userId); } else { unassignlist.push(userId); @@ -190,35 +243,30 @@ const projectController = function (Project) { }); const assignPromise = userProfile - .updateMany( - { _id: { $in: assignlist } }, - { $addToSet: { projects: project._id } }, - ) + .updateMany({ _id: { $in: assignlist } }, { $addToSet: { projects: project._id } }) .exec(); const unassignPromise = userProfile - .updateMany( - { _id: { $in: unassignlist } }, - { $pull: { projects: project._id } }, - ) + .updateMany({ _id: { $in: unassignlist } }, { $pull: { projects: project._id } }) .exec(); Promise.all([assignPromise, unassignPromise]) .then(() => { - res.status(200).send({ result: "Done" }); + res.status(200).send({ result: 'Done' }); }) .catch((error) => { res.status(500).send({ error }); }); }) - .catch((error) => { - res.status(500).send({ error }); + .catch((err) => { + logger.logException(err); + res.status(500).send('Error fetching project. Please try again.'); }); }; const getprojectMembership = async function (req, res) { const { projectId } = req.params; if (!mongoose.Types.ObjectId.isValid(projectId)) { - res.status(400).send({ error: "Invalid request" }); + res.status(400).send('Invalid request'); return; } const getId = await hasPermission(req.body.requestor, 'getProjectMembers'); diff --git a/src/controllers/reportsController.js b/src/controllers/reportsController.js index 0e7ac51f9..baca9ca13 100644 --- a/src/controllers/reportsController.js +++ b/src/controllers/reportsController.js @@ -17,7 +17,7 @@ const reportsController = function () { const summaries = reporthelper.formatSummaries(results); res.status(200).send(summaries); }) - .catch((error) => res.status(404).send(error)); + .catch(error => res.status(404).send(error)); }; /** diff --git a/src/controllers/rolePresetsController.js b/src/controllers/rolePresetsController.js index 454275126..daf1c10ab 100644 --- a/src/controllers/rolePresetsController.js +++ b/src/controllers/rolePresetsController.js @@ -1,26 +1,32 @@ -const { hasPermission } = require('../utilities/permissions'); +const helper = require('../utilities/permissions'); const rolePresetsController = function (Preset) { const getPresetsByRole = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'putRole')) { + if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { res.status(403).send('You are not authorized to make changes to roles.'); return; } const { roleName } = req.params; Preset.find({ roleName }) - .then((results) => { res.status(200).send(results); }) - .catch((error) => { res.status(400).send(error); }); + .then((results) => { + res.status(200).send(results); + }) + .catch((error) => { + res.status(400).send(error); + }); }; const createNewPreset = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'putRole')) { + if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { res.status(403).send('You are not authorized to make changes to roles.'); return; } if (!req.body.roleName || !req.body.presetName || !req.body.permissions) { - res.status(400).send({ error: 'roleName, presetName, and permissions are mandatory fields.' }); + res.status(400).send({ + error: 'roleName, presetName, and permissions are mandatory fields.', + }); return; } @@ -28,13 +34,14 @@ const rolePresetsController = function (Preset) { preset.roleName = req.body.roleName; preset.presetName = req.body.presetName; preset.permissions = req.body.permissions; - preset.save() + preset + .save() .then((result) => res.status(201).send({ newPreset: result, message: 'New preset created' })) .catch((error) => res.status(400).send({ error })); }; const updatePresetById = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'putRole')) { + if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { res.status(403).send('You are not authorized to make changes to roles.'); return; } @@ -45,15 +52,16 @@ const rolePresetsController = function (Preset) { record.roleName = req.body.roleName; record.presetName = req.body.presetName; record.permissions = req.body.permissions; - record.save() - .then((results) => res.status(200).send(results)) - .catch((errors) => res.status(400).send(errors)); + record + .save() + .then((results) => res.status(200).send(results)) + .catch((errors) => res.status(400).send(errors)); }) .catch((error) => res.status(400).send({ error })); }; const deletePresetById = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'putRole')) { + if (!(await helper.hasPermission(req.body.requestor, 'putRole'))) { res.status(403).send('You are not authorized to make changes to roles.'); return; } @@ -61,7 +69,8 @@ const rolePresetsController = function (Preset) { const { presetId } = req.params; Preset.findById(presetId) .then((result) => { - result.remove() + result + .remove() .then(res.status(200).send({ message: 'Deleted preset' })) .catch((error) => res.status(400).send({ error })); }) diff --git a/src/controllers/rolePresetsController.spec.js b/src/controllers/rolePresetsController.spec.js new file mode 100644 index 000000000..d009be44e --- /dev/null +++ b/src/controllers/rolePresetsController.spec.js @@ -0,0 +1,444 @@ +const rolePresetsController = require('./rolePresetsController'); +const { mockReq, mockRes, assertResMock } = require('../test'); +const Preset = require('../models/rolePreset'); +const Role = require('../models/role'); +const UserProfile = require('../models/userProfile'); +const helper = require('../utilities/permissions'); + +// Mock the models +jest.mock('../models/role'); +jest.mock('../models/userProfile'); + +const makeSut = () => { + const { createNewPreset, getPresetsByRole, updatePresetById, deletePresetById } = + rolePresetsController(Preset); + return { createNewPreset, getPresetsByRole, updatePresetById, deletePresetById }; +}; + +const flushPromises = () => new Promise(setImmediate); + +describe('rolePresets Controller', () => { + beforeEach(() => { + jest.clearAllMocks(); + Role.findOne = jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue({ permissions: ['someOtherPermission'] }), + }); + UserProfile.findById = jest.fn().mockReturnValue({ + select: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue({ permissions: { frontPermissions: [] } }), + }), + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('createNewPreset method', () => { + test("Ensure createNewPresets returns 403 if user doesn't have permissions for putRole", async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(false)); + + const response = await createNewPreset(mockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); + + assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); + }); + test('Ensure createNewPresetsreturns 400 if missing roleName', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + body: { + ...mockReq.body, + presetName: 'testPreset', + premissions: ['testPremissions'], + }, + }; + const response = await createNewPreset(newMockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock( + 400, + { + error: 'roleName, presetName, and permissions are mandatory fields.', + }, + response, + mockRes, + ); + }); + test('Ensure createNewPresets returns 400 if missing presetName', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + body: { + ...mockReq.body, + roleName: 'testRole', + premissions: ['testPremissions'], + }, + }; + const response = await createNewPreset(newMockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock( + 400, + { + error: 'roleName, presetName, and permissions are mandatory fields.', + }, + response, + mockRes, + ); + }); + test('Ensure createNewPresets returns 400 if missing permissions', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + body: { + ...mockReq.body, + roleName: 'testRole', + presetName: 'testPreset', + }, + }; + const response = await createNewPreset(newMockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock( + 400, + { + error: 'roleName, presetName, and permissions are mandatory fields.', + }, + response, + mockRes, + ); + }); + test('Ensure createNewPresets returns 400 if any error when saving new preset', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + roleName: 'some roleName', + presetName: 'some Preset', + permissions: ['test', 'write'], + }, + }; + jest + .spyOn(Preset.prototype, 'save') + .mockImplementationOnce(() => Promise.reject(new Error('Error when saving'))); + + const response = await createNewPreset(newMockReq, mockRes); + await flushPromises(); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(400, { error: new Error('Error when saving') }, response, mockRes); + }); + test('Ensure createNewPresets returns 201 if saving new preset successfully', async () => { + const { createNewPreset } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const data = { + roleName: 'testRoleName', + presetName: 'testPresetName', + premissions: ['somePremissions'], + }; + const newMockReq = { + ...mockReq, + body: { + ...mockReq.body, + roleName: 'some roleName', + presetName: 'some Preset', + permissions: ['test', 'write'], + }, + }; + jest.spyOn(Preset.prototype, 'save').mockImplementationOnce(() => Promise.resolve(data)); + + const response = await createNewPreset(newMockReq, mockRes); + await flushPromises(); + + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock( + 201, + { + newPreset: data, + message: 'New preset created', + }, + response, + mockRes, + ); + }); + }); + describe('getPresetsByRole method', () => { + test("Ensure getPresetsByRole returns 403 if user doesn't have permissions for putRole", async () => { + const { getPresetsByRole } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(false)); + + const response = await getPresetsByRole(mockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); + + assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); + }); + test('Ensure getPresetsByRole returns 400 if error in finding roleName', async () => { + const { getPresetsByRole } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + roleName: 'test roleName', + }, + }; + jest + .spyOn(Preset, 'find') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding'))); + const response = await getPresetsByRole(newMockReq, mockRes); + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(400, new Error('Error when finding'), response, mockRes); + }); + test('Ensure getPresetsByRole returns 200 if finding roleName successfully', async () => { + const { getPresetsByRole } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + roleName: 'test roleName', + }, + }; + const data = { + roleName: 'test roleName', + presetName: 'test Presetname2', + permissions: ['read', 'add'], + }; + jest.spyOn(Preset, 'find').mockImplementationOnce(() => Promise.resolve(data)); + const response = await getPresetsByRole(newMockReq, mockRes); + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(200, data, response, mockRes); + }); + }); + describe('updatePresetById method', () => { + test("Ensure updatePresetById returns 403 if user doesn't have permissions for putRole", async () => { + const { updatePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(false)); + + const response = await updatePresetById(mockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); + + assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); + }); + test('Ensure updatePresetById returns 400 if error in finding by id', async () => { + const { updatePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + }; + jest + .spyOn(Preset, 'findById') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding by id'))); + const response = await updatePresetById(newMockReq, mockRes); + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(400, { error: new Error('Error when finding by id') }, response, mockRes); + }); + test('Ensure updatePresetById returns 400 if error in saving results', async () => { + const { updatePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + body: { + ...mockReq.body, + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }, + }; + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockRejectedValue(new Error('Error when saving results')); + const response = await updatePresetById(newMockReq, mockRes); + + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); + assertResMock(400, new Error('Error when saving results'), response, mockRes); + }); + test('Ensure updatePresetById returns 200 if updatePreset by id successfully', async () => { + const { updatePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const data = { + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }; + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + body: { + ...mockReq.body, + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }, + }; + const findObj = { save: () => {} }; + const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(findObj); + jest.spyOn(findObj, 'save').mockResolvedValueOnce(data); + const response = await updatePresetById(newMockReq, mockRes); + + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); + assertResMock(200, data, response, mockRes); + }); + }); + describe('deletePresetById method', () => { + test("Ensure deletePresetById returns 403 if user doesn't have permissions for putRole", async () => { + const { deletePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(false)); + + const response = await deletePresetById(mockReq, mockRes); + + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'putRole'); + + assertResMock(403, 'You are not authorized to make changes to roles.', response, mockRes); + }); + test('Ensure deletePresetById returns 400 if error in finding by id', async () => { + const { deletePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + }; + jest + .spyOn(Preset, 'findById') + .mockImplementationOnce(() => Promise.reject(new Error('Error when finding by id'))); + const response = await deletePresetById(newMockReq, mockRes); + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + assertResMock(400, { error: new Error('Error when finding by id') }, response, mockRes); + }); + test('Ensure deletePresetById returns 400 if error when removing results', async () => { + const { deletePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + body: { + ...mockReq.body, + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }, + }; + const removeObj = { remove: () => {} }; + const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(removeObj); + jest.spyOn(removeObj, 'remove').mockRejectedValue({ error: 'Error when removing' }); + const response = await deletePresetById(newMockReq, mockRes); + + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); + assertResMock(400, { error: { error: 'Error when removing' } }, response, mockRes); + }); + test('Ensure deletePresetById returns 200 if deleting successfully', async () => { + const { deletePresetById } = makeSut(); + const hasPermissionSpy = jest + .spyOn(helper, 'hasPermission') + .mockImplementationOnce(() => Promise.resolve(true)); + const newMockReq = { + ...mockReq, + params: { + ...mockReq.params, + presetId: '7237f9af9820a0134ca79c5d', + }, + body: { + ...mockReq.body, + roleName: 'abc RoleName', + presetName: 'abd Preset', + permissions: ['readABC', 'writeABC'], + }, + }; + const removeObj = { remove: () => {} }; + const findByIdSpy = jest.spyOn(Preset, 'findById').mockResolvedValue(removeObj); + jest.spyOn(removeObj, 'remove').mockImplementationOnce(() => Promise.resolve(true)); + const response = await deletePresetById(newMockReq, mockRes); + + await flushPromises(); + expect(hasPermissionSpy).toHaveBeenCalledWith(newMockReq.body.requestor, 'putRole'); + + expect(findByIdSpy).toHaveBeenCalledWith(newMockReq.params.presetId); + assertResMock( + 200, + { + message: 'Deleted preset', + }, + response, + mockRes, + ); + }); + }); +}); diff --git a/src/controllers/rolesController.js b/src/controllers/rolesController.js index a69343520..90e820b27 100644 --- a/src/controllers/rolesController.js +++ b/src/controllers/rolesController.js @@ -1,22 +1,24 @@ -const UserProfile = require('../models/userProfile'); -const cache = require('../utilities/nodeCache')(); -const { hasPermission } = require('../utilities/permissions'); +const UserProfile = require("../models/userProfile"); +const cache = require("../utilities/nodeCache")(); +const { hasPermission } = require("../utilities/permissions"); const rolesController = function (Role) { const getAllRoles = function (req, res) { Role.find({}) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send({ error })); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send({ error })); }; const createNewRole = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'postRole')) { - res.status(403).send('You are not authorized to create new roles.'); + if (!(await hasPermission(req.body.requestor, "postRole"))) { + res.status(403).send("You are not authorized to create new roles."); return; } if (!req.body.roleName || !req.body.permissions) { - res.status(400).send({ error: 'roleName and permissions are mandatory fields.' }); + res + .status(400) + .send({ error: "roleName and permissions are mandatory fields." }); return; } @@ -25,7 +27,7 @@ const rolesController = function (Role) { role.permissions = req.body.permissions; role.permissionsBackEnd = req.body.permissionsBackEnd; - role.save().then((results) => res.status(201).send(results)).catch((err) => res.status(500).send({ err })); + role.save().then(results => res.status(201).send(results)).catch(err => res.status(500).send({ err })); }; const getRoleById = function (req, res) { @@ -33,47 +35,48 @@ const rolesController = function (Role) { Role.findById( roleId, ) - .then((results) => res.status(200).send(results)) - .catch((error) => res.status(404).send({ error })); + .then(results => res.status(200).send(results)) + .catch(error => res.status(404).send({ error })); }; const updateRoleById = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'putRole')) { - res.status(403).send('You are not authorized to make changes to roles.'); + if (!(await hasPermission(req.body.requestor, "putRole"))) { + res.status(403).send("You are not authorized to make changes to roles."); return; } const { roleId } = req.params; if (!req.body.permissions) { - res.status(400).send({ error: 'Permissions is a mandatory field' }); + res.status(400).send({ error: "Permissions is a mandatory field" }); return; - } + } Role.findById(roleId, (error, record) => { if (error || record === null) { - res.status(400).send('No valid records found'); + res.status(400).send("No valid records found"); return; } + record.roleName = req.body.roleName; record.permissions = req.body.permissions; record.permissionsBackEnd = req.body.permissionsBackEnd; record.save() - .then((results) => res.status(201).send(results)) - .catch((errors) => res.status(400).send(errors)); + .then(results => res.status(201).send(results)) + .catch(errors => res.status(400).send(errors)); }); }; const deleteRoleById = async function (req, res) { - if (!await hasPermission(req.body.requestor, 'deleteRole')) { - res.status(403).send('You are not authorized to delete roles.'); + if (!(await hasPermission(req.body.requestor, "deleteRole"))) { + res.status(403).send("You are not authorized to delete roles."); return; } const { roleId } = req.params; Role.findById(roleId) - .then((result) => ( + .then(result => ( result .remove() .then(UserProfile @@ -92,10 +95,10 @@ const rolesController = function (Role) { } res.status(200).send({ message: 'Deleted role' }); }) - .catch((error) => res.status(400).send({ error }))) - .catch((error) => res.status(400).send({ error })) + .catch(error => res.status(400).send({ error }))) + .catch(error => res.status(400).send({ error })) )) - .catch((error) => res.status(400).send({ error })); + .catch(error => res.status(400).send({ error })); }; return { diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index a939cdc90..1e019ffa1 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -14,6 +14,7 @@ const taskController = function (Task) { let query = { wbsId: { $in: [req.params.wbsId] }, level: { $in: [level] }, + isActive: { $ne: false }, }; const { mother } = req.params; @@ -248,6 +249,7 @@ const taskController = function (Task) { $and: [ { $or: [{ taskId: parentId1 }, { parentId1 }, { parentId1: null }] }, { wbsId: { $in: [wbsId] } }, + { isActive: { $ne: false } }, ], }).then((tasks) => { tasks = [...new Set(tasks.map((item) => item))]; @@ -823,26 +825,54 @@ const taskController = function (Task) { const getTasksByUserId = async (req, res) => { const { userId } = req.params; try { - Task.find( - { - 'resources.userID': mongoose.Types.ObjectId(userId), - }, - '-resources.profilePic', - ).then((results) => { - WBS.find({ - _id: { $in: results.map((item) => item.wbsId) }, - }).then((WBSs) => { - const resultsWithProjectsIds = results.map((item) => { - item.set( - 'projectId', - WBSs?.find((wbs) => wbs._id.toString() === item.wbsId.toString())?.projectId, - { strict: false }, - ); - return item; - }); - res.status(200).send(resultsWithProjectsIds); + const tasks = await Task.aggregate() + .match({ + resources: { + $elemMatch: { + userID: mongoose.Types.ObjectId(userId), + completedTask: { + $ne: true, + }, + }, + }, + isActive: { + $ne: false, + }, + }) + .lookup({ + from: 'wbs', + localField: 'wbsId', + foreignField: '_id', + as: 'wbs', + }) + .unwind({ + path: '$wbs', + includeArrayIndex: 'string', + preserveNullAndEmptyArrays: true, + }) + .addFields({ + wbsName: '$wbs.wbsName', + projectId: '$wbs.projectId', + }) + .lookup({ + from: 'projects', + localField: 'projectId', + foreignField: '_id', + as: 'project', + }) + .unwind({ + path: '$project', + includeArrayIndex: 'string', + preserveNullAndEmptyArrays: true, + }) + .addFields({ + projectName: '$project.projectName', + }) + .project({ + wbs: 0, + project: 0, }); - }); + res.status(200).send(tasks); } catch (error) { res.status(400).send(error); } diff --git a/src/controllers/taskNotificationController.js b/src/controllers/taskNotificationController.js index 5d999b800..256c5249e 100644 --- a/src/controllers/taskNotificationController.js +++ b/src/controllers/taskNotificationController.js @@ -20,7 +20,7 @@ const taskNotificationController = function (TaskNotification) { // If task notification with taskId and userId exists, don't do anything. // Else, create new task notification.image.png await Promise.all( - userIds.map(async (userId) => TaskNotification.updateOne( + userIds.map(async userId => TaskNotification.updateOne( { $and: [{ taskId }, { userId: mongoose.Types.ObjectId(userId) }], }, @@ -93,13 +93,13 @@ const taskNotificationController = function (TaskNotification) { result.dateRead = Date.now(); result .save() - .then((notification) => res.status(200).send(notification)) - .catch((error) => res.status(400).send(error)); + .then(notification => res.status(200).send(notification)) + .catch(error => res.status(400).send(error)); } else { res.status(404).send('TaskNotification not found.'); } }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }; return { diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index c03e5a375..15ecad80b 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -383,6 +383,26 @@ const updateTaskIdInTimeEntry = async (id, timeEntry) => { * Controller for timeEntry */ const timeEntrycontroller = function (TimeEntry) { + /** + * Helper func: Check if this is the first time entry for the given user id + * + * @param {Mongoose.ObjectId} personId + * @returns + */ + const checkIsUserFirstTimeEntry = async (personId) => { + try { + const timeEntry = await TimeEntry.findOne({ + personId, + }); + if (timeEntry) { + return false; + } + } catch (error) { + throw new Error(`Failed to check user with id ${personId} on time entry`); + } + return true; + }; + /** * Post a time entry */ @@ -467,10 +487,12 @@ const timeEntrycontroller = function (TimeEntry) { updateUserprofileTangibleIntangibleHrs(0, timeEntry.totalSeconds, userprofile); } - // see if this is the first time the user is logging time - if (userprofile.isFirstTimelog) { + // Replace the isFirstTimelog checking logic from the frontend to the backend + // Update the user start date to current date if this is the first time entry (Weekly blue square assignment related) + const isFirstTimeEntry = await checkIsUserFirstTimeEntry(timeEntry.personId); + if (isFirstTimeEntry) { userprofile.isFirstTimelog = false; - userprofile.createdDate = new Date(); // transfered from frontend logic, not sure why this is needed + userprofile.startDate = now; } await timeEntry.save({ session }); @@ -586,7 +608,6 @@ const timeEntrycontroller = function (TimeEntry) { const tangibilityChanged = initialIsTangible !== newIsTangible; const timeChanged = initialTotalSeconds !== newTotalSeconds; const dateOfWorkChanged = initialDateOfWork !== newDateOfWork; - timeEntry.notes = newNotes; timeEntry.totalSeconds = newTotalSeconds; timeEntry.isTangible = newIsTangible; @@ -604,7 +625,6 @@ const timeEntrycontroller = function (TimeEntry) { // tangiblity change usually only happens by itself via tangibility checkbox, // and it can't be changed by user directly (except for owner-like roles) // but here the other changes are also considered here for completeness - // change from tangible to intangible if (initialIsTangible) { // subtract initial logged hours from old task (if not null) @@ -755,7 +775,6 @@ const timeEntrycontroller = function (TimeEntry) { res.status(400).send({ message: 'No valid record found' }); return; } - const { personId, totalSeconds, dateOfWork, projectId, taskId, isTangible } = timeEntry; const isForAuthUser = personId.toString() === req.body.requestor.requestorId; @@ -830,6 +849,7 @@ const timeEntrycontroller = function (TimeEntry) { entryType: { $in: ['default', null] }, personId: userId, dateOfWork: { $gte: fromdate, $lte: todate }, + isActive: { $ne: false }, }).sort('-lastModifiedDateTime'); const results = await Promise.all( @@ -837,6 +857,18 @@ const timeEntrycontroller = function (TimeEntry) { timeEntry = { ...timeEntry.toObject() }; const { projectId, taskId } = timeEntry; if (!taskId) await updateTaskIdInTimeEntry(projectId, timeEntry); // if no taskId, then it might be old time entry data that didn't separate projectId with taskId + if (timeEntry.taskId) { + const task = await Task.findById(timeEntry.taskId); + if (task) { + timeEntry.taskName = task.taskName; + } + } + if (timeEntry.projectId) { + const project = await Project.findById(timeEntry.projectId); + if (project) { + timeEntry.projectName = project.projectName; + } + } const hours = Math.floor(timeEntry.totalSeconds / 3600); const minutes = Math.floor((timeEntry.totalSeconds % 3600) / 60); Object.assign(timeEntry, { hours, minutes, totalSeconds: undefined }); @@ -862,7 +894,7 @@ const timeEntrycontroller = function (TimeEntry) { personId: { $in: users }, dateOfWork: { $gte: fromDate, $lte: toDate }, }, - ' -createdDateTime', + '-createdDateTime', ) .populate('personId') .populate('projectId') @@ -871,7 +903,6 @@ const timeEntrycontroller = function (TimeEntry) { .sort({ lastModifiedDateTime: -1 }) .then((results) => { const data = []; - results.forEach((element) => { const record = {}; record._id = element._id; @@ -948,6 +979,7 @@ const timeEntrycontroller = function (TimeEntry) { { projectId, dateOfWork: { $gte: fromDate, $lte: todate }, + isActive: { $ne: false }, }, '-createdDateTime -lastModifiedDateTime', ) @@ -972,6 +1004,7 @@ const timeEntrycontroller = function (TimeEntry) { entryType: 'person', personId: { $in: users }, dateOfWork: { $gte: fromDate, $lte: toDate }, + isActive: { $ne: false }, }, ' -createdDateTime', ) @@ -1011,6 +1044,7 @@ const timeEntrycontroller = function (TimeEntry) { entryType: 'project', projectId: { $in: projects }, dateOfWork: { $gte: fromDate, $lte: toDate }, + isActive: { $ne: false }, }, ' -createdDateTime', ) @@ -1048,6 +1082,7 @@ const timeEntrycontroller = function (TimeEntry) { entryType: 'team', teamId: { $in: teams }, dateOfWork: { $gte: fromDate, $lte: toDate }, + isActive: { $ne: false }, }, ' -createdDateTime', ) @@ -1084,6 +1119,7 @@ const timeEntrycontroller = function (TimeEntry) { getLostTimeEntriesForUserList, getLostTimeEntriesForProjectList, getLostTimeEntriesForTeamList, + getTimeEntriesForReports, }; }; diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js new file mode 100644 index 000000000..f351c6e77 --- /dev/null +++ b/src/controllers/titleController.js @@ -0,0 +1,130 @@ +const Team = require('../models/team'); +const Project = require('../models/project'); + +const titlecontroller = function (Title) { + const getAllTitles = function (req, res) { + Title.find({}) + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); + }; + + const getTitleById = function (req, res) { + const { titleId } = req.params; + + Title.findById(titleId) + .then((results) => res.send(results)) + .catch((error) => res.send(error)); + }; + + 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; + 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 + .save() + .then((results) => res.status(200).send(results)) + .catch((error) => res.status(404).send(error)); + }; + + const deleteTitleById = async function (req, res) { + const { titleId } = req.params; + Title.deleteOne({ _id: titleId }) + .then((result) => res.send(result)) + .catch((error) => res.send(error)); + }; + + const deleteAllTitles = async function (req, res) { + Title.deleteMany({}) + .then((result) => { + if (result.deletedCount === 0) { + res.send({ message: 'No titles found to delete.' }); + } else { + res.send({ message: `${result.deletedCount} titles were deleted successfully.` }); + } + }) + .catch((error) => { + res.status(500).send(error); + }); + }; + + async function checkTeamCodeExists(teamCode) { + try { + const team = await Team.findOne({ teamCode }).exec(); + return !!team; + } catch (error) { + console.error('Error checking if team code exists:', error); + throw error; + } + } + + async function checkProjectExists(projectID) { + try { + const project = await Project.findOne({ _id: projectID }).exec(); + return !!project; + } catch (error) { + console.error('Error checking if project exists:', error); + throw error; + } + } + + return { + getAllTitles, + getTitleById, + postTitle, + deleteTitleById, + deleteAllTitles, + }; +}; + + module.exports = titlecontroller; diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 87e1381ba..76bcd525f 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -175,7 +175,7 @@ const userProfileController = function (UserProfile) { await UserProfile.find( {}, - '_id firstName lastName role weeklycommittedHours email permissions isActive reactivationDate createdDate endDate', + '_id firstName lastName role weeklycommittedHours email permissions isActive reactivationDate startDate createdDate endDate', ) .sort({ lastName: 1, @@ -327,6 +327,7 @@ const userProfileController = function (UserProfile) { up.teams = Array.from(new Set(req.body.teams)); up.projects = Array.from(new Set(req.body.projects)); up.createdDate = req.body.createdDate; + up.startDate = req.body.startDate ? req.body.startDate : req.body.createdDate; up.email = req.body.email; up.weeklySummaries = req.body.weeklySummaries || [{ summary: '' }]; up.weeklySummariesCount = req.body.weeklySummariesCount || 0; @@ -390,6 +391,7 @@ const userProfileController = function (UserProfile) { isActive: true, weeklycommittedHours: up.weeklycommittedHours, createdDate: up.createdDate.toISOString(), + startDate: up.startDate.toISOString(), _id: up._id, role: up.role, firstName: up.firstName, @@ -422,7 +424,9 @@ const userProfileController = function (UserProfile) { req.body.requestor.requestorId === userid) ); - if (!isRequestorAuthorized) { + const canManageAdminLinks = await hasPermission(req.body.requestor, 'manageAdminLinks'); + + if (!isRequestorAuthorized && !canManageAdminLinks) { res.status(403).send('You are not authorized to update this user'); return; } @@ -478,12 +482,10 @@ const userProfileController = function (UserProfile) { 'profilePic', 'firstName', 'lastName', - 'jobTitle', 'phoneNumber', 'bio', 'personalLinks', 'location', - 'profilePic', 'privacySettings', 'weeklySummaries', 'weeklySummariesCount', @@ -495,7 +497,6 @@ const userProfileController = function (UserProfile) { 'isFirstTimelog', 'teamCode', 'isVisible', - 'isRehireable', 'bioPosted', ]; @@ -536,8 +537,6 @@ const userProfileController = function (UserProfile) { 'role', 'isRehireable', 'isActive', - 'adminLinks', - 'isActive', 'weeklySummaries', 'weeklySummariesCount', 'mediaUrl', @@ -571,7 +570,7 @@ const userProfileController = function (UserProfile) { } if (req.body.projects !== undefined) { - record.projects = Array.from(new Set(req.body.projects)); + record.projects = req.body.projects.map((project) => project._id); } if (req.body.email !== undefined) { @@ -603,8 +602,8 @@ const userProfileController = function (UserProfile) { record.weeklycommittedHoursHistory.push(newEntry); } - if (req.body.createdDate !== undefined && record.createdDate !== req.body.createdDate) { - record.createdDate = moment(req.body.createdDate).toDate(); + if (req.body.startDate !== undefined && record.startDate !== req.body.startDate) { + record.startDate = moment(req.body.startDate).toDate(); // Make sure weeklycommittedHoursHistory isn't empty if (record.weeklycommittedHoursHistory.length === 0) { const newEntry = { @@ -614,7 +613,8 @@ const userProfileController = function (UserProfile) { record.weeklycommittedHoursHistory.push(newEntry); } // then also change the first committed history (index 0) - record.weeklycommittedHoursHistory[0].dateChanged = record.createdDate; + + record.weeklycommittedHoursHistory[0].dateChanged = record.startDate; } if ( @@ -640,7 +640,7 @@ const userProfileController = function (UserProfile) { userData.weeklycommittedHours = record.weeklycommittedHours; userData.email = record.email; userData.isActive = record.isActive; - userData.createdDate = record.createdDate.toISOString(); + userData.startDate = record.startDate.toISOString(); } } if ( @@ -662,6 +662,9 @@ const userProfileController = function (UserProfile) { results.firstName, results.lastName, results.email, + results.role, + results.startDate, + results.jobTitle[0], ); res.status(200).json({ _id: record._id, @@ -797,6 +800,7 @@ 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); @@ -1237,6 +1241,7 @@ const userProfileController = function (UserProfile) { } const user = await UserProfile.findById(req.params.userId) + .select('firstName lastName email role') .exec(); @@ -1341,10 +1346,29 @@ const userProfileController = function (UserProfile) { }; // Search for user by first name + // const getUserBySingleName = (req, res) => { + // const pattern = new RegExp(`^${ req.params.singleName}`, 'i'); + + // // Searches for first or last name + // UserProfile.find({ + // $or: [ + // { firstName: { $regex: pattern } }, + // { lastName: { $regex: pattern } }, + // ], + // }) + // .select('firstName lastName') + // .then((users) => { + // if (users.length === 0) { + // return res.status(404).send({ error: 'Users Not Found' }); + // } + // res.status(200).send(users); + // }) + // .catch((error) => res.status(500).send(error)); + // }; + const getUserBySingleName = (req, res) => { const pattern = new RegExp(`^${req.params.singleName}`, 'i'); - // Searches for first or last name UserProfile.find({ $or: [{ firstName: { $regex: pattern } }, { lastName: { $regex: pattern } }], }) @@ -1358,7 +1382,6 @@ const userProfileController = function (UserProfile) { }) .catch((error) => res.status(500).send(error)); }; - function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } @@ -1366,19 +1389,13 @@ const userProfileController = function (UserProfile) { // Search for user by full name (first and last) // eslint-disable-next-line consistent-return const getUserByFullName = (req, res) => { - // Creates an array containing the first and last name and filters out whitespace - const fullName = req.params.fullName.split(' ').filter((name) => name !== ''); - // Creates a partial match regex for both first and last name - const firstNameRegex = new RegExp(`^${escapeRegExp(fullName[0])}`, 'i'); - const lastNameRegex = new RegExp(`^${escapeRegExp(fullName[1])}`, 'i'); - - // Verfies both the first and last name are present - if (fullName.length < 2) { - return res.status(400).send({ error: 'Both first name and last name are required.' }); - } + // Sanitize user input and escape special characters + const sanitizedFullName = escapeRegExp(req.params.fullName.trim()); + // Create a regular expression to match the sanitized full name, ignoring case + const fullNameRegex = new RegExp(sanitizedFullName, 'i'); UserProfile.find({ - $and: [{ firstName: { $regex: firstNameRegex } }, { lastName: { $regex: lastNameRegex } }], + $or: [{ firstName: { $regex: fullNameRegex } }, { lastName: { $regex: fullNameRegex } }], }) .select('firstName lastName') // eslint-disable-next-line consistent-return @@ -1386,11 +1403,15 @@ const userProfileController = function (UserProfile) { if (users.length === 0) { return res.status(404).send({ error: 'Users Not Found' }); } + res.status(200).send(users); }) .catch((error) => res.status(500).send(error)); }; + // function escapeRegExp(string) { + // return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // } /** * Authorizes user to be able to add Weekly Report Recipients * diff --git a/src/controllers/wbsController.js b/src/controllers/wbsController.js index 3fdb1392c..074cfaf16 100644 --- a/src/controllers/wbsController.js +++ b/src/controllers/wbsController.js @@ -1,15 +1,15 @@ /* eslint-disable quotes */ /* eslint-disable no-unused-vars */ -const mongoose = require("mongoose"); -const { hasPermission } = require("../utilities/permissions"); -const Project = require("../models/project"); -const Task = require("../models/task"); +const mongoose = require('mongoose'); +const { hasPermission } = require('../utilities/permissions'); +const Project = require('../models/project'); +const Task = require('../models/task'); const wbsController = function (WBS) { const getAllWBS = function (req, res) { WBS.find( - { projectId: { $in: [req.params.projectId] } }, - "wbsName isActive modifiedDatetime", + { projectId: { $in: [req.params.projectId] }, isActive: { $ne: false } }, + 'wbsName isActive modifiedDatetime', ) .sort({ modifiedDatetime: -1 }) .then((results) => res.status(200).send(results)) @@ -17,10 +17,8 @@ const wbsController = function (WBS) { }; const postWBS = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "postWbs"))) { - res - .status(403) - .send({ error: "You are not authorized to create new projects." }); + if (!(await hasPermission(req.body.requestor, 'postWbs'))) { + res.status(403).send({ error: 'You are not authorized to create new projects.' }); return; } @@ -28,7 +26,7 @@ const wbsController = function (WBS) { res .status(400) // eslint-disable-next-line quotes - .send({ error: "WBS Name and active status are mandatory fields" }); + .send({ error: 'WBS Name and active status are mandatory fields' }); return; } @@ -40,12 +38,10 @@ const wbsController = function (WBS) { _wbs.modifiedDatetime = Date.now(); // adding a new wbs should change the modified date of parent project - const saveProject = Project.findById(req.params.id).then( - (currentProject) => { - currentProject.modifiedDatetime = Date.now(); - return currentProject.save(); - }, - ); + const saveProject = Project.findById(req.params.id).then((currentProject) => { + currentProject.modifiedDatetime = Date.now(); + return currentProject.save(); + }); _wbs .save() @@ -54,33 +50,29 @@ const wbsController = function (WBS) { }; const deleteWBS = async function (req, res) { - if (!(await hasPermission(req.body.requestor, "deleteWbs"))) { - res - .status(403) - .send({ error: "You are not authorized to delete projects." }); + if (!(await hasPermission(req.body.requestor, 'deleteWbs'))) { + res.status(403).send({ error: 'You are not authorized to delete projects.' }); return; } const { id } = req.params; WBS.findById(id, (error, record) => { if (error || !record || record === null || record.length === 0) { - res.status(400).send({ error: "No valid records found" }); + res.status(400).send({ error: 'No valid records found' }); return; } const removeWBS = record.remove(); Promise.all([removeWBS]) - .then(res.status(200).send({ message: " WBS successfully deleted" })) + .then(res.status(200).send({ message: ' WBS successfully deleted' })) .catch((errors) => { res.status(400).send(errors); }); - }).catch((errors) => { - res.status(400).send(errors); }); }; const getWBS = function (req, res) { - WBS.find() + WBS.find({ isActive: { $ne: false } }) .then((results) => res.status(200).send(results)) .catch((error) => res.status(500).send({ error })); }; @@ -94,35 +86,12 @@ const wbsController = function (WBS) { .catch((error) => res.status(404).send(error)); }; - const getWBSByUserId = async function (req, res) { - const { userId } = req.params; - try { - const result = await Task.aggregate() - .match({ "resources.userID": mongoose.Types.ObjectId(userId) }) - .project("wbsId -_id") - .group({ _id: "$wbsId" }) - .lookup({ - from: "wbs", - localField: "_id", - foreignField: "_id", - as: "wbs", - }) - .unwind("wbs") - .replaceRoot("wbs"); - - res.status(200).send(result); - } catch (error) { - res.status(404).send(error); - } - }; - return { postWBS, deleteWBS, getAllWBS, getWBS, getWBSById, - getWBSByUserId, }; }; diff --git a/src/controllers/wbsController.spec.js b/src/controllers/wbsController.spec.js new file mode 100644 index 000000000..a14712731 --- /dev/null +++ b/src/controllers/wbsController.spec.js @@ -0,0 +1,323 @@ +const mongoose = require('mongoose'); +// const Project = require('../models/project'); +const Task = require('../models/task'); +const WBS = require('../models/wbs'); +const wbsController = require('./wbsController'); +const helper = require('../utilities/permissions'); +const { mockReq, mockRes, assertResMock } = require('../test'); + +const makeSut = () => { + const { getAllWBS, postWBS, deleteWBS, getWBS, getWBSByUserId } = wbsController(WBS); + + return { getAllWBS, postWBS, deleteWBS, getWBS, getWBSByUserId }; +}; + +const flushPromises = async () => new Promise(setImmediate); + +const mockHasPermission = (value) => + jest.spyOn(helper, 'hasPermission').mockImplementationOnce(() => Promise.resolve(value)); + +describe('Wbs Controller', () => { + beforeEach(() => { + mockReq.params.projectId = '6237f9af9820a0134ca79c5d'; + mockReq.body.wbsName = 'New WBS'; + mockReq.body.isActive = true; + mockReq.params.id = '6237f9af9820a0134ca79c5c'; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('getAllWBS method', () => { + test('Returns 404 if an error occurs when querying the database.', async () => { + const { getAllWBS } = makeSut(); + + const errMsg = 'Error when sorting!'; + const findObj = { sort: () => {} }; + + const findSpy = jest.spyOn(WBS, 'find').mockReturnValueOnce(findObj); + const sortSpy = jest.spyOn(findObj, 'sort').mockRejectedValueOnce(new Error(errMsg)); + + const response = getAllWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(404, new Error(errMsg), response, mockRes); + expect(findSpy).toHaveBeenCalledWith( + { projectId: { $in: [mockReq.params.projectId] } }, + 'wbsName isActive modifiedDatetime', + ); + expect(sortSpy).toHaveBeenCalledWith({ modifiedDatetime: -1 }); + }); + + test('Returns 200 if all is successful', async () => { + const { getAllWBS } = makeSut(); + + const findObj = { sort: () => {} }; + const result = [{ id: 'randomId' }]; + + const findSpy = jest.spyOn(WBS, 'find').mockReturnValueOnce(findObj); + const sortSpy = jest.spyOn(findObj, 'sort').mockResolvedValueOnce(result); + + const response = getAllWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, result, response, mockRes); + expect(findSpy).toHaveBeenCalledWith( + { projectId: { $in: [mockReq.params.projectId] } }, + 'wbsName isActive modifiedDatetime', + ); + expect(sortSpy).toHaveBeenCalledWith({ modifiedDatetime: -1 }); + }); + }); + + describe('postWBS method', () => { + test('Returns 403 if the user does not have permission', async () => { + const { postWBS } = makeSut(); + const hasPermissionSpy = mockHasPermission(false); + + const response = await postWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock( + 403, + { error: 'You are not authorized to create new projects.' }, + response, + mockRes, + ); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'postWbs'); + }); + + test('Returns 400 if req.body does not contain wbsName or isActive', async () => { + const { postWBS } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + mockReq.body.wbsName = null; + mockReq.body.isActive = null; + + const response = await postWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock( + 400, + { error: 'WBS Name and active status are mandatory fields' }, + response, + mockRes, + ); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'postWbs'); + }); + + test('returns 500 if an error occurs when saving', async () => { + const { postWBS } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + const err = 'failed to save'; + jest.spyOn(WBS.prototype, 'save').mockRejectedValueOnce(new Error(err)); + const response = await postWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, { error: new Error(err) }, response, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'postWbs'); + }); + + test('Returns 201 if all is successful', async () => { + const { postWBS } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + + jest.spyOn(WBS.prototype, 'save').mockResolvedValueOnce({ _id: '123random' }); + const response = await postWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(201, { _id: '123random' }, response, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'postWbs'); + }); + }); + + describe('deleteWBS method', () => { + test('Returns 403 if the user does not have permission', async () => { + const { deleteWBS } = makeSut(); + const hasPermissionSpy = mockHasPermission(false); + + const response = await deleteWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock( + 403, + { error: 'You are not authorized to delete projects.' }, + response, + mockRes, + ); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteWbs'); + }); + + test('Returns 400 if and error occurs when querying DB', async () => { + const { deleteWBS } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + const findByIdSpy = jest + .spyOn(WBS, 'findById') + .mockImplementationOnce((_, cb) => cb(true, null)); + + const response = await deleteWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, { error: 'No valid records found' }, response, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteWbs'); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); + }); + + test('returns 400 if an error occurs when removing the WBS', async () => { + const { deleteWBS } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + const record = { _id: 'randomId', remove: () => {} }; + const err = 'Remove failed'; + + const findByIdSpy = jest + .spyOn(WBS, 'findById') + .mockImplementationOnce((_, cb) => cb(false, record)); + jest.spyOn(record, 'remove').mockRejectedValueOnce(new Error(err)); + + const response = await deleteWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(400, new Error(err), response, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteWbs'); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); + }); + + test('Returns 201 if all is successful', async () => { + const { deleteWBS } = makeSut(); + const hasPermissionSpy = mockHasPermission(true); + const record = { _id: 'randomId', remove: () => {} }; + + const findByIdSpy = jest + .spyOn(WBS, 'findById') + .mockImplementationOnce((_, cb) => cb(false, record)); + jest.spyOn(record, 'remove').mockResolvedValueOnce(true); + + const response = await deleteWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, { message: ' WBS successfully deleted' }, response, mockRes); + expect(hasPermissionSpy).toHaveBeenCalledWith(mockReq.body.requestor, 'deleteWbs'); + expect(findByIdSpy).toHaveBeenCalledWith(mockReq.params.id, expect.anything()); + }); + }); + + describe('getWBS method', () => { + test('Returns 500 if any errors occur when finding all WBS', async () => { + const { getWBS } = makeSut(); + const err = 'Error when finding'; + const findSpy = jest.spyOn(WBS, 'find').mockRejectedValueOnce(new Error(err)); + + const response = getWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(500, { error: new Error(err) }, response, mockRes); + expect(findSpy).toHaveBeenCalledWith(); + }); + + test('Returns 200 if all is successful', async () => { + const { getWBS } = makeSut(); + const wbs = [{ _id: 'randomId' }]; + const findSpy = jest.spyOn(WBS, 'find').mockResolvedValueOnce(wbs); + + const response = getWBS(mockReq, mockRes); + await flushPromises(); + + assertResMock(200, wbs, response, mockRes); + expect(findSpy).toHaveBeenCalledWith(); + }); + }); + + describe('getWBSByUserId method', () => { + test('Returns 404 if an error occurs in the aggregation query', async () => { + const { getWBSByUserId } = makeSut(); + + const aggregateObj = { match: () => {} }; + const aggregateSpy = jest.spyOn(Task, 'aggregate').mockReturnValueOnce(aggregateObj); + + const matchObj = { project: () => {} }; + const matchSpy = jest.spyOn(aggregateObj, 'match').mockReturnValueOnce(matchObj); + + const projectObj = { group: () => {} }; + const projectSpy = jest.spyOn(matchObj, 'project').mockReturnValueOnce(projectObj); + + const groupObj = { lookup: () => {} }; + const groupSpy = jest.spyOn(projectObj, 'group').mockReturnValueOnce(groupObj); + + const lookupObj = { unwind: () => {} }; + const lookupSpy = jest.spyOn(groupObj, 'lookup').mockReturnValueOnce(lookupObj); + + const unwindObj = { replaceRoot: () => {} }; + const unwindSpy = jest.spyOn(lookupObj, 'unwind').mockReturnValueOnce(unwindObj); + + const err = 'Error'; + const replaceRootSpy = jest + .spyOn(unwindObj, 'replaceRoot') + .mockRejectedValueOnce(new Error(err)); + + const response = await getWBSByUserId(mockReq, mockRes); + + assertResMock(404, new Error(err), response, mockRes); + + expect(aggregateSpy).toHaveBeenCalledWith(); + expect(matchSpy).toHaveBeenCalledWith({ + 'resources.userID': mongoose.Types.ObjectId(mockReq.params.userId), + }); + expect(projectSpy).toHaveBeenCalledWith('wbsId -_id'); + expect(groupSpy).toHaveBeenCalledWith({ _id: '$wbsId' }); + expect(lookupSpy).toHaveBeenCalledWith({ + from: 'wbs', + localField: '_id', + foreignField: '_id', + as: 'wbs', + }); + expect(unwindSpy).toHaveBeenCalledWith('wbs'); + expect(replaceRootSpy).toHaveBeenCalledWith('wbs'); + }); + + test('Returns 200 if all is successful', async () => { + const { getWBSByUserId } = makeSut(); + + const aggregateObj = { match: () => {} }; + const aggregateSpy = jest.spyOn(Task, 'aggregate').mockReturnValueOnce(aggregateObj); + + const matchObj = { project: () => {} }; + const matchSpy = jest.spyOn(aggregateObj, 'match').mockReturnValueOnce(matchObj); + + const projectObj = { group: () => {} }; + const projectSpy = jest.spyOn(matchObj, 'project').mockReturnValueOnce(projectObj); + + const groupObj = { lookup: () => {} }; + const groupSpy = jest.spyOn(projectObj, 'group').mockReturnValueOnce(groupObj); + + const lookupObj = { unwind: () => {} }; + const lookupSpy = jest.spyOn(groupObj, 'lookup').mockReturnValueOnce(lookupObj); + + const unwindObj = { replaceRoot: () => {} }; + const unwindSpy = jest.spyOn(lookupObj, 'unwind').mockReturnValueOnce(unwindObj); + + const result = [{ _id: 'randomid' }]; + const replaceRootSpy = jest.spyOn(unwindObj, 'replaceRoot').mockResolvedValueOnce(result); + + const response = await getWBSByUserId(mockReq, mockRes); + + assertResMock(200, result, response, mockRes); + + expect(aggregateSpy).toHaveBeenCalledWith(); + expect(matchSpy).toHaveBeenCalledWith({ + 'resources.userID': mongoose.Types.ObjectId(mockReq.params.userId), + }); + expect(projectSpy).toHaveBeenCalledWith('wbsId -_id'); + expect(groupSpy).toHaveBeenCalledWith({ _id: '$wbsId' }); + expect(lookupSpy).toHaveBeenCalledWith({ + from: 'wbs', + localField: '_id', + foreignField: '_id', + as: 'wbs', + }); + expect(unwindSpy).toHaveBeenCalledWith('wbs'); + expect(replaceRootSpy).toHaveBeenCalledWith('wbs'); + }); + }); +}); diff --git a/src/cronjobs/userProfileJobs.js b/src/cronjobs/userProfileJobs.js index 92e0d0e5d..f0f69e146 100644 --- a/src/cronjobs/userProfileJobs.js +++ b/src/cronjobs/userProfileJobs.js @@ -9,7 +9,7 @@ const userProfileJobs = () => { '1 0 * * 0', // Every Sunday, 1 minute past midnight. async () => { - const SUNDAY = 0; + const SUNDAY = 0; // will change back to 0 after fix if (moment().tz('America/Los_Angeles').day() === SUNDAY) { await userhelper.assignBlueSquareForTimeNotMet(); await userhelper.applyMissedHourForCoreTeam(); diff --git a/src/helpers/dashboardhelper.js b/src/helpers/dashboardhelper.js index d92aa83af..876d819ca 100644 --- a/src/helpers/dashboardhelper.js +++ b/src/helpers/dashboardhelper.js @@ -2,26 +2,17 @@ const moment = require('moment-timezone'); const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); const timeentry = require('../models/timeentry'); -const myTeam = require('./helperModels/myTeam'); const team = require('../models/team'); +const { hasPermission } = require('../utilities/permissions'); const dashboardhelper = function () { const personaldetails = function (userId) { - return userProfile.findById( - userId, - '_id firstName lastName role profilePic badgeCollection', - ); + return userProfile.findById(userId, '_id firstName lastName role profilePic badgeCollection'); }; const getOrgData = async function () { - 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'); + 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'); /** * Previous aggregate pipeline had two issues: @@ -74,10 +65,7 @@ const dashboardhelper = function () { { $not: [ { - $in: [ - '$$timeentry.entryType', - ['person', 'team', 'project'], - ], + $in: ['$$timeentry.entryType', ['person', 'team', 'project']], }, ], }, @@ -168,31 +156,19 @@ const dashboardhelper = function () { const getLeaderboard = async function (userId) { const userid = mongoose.Types.ObjectId(userId); try { - const userById = await userProfile.findOne( - { _id: userid, isActive: true }, - { role: 1 }, - ); + const userById = await userProfile.findOne({ _id: userid, isActive: true }, { role: 1 }); if (userById == null) return null; const userRole = userById.role; - const pdtstart = moment() - .tz('America/Los_Angeles') - .startOf('week') - .format('YYYY-MM-DD'); + 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'); + const pdtend = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); let teamMemberIds = [userid]; let teamMembers = []; - - if ( - userRole !== 'Administrator' - && userRole !== 'Owner' - && userRole !== 'Core Team' - ) { + const userAsRequestor = { role: userRole, requestorId: userId }; + const canSeeUsersInDashboard = await hasPermission(userAsRequestor, 'seeUsersInDashboard'); + if (!canSeeUsersInDashboard) { // Manager , Mentor , Volunteer ... , Show only team members const teamsResult = await team.find( { 'members.userId': { $in: [userid] } }, @@ -247,6 +223,7 @@ const dashboardhelper = function () { $lte: pdtend, }, personId: { $in: teamMemberIds }, + isActive: { $ne: false }, }); const timeEntryByPerson = {}; @@ -262,11 +239,9 @@ const dashboardhelper = function () { } if (timeEntry.isTangible === true) { - timeEntryByPerson[personIdStr].tangibleSeconds - += timeEntry.totalSeconds; + timeEntryByPerson[personIdStr].tangibleSeconds += timeEntry.totalSeconds; } else { - timeEntryByPerson[personIdStr].intangibleSeconds - += timeEntry.totalSeconds; + timeEntryByPerson[personIdStr].intangibleSeconds += timeEntry.totalSeconds; } timeEntryByPerson[personIdStr].totalSeconds += timeEntry.totalSeconds; @@ -291,12 +266,12 @@ const dashboardhelper = function () { totaltime_hrs: (timeEntryByPerson[teamMember._id.toString()]?.totalSeconds ?? 0) / 3600, percentagespentintangible: - timeEntryByPerson[teamMember._id.toString()] - && timeEntryByPerson[teamMember._id.toString()]?.totalSeconds !== 0 - && timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds !== 0 - ? (timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds - / timeEntryByPerson[teamMember._id.toString()]?.totalSeconds) - * 100 + timeEntryByPerson[teamMember._id.toString()] && + timeEntryByPerson[teamMember._id.toString()]?.totalSeconds !== 0 && + timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds !== 0 + ? ((timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds || 0) / + (timeEntryByPerson[teamMember._id.toString()]?.totalSeconds || 1)) * + 100 : 0, timeOffFrom: teamMember.timeOffFrom || null, timeOffTill: teamMember.timeOffTill || null, @@ -582,15 +557,9 @@ const dashboardhelper = function () { */ const getUserLaborData = async function (userId) { try { - const pdtStart = moment() - .tz('America/Los_Angeles') - .startOf('week') - .format('YYYY-MM-DD'); + 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'); + const pdtEnd = moment().tz('America/Los_Angeles').endOf('week').format('YYYY-MM-DD'); const user = await userProfile.findById({ _id: userId, @@ -602,6 +571,7 @@ const dashboardhelper = function () { $lte: pdtEnd, }, entryType: { $in: ['default', null] }, + isActive: { $ne: false }, personId: userId, }); @@ -627,8 +597,7 @@ const dashboardhelper = function () { totaltime_hrs: (tangibleSeconds + intangibleSeconds) / 3600, totaltangibletime_hrs: tangibleSeconds / 3600, totalintangibletime_hrs: intangibleSeconds / 3600, - percentagespentintangible: - (intangibleSeconds / tangibleSeconds) * 100, + percentagespentintangible: (intangibleSeconds / tangibleSeconds) * 100, timeOffFrom: user.timeOffFrom, timeOffTill: user.timeOffTill, endDate: user.endDate || null, @@ -745,10 +714,7 @@ const dashboardhelper = function () { { $not: [ { - $in: [ - '$$timeentry.entryType', - ['person', 'team', 'project'], - ], + $in: ['$$timeentry.entryType', ['person', 'team', 'project']], }, ], }, @@ -832,10 +798,7 @@ const dashboardhelper = function () { { $not: [ { - $in: [ - '$$timeentry.entryType', - ['person', 'team', 'project'], - ], + $in: ['$$timeentry.entryType', ['person', 'team', 'project']], }, ], }, diff --git a/src/helpers/helperModels/userProjects.js b/src/helpers/helperModels/userProjects.js deleted file mode 100644 index a2f1f2b5e..000000000 --- a/src/helpers/helperModels/userProjects.js +++ /dev/null @@ -1,17 +0,0 @@ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; - -const ProjectSchema = new Schema({ - projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'projects' }, - projectName: { type: String }, - category: { type: String }, -}); - -const userProjectSchema = new Schema({ - - _id: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, - projects: [ProjectSchema], -}); - -module.exports = mongoose.model('userProject', userProjectSchema, 'userProjects'); diff --git a/src/helpers/notificationhelper.js b/src/helpers/notificationhelper.js index 8742bc7d1..a061c80e9 100644 --- a/src/helpers/notificationhelper.js +++ b/src/helpers/notificationhelper.js @@ -1,51 +1,56 @@ -const Notification = require('../models/notification'); -const eventtypes = require('../constants/eventTypes'); -const notificationController = require('../controllers/notificationController')(Notification); -const userhelper = require('./userHelper')(); +/** + * Unused legacy code. Commented out to avoid confusion. Will delete in the next cycle. + * Commented by: Shengwei Peng + * Date: 2024-03-22 + */ +// const Notification = require('../models/notification'); +// const eventtypes = require('../constants/eventTypes'); +// const notificationController = require('../controllers/notificationController')(Notification); +// const userhelper = require('./userHelper')(); -const notificationhelper = function () { - const createnotification = function (notification) { - notificationController.createUserNotification(notification); - }; +// const notificationhelper = function () { + // const createnotification = function (notification) { + // notificationController.createUserNotification(notification); + // }; - const notificationcreated = function (requestor, assignedTo, description) { - userhelper.getUserName(requestor) - .then((result) => { - const notification = new Notification(); - notification.recipient = assignedTo; - notification.eventType = eventtypes.ActionCreated; - notification.message = `New action item ${description} created by ${result.firstName} ${result.lastName}`; - createnotification(notification); - }); - }; + // const notificationcreated = function (requestor, assignedTo, description) { + // userhelper.getUserName(requestor) + // .then((result) => { + // const notification = new Notification(); + // notification.recipient = assignedTo; + // notification.eventType = eventtypes.ActionCreated; + // notification.message = `New action item ${description} created by ${result.firstName} ${result.lastName}`; + // createnotification(notification); + // }); + // }; - const notificationedited = function (requestor, assignedTo, olddescription, newdescription) { - userhelper.getUserName(requestor) - .then((result) => { - const notification = new Notification(); - notification.recipient = assignedTo; - notification.eventType = eventtypes.ActionEdited; - notification.message = `Your action item was edited by ${result.firstName} ${result.lastName}. The old value was ${olddescription}. The new value is ${newdescription}`; - createnotification(notification); - }); - }; + // const notificationedited = function (requestor, assignedTo, olddescription, newdescription) { + // userhelper.getUserName(requestor) + // .then((result) => { + // const notification = new Notification(); + // notification.recipient = assignedTo; + // notification.eventType = eventtypes.ActionEdited; + // notification.message = `Your action item was edited by ${result.firstName} ${result.lastName}. The old value was ${olddescription}. The new value is ${newdescription}`; + // createnotification(notification); + // }); + // }; - const notificationdeleted = function (requestor, assignedTo, description) { - userhelper.getUserName(requestor) - .then((result) => { - const notification = new Notification(); - notification.recipient = assignedTo; - notification.eventType = eventtypes.ActionDeleted; - notification.message = `Your action item ${description} was deleted by ${result.firstName} ${result.lastName}.`; - createnotification(notification); - }); - }; + // const notificationdeleted = function (requestor, assignedTo, description) { + // userhelper.getUserName(requestor) + // .then((result) => { + // const notification = new Notification(); + // notification.recipient = assignedTo; + // notification.eventType = eventtypes.ActionDeleted; + // notification.message = `Your action item ${description} was deleted by ${result.firstName} ${result.lastName}.`; + // createnotification(notification); + // }); + // }; - return { - notificationcreated, - notificationedited, - notificationdeleted, - }; -}; + // return { + // notificationcreated, + // notificationedited, + // notificationdeleted, +// }; +// }; -module.exports = notificationhelper; +// module.exports = notificationhelper; diff --git a/src/helpers/taskHelper.js b/src/helpers/taskHelper.js index e892a562a..34fb36be8 100644 --- a/src/helpers/taskHelper.js +++ b/src/helpers/taskHelper.js @@ -5,12 +5,12 @@ const timeentry = require('../models/timeentry'); const team = require('../models/team'); const Task = require('../models/task'); const TaskNotification = require('../models/taskNotification'); +const { hasPermission } = require('../utilities/permissions'); const taskHelper = function () { const getTasksForTeams = async function (userId, requestor) { const userid = mongoose.Types.ObjectId(userId); const requestorId = mongoose.Types.ObjectId(requestor.requestorId); - const requestorRole = requestor.role; try { const userById = await userProfile.findOne( { _id: userid, isActive: true }, @@ -21,8 +21,11 @@ const taskHelper = function () { isVisible: 1, weeklycommittedHours: 1, weeklySummaries: 1, + weeklySummaryOption: 1, timeOffFrom: 1, timeOffTill: 1, + teamCode: 1, + teams: 1, adminLinks: 1, }, ); @@ -30,41 +33,40 @@ const taskHelper = function () { if (userById === null) return null; const userRole = userById.role; - 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'); + 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'); let teamMemberIds = [userid]; let teamMembers = []; - const isRequestorOwnerLike = [ - 'Administrator', - 'Owner', - 'Core Team', - ].includes(requestorRole); - const isUserOwnerLike = ['Administrator', 'Owner', 'Core Team'].includes( - userRole, - ); + const isRequestorOwnerLike = await hasPermission(requestor, 'seeUsersInDashboard'); + const userAsRequestor = { role: userRole, requestorId: userId }; + const isUserOwnerLike = await hasPermission(userAsRequestor, 'seeUsersInDashboard'); switch (true) { case isRequestorOwnerLike && isUserOwnerLike: { - teamMembers = await userProfile.find( - { isActive: true }, - { - role: 1, - firstName: 1, - lastName: 1, - weeklycommittedHours: 1, - timeOffFrom: 1, - timeOffTill: 1, - adminLinks: 1, - }, - ); + teamMembers = await userProfile + .find( + { isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + weeklySummaryOption: 1, + timeOffFrom: 1, + timeOffTill: 1, + teamCode: 1, + teams: 1, + adminLinks: 1, + }, + ) + .populate([ + { + path: 'teams', + select: 'teamName', + }, + ]); break; } case isRequestorOwnerLike && !isUserOwnerLike: { @@ -79,18 +81,28 @@ const taskHelper = function () { }); }); - teamMembers = await userProfile.find( - { _id: { $in: teamMemberIds }, isActive: true }, - { - role: 1, - firstName: 1, - lastName: 1, - weeklycommittedHours: 1, - timeOffFrom: 1, - timeOffTill: 1, - adminLinks: 1, - }, - ); + teamMembers = await userProfile + .find( + { _id: { $in: teamMemberIds }, isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + weeklySummaryOption: 1, + timeOffFrom: 1, + timeOffTill: 1, + teamCode: 1, + teams: 1, + adminLinks: 1, + }, + ) + .populate([ + { + path: 'teams', + select: 'teamName', + }, + ]); break; } default: { @@ -105,18 +117,28 @@ const taskHelper = function () { }); }); - teamMembers = await userProfile.find( - { _id: { $in: teamMemberIds }, isActive: true }, - { - role: 1, - firstName: 1, - lastName: 1, - weeklycommittedHours: 1, - timeOffFrom: 1, - timeOffTill: 1, - adminLinks: 1, - }, - ); + teamMembers = await userProfile + .find( + { _id: { $in: teamMemberIds }, isActive: true }, + { + role: 1, + firstName: 1, + lastName: 1, + weeklycommittedHours: 1, + weeklySummaryOption: 1, + timeOffFrom: 1, + timeOffTill: 1, + teamCode: 1, + teams: 1, + adminLinks: 1, + }, + ) + .populate([ + { + path: 'teams', + select: 'teamName', + }, + ]); } } @@ -128,6 +150,7 @@ const taskHelper = function () { $lte: pdtend, }, personId: { $in: teamMemberIds }, + isActive: { $ne: false }, }); const timeEntryByPerson = {}; @@ -141,8 +164,7 @@ const taskHelper = function () { }; } if (timeEntry.isTangible) { - timeEntryByPerson[personIdStr].tangibleSeconds - += timeEntry.totalSeconds; + timeEntryByPerson[personIdStr].tangibleSeconds += timeEntry.totalSeconds; } timeEntryByPerson[personIdStr].totalSeconds += timeEntry.totalSeconds; }); @@ -165,13 +187,9 @@ const taskHelper = function () { const taskNdUserID = `${taskIdStr},${userIdStr}`; if (taskNotificationByTaskNdUser[taskNdUserID]) { - taskNotificationByTaskNdUser[taskNdUserID].push( - teamMemberTaskNotification, - ); + taskNotificationByTaskNdUser[taskNdUserID].push(teamMemberTaskNotification); } else { - taskNotificationByTaskNdUser[taskNdUserID] = [ - teamMemberTaskNotification, - ]; + taskNotificationByTaskNdUser[taskNdUserID] = [teamMemberTaskNotification]; } }); @@ -185,7 +203,11 @@ const taskHelper = function () { teamMemberTask.resources.forEach((resource) => { const resourceIdStr = resource.userID?.toString(); const taskNdUserID = `${taskIdStr},${resourceIdStr}`; - _teamMemberTask.taskNotifications = taskNotificationByTaskNdUser[taskNdUserID] || []; + // initialize taskNotifications if not exists + if (!_teamMemberTask.taskNotifications) _teamMemberTask.taskNotifications = []; + // push all notifications into the list if taskNdUserId key exists + if (taskNotificationByTaskNdUser[taskNdUserID]) + _teamMemberTask.taskNotifications.push(...taskNotificationByTaskNdUser[taskNdUserID]); if (taskByPerson[resourceIdStr]) { taskByPerson[resourceIdStr].push(_teamMemberTask); } else { @@ -196,20 +218,22 @@ const taskHelper = function () { const teamMemberTasksData = []; teamMembers.forEach((teamMember) => { + const timeEntry = timeEntryByPerson[teamMember._id.toString()]; + const tangible = timeEntry?.tangibleSeconds || 0; + const total = timeEntry?.totalSeconds || 0; const obj = { personId: teamMember._id, role: teamMember.role, name: `${teamMember.firstName} ${teamMember.lastName}`, weeklycommittedHours: teamMember.weeklycommittedHours, - totaltangibletime_hrs: - timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds - / 3600 || 0, - totaltime_hrs: - timeEntryByPerson[teamMember._id.toString()]?.totalSeconds / 3600 - || 0, + weeklySummaryOption: teamMember.weeklySummaryOption || null, + totaltangibletime_hrs: tangible / 3600, + totaltime_hrs: total / 3600, tasks: taskByPerson[teamMember._id.toString()] || [], timeOffFrom: teamMember.timeOffFrom || null, timeOffTill: teamMember.timeOffTill || null, + teamCode: teamMember.teamCode || null, + teams: teamMember.teams || null, adminLinks: teamMember.adminLinks || null, }; teamMemberTasksData.push(obj); @@ -505,14 +529,8 @@ const taskHelper = function () { // ]); }; const getTasksForSingleUser = function (userId) { - 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'); + 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 userProfile.aggregate([ { $match: { @@ -573,6 +591,9 @@ const taskHelper = function () { { $in: ['$$timeentry.entryType', ['default', null]], }, + { + $ne: ['$$timeentry.isActive', false], + }, ], }, }, @@ -672,10 +693,15 @@ const taskHelper = function () { { $project: { tasks: { - resources: { - profilePic: 0, + $filter: { + input: '$tasks', + as: 'task', + cond: { + $ne: ['$$task.isActive', false], + }, }, }, + 'tasks.resources.profilePic': 0, }, }, { diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 13781fcee..493270170 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -1,6 +1,15 @@ /* 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'); @@ -16,6 +25,9 @@ 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) @@ -94,6 +106,7 @@ const userHelper = function () { timeRemaining, coreTeamExtraHour, requestForTimeOffEmailBody, + administrativeContent, ) { let finalParagraph = ''; @@ -105,16 +118,54 @@ const userHelper = function () { timeRemaining + coreTeamExtraHour } 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.`; } - + // 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')) { + 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: ${infringement.date}

\ -

Description: ${requestForTimeOffEmailBody || infringement.description}

+

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

\ +

Description: ${emailDescription}

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

${finalParagraph} -

Thank you, One Community

`; +

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; }; @@ -239,6 +290,7 @@ const userHelper = function () { Name: ${firstName} ${lastName}

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

@@ -353,10 +405,24 @@ const userHelper = function () { { 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]; @@ -392,6 +458,32 @@ const userHelper = function () { 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 hour logged + * + * Notes: + * 1. Start date is automatically updated upon frist 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) { + isNewUser = true; + } + + if ( + userStartDate.isAfter(pdtEndOfLastWeek) || + (userStartDate.isAfter(pdtStartOfLastWeek) && + userStartDate.isBefore(pdtEndOfLastWeek) && + timeUtils.getDayOfWeekStringFromUTC(person.startDate) > 1) + ) { + isNewUser = true; + } + const updateResult = await userProfile.findByIdAndUpdate( personId, { @@ -431,7 +523,8 @@ const userHelper = function () { break; } } - + // use histroy Infringements to align the highlight requirements + let historyInfringements = 'No Previous Infringements.'; if (oldInfringements.length) { userProfile.findByIdAndUpdate( personId, @@ -442,6 +535,57 @@ const userHelper = function () { }, { 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')) { + 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. @@ -467,13 +611,13 @@ const userHelper = function () { // eslint-disable-next-line prefer-destructuring requestForTimeOff = requestsForTimeOff[0]; requestForTimeOffStartingDate = moment(requestForTimeOff.startingDate).format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', ); requestForTimeOffEndingDate = moment(requestForTimeOff.endingDate).format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', ); requestForTimeOffreason = requestForTimeOff.reason; - requestForTimeOffEmailBody = `You had scheduled time off From ${requestForTimeOffStartingDate}, To ${requestForTimeOffEndingDate}, due to: ${requestForTimeOffreason}`; + requestForTimeOffEmailBody = `You had scheduled time off From ${requestForTimeOffStartingDate}, To ${requestForTimeOffEndingDate}, due to: ${requestForTimeOffreason}`; } if (timeNotMet || !hasWeeklySummary) { @@ -482,9 +626,9 @@ const userHelper = function () { } 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 YYYY-MM-DD', + 'dddd M-D-YYYY', )} and ending ${pdtEndOfLastWeek.format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ person.weeklycommittedHours } hours + ${ @@ -500,15 +644,15 @@ const userHelper = function () { 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 YYYY-MM-DD', - )} and ending ${pdtEndOfLastWeek.format('dddd YYYY-MM-DD')}.`; + '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 YYYY-MM-DD', + 'dddd M-D-YYYY', )} and ending ${pdtEndOfLastWeek.format( - 'dddd YYYY-MM-DD', + 'dddd M-D-YYYY', )}, you logged ${timeSpent.toFixed(2)} hours against a committed effort of ${ user.weeklycommittedHours } hours + ${ @@ -524,13 +668,13 @@ const userHelper = function () { 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 YYYY-MM-DD', - )} and ending ${pdtEndOfLastWeek.format('dddd YYYY-MM-DD')}.`; + '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 YYYY-MM-DD', - )} and ending ${pdtEndOfLastWeek.format('dddd YYYY-MM-DD')}.`; + 'dddd M-D-YYYY', + )} and ending ${pdtEndOfLastWeek.format('dddd M-D-YYYY')}.`; } const infringement = { @@ -658,9 +802,24 @@ const userHelper = function () { } } } + 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); } @@ -862,13 +1021,81 @@ const userHelper = function () { } }; - const notifyInfringements = function (original, current, firstName, lastName, emailAddress) { + 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')) { + 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), ); @@ -876,8 +1103,16 @@ const userHelper = function () { emailSender( emailAddress, 'New Infringement Assigned', - getInfringementEmailBody(firstName, lastName, element, totalInfringements), - + getInfringementEmailBody( + firstName, + lastName, + element, + totalInfringements, + undefined, + undefined, + undefined, + administrativeContent, + ), null, 'onecommunityglobal@gmail.com', emailAddress, @@ -1184,6 +1419,44 @@ const userHelper = function () { } }); }; + + 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) { @@ -1204,17 +1477,20 @@ const userHelper = function () { } } 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 >= 1 && - user.lastWeekTangibleHrs === user.personalBestMaxHrs + user.lastWeekTangibleHrs >= user.personalBestMaxHrs && + !badgeOfType.earnedDate.includes(currentDate) + ) { if (badgeOfType) { - changeBadgeCount( + increaseBadgeCount( personId, - mongoose.Types.ObjectId(badgeOfType._id), - user.personalBestMaxHrs, + 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); } @@ -1252,9 +1528,8 @@ const userHelper = function () { } }; - // 'X Hours for X Week Streak', - const checkXHrsForXWeeks = async function (personId, user, badgeCollection) { - // Handle Increasing the 1 week streak badges + // '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') { @@ -1284,6 +1559,13 @@ const userHelper = function () { return true; }); }); + }; + + // 'X Hours for X Week Streak', + const checkXHrsForXWeeks = async function (personId, user, badgeCollection) { + // Handle Increasing the 1 week streak badges + await checkXHrsInOneWeek(personId, user, badgeCollection); + // Check each Streak Greater than One to check if it works await badge .aggregate([ @@ -1522,7 +1804,8 @@ const userHelper = function () { 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); @@ -1608,11 +1891,15 @@ const userHelper = function () { } }; - /* Function for deleting expired tokens used in new user setup from database */ + // 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 currentDate = new Date(); + const ninetyDaysAgo = moment().subtract(90, 'days').toDate(); try { - await token.deleteMany({ expiration: { $lt: currentDate } }); + await token.deleteMany({ isCancelled: true, expiration: { $lt: ninetyDaysAgo } }); } catch (error) { logger.logException(error); } diff --git a/src/models/bmdashboard/buildingInventoryItem.js b/src/models/bmdashboard/buildingInventoryItem.js index 5e0028968..226e3e328 100644 --- a/src/models/bmdashboard/buildingInventoryItem.js +++ b/src/models/bmdashboard/buildingInventoryItem.js @@ -44,7 +44,8 @@ const largeItemBaseSchema = mongoose.Schema({ // actual purchases (once there is a system) may need their own subdoc // subdoc may contain below purchaseStatus and rental fields // for now they have default dummy values - purchaseStatus: { type: String, enum: ['Rental', 'Purchase'], default: 'Rental' }, + purchaseStatus: { type: String, enum: ['Rental', 'Purchase','Needed', 'Purchased'], default: 'Rental' }, + condition: { type: String, enum: ['Like New', 'Good', 'Worn', 'Lost', 'Needs Repair', 'Needs Replacing'], default: 'Like New'}, // TODO: rental fields should be required if purchaseStatus === "Rental" rentedOnDate: { type: Date, default: Date.now() }, rentalDueDate: { type: Date, default: new Date(Date.now() + (3600 * 1000 * 24 * 14)) }, @@ -73,6 +74,7 @@ const largeItemBaseSchema = mongoose.Schema({ responsibleUser: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, type: { type: String, enum: ['Check In', 'Check Out'] }, }], + userResponsible: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, //new field }); const largeItemBase = mongoose.model('largeItemBase', largeItemBaseSchema, 'buildingInventoryItems'); @@ -126,6 +128,10 @@ const buildingReusable = smallItemBase.discriminator('reusable_item', new mongoo const buildingTool = largeItemBase.discriminator('tool_item', new mongoose.Schema({ // TODO: add function to create simple numeric code for on-site tool tracking code: { type: String, default: '001' }, + purchaseStatus: { + type: String, + enum: ['Needed', 'Purchased'] // Override enum values +} })); //----------------- @@ -138,11 +144,8 @@ const buildingTool = largeItemBase.discriminator('tool_item', new mongoose.Schem // ex: tractors, excavators, bulldozers const buildingEquipment = largeItemBase.discriminator('equipment_item', new mongoose.Schema({ - // isTracked: { type: Boolean, required: true }, // has asset tracker - // // assetTracker: { type: String, required: () => this.isTracked }, // required if isTracked = true (syntax?) - // assetTracker: { type: String, - // required() { return this.prop }, - // } + isTracked: { type: Boolean, required: true }, // has asset tracker + assetTracker: { type: String, required: () => this.isTracked }, // required if isTracked = true (syntax?) })); module.exports = { diff --git a/src/models/bmdashboard/buildingInventoryType.js b/src/models/bmdashboard/buildingInventoryType.js index 9173bf5cc..fdd728029 100644 --- a/src/models/bmdashboard/buildingInventoryType.js +++ b/src/models/bmdashboard/buildingInventoryType.js @@ -1,7 +1,6 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; - //--------------------------- // BASE INVENTORY TYPE SCHEMA //--------------------------- @@ -13,7 +12,7 @@ const invTypeBaseSchema = new Schema({ name: { type: String, required: true }, description: { type: String, required: true, maxLength: 150 }, imageUrl: String, - createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfiles' }, + createdBy: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, }); const invTypeBase = mongoose.model('invTypeBase', invTypeBaseSchema, 'buildingInventoryTypes'); @@ -60,7 +59,13 @@ const reusableType = invTypeBase.discriminator('reusable_type', new mongoose.Sch const toolType = invTypeBase.discriminator('tool_type', new mongoose.Schema({ category: { type: String, enum: ['Tool'] }, isPowered: { type: Boolean, required: true }, - powerSource: { type: String, required: () => this.isPowered }, // required if isPowered = true (syntax?) + powerSource: { type: String, required: function() { + return this.isPowered; // required if isPowered = true + }, +}, + available: [{type: mongoose.SchemaTypes.ObjectId, ref: 'tool_item'}], + using: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'tool_item' }], + //add a date last updated field? })); //--------------------------- diff --git a/src/models/notification.js b/src/models/notification.js index f75af20e8..d2dfd2e33 100644 --- a/src/models/notification.js +++ b/src/models/notification.js @@ -2,13 +2,49 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; +/** + * This model intends to store notifications for users. This will be fetched and displayed + * when the user is on dashboard page. Currently, the notifications are not sent to the user + * in real time. The user has to visit the dashboard to fetch the notifications. + * + * There are two types of notifications: + * 1. System generated notifications + * - Notifications generated by the system or shceduled tasks (Note: system generated + * notifications will find a owner user and set it as the sender of the notification) + * 2. User generated notifications + * - Notifications generated by users (admin and owner) + * */ + const notificationSchema = new Schema({ message: { type: String, required: true }, - recipient: { type: Schema.Types.ObjectId, ref: 'userProfile' }, + sender: { type: Schema.Types.ObjectId, ref: 'userProfile', required: true }, + recipient: { type: Schema.Types.ObjectId, ref: 'userProfile', required: true }, + isSystemGenerated: { type: Boolean, default: true }, isRead: { type: Boolean, default: false }, - eventType: { type: String }, + createdTimeStamps: { type: Date, default: Date.now }, + +}, { optimisticConcurrency: true }); // Enable optimistic concurrency control + +// Populate sender and recipient info +notificationSchema.virtual('senderInfo', { + ref: 'userProfile', + localField: 'sender', + foreignField: '_id', + justOne: true, + options: { select: 'firstName lastName email' }, +}); +notificationSchema.virtual('recipientInfo', { + ref: 'userProfile', + localField: 'recipient', + foreignField: '_id', + justOne: true, + options: { select: 'firstName lastName email' }, }); + +notificationSchema.index({ recipient: 1, createdTimeStamps: 1, isRead: 1 }); +notificationSchema.index({ sender: 1, createdTimeStamps: 1 }); + module.exports = mongoose.model('notification', notificationSchema, 'notifications'); diff --git a/src/models/profileInitialSetupToken.js b/src/models/profileInitialSetupToken.js index 77b830229..f8295fee7 100644 --- a/src/models/profileInitialSetupToken.js +++ b/src/models/profileInitialSetupToken.js @@ -19,6 +19,25 @@ const profileInitialSetupTokenSchema = new mongoose.Schema({ type: Date, required: true, }, + // New fields added to the schema + createdDate: { + type: Date, + required: true, + default: Date.now(), + }, + isCancelled: { + type: Boolean, + required: true, + default: false, + }, + isSetupCompleted: { + type: Boolean, + required: true, + default: false, + }, }); module.exports = mongoose.model('profileInitialSetupToken', profileInitialSetupTokenSchema, 'profileInitialSetupToken'); +// Table indexes +profileInitialSetupTokenSchema.index({ createdDate: -1 }, { token: 1 }); +profileInitialSetupTokenSchema.index({ isSetupCompleted: -1, createdDate: -1 }); diff --git a/src/models/project.js b/src/models/project.js index 6a78a0b31..1ef8b269e 100644 --- a/src/models/project.js +++ b/src/models/project.js @@ -5,9 +5,24 @@ const { Schema } = mongoose; const projectschema = new Schema({ projectName: { type: String, required: true, unique: true }, isActive: { type: Boolean, default: true }, + isArchived: { type: Boolean, default: false }, createdDatetime: { type: Date }, modifiedDatetime: { type: Date, default: Date.now() }, - category: { type: String, enum: ['Food', 'Energy', 'Housing', 'Education', 'Society', 'Economics', 'Stewardship', 'Other', 'Unspecified'], default: 'Other' }, + category: { + type: String, + enum: [ + 'Food', + 'Energy', + 'Housing', + 'Education', + 'Society', + 'Economics', + 'Stewardship', + 'Other', + 'Unspecified', + ], + default: 'Other', + }, }); module.exports = mongoose.model('project', projectschema, 'projects'); diff --git a/src/models/timeentry.js b/src/models/timeentry.js index 4ae0b94fa..ea5303b3a 100644 --- a/src/models/timeentry.js +++ b/src/models/timeentry.js @@ -15,6 +15,7 @@ const TimeEntry = new Schema({ isTangible: { type: Boolean, default: false }, createdDateTime: { type: Date }, lastModifiedDateTime: { type: Date, default: Date.now }, + isActive: { type: Boolean, default: true }, }); module.exports = mongoose.model('timeEntry', TimeEntry, 'timeEntries'); diff --git a/src/models/title.js b/src/models/title.js new file mode 100644 index 000000000..64b9aed92 --- /dev/null +++ b/src/models/title.js @@ -0,0 +1,21 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const title = new Schema({ + titleName: { type: String, required: true }, + teamCode: { type: String, require: true }, + projectAssigned: { + projectName: { type: String, required: true }, + _id: { type: String, required: true }, + }, + mediaFolder: { type: String, require: true }, + teamAssiged: { + teamName: { type: String }, + _id: { type: String }, + }, + shortName: { type: String, require: true }, + +}); + +module.exports = mongoose.model('title', title, 'titles'); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 1120aa697..2f082a36d 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -6,8 +6,10 @@ const validate = require('mongoose-validator'); const bcrypt = require('bcryptjs'); const SALT_ROUNDS = 10; -const nextDay = new Date(); -nextDay.setDate(nextDay.getDate() + 1); +// Update createdDate to be the current date from the next day +// const nextDay = new Date(); +// nextDay.setDate(nextDay.getDate() + 1); +const today = new Date(); const userProfileSchema = new Schema({ password: { @@ -47,7 +49,7 @@ const userProfileSchema = new Schema({ index: true, }, phoneNumber: [{ type: String, phoneNumber: String }], - jobTitle: [{ type: String, jobTitle: String }], + jobTitle: [{ type: String, jobTitle: String, required: true }], bio: { type: String }, email: { type: String, diff --git a/src/models/wbs.js b/src/models/wbs.js index 73f9fd413..bcfbab074 100644 --- a/src/models/wbs.js +++ b/src/models/wbs.js @@ -3,13 +3,11 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; const wbsschema = new Schema({ - wbsName: { type: String, required: true }, projectId: { type: mongoose.SchemaTypes.ObjectId, ref: 'project' }, isActive: { type: Boolean, default: true }, createdDatetime: { type: Date }, modifiedDatetime: { type: Date, default: Date.now() }, - }); module.exports = mongoose.model('wbs', wbsschema, 'wbs'); diff --git a/src/routes/actionItemRouter.js b/src/routes/actionItemRouter.js index 807dddeac..606d0dcba 100644 --- a/src/routes/actionItemRouter.js +++ b/src/routes/actionItemRouter.js @@ -1,20 +1,25 @@ -const express = require('express'); +/** + * Unused legacy code. Commented out to avoid confusion. Will delete in the next cycle. + * Commented by: Shengwei Peng + * Date: 2024-03-22 + */ +// const express = require('express'); -const routes = function (actionItem) { - const controller = require('../controllers/actionItemController')(actionItem); - const actionItemRouter = express.Router(); +// const routes = function (actionItem) { +// const controller = require('../controllers/actionItemController')(actionItem); +// const actionItemRouter = express.Router(); - actionItemRouter.route('/actionItem') - .post(controller.postactionItem); +// actionItemRouter.route('/actionItem') +// .post(controller.postactionItem); - actionItemRouter.route('/actionItem/user/:userId') - .get(controller.getactionItem); +// actionItemRouter.route('/actionItem/user/:userId') +// .get(controller.getactionItem); - actionItemRouter.route('/actionItem/:actionItemId') - .delete(controller.deleteactionItem) - .put(controller.editactionItem); +// actionItemRouter.route('/actionItem/:actionItemId') +// .delete(controller.deleteactionItem) +// .put(controller.editactionItem); - return actionItemRouter; -}; +// return actionItemRouter; +// }; -module.exports = routes; +// module.exports = routes; diff --git a/src/routes/badgeRouter.js b/src/routes/badgeRouter.js index c9dc6ee7b..3f3c8b892 100644 --- a/src/routes/badgeRouter.js +++ b/src/routes/badgeRouter.js @@ -4,7 +4,7 @@ const routes = function (badge) { const controller = require('../controllers/badgeController')(badge); const badgeRouter = express.Router(); - + badgeRouter.route('/badge') .get(controller.getAllBadges) .post(controller.postBadge); diff --git a/src/routes/badgeRouter.test.js b/src/routes/badgeRouter.test.js index e9b47be08..377f6bd00 100644 --- a/src/routes/badgeRouter.test.js +++ b/src/routes/badgeRouter.test.js @@ -8,6 +8,7 @@ const { mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, } = require('../test'); const Badge = require('../models/badge'); +const UserProfile = require('../models/userProfile'); const agent = request.agent(app); @@ -45,7 +46,13 @@ describe('actionItem routes', () => { }; // create 2 roles. One with permission and one without - await createRole('Administrator', ['createBadges', 'seeBadges', 'assignBadges']); + await createRole('Administrator', [ + 'createBadges', + 'seeBadges', + 'assignBadges', + 'deleteBadges', + 'updateBadges', + ]); await createRole('Volunteer', []); }); @@ -63,6 +70,8 @@ describe('actionItem routes', () => { await agent.post('/api/badge').send(reqBody).expect(401); await agent.get('/api/badge').send(reqBody).expect(401); await agent.put(`/api/badge/assign/randomId`).send(reqBody).expect(401); + await agent.delete('/api/badge/randomid').send(reqBody).expect(401); + await agent.put('/api/badge/randomid').send(reqBody).expect(401); }); it('Should return 404 if the route does not exist', async () => { @@ -73,6 +82,16 @@ describe('actionItem routes', () => { .set('Authorization', adminToken) .send(reqBody) .expect(404); + await agent + .delete(`/api/badges/randomId`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(404); + await agent + .put(`/api/badges/randomId`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(404); }); }); @@ -246,4 +265,155 @@ describe('actionItem routes', () => { expect(response.text).toBe(JSON.stringify(volunteerUser._id)); }); }); + + describe('delete badge route', () => { + it('should return 403 if the user does not have permission', async () => { + const response = await agent + .delete(`/api/badge/${adminUser._id}`) + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + + expect(response.body).toEqual({ error: 'You are not authorized to delete badges.' }); + }); + + it('Should return 400 if no badge was found', async () => { + const response = await agent + .delete(`/api/badge/${adminUser._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + + expect(response.body).toEqual({ error: 'No valid records found' }); + }); + + it('Should return 200 if all is successful', async () => { + // create a new badge to be removed + const _badge = new Badge(); + + _badge.badgeName = reqBody.badgeName; + _badge.category = reqBody.category; + _badge.multiple = reqBody.multiple; + _badge.totalHrs = reqBody.totalHrs; + _badge.weeks = reqBody.weeks; + _badge.months = reqBody.months; + _badge.people = reqBody.people; + _badge.project = reqBody.project; + _badge.imageUrl = reqBody.imageUrl; + _badge.ranking = reqBody.ranking; + _badge.description = reqBody.description; + _badge.showReport = reqBody.showReport; + _badge.type = reqBody.type; + + const badge = await _badge.save(); + + const response = await agent + .delete(`/api/badge/${badge._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toEqual({ + message: 'Badge successfully deleted and user profiles updated', + }); + }); + + it('Should remove all instasnces of the badge from user profiles', async () => { + const _badge = new Badge(); + + _badge.badgeName = reqBody.badgeName; + _badge.category = reqBody.category; + _badge.multiple = reqBody.multiple; + _badge.totalHrs = reqBody.totalHrs; + _badge.weeks = reqBody.weeks; + _badge.months = reqBody.months; + _badge.people = reqBody.people; + _badge.project = reqBody.project; + _badge.imageUrl = reqBody.imageUrl; + _badge.ranking = reqBody.ranking; + _badge.description = reqBody.description; + _badge.showReport = reqBody.showReport; + _badge.type = reqBody.type; + + const badge = await _badge.save(); + const date = new Date(); + adminUser.badgeCollection = [ + { + badge: badge._id, + count: 1, + earnedDate: [date], + lastModified: date, + hasBadgeDeletionImpact: false, + featured: false, + }, + ]; + + await adminUser.save(); + + expect(adminUser.badgeCollection.length).toBe(1); + + const response = await agent + .delete(`/api/badge/${badge._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toEqual({ + message: 'Badge successfully deleted and user profiles updated', + }); + const newAdminProfile = await UserProfile.findById(adminUser._id); + expect(newAdminProfile.badgeCollection.length).toBe(0); + }); + }); + + describe('update badge route', () => { + it('Should return 403 if the user does not have permission', async () => { + const response = await agent + .put(`/api/badge/${adminUser._id}`) + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + + expect(response.body).toEqual({ error: 'You are not authorized to update badges.' }); + }); + + it('Should return 400 if no badge is found', async () => { + const response = await agent + .put(`/api/badge/${adminUser._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + + expect(response.body).toEqual({ error: 'No valid records found' }); + }); + + it('Should return 200 if all is successful', async () => { + // create badge to be modified + const _badge = new Badge(); + + _badge.badgeName = reqBody.badgeName; + _badge.category = reqBody.category; + _badge.multiple = reqBody.multiple; + _badge.totalHrs = reqBody.totalHrs; + _badge.weeks = reqBody.weeks; + _badge.months = reqBody.months; + _badge.people = reqBody.people; + _badge.project = reqBody.project; + _badge.imageUrl = reqBody.imageUrl; + _badge.ranking = reqBody.ranking; + _badge.description = reqBody.description; + _badge.showReport = reqBody.showReport; + _badge.type = reqBody.type; + + const badge = await _badge.save(); + + const response = await agent + .put(`/api/badge/${badge._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toEqual({ message: 'Badge successfully updated' }); + }); + }); }); diff --git a/src/routes/bmdashboard/bmConsumablesRouter.js b/src/routes/bmdashboard/bmConsumablesRouter.js index 2f3effabb..5a28b7c09 100644 --- a/src/routes/bmdashboard/bmConsumablesRouter.js +++ b/src/routes/bmdashboard/bmConsumablesRouter.js @@ -12,6 +12,9 @@ const routes = function (BuildingConsumable) { controller.bmPurchaseConsumables, ); + BuildingConsumableController.route('/updateConsumablesRecord') + .post(controller.bmPostConsumableUpdateRecord); + return BuildingConsumableController; }; diff --git a/src/routes/bmdashboard/bmEquipmentRouter.js b/src/routes/bmdashboard/bmEquipmentRouter.js index 8ec970767..111d50f77 100644 --- a/src/routes/bmdashboard/bmEquipmentRouter.js +++ b/src/routes/bmdashboard/bmEquipmentRouter.js @@ -2,7 +2,9 @@ const express = require('express'); const routes = function (BuildingEquipment) { const equipmentRouter = express.Router(); - const controller = require('../../controllers/bmdashboard/bmEquipmentController')(BuildingEquipment); + const controller = require('../../controllers/bmdashboard/bmEquipmentController')( + BuildingEquipment, + ); equipmentRouter.route('/equipment/:equipmentId').get(controller.fetchSingleEquipment); diff --git a/src/routes/bmdashboard/bmInventoryTypeRouter.js b/src/routes/bmdashboard/bmInventoryTypeRouter.js index 0592c8702..3d940ac61 100644 --- a/src/routes/bmdashboard/bmInventoryTypeRouter.js +++ b/src/routes/bmdashboard/bmInventoryTypeRouter.js @@ -24,8 +24,6 @@ const routes = function (baseInvType, matType, consType, reusType, toolType, equ inventoryTypeRouter.route('/invtypes/equipment').post(controller.addEquipmentType); - inventoryTypeRouter.route('/invtypes/equipments').get(controller.fetchEquipmentTypes); - inventoryTypeRouter.route('/invtypes/consumables').get(controller.fetchConsumableTypes); // Route for fetching types by selected type diff --git a/src/routes/bmdashboard/bmToolRouter.js b/src/routes/bmdashboard/bmToolRouter.js index e58895433..5a9a96e78 100644 --- a/src/routes/bmdashboard/bmToolRouter.js +++ b/src/routes/bmdashboard/bmToolRouter.js @@ -1,8 +1,11 @@ const express = require('express'); -const routes = function (BuildingTool) { +const routes = function (BuildingTool, ToolType) { const toolRouter = express.Router(); - const controller = require('../../controllers/bmdashboard/bmToolController')(BuildingTool); + const controller = require('../../controllers/bmdashboard/bmToolController')(BuildingTool, ToolType); + + toolRouter.route('/tools') + .get(controller.fetchAllTools); toolRouter.route('/tools/:toolId') .get(controller.fetchSingleTool); @@ -10,6 +13,9 @@ const routes = function (BuildingTool) { toolRouter.route('/tools/purchase') .post(controller.bmPurchaseTools); + toolRouter.route('/tools/log') + .post(controller.bmLogTools); + return toolRouter; }; diff --git a/src/routes/mouseoverTextRouter.test.js b/src/routes/mouseoverTextRouter.test.js new file mode 100644 index 000000000..bbdebf70c --- /dev/null +++ b/src/routes/mouseoverTextRouter.test.js @@ -0,0 +1,98 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); +const { app } = require('../app'); +const { + mockReq, + createUser, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, +} = require('../test'); +const MouseoverText = require('../models/mouseoverText'); + +const agent = request.agent(app); + +describe('mouseoverText routes', () => { + let adminUser; + let adminToken; + let reqBody = { + ...mockReq.body, + }; + + beforeAll(async () => { + await dbConnect(); + adminUser = await createUser(); + adminToken = jwtPayload(adminUser); + }); + + beforeEach(async () => { + await dbClearCollections('mouseoverText'); + reqBody = { + ...reqBody, + newMouseoverText: 'new mouseoverText', + }; + }); + + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + + describe('mouseoverTextRoutes', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.post('/api/mouseoverText').send(reqBody).expect(401); + await agent.get('/api/mouseoverText').send(reqBody).expect(401); + await agent.put(`/api/mouseoverText/randomId`).send(reqBody).expect(401); + }); + }); + describe('createMouseoverText route', () => { + it('Should return 201 if create new mouseoverText successfully', async () => { + const response = await agent + .post('/api/mouseoverText') + .send(reqBody) + .set('Authorization', adminToken) + .expect(201); + + expect(response.body).toEqual({ + mouseoverText: { + _id: expect.anything(), + __v: expect.anything(), + mouseoverText: reqBody.newMouseoverText, + }, + _serverMessage: 'MouseoverText succesfuly created!', + }); + }); + }); + describe('getMouseoverText route', () => { + it('Should return 201 if create new mouseoverText successfully', async () => { + const _mouseoverText = new MouseoverText(); + _mouseoverText.mouseoverText = 'sample mouseoverText'; + await _mouseoverText.save(); + await agent.get('/api/mouseoverText').set('Authorization', adminToken).expect(200); + }); + }); + describe('updateMouseoverText route', () => { + it('Should return 500 if any error in finding mouseoverText by Id', async () => { + reqBody.newMouseoverText = null; + const response = await agent + .put('/api/mouseoverText/randomId') + .send(reqBody) + .set('Authorization', adminToken) + .expect(500); + expect(response.text).toEqual('MouseoverText not found with the given ID'); + }); + it('Should return 201 if updating mouseoverText successfully', async () => { + const _mouseoverText = new MouseoverText(); + _mouseoverText.mouseoverText = 'sample mouseoverText'; + const mouseoverText = await _mouseoverText.save(); + const response = await agent + .put(`/api/mouseoverText/${mouseoverText._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(201); + expect(response.body).toEqual({ + _id: expect.anything(), + __v: expect.anything(), + mouseoverText: reqBody.newMouseoverText, + }); + }); + }); +}); diff --git a/src/routes/notificationRouter.js b/src/routes/notificationRouter.js index d5883cb45..1c4c8c667 100644 --- a/src/routes/notificationRouter.js +++ b/src/routes/notificationRouter.js @@ -1,15 +1,33 @@ const express = require('express'); -const routes = function (notification) { - const controller = require('../controllers/notificationController')(notification); +const routes = function () { + const controller = require('../controllers/notificationController')(); const notificationRouter = express.Router(); - notificationRouter.route('/notification/user/:userId') + notificationRouter + .route('/notification/user/:userId') .get(controller.getUserNotifications); - notificationRouter.route('/notification/:notificationId') + notificationRouter + .route('/notification/unread/user/:userId') + .get(controller.getUnreadUserNotifications); + + notificationRouter + .route('/notification/sendHistory/') + .get(controller.getSentNotifications); + + notificationRouter + .route('/notification/') + .post(controller.createUserNotification); + + notificationRouter + .route('/notification/:notificationId') .delete(controller.deleteUserNotification); + notificationRouter + .route('/notification/markRead/:notificationId') + .post(controller.markNotificationAsRead); + return notificationRouter; }; diff --git a/src/routes/profileInitialSetupRouter.js b/src/routes/profileInitialSetupRouter.js index 11e0bc316..836ad4abc 100644 --- a/src/routes/profileInitialSetupRouter.js +++ b/src/routes/profileInitialSetupRouter.js @@ -1,34 +1,38 @@ -const express = require('express'); +const express = require("express"); const routes = function ( ProfileInitialSetupToken, userProfile, Project, - mapLocations, + mapLocations ) { const ProfileInitialSetup = express.Router(); - const controller = require('../controllers/profileInitialSetupController')( + const controller = require("../controllers/profileInitialSetupController")( ProfileInitialSetupToken, userProfile, Project, - mapLocations, + mapLocations ); - ProfileInitialSetup.route('/getInitialSetuptoken').post( - controller.getSetupToken, + ProfileInitialSetup.route("/getInitialSetuptoken").post( + controller.getSetupToken ); - ProfileInitialSetup.route('/ProfileInitialSetup').post( - controller.setUpNewUser, + ProfileInitialSetup.route("/ProfileInitialSetup").post( + controller.setUpNewUser ); - ProfileInitialSetup.route('/validateToken').post( - controller.validateSetupToken, + ProfileInitialSetup.route("/validateToken").post( + controller.validateSetupToken ); - ProfileInitialSetup.route('/getTimeZoneAPIKeyByToken').post( - controller.getTimeZoneAPIKeyByToken, + ProfileInitialSetup.route("/getTimeZoneAPIKeyByToken").post( + controller.getTimeZoneAPIKeyByToken ); - ProfileInitialSetup.route('/getTotalCountryCount').get( - controller.getTotalCountryCount, + ProfileInitialSetup.route("/getTotalCountryCount").get( + controller.getTotalCountryCount ); + ProfileInitialSetup.route('/getSetupInvitation').get(controller.getSetupInvitation); + ProfileInitialSetup.route('/refreshSetupInvitationToken').post(controller.refreshSetupInvitation); + ProfileInitialSetup.route('/cancelSetupInvitationToken').post(controller.cancelSetupInvitation); + return ProfileInitialSetup; }; diff --git a/src/routes/rolePresetRouter.test.js b/src/routes/rolePresetRouter.test.js new file mode 100644 index 000000000..31199be96 --- /dev/null +++ b/src/routes/rolePresetRouter.test.js @@ -0,0 +1,238 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); +const { app } = require('../app'); +const { + mockReq, + createUser, + createRole, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, +} = require('../test'); +const RolePreset = require('../models/rolePreset'); + +const agent = request.agent(app); + +describe('rolePreset routes', () => { + let adminUser; + let adminToken; + let volunteerUser; + let volunteerToken; + let reqBody = { + ...mockReq.body, + }; + + beforeAll(async () => { + await dbConnect(); + adminUser = await createUser(); + volunteerUser = await createUser(); + volunteerUser.role = 'Volunteer'; + adminToken = jwtPayload(adminUser); + volunteerToken = jwtPayload(volunteerUser); + // create 2 roles. One with permission and one without + await createRole('Administrator', ['putRole']); + await createRole('Volunteer', []); + }); + + beforeEach(async () => { + await dbClearCollections('rolePreset'); + reqBody = { + ...reqBody, + roleName: 'some roleName', + presetName: 'some Preset', + permissions: ['test', 'write'], + }; + }); + + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + + describe('rolePresetRoutes', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.post('/api/rolePreset').send(reqBody).expect(401); + await agent.get('/api/rolePreset/randomRoleName').send(reqBody).expect(401); + await agent.put(`/api/rolePreset/randomId`).send(reqBody).expect(401); + await agent.delete('/api/rolePreser/randomId').send(reqBody).expect(401); + }); + }); + + describe('Post rolePreset route', () => { + it('Should return 403 if user does not have permissions', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + expect(response.text).toEqual('You are not authorized to make changes to roles.'); + }); + + it('Should return 400 if missing roleName', async () => { + reqBody.roleName = null; + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + + expect(response.body).toEqual({ + error: 'roleName, presetName, and permissions are mandatory fields.', + }); + }); + + it('Should return 400 if missing presetName', async () => { + reqBody.presetName = null; + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + + expect(response.body).toEqual({ + error: 'roleName, presetName, and permissions are mandatory fields.', + }); + }); + + it('Should return 400 if missing permissions', async () => { + reqBody.permissions = null; + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + + expect(response.body).toEqual({ + error: 'roleName, presetName, and permissions are mandatory fields.', + }); + }); + + it('Should return 201 if the rolePreset is successfully created', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', adminToken) + .expect(201); + + expect(response.body).toEqual({ + newPreset: { + _id: expect.anything(), + __v: expect.anything(), + roleName: reqBody.roleName, + presetName: reqBody.presetName, + permissions: reqBody.permissions, + }, + message: 'New preset created', + }); + }); + }); + + describe('get Presets ByRole route', () => { + it('Should return 403 if user does not have permissions', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + + expect(response.text).toEqual('You are not authorized to make changes to roles.'); + }); + + it('Should return 200 if getPreset By role successfully', async () => { + const _rolePreset = new RolePreset(); + _rolePreset.roleName = 'sample roleName'; + _rolePreset.presetName = 'sample presetName'; + _rolePreset.permissions = ['sample permissions']; + const rolePreset = await _rolePreset.save(); + const response = await agent + .get(`/api/rolePreset/${rolePreset.roleName}`) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toEqual([ + { + _id: expect.anything(), + __v: expect.anything(), + roleName: rolePreset.roleName, + presetName: rolePreset.presetName, + permissions: expect.arrayContaining(rolePreset.permissions), + }, + ]); + }); + }); + describe('update Preset route', () => { + it('Should return 403 if user does not have permissions', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + + expect(response.text).toEqual('You are not authorized to make changes to roles.'); + }); + + it('Should return 400 if the route does not exist', async () => { + await agent + .put('/api/rolePreset/randomId123') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + }); + + it('Should return 200 if update Preset By Id successfully', async () => { + const _rolePreset = new RolePreset(); + _rolePreset.roleName = reqBody.roleName; + _rolePreset.presetName = reqBody.presetName; + _rolePreset.permissions = reqBody.permissions; + const rolePreset = await _rolePreset.save(); + const response = await agent + .put(`/api/rolePreset/${rolePreset._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toEqual({ + _id: expect.anything(), + __v: expect.anything(), + roleName: reqBody.roleName, + presetName: reqBody.presetName, + permissions: expect.arrayContaining(reqBody.permissions), + }); + }); + }); + describe('delete Preset route', () => { + it('Should return 403 if user does not have permissions', async () => { + const response = await agent + .post('/api/rolePreset') + .send(reqBody) + .set('Authorization', volunteerToken) + .expect(403); + + expect(response.text).toEqual('You are not authorized to make changes to roles.'); + }); + + it('Should return 400 if the route does not exist', async () => { + await agent + .delete('/api/rolePreset/randomId123') + .send(reqBody) + .set('Authorization', adminToken) + .expect(400); + }); + + it('Should return 200 if update Preset By Id successfully', async () => { + const _rolePreset = new RolePreset(); + _rolePreset.roleName = reqBody.roleName; + _rolePreset.presetName = reqBody.presetName; + _rolePreset.permissions = reqBody.permissions; + const rolePreset = await _rolePreset.save(); + + const response = await agent + .delete(`/api/rolePreset/${rolePreset._id}`) + .send(reqBody) + .set('Authorization', adminToken) + .expect(200); + + expect(response.body).toEqual({ + message: 'Deleted preset', + }); + }); + }); +}); diff --git a/src/routes/taskRouter.js b/src/routes/taskRouter.js index b404cea0a..4f91dc4b2 100644 --- a/src/routes/taskRouter.js +++ b/src/routes/taskRouter.js @@ -2,56 +2,42 @@ const express = require('express'); const routes = function (task, userProfile) { const controller = require('../controllers/taskController')(task, userProfile); - const wbsRouter = express.Router(); + const taskRouter = express.Router(); - wbsRouter.route('/tasks/:wbsId/:level/:mother') + taskRouter + .route('/tasks/:wbsId/:level/:mother') .get(controller.getTasks) .put(controller.fixTasks); - wbsRouter.route('/task/:id') - .post(controller.postTask) - .get(controller.getTaskById); + taskRouter.route('/task/:id').post(controller.postTask).get(controller.getTaskById); - wbsRouter.route('/task/import/:id') - .post(controller.importTask); + taskRouter.route('/task/import/:id').post(controller.importTask); - wbsRouter.route('/task/del/:taskId/:mother') - .post(controller.deleteTask); + taskRouter.route('/task/del/:taskId/:mother').post(controller.deleteTask); - wbsRouter.route('/task/wbs/:wbsId') - .get(controller.getWBSId); + taskRouter.route('/task/wbs/:wbsId').get(controller.getWBSId); - wbsRouter.route('/task/wbs/del/:wbsId') - .post(controller.deleteTaskByWBS); + taskRouter.route('/task/wbs/del/:wbsId').post(controller.deleteTaskByWBS); - wbsRouter.route('/task/update/:taskId') - .put(controller.updateTask); + taskRouter.route('/task/update/:taskId').put(controller.updateTask); - wbsRouter.route('/task/updateStatus/:taskId') - .put(controller.updateTaskStatus); + taskRouter.route('/task/updateStatus/:taskId').put(controller.updateTaskStatus); - wbsRouter.route('/task/updateAllParents/:wbsId/') - .put(controller.updateAllParents); + taskRouter.route('/task/updateAllParents/:wbsId/').put(controller.updateAllParents); - wbsRouter.route('/tasks/swap/') - .put(controller.swap); + taskRouter.route('/tasks/swap/').put(controller.swap); - wbsRouter.route('/tasks/update/num') - .put(controller.updateNum); + taskRouter.route('/tasks/update/num').put(controller.updateNum); - wbsRouter.route('/tasks/moveTasks/:wbsId') - .put(controller.moveTask); + taskRouter.route('/tasks/moveTasks/:wbsId').put(controller.moveTask); - wbsRouter.route('/tasks/user/:userId') - .get(controller.getTasksByUserId); + taskRouter.route('/tasks/user/:userId').get(controller.getTasksByUserId); - wbsRouter.route('/user/:userId/teams/tasks') - .get(controller.getTasksForTeamsByUser); + taskRouter.route('/user/:userId/teams/tasks').get(controller.getTasksForTeamsByUser); - wbsRouter.route('/tasks/reviewreq/:userId') - .post(controller.sendReviewReq); + taskRouter.route('/tasks/reviewreq/:userId').post(controller.sendReviewReq); - return wbsRouter; + return taskRouter; }; module.exports = routes; diff --git a/src/routes/timeentryRouter.js b/src/routes/timeentryRouter.js index 88f203e94..10ec0d6d0 100644 --- a/src/routes/timeentryRouter.js +++ b/src/routes/timeentryRouter.js @@ -17,6 +17,8 @@ const routes = function (TimeEntry) { TimeEntryRouter.route('/TimeEntry/users').post(controller.getTimeEntriesForUsersList); + TimeEntryRouter.route('/TimeEntry/reports').post(controller.getTimeEntriesForReports); + TimeEntryRouter.route('/TimeEntry/lostUsers').post(controller.getLostTimeEntriesForUserList); TimeEntryRouter.route('/TimeEntry/lostProjects').post( diff --git a/src/routes/titleRouter.js b/src/routes/titleRouter.js new file mode 100644 index 000000000..f12cb5ec7 --- /dev/null +++ b/src/routes/titleRouter.js @@ -0,0 +1,22 @@ +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); + + titleRouter.route('/title/:titleId') + .get(controller.getTitleById) + .put(controller.deleteTitleById); + + titleRouter.route('/title/deleteAll') + .get(controller.deleteAllTitles); + + return titleRouter; +}; + +module.exports = router; diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index 0f27abe33..e6892026b 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -20,10 +20,10 @@ const routes = function (userProfile) { .route('/userProfile/:userId') .get(controller.getUserById) .put( - body('firstName').customSanitizer((value) => value.trim()), - body('lastName').customSanitizer((value) => value.trim()), - body('personalLinks').customSanitizer((value) => - value.map((link) => { + body('firstName').customSanitizer((req) => req.trim()), + body('lastName').customSanitizer((req) => req.trim()), + body('personalLinks').customSanitizer((req) => + req.map((link) => { if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { return { ...link, @@ -34,8 +34,8 @@ const routes = function (userProfile) { throw new Error('Url not valid'); }), ), - body('adminLinks').customSanitizer((value) => - value.map((link) => { + body('adminLinks').customSanitizer((req) => + req.map((link) => { if (link.Name.replace(/\s/g, '') || link.Link.replace(/\s/g, '')) { return { ...link, diff --git a/src/routes/wbsRouter.js b/src/routes/wbsRouter.js index a5eb2d126..8a646c757 100644 --- a/src/routes/wbsRouter.js +++ b/src/routes/wbsRouter.js @@ -6,15 +6,9 @@ const routes = function (wbs) { wbsRouter.route('/wbs/:projectId').get(controller.getAllWBS); - wbsRouter.route('/wbs/:id') - .post(controller.postWBS) - .delete(controller.deleteWBS); + wbsRouter.route('/wbs/:id').post(controller.postWBS).delete(controller.deleteWBS); - wbsRouter.route('/wbsId/:id') - .get(controller.getWBSById); - - wbsRouter.route('/wbs/user/:userId') - .get(controller.getWBSByUserId); + wbsRouter.route('/wbsId/:id').get(controller.getWBSById); wbsRouter.route('/wbs').get(controller.getWBS); diff --git a/src/routes/wbsRouter.test.js b/src/routes/wbsRouter.test.js new file mode 100644 index 000000000..870810915 --- /dev/null +++ b/src/routes/wbsRouter.test.js @@ -0,0 +1,353 @@ +const request = require('supertest'); +const { jwtPayload } = require('../test'); +const { app } = require('../app'); +const Project = require('../models/project'); +const WBS = require('../models/wbs'); +const Task = require('../models/task'); +const { + mockReq, + createUser, + createRole, + mongoHelper: { dbConnect, dbDisconnect, dbClearCollections, dbClearAll }, +} = require('../test'); + +const agent = request.agent(app); + +describe('actionItem routes', () => { + let adminUser; + let volunteerUser; + let adminToken; + let volunteerToken; + let project; + let reqBody = { + ...mockReq.body, + }; + + beforeAll(async () => { + await dbConnect(); + adminUser = await createUser(); + volunteerUser = await createUser(); + volunteerUser.role = 'Volunteer'; + adminToken = jwtPayload(adminUser); + volunteerToken = jwtPayload(volunteerUser); + + // create 2 roles. One with permission and one without + await createRole('Administrator', ['postWbs', 'deleteWbs']); + await createRole('Volunteer', []); + + // create a project so we can create new wbs tasks + const _project = new Project(); + _project.projectName = 'Test project'; + _project.isActive = true; + _project.createdDatetime = new Date('2024-05-01'); + _project.modifiedDatetime = new Date('2024-05-01'); + _project.category = 'Food'; + + project = await _project.save(); + }); + + beforeEach(async () => { + await dbClearCollections('wbs'); + reqBody = { + ...reqBody, + wbsName: 'Sample WBS', + isActive: true, + }; + }); + + afterAll(async () => { + await dbClearAll(); + await dbDisconnect(); + }); + + describe('wbsRouter tests', () => { + it('should return 401 if authorization header is not present', async () => { + await agent.get('/api/wbs/randomId').send(reqBody).expect(401); + await agent.post('/api/wbs/randomId').send(reqBody).expect(401); + await agent.delete('/api/wbs/randomId').send(reqBody).expect(401); + await agent.get('/api/wbsId/randomId').send(reqBody).expect(401); + await agent.get('/api/wbs/user/randomId').send(reqBody).expect(401); + await agent.get('/api/wbs').send(reqBody).expect(401); + }); + + it('should return 404 if the route does not exist', async () => { + await agent + .get('/api/wibs/randomId') + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(404); + await agent + .post('/api/wibs/randomId') + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(404); + await agent + .delete('/api/wibs/randomId') + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(404); + await agent + .get('/api/wibsId/randomId') + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(404); + await agent + .get('/api/wibs/user/randomId') + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(404); + await agent.get('/api/wibs').set('Authorization', volunteerToken).send(reqBody).expect(404); + }); + + describe('getAllWBS routes', () => { + it("Should return 200 and an array of wbs' on success", async () => { + // now we create a wbs for the project + const _wbs = new WBS(); + + _wbs.wbsName = 'Sample WBS'; + _wbs.projectId = project._id; + _wbs.isActive = true; + _wbs.createdDatetime = new Date('2024-05-01'); + _wbs.modifiedDatetime = new Date('2024-05-01'); + + const wbs = await _wbs.save(); + + const response = await agent + .get(`/api/wbs/${project._id}`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(200); + + // Compare with the expected value + expect(response.body).toEqual([ + { + _id: wbs._id.toString(), + modifiedDatetime: expect.anything(), + wbsName: wbs.wbsName, + isActive: wbs.isActive, + }, + ]); + }); + }); + + describe('postWBS route', () => { + it('Should return 403 if user does not have permission', async () => { + await agent + .post(`/api/wbs/randomId`) + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(403); + }); + + it('Should return 400 if wbsName or isActive is missing from req body.', async () => { + reqBody.wbsName = null; + reqBody.isActive = null; + + const res = await agent + .post(`/api/wbs/randomId`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(400); + + expect(res.body).toEqual({ error: 'WBS Name and active status are mandatory fields' }); + }); + + it('Should create a new wbs and return 201 if all is successful', async () => { + const res = await agent + .post(`/api/wbs/${project._id}`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(201); + + expect(res.body).toEqual({ + __v: expect.anything(), + _id: expect.anything(), + projectId: project._id.toString(), + wbsName: reqBody.wbsName, + isActive: reqBody.isActive, + createdDatetime: expect.anything(), + modifiedDatetime: expect.anything(), + }); + }); + }); + + describe('deleteWBS route', () => { + it('Should return 403 if user does not have permission', async () => { + await agent + .delete(`/api/wbs/randomId`) + .set('Authorization', volunteerToken) + .send(reqBody) + .expect(403); + }); + + it('Should return 400 if no record was found', async () => { + const res = await agent + .delete(`/api/wbs/randomId`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(400); + + expect(res.body).toEqual({ error: 'No valid records found' }); + }); + + it('Should return 200 and delete the wbs on success', async () => { + // first lets create the wbs to delete. + const _wbs = new WBS(); + + _wbs.wbsName = 'Sample WBS'; + _wbs.projectId = project._id; + _wbs.isActive = true; + _wbs.createdDatetime = new Date('2024-05-01'); + _wbs.modifiedDatetime = new Date('2024-05-01'); + + const wbs = await _wbs.save(); + + const res = await agent + .delete(`/api/wbs/${wbs._id}`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(200); + + expect(res.body).toEqual({ message: ' WBS successfully deleted' }); + }); + }); + + describe('GetByID route', () => { + it('Should return 200 on success', async () => { + const _wbs = new WBS(); + + _wbs.wbsName = 'Sample WBS'; + _wbs.projectId = project._id; + _wbs.isActive = true; + _wbs.createdDatetime = new Date('2024-05-01'); + _wbs.modifiedDatetime = new Date('2024-05-01'); + + const wbs = await _wbs.save(); + + const res = await agent + .get(`/api/wbsId/${wbs._id}`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(200); + + expect(res.body).toEqual({ + __v: expect.anything(), + _id: wbs._id.toString(), + wbsName: wbs.wbsName, + projectId: project._id.toString(), + isActive: wbs.isActive, + createdDatetime: expect.anything(), + modifiedDatetime: expect.anything(), + }); + }); + }); + + describe('getWBS route', () => { + it('Should return 200 and return all wbs', async () => { + const _wbs = new WBS(); + + _wbs.wbsName = 'Sample WBS'; + _wbs.projectId = project._id; + _wbs.isActive = true; + _wbs.createdDatetime = new Date('2024-05-01'); + _wbs.modifiedDatetime = new Date('2024-05-01'); + + const wbs = await _wbs.save(); + + const res = await agent + .get(`/api/wbs`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(200); + + expect(res.body).toEqual([ + { + __v: expect.anything(), + _id: wbs._id.toString(), + wbsName: wbs.wbsName, + projectId: project._id.toString(), + isActive: wbs.isActive, + createdDatetime: expect.anything(), + modifiedDatetime: expect.anything(), + }, + ]); + }); + }); + + describe('Get wbs by user id route', () => { + it('Should return 200 and give the wbs related to user', async () => { + // create a wbs + const _wbs = new WBS(); + + _wbs.wbsName = 'Sample WBS'; + _wbs.projectId = project._id; + _wbs.isActive = true; + _wbs.createdDatetime = new Date('2024-05-01'); + _wbs.modifiedDatetime = new Date('2024-05-01'); + + const wbs = await _wbs.save(); + + // create a new task and link it to a user and wbs + const _task = new Task(); + + // Assign values to the task properties + _task.taskName = 'Sample Task'; + _task.wbsId = wbs._id; + _task.num = '123'; + _task.level = 1; + // Add resources + _task.resources.push({ + name: 'John Doe', + userID: adminUser._id, + profilePic: 'https://example.com/profilepic.jpg', + completedTask: false, + reviewStatus: 'Unsubmitted', + }); + // Set other properties + _task.isAssigned = true; + _task.status = 'Not Started'; + _task.hoursBest = 0.0; + _task.hoursWorst = 0.0; + _task.hoursMost = 0.0; + _task.hoursLogged = 0.0; + _task.estimatedHours = 10.0; + _task.startedDatetime = new Date('2024-05-01'); + _task.dueDatetime = new Date('2024-06-01'); + _task.links = ['https://example.com/link1', 'https://example.com/link2']; + _task.relatedWorkLinks = ['https://example.com/related1', 'https://example.com/related2']; + _task.category = 'Sample Category'; + _task.position = 1; + _task.isActive = true; + _task.hasChild = false; + _task.childrenQty = 0; + _task.createdDatetime = new Date(); + _task.modifiedDatetime = new Date(); + _task.whyInfo = 'Sample why info'; + _task.intentInfo = 'Sample intent info'; + _task.endstateInfo = 'Sample endstate info'; + _task.classification = 'Sample classification'; + + // Save the task to the database + const task = await _task.save(); + + const res = await agent + .get(`/api/wbs/user/${adminUser._id}`) + .set('Authorization', adminToken) + .send(reqBody) + .expect(200); + + expect(res.body).toEqual([ + { + __v: expect.anything(), + _id: wbs._id.toString(), + createdDatetime: expect.anything(), + modifiedDatetime: expect.anything(), + isActive: task.isActive, + projectId: wbs.projectId.toString(), + wbsName: wbs.wbsName, + }, + ]); + }); + }); + }); +}); diff --git a/src/startup/db.js b/src/startup/db.js index 4308c5fd5..c3c61807c 100644 --- a/src/startup/db.js +++ b/src/startup/db.js @@ -23,8 +23,8 @@ const afterConnect = async () => { role: 'Volunteer', password: process.env.DEF_PWD, }) - .then((result) => logger.logInfo(`TimeArchive account was created with id of ${result._id}`)) - .catch((error) => logger.logException(error)); + .then(result => logger.logInfo(`TimeArchive account was created with id of ${result._id}`)) + .catch(error => logger.logException(error)); } } catch (error) { throw new Error(error); @@ -40,5 +40,5 @@ module.exports = function () { useFindAndModify: false, }) .then(afterConnect) - .catch((err) => logger.logException(err)); + .catch(err => logger.logException(err)); }; diff --git a/src/startup/routes.js b/src/startup/routes.js index 2ecf9f322..6856d2bb3 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -3,7 +3,7 @@ const userProfile = require('../models/userProfile'); const project = require('../models/project'); const information = require('../models/information'); const team = require('../models/team'); -const actionItem = require('../models/actionItem'); +// const actionItem = require('../models/actionItem'); const notification = require('../models/notification'); const wbs = require('../models/wbs'); const task = require('../models/task'); @@ -42,9 +42,10 @@ const { buildingReusable, buildingMaterial, buildingTool, + buildingEquipment, } = require('../models/bmdashboard/buildingInventoryItem'); -// const buildingTool = require('../models/bmdashboard/buildingTool'); const timeOffRequest = require('../models/timeOffRequest'); +const followUp = require('../models/followUp'); const userProfileRouter = require('../routes/userProfileRouter')(userProfile); const warningRouter = require('../routes/warningRouter')(userProfile); @@ -54,8 +55,8 @@ const timeEntryRouter = require('../routes/timeentryRouter')(timeEntry); const projectRouter = require('../routes/projectRouter')(project); const informationRouter = require('../routes/informationRouter')(information); const teamRouter = require('../routes/teamRouter')(team); -const actionItemRouter = require('../routes/actionItemRouter')(actionItem); -const notificationRouter = require('../routes/notificationRouter')(notification); +// const actionItemRouter = require('../routes/actionItemRouter')(actionItem); +const notificationRouter = require('../routes/notificationRouter')(); const loginRouter = require('../routes/loginRouter')(); const forgotPwdRouter = require('../routes/forgotPwdRouter')(userProfile); const forcePwdRouter = require('../routes/forcePwdRouter')(userProfile); @@ -94,13 +95,12 @@ const timeOffRequestRouter = require('../routes/timeOffRequestRouter')( team, userProfile, ); +const followUpRouter = require('../routes/followUpRouter')(followUp); // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(buildingMaterial); const bmReusableRouter = require('../routes/bmdashboard/bmReusableRouter')(buildingReusable); -const bmToolRouter = require('../routes/bmdashboard/bmToolRouter')(buildingTool); -// const bmEquipmentRouter = require('../routes/bmdashboard/bmEquipmentRouter')(buildingEquipment); const bmProjectRouter = require('../routes/bmdashboard/bmProjectRouter')(buildingProject); const bmNewLessonRouter = require('../routes/bmdashboard/bmNewLessonRouter')(buildingNewLesson); const bmConsumablesRouter = require('../routes/bmdashboard/bmConsumablesRouter')( @@ -115,6 +115,9 @@ const bmInventoryTypeRouter = require('../routes/bmdashboard/bmInventoryTypeRout equipmentType, ); +const titleRouter = require('../routes/titleRouter')(title); +const bmToolRouter = require('../routes/bmdashboard/bmToolRouter')(buildingTool, toolType); +const bmEquipmentRouter = require('../routes/bmdashboard/bmEquipmentRouter')(buildingEquipment); const bmIssueRouter = require('../routes/bmdashboard/bmIssueRouter')(buildingIssue); const blueSquareEmailAssignmentRouter = require('../routes/BlueSquareEmailAssignmentRouter')(blueSquareEmailAssignment,userProfile) @@ -129,7 +132,7 @@ module.exports = function (app) { app.use('/api', dashboardRouter); app.use('/api', timeEntryRouter); app.use('/api', teamRouter); - app.use('/api', actionItemRouter); + // app.use('/api', actionItemRouter); app.use('/api', notificationRouter); app.use('/api', reportsRouter); app.use('/api', wbsRouter); @@ -165,7 +168,7 @@ module.exports = function (app) { app.use('/api/bm', bmNewLessonRouter); app.use('/api/bm', bmInventoryTypeRouter); app.use('/api/bm', bmToolRouter); + app.use('/api/bm', bmEquipmentRouter); app.use('/api/bm', bmConsumablesRouter); - app.use('/api', timeOffRequestRouter); app.use('api', bmIssueRouter); }; diff --git a/src/utilities/addMembersToTeams.js b/src/utilities/addMembersToTeams.js index 62562a9cc..b637fa2c1 100644 --- a/src/utilities/addMembersToTeams.js +++ b/src/utilities/addMembersToTeams.js @@ -11,21 +11,21 @@ const UserProfile = require('../models/userProfile'); const Teams = require('../models/team'); const addMembersField = async () => { - await Teams.updateMany({}, { $set: { members: [] } }).catch((error) => logger.logException('Error adding field:', error)); + await Teams.updateMany({}, { $set: { members: [] } }).catch(error => logger.logException('Error adding field:', error)); const allUsers = await UserProfile.find({}); const updateOperations = allUsers .map((user) => { const { _id, teams, createdDate } = user; - return teams.map((team) => Teams.updateOne({ _id: team }, { $addToSet: { members: { userId: _id, addDateTime: createdDate } } })); + return teams.map(team => Teams.updateOne({ _id: team }, { $addToSet: { members: { userId: _id, addDateTime: createdDate } } })); }) .flat(); - await Promise.all(updateOperations).catch((error) => logger.logException(error)); + await Promise.all(updateOperations).catch(error => logger.logException(error)); }; const deleteMembersField = async () => { - await Teams.updateMany({}, { $unset: { members: '' } }).catch((err) => console.error(err)); + await Teams.updateMany({}, { $unset: { members: '' } }).catch(err => console.error(err)); }; const run = () => { @@ -42,7 +42,7 @@ const run = () => { }) // .then(deleteMembersField) .then(addMembersField) - .catch((err) => logger.logException(err)) + .catch(err => logger.logException(err)) .finally(() => { mongoose.connection.close(); console.log('Done! ✅'); diff --git a/src/utilities/addNewField.js b/src/utilities/addNewField.js index c63085e94..947953683 100644 --- a/src/utilities/addNewField.js +++ b/src/utilities/addNewField.js @@ -94,7 +94,7 @@ const run = function () { // .then(deleteUserField) .then(addNewField) .then(checkNewField) - .catch((err) => logger.logException(err)); // handles errors from the connect function + .catch(err => logger.logException(err)); // handles errors from the connect function }; run(); diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 8ffde16f5..2c51a28d0 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -39,9 +39,9 @@ const permissionsRoles = [ // Time Entries 'editTimeEntry', 'deleteTimeEntry', - // 'postTimeEntry',? + 'postTimeEntry', // User Profile - 'putRole', + 'putUserProfilePermissions', 'postUserProfile', 'putUserProfile', 'putUserProfileImportantInfo', @@ -77,15 +77,25 @@ const permissionsRoles = [ 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', + + // Title + 'seeQSC', + 'addNewTitle', + 'assignTitle', + + 'seeUsersInDashboard', + 'editTeamCode', ], }, { roleName: 'Volunteer', - permissions: ['getReporteesLimitRoles', 'suggestTask'], + permissions: ['suggestTask'], }, { roleName: 'Core Team', permissions: [ + 'getReports', + 'getWeeklySummaries', 'getUserProfiles', 'getProjectMembers', 'getAllInvInProjectWBS', @@ -102,20 +112,19 @@ const permissionsRoles = [ 'getAllInvType', 'postInvType', 'getWeeklySummaries', - 'getReports', 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', + 'seeUsersInDashboard', ], }, { roleName: 'Manager', permissions: [ - 'getUserProfiles', - 'getProjectMembers', - 'putUserProfile', - 'infringementAuthorizer', 'getReporteesLimitRoles', + 'postTask', 'updateTask', + 'suggestTask', + 'putReviewStatus', 'putTeam', 'getAllInvInProjectWBS', 'postInvInProjectWBS', @@ -137,11 +146,9 @@ const permissionsRoles = [ { roleName: 'Mentor', permissions: [ + 'updateTask', 'suggestTask', - 'getUserProfiles', - 'getProjectMembers', - 'putUserProfile', - 'infringementAuthorizer', + 'putReviewStatus', 'getReporteesLimitRoles', 'getAllInvInProjectWBS', 'postInvInProjectWBS', @@ -168,6 +175,9 @@ const permissionsRoles = [ 'putRole', 'addDeleteEditOwners', 'putUserProfilePermissions', + 'highlightEligibleBios', + 'manageTimeOffRequests', + 'changeUserRehireableStatus', 'changeUserStatus', 'seeBadges', 'assignBadges', @@ -186,12 +196,16 @@ const permissionsRoles = [ 'updateTask', 'swapTask', 'deleteTask', + 'resolveTask', + 'suggestTask', + 'putReviewStatus', 'postTeam', 'deleteTeam', 'putTeam', 'assignTeamToUsers', 'editTimeEntry', 'deleteTimeEntry', + 'postTimeEntry', 'updatePassword', 'getUserProfiles', 'getProjectMembers', @@ -222,7 +236,16 @@ const permissionsRoles = [ 'checkLeadTeamOfXplus', 'editTeamCode', 'totalValidWeeklySummaries', + + // Title + 'seeQSC', + 'addNewTitle', + 'assignTitle', + + 'seeUsersInDashboard', + 'changeUserRehireableStatus', + 'manageAdminLinks', ], }, ]; @@ -271,16 +294,17 @@ const createInitialPermissions = async () => { } // Update Default presets + const defaultName = 'hard-coded default'; const presetDataBase = allPresets.find( - (preset) => preset.roleName === roleName && preset.presetName === 'default', + (preset) => preset.roleName === roleName && preset.presetName === defaultName, ); // If role does not exist in db, create it if (!presetDataBase) { const defaultPreset = new RolePreset(); defaultPreset.roleName = roleName; - defaultPreset.presetName = 'default'; + defaultPreset.presetName = defaultName; defaultPreset.permissions = permissions; defaultPreset.save(); diff --git a/src/utilities/timeUtils.js b/src/utilities/timeUtils.js index 865aabaa3..9239a38de 100644 --- a/src/utilities/timeUtils.js +++ b/src/utilities/timeUtils.js @@ -1,13 +1,11 @@ -/* eslint-disable no-unused-vars */ -/* eslint-disable camelcase */ const moment = require('moment-timezone'); // converts date to desired format such as Aug-30-2023 -export const formatDateAndTime = (date) => moment(date).format('MMM-DD-YY, h:mm:ss a'); -export const formatDate = (date) => moment(date).tz('America/Los_Angeles').format('MMM-DD-YY'); +const formatDateAndTime = (date) => moment(date).format('MMM-DD-YY, h:mm:ss a'); +const formatDate = (date) => moment(date).tz('America/Los_Angeles').format('MMM-DD-YY'); // converts time to AM/PM format. E.g., '2023-09-21T07:08:09-07:00' becomes '7:08:09 AM'. -export const formatted_AM_PM_Time = (date) => moment(date).format('h:mm:ss A'); -export const formatCreatedDate = (date) => moment(date).format('MM/DD'); +const formattedAmPmTime = (date) => moment(date).format('h:mm:ss A'); +const formatCreatedDate = (date) => moment(date).format('MM/DD'); /** * Constants for day of week. Starting from Sunday. @@ -19,4 +17,13 @@ const DAY_OF_WEEK = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Fr * @param {String} utcTs A UTC timestamp String * @returns {Number} The day of the week. Starting from Sunday. 0 -> Sunday */ -export const getDayOfWeekStringFromUTC = (utcTs) => moment(utcTs).day(); +const getDayOfWeekStringFromUTC = (utcTs) => moment(utcTs).day(); + +module.exports = { + formatDateAndTime, + formatDate, + formattedAmPmTime, + formatCreatedDate, + DAY_OF_WEEK, + getDayOfWeekStringFromUTC, +}; diff --git a/src/utilities/yearMonthDayDateValidator.js b/src/utilities/yearMonthDayDateValidator.js index 82a32a263..0568329a9 100644 --- a/src/utilities/yearMonthDayDateValidator.js +++ b/src/utilities/yearMonthDayDateValidator.js @@ -1,5 +1,5 @@ const moment = require('moment-timezone'); -const yearMonthDayDateValidator = (inputDate) => (moment(inputDate, 'YYYY-MM-DD', true).format() !== 'Invalid date'); +const yearMonthDayDateValidator = inputDate => (moment(inputDate, 'YYYY-MM-DD', true).format() !== 'Invalid date'); module.exports = yearMonthDayDateValidator; diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index 89c774caf..4fed5334c 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -4,7 +4,7 @@ const moment = require('moment'); const Timer = require('../../models/timer'); const logger = require('../../startup/logger'); -export const getClient = async (clients, userId) => { +const getClient = async (clients, userId) => { // In case of there is already a connection that is open for this user // for example user open a new connection if (!clients.has(userId)) { @@ -14,26 +14,22 @@ export const getClient = async (clients, userId) => { clients.set(userId, timer); } catch (e) { logger.logException(e); - throw new Error( - 'Something happened when trying to retrieve timer from mongo', - ); + throw new Error('Something happened when trying to retrieve timer from mongo'); } } return clients.get(userId); }; -export const saveClient = async (client) => { +const saveClient = async (client) => { try { await Timer.findOneAndUpdate({ userId: client.userId }, client); } catch (e) { logger.logException(e); - throw new Error( - `Something happened when trying to save user timer to mongo, Error: ${e}`, - ); + throw new Error(`Something happened when trying to save user timer to mongo, Error: ${e}`); } }; -export const action = { +const action = { START_TIMER: 'START_TIMER', PAUSE_TIMER: 'PAUSE_TIMER', STOP_TIMER: 'STOP_TIMER', @@ -119,10 +115,7 @@ const setGoal = (client, msg) => { const addGoal = (client, msg) => { const duration = parseInt(msg.split('=')[1]); - const goalAfterAddition = moment - .duration(client.goal) - .add(duration, 'milliseconds') - .asHours(); + const goalAfterAddition = moment.duration(client.goal).add(duration, 'milliseconds').asHours(); if (goalAfterAddition > MAX_HOURS) return; @@ -163,7 +156,7 @@ const removeGoal = (client, msg) => { .toFixed(); }; -export const handleMessage = async (msg, clients, userId) => { +const handleMessage = async (msg, clients, userId) => { if (!clients.has(userId)) { throw new Error('It should have this user in memory'); } @@ -215,3 +208,13 @@ export const handleMessage = async (msg, clients, userId) => { if (resp === null) resp = client; return JSON.stringify(resp); }; + +module.exports = { + getClient, + handleMessage, + action, + MAX_HOURS, + MIN_MINS, + saveClient, + updatedTimeSinceStart, +}; diff --git a/src/websockets/TimerService/connectionsHandler.js b/src/websockets/TimerService/connectionsHandler.js index 46f136b3d..6658321bf 100644 --- a/src/websockets/TimerService/connectionsHandler.js +++ b/src/websockets/TimerService/connectionsHandler.js @@ -21,7 +21,7 @@ export function removeConnection(connections, userId, connToRemove) { const userConnetions = connections.get(userId); if (!userConnetions) return; - const newConns = userConnetions.filter((conn) => conn !== connToRemove); + const newConns = userConnetions.filter(conn => conn !== connToRemove); if (newConns.length === 0) connections.delete(userId); else connections.set(userId, newConns); } @@ -46,5 +46,5 @@ export function broadcastToSameUser(connections, userId, data) { export function hasOtherConn(connections, userId, anotherConn) { if (!connections.has(userId)) return false; const userConnections = connections.get(userId); - return userConnections.some((con) => con !== anotherConn && con.readyState === WebSocket.OPEN); + return userConnections.some(con => con !== anotherConn && con.readyState === WebSocket.OPEN); }