From a56b80d8bd5ae4c2ca49a32ab730e26ea84e2938 Mon Sep 17 00:00:00 2001 From: Shivansh Sharma Date: Thu, 24 Aug 2023 18:20:23 -0700 Subject: [PATCH 01/22] Changes made in getInfringementEmailBody function --- src/helpers/userHelper.js | 45 +++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index f9428b3a1..e1898f334 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -85,17 +85,28 @@ const userHelper = function () { lastName, infringement, totalInfringements, + timeRemaining, ) { + let final_paragraph = ''; + + if (timeRemaining == undefined) { + final_paragraph = '

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

'; + } else { + final_paragraph = `

Life happens and we understand that. Please make up the missed hours this following week though to avoid getting another blue square. So you know what’s needed, the missing/incomplete hours (${timeRemaining} hours) have been added to your current week and this new weekly total can be seen at the top of your dashboard.

+

Reminder also that each blue square is removed from your profile 1 year after it was issued.

`; + } + 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: ${infringement.description}

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

-

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

+ .localeData() + .ordinal(totalInfringements)} blue square of 5.

+ ${final_paragraph}

Thank you,
One Community

`; + return text; }; @@ -299,7 +310,7 @@ const userHelper = function () { for (let i = 0; i < users.length; i += 1) { const user = users[i]; - + const person = await userProfile.findById(user._id); const personId = mongoose.Types.ObjectId(user._id); let hasWeeklySummary = false; @@ -327,6 +338,9 @@ const userHelper = function () { const timeNotMet = timeSpent < weeklycommittedHours; let description; + const timeRemaining = weeklycommittedHours - timeSpent; + + const updateResult = await userProfile.findByIdAndUpdate( personId, { @@ -409,15 +423,28 @@ const userHelper = function () { { new: true }, ); - emailSender( - status.email, - 'New Infringement Assigned', - getInfringementEmailBody( + let emailBody = ''; + if (person.role == 'Core Team' && timeRemaining > 0) { + emailBody = getInfringementEmailBody( + status.firstName, + status.lastName, + infringement, + status.infringements.length, + timeRemaining, + ); + } else { + emailBody = getInfringementEmailBody( status.firstName, status.lastName, infringement, status.infringements.length, - ), + ); + } + + emailSender( + status.email, + 'New Infringement Assigned', + emailBody, null, 'onecommunityglobal@gmail.com', ); From 346082b365e833b1439b4ce60777ee4417ca5f58 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Tue, 29 Aug 2023 12:14:47 +0800 Subject: [PATCH 02/22] solve conflicts --- src/controllers/userProfileController.js | 1 + src/helpers/reporthelper.js | 3 +++ src/models/userProfile.js | 1 + 3 files changed, 5 insertions(+) diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index 9d0e7e63a..6b6cee9b1 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -284,6 +284,7 @@ const userProfileController = function (UserProfile) { record.totalIntangibleHrs = req.body.totalIntangibleHrs; record.bioPosted = req.body.bioPosted || 'default'; record.isFirstTimelog = req.body.isFirstTimelog; + record.teamCode = req.body.teamCode; // find userData in cache const isUserInCache = cache.hasCache('allusers'); diff --git a/src/helpers/reporthelper.js b/src/helpers/reporthelper.js index 4b66c7d65..317605dd4 100644 --- a/src/helpers/reporthelper.js +++ b/src/helpers/reporthelper.js @@ -112,6 +112,9 @@ const reporthelper = function () { }, }, }, + teamCode: { + $ifNull: ['$teamCode', ''], + }, role: 1, weeklySummaries: { $filter: { diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 79223fbe2..aa728f968 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -154,6 +154,7 @@ const userProfileSchema = new Schema({ weeklySummaryOption: { type: String }, bioPosted: { type: String, default: 'default' }, isFirstTimelog: { type: Boolean, default: true}, + teamCode: { type: String, default: '' }, infoCollections: [ { areaName:{type: String}, areaContent:{type:String}, From 0b63eac581a39838425c7f698cc3758903c4236c Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Thu, 31 Aug 2023 15:28:57 +0800 Subject: [PATCH 03/22] add Edit Team Code permission to Owner --- src/utilities/createInitialPermissions.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index 9ac0f12b0..e42ca9a15 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -209,6 +209,7 @@ const permissionsRoles = [ 'getWeeklySummaries', 'getTimeZoneAPIKey', 'checkLeadTeamOfXplus', + 'editTeamCode', ], }, ]; From 84b3baf05c234f43ba2099f080d855e729490854 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Wed, 6 Sep 2023 09:15:41 +0800 Subject: [PATCH 04/22] add teamCode Property to team object --- src/controllers/teamController.js | 1 + src/models/team.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 1072f1fb4..0ff777870 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -71,6 +71,7 @@ const teamcontroller = function (Team) { } record.teamName = req.body.teamName; record.isActive = req.body.isActive; + record.teamCode = req.body.teamCode; record.createdDatetime = Date.now(); record.modifiedDatetime = Date.now(); diff --git a/src/models/team.js b/src/models/team.js index 8d46db283..a57d7bb27 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -13,6 +13,7 @@ const team = new Schema({ addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, }, ], + teamCode: { type: 'String', default: '' }, }); module.exports = mongoose.model('team', team, 'teams'); From 3d56ee05c122529239361e23ed58439218accdea Mon Sep 17 00:00:00 2001 From: robertoooc Date: Sat, 7 Oct 2023 16:51:34 -0700 Subject: [PATCH 05/22] added logic to clear cache when updating badge so user info is up to date --- src/controllers/badgeController.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/controllers/badgeController.js b/src/controllers/badgeController.js index 6238c381e..e3768884a 100644 --- a/src/controllers/badgeController.js +++ b/src/controllers/badgeController.js @@ -2,6 +2,7 @@ const mongoose = require('mongoose'); const UserProfile = require('../models/userProfile'); const { hasPermission, hasIndividualPermission } = require('../utilities/permissions'); const escapeRegex = require('../utilities/escapeRegex'); +const cache = require('../utilities/nodeCache')(); const badgeController = function (Badge) { const getAllBadges = async function (req, res) { @@ -47,6 +48,8 @@ const badgeController = function (Badge) { if (result) { record.badgeCollection = req.body.badgeCollection; + if (cache.hasCache(`user-${userToBeAssigned}`)) cache.removeCache(`user-${userToBeAssigned}`); + record.save() .then(results => res.status(201).send(results._id)) .catch(errors => res.status(500).send(errors)); From 9a7bfb948da6f7b0c2beeb7cf310e3addeaa72b6 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Thu, 19 Oct 2023 10:06:48 +0800 Subject: [PATCH 06/22] add validator of regex and authorization --- src/controllers/teamController.js | 11 ++++++++++- src/models/team.js | 13 ++++++++++++- src/models/userProfile.js | 17 ++++++++++++++--- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index 5215b024b..b204875a5 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -70,6 +70,15 @@ const teamcontroller = function (Team) { res.status(400).send('No valid records found'); return; } + + const canEditTeamCode = req.body.requestor.role === 'Owner' + || req.body.requestor.permissions?.frontPermissions.includes('editTeamCode'); + + if (!canEditTeamCode) { + res.status(403).send('You are not authorized to edit team code.'); + return; + } + record.teamName = req.body.teamName; record.isActive = req.body.isActive; record.teamCode = req.body.teamCode; @@ -116,7 +125,7 @@ const teamcontroller = function (Team) { users.forEach((element) => { const { userId, operation } = element; // if user's profile is stored in cache, clear it so when you visit their profile page it will be up to date - if(cache.hasCache(`user-${userId}`)) cache.removeCache(`user-${userId}`); + if (cache.hasCache(`user-${userId}`)) cache.removeCache(`user-${userId}`); if (operation === 'Assign') { assignlist.push(userId); diff --git a/src/models/team.js b/src/models/team.js index a57d7bb27..97f8dc360 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -13,7 +13,18 @@ const team = new Schema({ addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, }, ], - teamCode: { type: 'String', default: '' }, + teamCode: { + type: 'String', + default: '', + validate: { + validator(v) { + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$/; + return teamCoderegex.test(v); + }, + message: + 'Please enter a code in the format of A-AAA or AAAAA', + }, + }, }); module.exports = mongoose.model('team', team, 'teams'); diff --git a/src/models/userProfile.js b/src/models/userProfile.js index a58d1d293..4739a05e7 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -7,7 +7,7 @@ const bcrypt = require('bcryptjs'); const SALT_ROUNDS = 10; const nextDay = new Date(); -nextDay.setDate(nextDay.getDate()+1); +nextDay.setDate(nextDay.getDate() + 1); const userProfileSchema = new Schema({ password: { @@ -153,8 +153,19 @@ const userProfileSchema = new Schema({ isVisible: { type: Boolean, default: false }, weeklySummaryOption: { type: String }, bioPosted: { type: String, default: 'default' }, - isFirstTimelog: { type: Boolean, default: true}, - teamCode: { type: String, default: '' }, + isFirstTimelog: { type: Boolean, default: true }, + teamCode: { + type: String, + default: '', + validate: { + validator(v) { + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$/; + return teamCoderegex.test(v); + }, + message: + 'Please enter a code in the format of A-AAA or AAAAA', + }, + }, infoCollections: [ { areaName: { type: String }, From 63de1d12b07af4f84cc32fa5407af4e2edeaddb6 Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Fri, 20 Oct 2023 09:21:52 -0700 Subject: [PATCH 07/22] add bm proj route, router, controller --- .../bmdashboard/bmProjectsController.js | 18 ++++++++++++++++++ src/routes/bmdashboard/bmMaterialsRouter.js | 4 ++-- src/routes/bmdashboard/bmProjectsRouter.js | 14 ++++++++++++++ src/startup/routes.js | 2 ++ 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/controllers/bmdashboard/bmProjectsController.js create mode 100644 src/routes/bmdashboard/bmProjectsRouter.js diff --git a/src/controllers/bmdashboard/bmProjectsController.js b/src/controllers/bmdashboard/bmProjectsController.js new file mode 100644 index 000000000..946869bce --- /dev/null +++ b/src/controllers/bmdashboard/bmProjectsController.js @@ -0,0 +1,18 @@ +const mongoose = require('mongoose'); + +const bmMProjectsController = function () { + // fetches projects with reference to BM userProfile id + const bmProjectsSummary = async function _projSumm(req, res) { + try { + res.json({ message: 'Hello world' }); + + // .then(results => res.status(200).send(results)) + // .catch(error => res.status(500).send(error)) + } catch (err) { + res.json(err); + } + }; + return { bmProjectsSummary }; +}; + +module.exports = bmMProjectsController; diff --git a/src/routes/bmdashboard/bmMaterialsRouter.js b/src/routes/bmdashboard/bmMaterialsRouter.js index ab8a67388..1188acf9d 100644 --- a/src/routes/bmdashboard/bmMaterialsRouter.js +++ b/src/routes/bmdashboard/bmMaterialsRouter.js @@ -8,6 +8,6 @@ materialsRouter.route('/materials') .get(controller.bmMaterialsList); return materialsRouter; -} +}; -module.exports = routes; \ No newline at end of file +module.exports = routes; diff --git a/src/routes/bmdashboard/bmProjectsRouter.js b/src/routes/bmdashboard/bmProjectsRouter.js new file mode 100644 index 000000000..204efbfad --- /dev/null +++ b/src/routes/bmdashboard/bmProjectsRouter.js @@ -0,0 +1,14 @@ +const express = require('express'); + +const routes = function () { + const projectsRouter = express.Router(); + const controller = require('../../controllers/bmdashboard/bmProjectsController')(); +// const controller = require('../../controllers/bmdashboard/bmMaterialsController')(itemMaterial); + +projectsRouter.route('/projects') + .get(controller.bmProjectsSummary); + + return projectsRouter; +}; + +module.exports = routes; diff --git a/src/startup/routes.js b/src/startup/routes.js index 2fd7337a6..7208535d8 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -61,6 +61,7 @@ const mouseoverTextRouter = require('../routes/mouseoverTextRouter')(mouseoverTe // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(inventoryItemMaterial); +const bmProjectsRouter = require('../routes/bmdashboard/bmProjectsRouter')(); module.exports = function (app) { app.use('/api', forgotPwdRouter); @@ -96,4 +97,5 @@ module.exports = function (app) { // bm dashboard app.use('/api/bm', bmLoginRouter); app.use('/api/bm', bmMaterialsRouter); + app.use('/api/bm', bmProjectsRouter); }; From 701be2501cf6f82da6eb90723c1d4fc55f5a6eed Mon Sep 17 00:00:00 2001 From: wang9hu Date: Wed, 25 Oct 2023 17:19:35 -0700 Subject: [PATCH 08/22] cleanup old timer related code and files --- src/controllers/REAL_TIME_timerController.js | 155 ------------------- src/models/REAL_TIME_timer.js | 13 -- src/models/oldTimer.js | 14 -- src/routes/timerRouter.js | 15 -- src/startup/routes.js | 3 - 5 files changed, 200 deletions(-) delete mode 100644 src/controllers/REAL_TIME_timerController.js delete mode 100644 src/models/REAL_TIME_timer.js delete mode 100644 src/models/oldTimer.js delete mode 100644 src/routes/timerRouter.js diff --git a/src/controllers/REAL_TIME_timerController.js b/src/controllers/REAL_TIME_timerController.js deleted file mode 100644 index 699dcddef..000000000 --- a/src/controllers/REAL_TIME_timerController.js +++ /dev/null @@ -1,155 +0,0 @@ - -const logger = require('../startup/logger'); -const OldTimer = require('../models/oldTimer'); - -const timerController = function (Timer) { - const getTimerFromDatabase = async ({ userId }) => { - try { - const timerObject = await Timer.findOne({ userId }).exec(); - if (!timerObject) { - const newRecord = { - userId, - totalSeconds: 0, - isRunning: false, - isApplicationPaused: false, - isUserPaused: false, - }; - const newTimer = await Timer.create(newRecord); - return newTimer; - } - return timerObject; - } catch (e) { - logger.logException(e); - throw new Error('Issue trying to retrieve timer data from MongoDB'); - } - }; - - const setTimerToDatabase = async ({ - userId, - timerObject: { - totalSeconds, - isRunning, - isUserPaused, - isApplicationPaused, - } = {}, - } = {}) => { - try { - const update = { - $set: { - totalSeconds, - isRunning, - isUserPaused, - isApplicationPaused, - }, - }; - - const options = { - upsert: true, - new: true, - setDefaultsOnInsert: true, - rawResult: true, - }; - - return await Timer.findOneAndUpdate({ userId }, update, options).exec(); - } catch (e) { - logger.logException(e); - throw new Error('Issue trying to set timer data from MongoDB'); - } - }; - - const putTimer = function (req, res) { - const { userId } = req.params; - - const query = { userId }; - const update = { - $set: { - pausedAt: req.body.pausedAt, - isWorking: req.body.isWorking, - started: req.body.isWorking ? Date.now() : null, - lastAccess: Date.now(), - }, - }; - const options = { - upsert: true, new: true, setDefaultsOnInsert: true, rawResult: true, - }; - - OldTimer.findOneAndUpdate(query, update, options, (error, rawResult) => { - if (error) { - return res.status(500).send({ error }); - } - - if (rawResult === null || rawResult.value === undefined || rawResult.value === null - || rawResult.lastErrorObject === null || rawResult.lastErrorObject === undefined - || rawResult.value.length === 0) { - return res.status(500).send('Update/Upsert timer date failed'); - } - - if (rawResult.lastErrorObject.updatedExisting === true) { - return res.status(200).send({ message: 'updated timer data' }); - } - if (rawResult.lastErrorObject.updatedExisting === false - && rawResult.lastErrorObject.upserted !== undefined && rawResult.lastErrorObject.upserted !== null) { - return res.status(201).send({ _id: rawResult.lastErrorObject.upserted }); - } - return res.status(500).send('Update/Upsert timer date failed'); - }); - }; - - const timePassed = (timer) => { - if (!timer.started) { return 0; } - const now = timer.timedOut ? timer.lastAccess : Date.now(); - return Math.floor((now - timer.started) / 1000); - }; - - const adjust = (timer, cb) => { - const oneMin = 60 * 1000; - const fiveMin = 5 * oneMin; - const timeSinceLastAccess = timer.lastAccess ? (Date.now() - timer.lastAccess) : 0; - const setLastAccess = !timer.lastAccess || (timeSinceLastAccess > oneMin); - - timer.timedOut = timer.isWorking && (timeSinceLastAccess > fiveMin); - timer.seconds = timer.pausedAt + timePassed(timer); - - if (timer.timedOut) { - return OldTimer.findOneAndUpdate({ userId: timer.userId }, { - isWorking: false, - pauseAt: timer.seconds, - started: null, - lastAccess: Date.now(), - }).then(() => cb(timer)); - } - if (setLastAccess) { - return OldTimer.findOneAndUpdate({ userId: timer.userId }, { lastAccess: Date.now() }).then(() => cb(timer)); - } - - return cb(timer); - }; - - const getTimer = function (req, res) { - const { userId } = req.params; - - OldTimer.findOne({ userId }).lean().exec((error, record) => { - if (error) { - return res.status(500).send(error); - } - if (record === null) { - if (req.body.requestor.requestorId === userId) { - const newRecord = { - userId, - pausedAt: 0, - isWorking: false, - }; - return OldTimer.create(newRecord).then(result => res.status(200).send(result)).catch(() => res.status(400).send('Timer record not found for the given user ID')); - } - return res.status(400).send('Timer record not found for the given user ID'); - } - return adjust(record, (timer) => { res.status(200).send(timer); }); - }); - }; - - return { - putTimer, getTimer, getTimerFromDatabase, setTimerToDatabase, - }; -}; - -module.exports = timerController; diff --git a/src/models/REAL_TIME_timer.js b/src/models/REAL_TIME_timer.js deleted file mode 100644 index 4e143aeaa..000000000 --- a/src/models/REAL_TIME_timer.js +++ /dev/null @@ -1,13 +0,0 @@ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; - -const timerSchema = new Schema({ - userId: { type: Schema.Types.ObjectId, required: true, ref: 'userProfile' }, - totalSeconds: { type: Number, default: 0 }, - isRunning: { type: Boolean, default: false }, - isUserPaused: { type: Boolean, default: false }, - isApplicationPaused: { type: Boolean, default: false }, -}); - -module.exports = mongoose.model('newTimer', timerSchema, 'newTimers'); diff --git a/src/models/oldTimer.js b/src/models/oldTimer.js deleted file mode 100644 index dca0ade1a..000000000 --- a/src/models/oldTimer.js +++ /dev/null @@ -1,14 +0,0 @@ -const mongoose = require('mongoose'); - -const { Schema } = mongoose; - -const timerSchema = new Schema({ - userId: { type: Schema.Types.ObjectId, required: true, ref: 'userProfile' }, - pausedAt: { type: Number, default: 0 }, - isWorking: { type: Boolean, default: false }, - started: { type: Date }, - lastAccess: { type: Date }, - }); - - -module.exports = mongoose.model('timer', timerSchema, 'timers'); diff --git a/src/routes/timerRouter.js b/src/routes/timerRouter.js deleted file mode 100644 index 094b2ba81..000000000 --- a/src/routes/timerRouter.js +++ /dev/null @@ -1,15 +0,0 @@ -const express = require('express'); - -const routes = function (Timer) { - const TimerRouter = express.Router(); - - const controller = require('../controllers/REAL_TIME_timerController')(Timer); - - TimerRouter.route('/timer/:userId') - .put(controller.putTimer) - .get(controller.getTimer); - - return TimerRouter; -}; - -module.exports = routes; diff --git a/src/startup/routes.js b/src/startup/routes.js index 2fd7337a6..eae746e24 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -7,7 +7,6 @@ const actionItem = require('../models/actionItem'); const notification = require('../models/notification'); const wbs = require('../models/wbs'); const task = require('../models/task'); -const timer = require('../models/timer'); const popup = require('../models/popupEditor'); const popupBackup = require('../models/popupEditorBackup'); const taskNotification = require('../models/taskNotification'); @@ -38,7 +37,6 @@ const forcePwdRouter = require('../routes/forcePwdRouter')(userProfile); const reportsRouter = require('../routes/reportsRouter')(); const wbsRouter = require('../routes/wbsRouter')(wbs); const taskRouter = require('../routes/taskRouter')(task); -const timerRouter = require('../routes/timerRouter')(timer); const popupRouter = require('../routes/popupEditorRouter')(popup); const popupBackupRouter = require('../routes/popupEditorBackupRouter')(popupBackup); const taskNotificationRouter = require('../routes/taskNotificationRouter')(taskNotification); @@ -76,7 +74,6 @@ module.exports = function (app) { app.use('/api', reportsRouter); app.use('/api', wbsRouter); app.use('/api', taskRouter); - app.use('/api', timerRouter); app.use('/api', popupRouter); app.use('/api', popupBackupRouter); app.use('/api', taskNotificationRouter); From 467cbb84140103fcdd0ad97c728584d1d6156229 Mon Sep 17 00:00:00 2001 From: wang9hu Date: Wed, 25 Oct 2023 18:31:29 -0700 Subject: [PATCH 09/22] remove 2h default time when clear timer --- src/websockets/TimerService/clientsHandler.js | 77 +------------------ 1 file changed, 1 insertion(+), 76 deletions(-) diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index 6990ead71..e9e932fd7 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -4,13 +4,6 @@ const moment = require('moment'); const Timer = require('../../models/timer'); const logger = require('../../startup/logger'); -/** - * Here we get the timer. - * If the timer already exists in memory, we return it. - * If it doesn't exist, we try to get it from MongoDB. - * If it doesn't exist in MongoDB, we create it and save it to MongoDB. - * Then we save it to memory and return it. - */ export 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 @@ -29,13 +22,6 @@ export const getClient = async (clients, userId) => { return clients.get(userId); }; -/** - * Save client info to database - * Save under these conditions: - * connection is normally closed (paused and closed); - * connection is forced-paused (timer still on and connection closed) - * message: STOP_TIMER - */ export const saveClient = async (client) => { try { await Timer.findOneAndUpdate({ userId: client.userId }, client); @@ -47,10 +33,6 @@ export const saveClient = async (client) => { } }; -/** - * This is the contract between client and server. - * The client can send one of the following messages to the server: - */ export const action = { START_TIMER: 'START_TIMER', PAUSE_TIMER: 'PAUSE_TIMER', @@ -75,12 +57,6 @@ const updatedTimeSinceStart = (client) => { return updatedTime > 0 ? updatedTime : 0; }; -/** - * Here we start the timer, if it is not already started. - * We set the last access time to now, and set the paused and stopped flags to false. - * If the timer was paused, we need to check if it was paused by the user or by the server. - * If it was paused by the server, we need to set the forcedPause flag to true. - */ const startTimer = (client) => { client.startAt = moment.utc(); client.paused = false; @@ -91,31 +67,19 @@ const startTimer = (client) => { if (client.forcedPause) client.forcedPause = false; }; -/** - * Here we pause the timer, if it is not already paused. - * We get the total elapsed time since the last access, and set it as the new time. - * We set the last access time to now, and set the paused flag to true. - * If the timer was paused by the server, we need to set the forcedPause flag to true. - * It'll only be triggered when the user closes the connection sudenlly or lacks of ACKs. - */ const pauseTimer = (client, forced = false) => { client.time = updatedTimeSinceStart(client); - client.startAt = moment.invalid(); + client.startAt = moment.invalid(); // invalid can not be saved in database client.paused = true; if (forced) client.forcedPause = true; }; -// Here we acknowledge the forced pause. To prevent the modal for beeing displayed again. const ackForcedPause = (client) => { client.forcedPause = false; client.paused = true; client.startAt = moment.invalid(); }; -/** - * Here we stop the timer. - * We pause the timer and set the stopped flag to true. - */ const stopTimer = (client) => { client.startAt = moment.invalid(); client.started = false; @@ -123,25 +87,11 @@ const stopTimer = (client) => { client.forcedPause = false; }; -/** - * Here we clear the timer. - * We pause the timer and check it's mode to set the time to 0 or the goal. - * Then we set the stopped flag to false. - */ const clearTimer = (client) => { stopTimer(client); - client.goal = moment.duration(2, 'hours').asMilliseconds(); client.time = client.goal; }; -// Here we set the goal and time to the goal time. -/** - * Here we set the goal. - * if timer has not started, we set both time and goal to the new goal - * if timer has started, we calculate the passed time and remove that from new goal - * and if passed time is greater than new goal, then set time to 0, but this should - * not be prohibited by frontend. - */ const setGoal = (client, msg) => { const newGoal = parseInt(msg.split('=')[1]); if (!client.started) { @@ -159,13 +109,6 @@ const setGoal = (client, msg) => { } }; -/** - * Here we add the goal time. - * Each addition add 15min - * First we get the goal time from the message. - * Then we add it to the current goal time and set it as the new goal time. - * We also add it to the current time and set it as the new time. - */ const addGoal = (client, msg) => { const duration = parseInt(msg.split('=')[1]); const goalAfterAddition = moment @@ -187,14 +130,6 @@ const addGoal = (client, msg) => { .toFixed(); }; -/** - * Here we try to remove a goal time. - * First we get the goal time from the message. - * Then we subtract it from the current goal time and set it as the new goal time. - * We also subtract it from the current time and set it as the new time. - * If the new goal time is less than 15 minutes, we don't do anything. - * If the new time is less than 0, we set it to 0. - */ const removeGoal = (client, msg) => { const duration = parseInt(msg.split('=')[1]); const goalAfterRemoval = moment @@ -220,16 +155,6 @@ const removeGoal = (client, msg) => { .toFixed(); }; - -/** - * Here is were we handle the messages. - * First we check if the user is in memory, if not, we throw an error. - * Then we parse the request and check which action it is and call the corresponding function. - * If we don't have a match, we just return an error. - * The only operation that we write to Mongo it's the stop timer. Other operations are just in memory. - * So the slowest part of the app is the save to Mongo. - * Then we update the current client in hash map and return the response. - */ export const handleMessage = async (msg, clients, userId) => { if (!clients.has(userId)) { throw new Error('It should have this user in memory'); From 29e9bb2c3fddcf4d1a47607691c0ba2e0a4d0d12 Mon Sep 17 00:00:00 2001 From: wang9hu Date: Wed, 25 Oct 2023 23:36:47 -0700 Subject: [PATCH 10/22] make timer keep remaining time as goal after log time, and add initial goal in timer modal --- src/models/timer.js | 1 + src/websockets/TimerService/clientsHandler.js | 24 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/models/timer.js b/src/models/timer.js index 8afbad119..09c2f89da 100644 --- a/src/models/timer.js +++ b/src/models/timer.js @@ -8,6 +8,7 @@ const timerSchema = new Schema({ startAt: { type: Date, default: Date.now }, time: { type: Number, default: 900000 }, goal: { type: Number, default: 900000 }, + initialGoal: { type: Number, default: 900000 }, paused: { type: Boolean, default: false }, forcedPause: { type: Boolean, default: false }, started: { type: Boolean, default: false }, diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index e9e932fd7..3bb2c358d 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -81,32 +81,30 @@ const ackForcedPause = (client) => { }; const stopTimer = (client) => { + if (client.started) pauseTimer(client); client.startAt = moment.invalid(); client.started = false; client.pause = false; client.forcedPause = false; + if (client.time === 0) { + client.goal = client.initialGoal; + client.time = client.goal; + } else { + client.goal = client.time; + } }; const clearTimer = (client) => { stopTimer(client); + client.goal = client.initialGoal; client.time = client.goal; }; const setGoal = (client, msg) => { const newGoal = parseInt(msg.split('=')[1]); - if (!client.started) { - client.goal = newGoal; - client.time = newGoal; - } else { - const passedTime = client.goal - client.time; - if (passedTime >= newGoal) { - client.time = 0; - client.goal = passedTime; - } else { - client.time = newGoal - passedTime; - client.goal = newGoal; - } - } + client.goal = newGoal; + client.time = newGoal; + client.initialGoal = newGoal; }; const addGoal = (client, msg) => { From 8d37664a8b3ca4603ee5e9cd9f8f55d5c98c50a0 Mon Sep 17 00:00:00 2001 From: Carl Bebli Date: Thu, 26 Oct 2023 16:56:16 +0000 Subject: [PATCH 11/22] included replyTo field to the emailOptions --- src/utilities/emailSender.js | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index 4841d1410..98ca3e337 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -1,7 +1,6 @@ -const nodemailer = require('nodemailer'); -const { google } = require('googleapis'); -const logger = require('../startup/logger'); - +const nodemailer = require("nodemailer"); +const { google } = require("googleapis"); +const logger = require("../startup/logger"); const closure = () => { const queue = []; @@ -13,9 +12,9 @@ const closure = () => { const REFRESH_TOKEN = process.env.REACT_APP_EMAIL_REFRESH_TOKEN; // Create the email envelope (transport) const transporter = nodemailer.createTransport({ - service: 'gmail', + service: "gmail", auth: { - type: 'OAuth2', + type: "OAuth2", user: CLIENT_EMAIL, clientId: CLIENT_ID, clientSecret: CLIENT_SECRET, @@ -25,7 +24,7 @@ const closure = () => { const OAuth2Client = new google.auth.OAuth2( CLIENT_ID, CLIENT_SECRET, - REDIRECT_URI, + REDIRECT_URI ); OAuth2Client.setCredentials({ refresh_token: REFRESH_TOKEN }); @@ -35,9 +34,7 @@ const closure = () => { if (!nextItem) return; - const { - recipient, subject, message, cc, bcc, - } = nextItem; + const { recipient, subject, message, cc, bcc, replyTo } = nextItem; try { // Generate the accessToken on the fly @@ -51,6 +48,7 @@ const closure = () => { bcc, subject, html: message, + replyTo, auth: { user: CLIENT_EMAIL, refreshToken: REFRESH_TOKEN, @@ -65,10 +63,22 @@ const closure = () => { } }, process.env.MAIL_QUEUE_INTERVAL || 1000); - const emailSender = function (recipient, subject, message, cc = null, bcc = null) { + const emailSender = function ( + recipient, + subject, + message, + cc = null, + bcc = null, + replyTo = null + ) { if (process.env.sendEmail) { queue.push({ - recipient, subject, message, cc, bcc, + recipient, + subject, + message, + cc, + bcc, + replyTo, }); } }; From 326f7fece0a63501551c69ff5e1a7c87cc19a28e Mon Sep 17 00:00:00 2001 From: Carl Bebli Date: Thu, 26 Oct 2023 16:57:01 +0000 Subject: [PATCH 12/22] improved the message received by one community when a user makes a suggestion --- src/controllers/dashBoardController.js | 120 ++++++++++++++----------- 1 file changed, 66 insertions(+), 54 deletions(-) diff --git a/src/controllers/dashBoardController.js b/src/controllers/dashBoardController.js index c9cdbd588..aacc915dd 100644 --- a/src/controllers/dashBoardController.js +++ b/src/controllers/dashBoardController.js @@ -1,8 +1,8 @@ -const mongoose = require("mongoose"); -const path = require("path"); -const fs = require("fs/promises"); -const dashboardhelper = require("../helpers/dashboardhelper")(); -const emailSender = require("../utilities/emailSender"); +const path = require('path'); +const fs = require('fs/promises'); +const mongoose = require('mongoose'); +const dashboardhelper = require('../helpers/dashboardhelper')(); +const emailSender = require('../utilities/emailSender'); const dashboardcontroller = function () { const dashboarddata = function (req, res) { @@ -20,13 +20,13 @@ 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) { const emptyresult = [ { - projectName: "", + projectName: '', timeSpent_hrs: 0, }, ]; @@ -42,7 +42,7 @@ const dashboardcontroller = function () { const laborthisweek = dashboardhelper.laborthisweek( userId, req.params.fromDate, - req.params.toDate + req.params.toDate, ); laborthisweek.then((results) => { res.send(results).status(200); @@ -63,7 +63,7 @@ const dashboardcontroller = function () { }); } }) - .catch((error) => res.status(400).send(error)); + .catch(error => res.status(400).send(error)); }; const orgData = function (req, res) { @@ -73,7 +73,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 ( @@ -85,7 +85,7 @@ const dashboardcontroller = function () { expected, actual, visual, - severity + severity, ) { const text = `New Bug Report From ${firstName} ${lastName}:

[Feature Name] Bug Title:

@@ -130,32 +130,32 @@ const dashboardcontroller = function () { expected, actual, visual, - severity + severity, ); try { emailSender( - "onecommunityglobal@gmail.com", + 'onecommunityglobal@gmail.com', `Bug Rport from ${firstName} ${lastName}`, emailBody, - email + email, ); - res.status(200).send("Success"); + res.status(200).send('Success'); } catch { - res.status(500).send("Failed"); + res.status(500).send('Failed'); } }; const suggestionData = { suggestion: [ - "Identify and remedy poor client and/or user service experiences", - "Identify bright spots and enhance positive service experiences", - "Make fundamental changes to our programs and/or operations", - "Inform the development of new programs/projects", - "Identify where we are less inclusive or equitable across demographic groups", - "Strengthen relationships with the people we serve", + 'Identify and remedy poor client and/or user service experiences', + 'Identify bright spots and enhance positive service experiences', + 'Make fundamental changes to our programs and/or operations', + 'Inform the development of new programs/projects', + 'Identify where we are less inclusive or equitable across demographic groups', + 'Strengthen relationships with the people we serve', "Understand people's needs and how we can help them achieve their goals", - "Other", + 'Other', ], field: [], }; @@ -164,42 +164,54 @@ const dashboardcontroller = function () { let fieldaaray = []; if (suggestionData.field.length) { fieldaaray = suggestionData.field.map( - (item) => `

${item}

-

${args[3][item]}

` + item => `

${item}

+

${args[3][item]}

`, ); } - const text = `New Suggestion: -

Suggestion Category:

-

${args[0]}

-

Suggestion:

-

${args[1]}

- ${fieldaaray.length > 0 ? fieldaaray : ""} -

Wants Feedback:

-

${args[2]}

-

Thank you,
- One Community

`; + const text = `New Suggestion From ${args[3].firstName} ${ + args[3].lastName + }: +

Suggestion Category:

+

${args[0]}

+

Suggestion:

+

${args[1]}

+ ${fieldaaray.length > 0 ? fieldaaray : ''} +

Name of Suggester:

+

${args[3].firstName} ${args[3].lastName}

+

Email of Suggester:

+

${args[4]}

+

Wants Feedback:

+

${args[2]}

+

Thank you,
+ One Community

`; return text; }; // send suggestion email const sendMakeSuggestion = async (req, res) => { - const { suggestioncate, suggestion, confirm, ...rest } = req.body; + const { + suggestioncate, suggestion, confirm, email, ...rest +} = req.body; const emailBody = await getsuggestionEmailBody( suggestioncate, suggestion, confirm, - rest + rest, + email, ); try { emailSender( - "onecommunityglobal@gmail.com", - "A new suggestion", - emailBody + 'beblicarl.cb@gmail.com', + 'A new suggestion', + emailBody, + null, + null, + email, ); - res.status(200).send("Success"); + res.status(200).send('Success'); } catch { - res.status(500).send("Failed"); + res.status(500).send('Failed'); } }; @@ -208,40 +220,40 @@ const dashboardcontroller = function () { if (suggestionData) { res.status(200).send(suggestionData); } else { - res.status(404).send("Suggestion data not found."); + res.status(404).send('Suggestion data not found.'); } } catch (error) { - console.error("Error getting suggestion data:", error); - res.status(500).send("Internal Server Error"); + console.error('Error getting suggestion data:', error); + res.status(500).send('Internal Server Error'); } }; const editSuggestionOption = async (req, res) => { try { if (req.body.suggestion) { - if (req.body.action === "add") { + if (req.body.action === 'add') { suggestionData.suggestion.unshift(req.body.newField); } - if (req.body.action === "delete") { + 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 { - if (req.body.action === "add") { + if (req.body.action === 'add') { suggestionData.field.unshift(req.body.newField); } - if (req.body.action === "delete") { + if (req.body.action === 'delete') { suggestionData.field = suggestionData.field.filter( - (item) => item !== req.body.newField + item => item !== req.body.newField, ); } } - res.status(200).send("success"); + res.status(200).send('success'); } catch (error) { - console.error("Error editing suggestion option:", error); - res.status(500).send("Internal Server Error"); + console.error('Error editing suggestion option:', error); + res.status(500).send('Internal Server Error'); } }; From 815b0f5ae588efb855fbde13fa65f301280b7686 Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Thu, 26 Oct 2023 13:23:24 -0700 Subject: [PATCH 13/22] update projects controller to return basic proj info --- .../bmdashboard/bmMaterialsController.js | 24 +++++++++---------- .../bmdashboard/bmProjectsController.js | 23 ++++++++++++++---- src/routes/bmdashboard/bmProjectsRouter.js | 7 +++--- src/startup/routes.js | 2 +- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/controllers/bmdashboard/bmMaterialsController.js b/src/controllers/bmdashboard/bmMaterialsController.js index a31ed460e..fb6003e90 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 (ItemMaterial) { const bmMaterialsList = async function _matsList(req, res) { @@ -7,37 +7,37 @@ const bmMaterialsController = function (ItemMaterial) { .populate([ { path: 'project', - select: '_id projectName' + select: '_id projectName', }, { path: 'inventoryItemType', - select: '_id name uom totalStock totalAvailable' + select: '_id name uom totalStock totalAvailable', }, { path: 'usageRecord', populate: { path: 'createdBy', - select: '_id firstName lastName' - } + select: '_id firstName lastName', + }, }, { path: 'updateRecord', populate: { path: 'createdBy', - select: '_id firstName lastName' - } + select: '_id firstName lastName', + }, }, { path: 'purchaseRecord', populate: { path: 'createdBy', - select: '_id firstName lastName' - } - } + select: '_id firstName lastName', + }, + }, ]) .exec() .then(results => res.status(200).send(results)) - .catch(error => res.status(500).send(error)) + .catch(error => res.status(500).send(error)); } catch (err) { res.json(err); } @@ -45,4 +45,4 @@ const bmMaterialsController = function (ItemMaterial) { return { bmMaterialsList }; }; -module.exports = bmMaterialsController; \ No newline at end of file +module.exports = bmMaterialsController; diff --git a/src/controllers/bmdashboard/bmProjectsController.js b/src/controllers/bmdashboard/bmProjectsController.js index 946869bce..f80eea6c0 100644 --- a/src/controllers/bmdashboard/bmProjectsController.js +++ b/src/controllers/bmdashboard/bmProjectsController.js @@ -1,13 +1,26 @@ const mongoose = require('mongoose'); -const bmMProjectsController = function () { - // fetches projects with reference to BM userProfile id +const bmMProjectsController = function (UserProfile) { const bmProjectsSummary = async function _projSumm(req, res) { + const { userId } = req.params; try { - res.json({ message: 'Hello world' }); + const projectData = await UserProfile + // fetch user profile, return only projects array + .findOne({ _id: userId }, { projects: 1 }) + // populate data with projects documents using the ObjectId in the projects array + .populate({ + path: 'projects', + // limit to projects with category value 'Housing' + match: { category: 'Housing' }, + // returns only these fields + select: '_id projectName isActive createdDatetime', + }) + .exec() + .then(result => result.projects) + .catch(error => res.status(500).send(error)); - // .then(results => res.status(200).send(results)) - // .catch(error => res.status(500).send(error)) + // for each project, find all materials in the project + res.status(200).send(projectData); } catch (err) { res.json(err); } diff --git a/src/routes/bmdashboard/bmProjectsRouter.js b/src/routes/bmdashboard/bmProjectsRouter.js index 204efbfad..05adfa59d 100644 --- a/src/routes/bmdashboard/bmProjectsRouter.js +++ b/src/routes/bmdashboard/bmProjectsRouter.js @@ -1,11 +1,10 @@ const express = require('express'); -const routes = function () { +const routes = function (userProfile) { const projectsRouter = express.Router(); - const controller = require('../../controllers/bmdashboard/bmProjectsController')(); -// const controller = require('../../controllers/bmdashboard/bmMaterialsController')(itemMaterial); + const controller = require('../../controllers/bmdashboard/bmProjectsController')(userProfile); -projectsRouter.route('/projects') +projectsRouter.route('/projects/:userId') .get(controller.bmProjectsSummary); return projectsRouter; diff --git a/src/startup/routes.js b/src/startup/routes.js index 7208535d8..aa786dbbd 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -61,7 +61,7 @@ const mouseoverTextRouter = require('../routes/mouseoverTextRouter')(mouseoverTe // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(inventoryItemMaterial); -const bmProjectsRouter = require('../routes/bmdashboard/bmProjectsRouter')(); +const bmProjectsRouter = require('../routes/bmdashboard/bmProjectsRouter')(userProfile); module.exports = function (app) { app.use('/api', forgotPwdRouter); From bd8f4de8a7ff455500408c608a32f81ddb4c79a3 Mon Sep 17 00:00:00 2001 From: wang9hu Date: Sun, 29 Oct 2023 14:49:00 -0700 Subject: [PATCH 14/22] make timer chime consistent over all tabs --- src/models/timer.js | 1 + src/websockets/TimerService/clientsHandler.js | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/models/timer.js b/src/models/timer.js index 09c2f89da..f50921fb3 100644 --- a/src/models/timer.js +++ b/src/models/timer.js @@ -9,6 +9,7 @@ const timerSchema = new Schema({ time: { type: Number, default: 900000 }, goal: { type: Number, default: 900000 }, initialGoal: { type: Number, default: 900000 }, + chiming: { type: Boolean, default: false }, paused: { type: Boolean, default: false }, forcedPause: { type: Boolean, default: false }, started: { type: Boolean, default: false }, diff --git a/src/websockets/TimerService/clientsHandler.js b/src/websockets/TimerService/clientsHandler.js index 3bb2c358d..60eb33fa4 100644 --- a/src/websockets/TimerService/clientsHandler.js +++ b/src/websockets/TimerService/clientsHandler.js @@ -43,6 +43,7 @@ export const action = { REMOVE_GOAL: 'REMOVE_FROM_GOAL=', FORCED_PAUSE: 'FORCED_PAUSE', ACK_FORCED: 'ACK_FORCED', + START_CHIME: 'START_CHIME', }; const MAX_HOURS = 5; @@ -69,11 +70,17 @@ const startTimer = (client) => { const pauseTimer = (client, forced = false) => { client.time = updatedTimeSinceStart(client); + if (client.time === 0) client.chiming = true; client.startAt = moment.invalid(); // invalid can not be saved in database client.paused = true; if (forced) client.forcedPause = true; }; +const startChime = (client, msg) => { + const state = msg.split('=')[1]; + client.chiming = state === 'true'; +}; + const ackForcedPause = (client) => { client.forcedPause = false; client.paused = true; @@ -86,6 +93,7 @@ const stopTimer = (client) => { client.started = false; client.pause = false; client.forcedPause = false; + if (client.chiming) client.chiming = false; if (client.time === 0) { client.goal = client.initialGoal; client.time = client.goal; @@ -97,6 +105,7 @@ const stopTimer = (client) => { const clearTimer = (client) => { stopTimer(client); client.goal = client.initialGoal; + client.chiming = false; client.time = client.goal; }; @@ -175,6 +184,9 @@ export const handleMessage = async (msg, clients, userId) => { case req.match(/REMOVE_FROM_GOAL=/i)?.input: removeGoal(client, req); break; + case req.match(/START_CHIME=/i)?.input: + startChime(client, req); + break; case action.PAUSE_TIMER: pauseTimer(client); break; From 64f6c4121012e4e10d01568e847a6172562a2512 Mon Sep 17 00:00:00 2001 From: AriaYu927 Date: Tue, 31 Oct 2023 16:04:22 +0800 Subject: [PATCH 15/22] set default value for createdDatetime --- src/models/team.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/team.js b/src/models/team.js index 97f8dc360..00fbaf8e3 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -5,7 +5,7 @@ const { Schema } = mongoose; const team = new Schema({ teamName: { type: 'String', required: true }, isActive: { type: 'Boolean', required: true, default: true }, - createdDatetime: { type: Date }, + createdDatetime: { type: Date, default: Date.now() }, modifiedDatetime: { type: Date, default: Date.now() }, members: [ { From 66c6c3c8d8d1f637efc7cb850b03f269149f428c Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Wed, 1 Nov 2023 09:22:24 -0700 Subject: [PATCH 16/22] add building project schema. update project summary controller. --- .../bmdashboard/bmProjectController.js | 27 ++++++++++++++++ .../bmdashboard/bmProjectsController.js | 31 ------------------- src/models/bmdashboard/buildingProject.js | 15 +++++++++ src/models/inventoryItemMaterial.js | 6 ++-- src/routes/bmdashboard/bmProjectRouter.js | 13 ++++++++ src/routes/bmdashboard/bmProjectsRouter.js | 13 -------- src/startup/routes.js | 6 ++-- 7 files changed, 62 insertions(+), 49 deletions(-) create mode 100644 src/controllers/bmdashboard/bmProjectController.js delete mode 100644 src/controllers/bmdashboard/bmProjectsController.js create mode 100644 src/models/bmdashboard/buildingProject.js create mode 100644 src/routes/bmdashboard/bmProjectRouter.js delete mode 100644 src/routes/bmdashboard/bmProjectsRouter.js diff --git a/src/controllers/bmdashboard/bmProjectController.js b/src/controllers/bmdashboard/bmProjectController.js new file mode 100644 index 000000000..96f19ab60 --- /dev/null +++ b/src/controllers/bmdashboard/bmProjectController.js @@ -0,0 +1,27 @@ +const bmMProjectController = function (BuildingProject) { + const bmProjectSummary = async function _projSumm(req, res) { + try { + const projectData = await BuildingProject + .find() + .populate([ + { + path: 'buildingManager', + select: '_id firstName lastName email', + }, + { + path: 'team', + select: '_id firstName lastName email', + }, + ]) + .exec() + .then(result => result) + .catch(error => res.status(500).send(error)); + res.status(200).send(projectData); + } catch (err) { + res.json(err); + } + }; + return { bmProjectSummary }; +}; + +module.exports = bmMProjectController; diff --git a/src/controllers/bmdashboard/bmProjectsController.js b/src/controllers/bmdashboard/bmProjectsController.js deleted file mode 100644 index f80eea6c0..000000000 --- a/src/controllers/bmdashboard/bmProjectsController.js +++ /dev/null @@ -1,31 +0,0 @@ -const mongoose = require('mongoose'); - -const bmMProjectsController = function (UserProfile) { - const bmProjectsSummary = async function _projSumm(req, res) { - const { userId } = req.params; - try { - const projectData = await UserProfile - // fetch user profile, return only projects array - .findOne({ _id: userId }, { projects: 1 }) - // populate data with projects documents using the ObjectId in the projects array - .populate({ - path: 'projects', - // limit to projects with category value 'Housing' - match: { category: 'Housing' }, - // returns only these fields - select: '_id projectName isActive createdDatetime', - }) - .exec() - .then(result => result.projects) - .catch(error => res.status(500).send(error)); - - // for each project, find all materials in the project - res.status(200).send(projectData); - } catch (err) { - res.json(err); - } - }; - return { bmProjectsSummary }; -}; - -module.exports = bmMProjectsController; diff --git a/src/models/bmdashboard/buildingProject.js b/src/models/bmdashboard/buildingProject.js new file mode 100644 index 000000000..566bc124e --- /dev/null +++ b/src/models/bmdashboard/buildingProject.js @@ -0,0 +1,15 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const buildingProject = new Schema({ + isActive: Boolean, + name: String, + template: String, // construction template (ie Earthbag Village) + location: String, // use lat/lng instead? + dateCreated: { type: Date, default: Date.now }, + buildingManager: { type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }, // BM's id + team: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'userProfile' }], +}); + +module.exports = mongoose.model('buildingProject', buildingProject, 'buildingProjects'); diff --git a/src/models/inventoryItemMaterial.js b/src/models/inventoryItemMaterial.js index e6153af45..8460ecd6e 100644 --- a/src/models/inventoryItemMaterial.js +++ b/src/models/inventoryItemMaterial.js @@ -29,12 +29,12 @@ const InventoryItemMaterial = new Schema({ poId: { type: String, required: true }, sellerId: { type: String, required: true }, quantity: { type: Number, required: true }, // adds to stockBought - unitPrice: {type: Number, required: true}, + unitPrice: { type: Number, required: true }, currency: { type: String, required: true }, subtotal: { type: Number, required: true }, tax: { type: Number, required: true }, shipping: { type: Number, required: true }, - }] -}) + }], +}); module.exports = mongoose.model('inventoryItemMaterial', InventoryItemMaterial, 'inventoryMaterial'); diff --git a/src/routes/bmdashboard/bmProjectRouter.js b/src/routes/bmdashboard/bmProjectRouter.js new file mode 100644 index 000000000..6bd535ae1 --- /dev/null +++ b/src/routes/bmdashboard/bmProjectRouter.js @@ -0,0 +1,13 @@ +const express = require('express'); + +const routes = function (buildingProject) { + const projectRouter = express.Router(); + const controller = require('../../controllers/bmdashboard/bmProjectController')(buildingProject); + +projectRouter.route('/projects') + .get(controller.bmProjectSummary); + + return projectRouter; +}; + +module.exports = routes; diff --git a/src/routes/bmdashboard/bmProjectsRouter.js b/src/routes/bmdashboard/bmProjectsRouter.js deleted file mode 100644 index 05adfa59d..000000000 --- a/src/routes/bmdashboard/bmProjectsRouter.js +++ /dev/null @@ -1,13 +0,0 @@ -const express = require('express'); - -const routes = function (userProfile) { - const projectsRouter = express.Router(); - const controller = require('../../controllers/bmdashboard/bmProjectsController')(userProfile); - -projectsRouter.route('/projects/:userId') - .get(controller.bmProjectsSummary); - - return projectsRouter; -}; - -module.exports = routes; diff --git a/src/startup/routes.js b/src/startup/routes.js index aa786dbbd..7f9446315 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -22,6 +22,8 @@ const profileInitialSetuptoken = require('../models/profileInitialSetupToken'); const reason = require('../models/reason'); const mouseoverText = require('../models/mouseoverText'); const inventoryItemMaterial = require('../models/inventoryItemMaterial'); +const buildingProject = require('../models/bmdashboard/buildingProject'); + const userProfileRouter = require('../routes/userProfileRouter')(userProfile); const badgeRouter = require('../routes/badgeRouter')(badge); @@ -61,7 +63,7 @@ const mouseoverTextRouter = require('../routes/mouseoverTextRouter')(mouseoverTe // bm dashboard const bmLoginRouter = require('../routes/bmdashboard/bmLoginRouter')(); const bmMaterialsRouter = require('../routes/bmdashboard/bmMaterialsRouter')(inventoryItemMaterial); -const bmProjectsRouter = require('../routes/bmdashboard/bmProjectsRouter')(userProfile); +const bmProjectRouter = require('../routes/bmdashboard/bmProjectRouter')(buildingProject); module.exports = function (app) { app.use('/api', forgotPwdRouter); @@ -97,5 +99,5 @@ module.exports = function (app) { // bm dashboard app.use('/api/bm', bmLoginRouter); app.use('/api/bm', bmMaterialsRouter); - app.use('/api/bm', bmProjectsRouter); + app.use('/api/bm', bmProjectRouter); }; From 4e318bc4046fb42324a9b12b0adc5c1522208f5c Mon Sep 17 00:00:00 2001 From: wang9hu Date: Sat, 4 Nov 2023 17:01:10 -0700 Subject: [PATCH 17/22] add empty string in teamCode validator for creating new user --- src/models/userProfile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 4739a05e7..3219fec18 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -159,7 +159,7 @@ const userProfileSchema = new Schema({ default: '', validate: { validator(v) { - const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$/; + const teamCoderegex = /^([a-zA-Z]-[a-zA-Z]{3}|[a-zA-Z]{5})$|^$/; return teamCoderegex.test(v); }, message: From 0058b42a8bd1b7ad00cd4e9aeec2007ec40c6c2d Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Mon, 6 Nov 2023 13:02:46 -0800 Subject: [PATCH 18/22] add fetch single project api. update fetch all projects query. --- .../bmdashboard/bmProjectController.js | 43 +++++++++++++++++-- src/routes/bmdashboard/bmProjectRouter.js | 7 ++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/controllers/bmdashboard/bmProjectController.js b/src/controllers/bmdashboard/bmProjectController.js index 96f19ab60..3e88ea08a 100644 --- a/src/controllers/bmdashboard/bmProjectController.js +++ b/src/controllers/bmdashboard/bmProjectController.js @@ -1,8 +1,10 @@ const bmMProjectController = function (BuildingProject) { - const bmProjectSummary = async function _projSumm(req, res) { - try { + // fetches all projects by building manager id + const fetchAllProjects = async (req, res) => { + const { userId } = req.params; + try { const projectData = await BuildingProject - .find() + .find({ buildingManager: userId }) .populate([ { path: 'buildingManager', @@ -21,7 +23,40 @@ const bmMProjectController = function (BuildingProject) { res.json(err); } }; - return { bmProjectSummary }; + + // fetches single project by project id + const fetchSingleProject = async (req, res) => { + const { userId, projectId } = req.params; + try { + BuildingProject + .findById(projectId) + .populate([ + { + path: 'buildingManager', + select: '_id firstName lastName email', + }, + { + path: 'team', + select: '_id firstName lastName email', + }, + ]) + .exec() + .then((project) => { + // authenticate request by comparing userId param with buildingManager id field + // ObjectId must be converted to string + if (userId !== project.buildingManager._id.toString()) { + return res.status(403).send({ + message: 'You are not authorized to view this record.', + }); + } + return res.status(200).send(project); + }) + .catch(error => res.status(500).send(error)); + } catch (err) { + res.json(err); + } + }; + return { fetchAllProjects, fetchSingleProject }; }; module.exports = bmMProjectController; diff --git a/src/routes/bmdashboard/bmProjectRouter.js b/src/routes/bmdashboard/bmProjectRouter.js index 6bd535ae1..7069950dc 100644 --- a/src/routes/bmdashboard/bmProjectRouter.js +++ b/src/routes/bmdashboard/bmProjectRouter.js @@ -4,8 +4,11 @@ const routes = function (buildingProject) { const projectRouter = express.Router(); const controller = require('../../controllers/bmdashboard/bmProjectController')(buildingProject); -projectRouter.route('/projects') - .get(controller.bmProjectSummary); +projectRouter.route('/projects/:userId') + .get(controller.fetchAllProjects); + +projectRouter.route('/projects/:userId/:projectId') + .get(controller.fetchSingleProject); return projectRouter; }; From ec5d40584a18cd5de8d8b6f255dc54acc68a2f20 Mon Sep 17 00:00:00 2001 From: Tim Kent Date: Tue, 7 Nov 2023 12:41:53 -0800 Subject: [PATCH 19/22] update project routes. add auth checks to controllers. --- .../bmdashboard/bmProjectController.js | 47 +++++++++++++------ src/routes/bmdashboard/bmProjectRouter.js | 4 +- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/controllers/bmdashboard/bmProjectController.js b/src/controllers/bmdashboard/bmProjectController.js index 3e88ea08a..929aba4ba 100644 --- a/src/controllers/bmdashboard/bmProjectController.js +++ b/src/controllers/bmdashboard/bmProjectController.js @@ -1,10 +1,21 @@ +// TODO: uncomment when executing auth checks +// const jwt = require('jsonwebtoken'); +// const config = require('../../config'); + const bmMProjectController = function (BuildingProject) { - // fetches all projects by building manager id + // TODO: uncomment when executing auth checks + // const { JWT_SECRET } = config; + const fetchAllProjects = async (req, res) => { - const { userId } = req.params; - try { + //! Note: for easier testing this route currently returns all projects from the db + // TODO: uncomment the lines below to return only projects where field buildingManager === userid + // const token = req.headers.authorization; + // const { userid } = jwt.verify(token, JWT_SECRET); + try { const projectData = await BuildingProject - .find({ buildingManager: userId }) + // TODO: uncomment this line to filter by buildingManager field + // .find({ buildingManager: userid }) + .find() .populate([ { path: 'buildingManager', @@ -26,7 +37,11 @@ const bmMProjectController = function (BuildingProject) { // fetches single project by project id const fetchSingleProject = async (req, res) => { - const { userId, projectId } = req.params; + //! Note: for easier testing this route currently returns the project without an auth check + // TODO: uncomment the lines below to check the user's ability to view the current project + // const token = req.headers.authorization; + // const { userid } = jwt.verify(token, JWT_SECRET); + const { projectId } = req.params; try { BuildingProject .findById(projectId) @@ -41,16 +56,18 @@ const bmMProjectController = function (BuildingProject) { }, ]) .exec() - .then((project) => { - // authenticate request by comparing userId param with buildingManager id field - // ObjectId must be converted to string - if (userId !== project.buildingManager._id.toString()) { - return res.status(403).send({ - message: 'You are not authorized to view this record.', - }); - } - return 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 + // .then((project) => { + // if (userid !== project.buildingManager._id.toString()) { + // return res.status(403).send({ + // message: 'You are not authorized to view this record.', + // }); + // } + // return res.status(200).send(project); + // }) .catch(error => res.status(500).send(error)); } catch (err) { res.json(err); diff --git a/src/routes/bmdashboard/bmProjectRouter.js b/src/routes/bmdashboard/bmProjectRouter.js index 7069950dc..d60ea9b2b 100644 --- a/src/routes/bmdashboard/bmProjectRouter.js +++ b/src/routes/bmdashboard/bmProjectRouter.js @@ -4,10 +4,10 @@ const routes = function (buildingProject) { const projectRouter = express.Router(); const controller = require('../../controllers/bmdashboard/bmProjectController')(buildingProject); -projectRouter.route('/projects/:userId') +projectRouter.route('/projects') .get(controller.fetchAllProjects); -projectRouter.route('/projects/:userId/:projectId') +projectRouter.route('/project/:projectId') .get(controller.fetchSingleProject); return projectRouter; From dbfc6bef60dbf37e0f9f8da70b55ee947fec4a73 Mon Sep 17 00:00:00 2001 From: wang9hu Date: Tue, 7 Nov 2023 23:05:08 -0800 Subject: [PATCH 20/22] make sure escapeRegex check entire string for existing info name --- src/utilities/escapeRegex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/escapeRegex.js b/src/utilities/escapeRegex.js index 10fa2e61e..01a65ea50 100644 --- a/src/utilities/escapeRegex.js +++ b/src/utilities/escapeRegex.js @@ -1,6 +1,6 @@ const escapeRegex = function (text) { - return text.replace(/[[\]{}()*+?.\\^$|]/g, '\\$&'); + return `^${text.replace(/[[\]{}()*+?.\\^$|]/g, '\\$&')}&`; }; module.exports = escapeRegex; From 9512ea0b5241968f5721c8db526c207807f860b0 Mon Sep 17 00:00:00 2001 From: Carl Bebli Date: Mon, 13 Nov 2023 11:56:18 +0000 Subject: [PATCH 21/22] added new fields to the email options and edited the email body --- src/controllers/dashBoardController.js | 109 ++++++++++++------------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/src/controllers/dashBoardController.js b/src/controllers/dashBoardController.js index aacc915dd..68e88ae58 100644 --- a/src/controllers/dashBoardController.js +++ b/src/controllers/dashBoardController.js @@ -1,8 +1,8 @@ -const path = require('path'); -const fs = require('fs/promises'); -const mongoose = require('mongoose'); -const dashboardhelper = require('../helpers/dashboardhelper')(); -const emailSender = require('../utilities/emailSender'); +const path = require("path"); +const fs = require("fs/promises"); +const mongoose = require("mongoose"); +const dashboardhelper = require("../helpers/dashboardhelper")(); +const emailSender = require("../utilities/emailSender"); const dashboardcontroller = function () { const dashboarddata = function (req, res) { @@ -20,13 +20,13 @@ 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) { const emptyresult = [ { - projectName: '', + projectName: "", timeSpent_hrs: 0, }, ]; @@ -42,7 +42,7 @@ const dashboardcontroller = function () { const laborthisweek = dashboardhelper.laborthisweek( userId, req.params.fromDate, - req.params.toDate, + req.params.toDate ); laborthisweek.then((results) => { res.send(results).status(200); @@ -63,7 +63,7 @@ const dashboardcontroller = function () { }); } }) - .catch(error => res.status(400).send(error)); + .catch((error) => res.status(400).send(error)); }; const orgData = function (req, res) { @@ -73,7 +73,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 ( @@ -85,7 +85,7 @@ const dashboardcontroller = function () { expected, actual, visual, - severity, + severity ) { const text = `New Bug Report From ${firstName} ${lastName}:

[Feature Name] Bug Title:

@@ -130,32 +130,32 @@ const dashboardcontroller = function () { expected, actual, visual, - severity, + severity ); try { emailSender( - 'onecommunityglobal@gmail.com', + "onecommunityglobal@gmail.com", `Bug Rport from ${firstName} ${lastName}`, emailBody, - email, + email ); - res.status(200).send('Success'); + res.status(200).send("Success"); } catch { - res.status(500).send('Failed'); + res.status(500).send("Failed"); } }; const suggestionData = { suggestion: [ - 'Identify and remedy poor client and/or user service experiences', - 'Identify bright spots and enhance positive service experiences', - 'Make fundamental changes to our programs and/or operations', - 'Inform the development of new programs/projects', - 'Identify where we are less inclusive or equitable across demographic groups', - 'Strengthen relationships with the people we serve', + "Identify and remedy poor client and/or user service experiences", + "Identify bright spots and enhance positive service experiences", + "Make fundamental changes to our programs and/or operations", + "Inform the development of new programs/projects", + "Identify where we are less inclusive or equitable across demographic groups", + "Strengthen relationships with the people we serve", "Understand people's needs and how we can help them achieve their goals", - 'Other', + "Other", ], field: [], }; @@ -164,54 +164,53 @@ const dashboardcontroller = function () { let fieldaaray = []; if (suggestionData.field.length) { fieldaaray = suggestionData.field.map( - item => `

${item}

-

${args[3][item]}

`, + (item) => `

${item}

+

${args[3][item]}

` ); } const text = `New Suggestion From ${args[3].firstName} ${ args[3].lastName - }: -

Suggestion Category:

+ } + : + ⚹ Suggestion Category:

${args[0]}

-

Suggestion:

+ ⚹ Suggestion:

${args[1]}

- ${fieldaaray.length > 0 ? fieldaaray : ''} -

Name of Suggester:

+ ${fieldaaray.length > 0 ? fieldaaray : ""} + ⚹ Name of Suggester:

${args[3].firstName} ${args[3].lastName}

-

Email of Suggester:

+ ⚹ Email of Suggester:

${args[4]}

-

Wants Feedback:

+ ⚹ Wants Feedback:

${args[2]}

-

Thank you,
- One Community

`; + Thank you,
+ One Community
`; return text; }; // 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( - 'beblicarl.cb@gmail.com', - 'A new suggestion', + "onecommunityglobal@gmail.com", + "A new suggestion", emailBody, null, null, - email, + email ); - res.status(200).send('Success'); + res.status(200).send("Success"); } catch { - res.status(500).send('Failed'); + res.status(500).send("Failed"); } }; @@ -220,40 +219,40 @@ const dashboardcontroller = function () { if (suggestionData) { res.status(200).send(suggestionData); } else { - res.status(404).send('Suggestion data not found.'); + res.status(404).send("Suggestion data not found."); } } catch (error) { - console.error('Error getting suggestion data:', error); - res.status(500).send('Internal Server Error'); + console.error("Error getting suggestion data:", error); + res.status(500).send("Internal Server Error"); } }; const editSuggestionOption = async (req, res) => { try { if (req.body.suggestion) { - if (req.body.action === 'add') { + if (req.body.action === "add") { suggestionData.suggestion.unshift(req.body.newField); } - if (req.body.action === 'delete') { + 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 { - if (req.body.action === 'add') { + if (req.body.action === "add") { suggestionData.field.unshift(req.body.newField); } - if (req.body.action === 'delete') { + if (req.body.action === "delete") { suggestionData.field = suggestionData.field.filter( - item => item !== req.body.newField, + (item) => item !== req.body.newField ); } } - res.status(200).send('success'); + res.status(200).send("success"); } catch (error) { - console.error('Error editing suggestion option:', error); - res.status(500).send('Internal Server Error'); + console.error("Error editing suggestion option:", error); + res.status(500).send("Internal Server Error"); } }; From db3a8703a27fce2f9b3d926f725d0c889bb7ee2b Mon Sep 17 00:00:00 2001 From: Carl Bebli Date: Mon, 13 Nov 2023 11:58:25 +0000 Subject: [PATCH 22/22] added new fields in the email options --- src/utilities/emailSender.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index 98ca3e337..b4864add6 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -1,6 +1,6 @@ -const nodemailer = require("nodemailer"); -const { google } = require("googleapis"); -const logger = require("../startup/logger"); +const nodemailer = require('nodemailer'); +const { google } = require('googleapis'); +const logger = require('../startup/logger'); const closure = () => { const queue = []; @@ -12,9 +12,9 @@ const closure = () => { const REFRESH_TOKEN = process.env.REACT_APP_EMAIL_REFRESH_TOKEN; // Create the email envelope (transport) const transporter = nodemailer.createTransport({ - service: "gmail", + service: 'gmail', auth: { - type: "OAuth2", + type: 'OAuth2', user: CLIENT_EMAIL, clientId: CLIENT_ID, clientSecret: CLIENT_SECRET, @@ -24,7 +24,7 @@ const closure = () => { const OAuth2Client = new google.auth.OAuth2( CLIENT_ID, CLIENT_SECRET, - REDIRECT_URI + REDIRECT_URI, ); OAuth2Client.setCredentials({ refresh_token: REFRESH_TOKEN }); @@ -34,7 +34,9 @@ const closure = () => { if (!nextItem) return; - const { recipient, subject, message, cc, bcc, replyTo } = nextItem; + const { + recipient, subject, message, cc, bcc, replyTo, +} = nextItem; try { // Generate the accessToken on the fly @@ -69,7 +71,7 @@ const closure = () => { message, cc = null, bcc = null, - replyTo = null + replyTo = null, ) { if (process.env.sendEmail) { queue.push({