diff --git a/.DS_Store b/.DS_Store index 1ad1b253c..8d8d400ad 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.github/workflows/development_hgn-rest-dev.yml b/.github/workflows/development_hgn-rest-dev.yml index 7d822c869..05030cf70 100644 --- a/.github/workflows/development_hgn-rest-dev.yml +++ b/.github/workflows/development_hgn-rest-dev.yml @@ -1,20 +1,6 @@ # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy # More GitHub Actions for Azure: https://github.com/Azure/actions -# CONFIGURATION -# For help, go to https://github.com/Azure/Actions -# -# 1. Set up the following secrets in your repository: -# AZURE_WEBAPP_PUBLISH_PROFILE -# -# 2. Change these variables for your configuration: - - - - -# For more information on GitHub Actions for Azure, refer to https://github.com/Azure/Actions -# For more samples to get started with GitHub Action workflows to deploy to Azure, refer to https://github.com/Azure/actions-workflow-samples - name: Build and deploy Node.js app to Azure Web App - hgn-rest-dev on: @@ -23,33 +9,53 @@ on: - development workflow_dispatch: -env: - AZURE_WEBAPP_NAME: hgn-rest-dev # set this to your application's name - AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - NODE_VERSION: '14.x' # set this to the node version to use - jobs: - build-and-deploy: - name: Build and Deploy + build: runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: 14 + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build environment: name: 'Production' url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + steps: - - uses: actions/checkout@master - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - name: npm install, build, and test - run: | - # Build and test the project, then - # deploy to Azure Web App. - npm install - npm run build --if-present - - name: 'Deploy to Azure WebApp' - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D3183F132BC14D79A19DC24953125EA4 }} - package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-rest-dev' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_37BDFCF15560478F9069CEF1EA105BF0 }} diff --git a/.github/workflows/main_hgn-rest-beta.yml b/.github/workflows/main_hgn-rest-beta.yml index 5db4b0e97..24d316a97 100644 --- a/.github/workflows/main_hgn-rest-beta.yml +++ b/.github/workflows/main_hgn-rest-beta.yml @@ -1,20 +1,6 @@ -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy # More GitHub Actions for Azure: https://github.com/Azure/actions -# CONFIGURATION -# For help, go to https://github.com/Azure/Actions -# -# 1. Set up the following secrets in your repository: -# AZURE_WEBAPP_PUBLISH_PROFILE -# -# 2. Change these variables for your configuration: - - - - -# For more information on GitHub Actions for Azure, refer to https://github.com/Azure/Actions -# For more samples to get started with GitHub Action workflows to deploy to Azure, refer to https://github.com/Azure/actions-workflow-samples - name: Build and deploy Node.js app to Azure Web App - hgn-rest-beta on: @@ -23,35 +9,53 @@ on: - main workflow_dispatch: -env: - AZURE_WEBAPP_NAME: hgn-rest-beta # set this to your application's name - AZURE_WEBAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root - NODE_VERSION: '14.x' # set this to the node version to use - jobs: - build-and-deploy: - name: Build and Deploy + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: 14 + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: runs-on: ubuntu-latest + needs: build environment: name: 'Production' url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + steps: - - uses: actions/checkout@master - - name: Use Node.js ${{ env.NODE_VERSION }} - uses: actions/setup-node@v1 - with: - node-version: ${{ env.NODE_VERSION }} - - name: npm install, build, and test - run: | - # Build and test the project, then - # deploy to Azure Web App. - npm install - npm run build --if-present - - name: 'Deploy to Azure WebApp' - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ env.AZURE_WEBAPP_NAME }} - publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_47800FE52B59410A903D5C41C2F9C10F }} - package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} -# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy -# More GitHub Actions for Azure: https://github.com/Azure/actions + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-rest-beta' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_DB4151C3B2C645B88AD84509DA4DD53D }} diff --git a/.github/workflows/main_hgn-rest.yml b/.github/workflows/main_hgn-rest.yml new file mode 100644 index 000000000..8b6a4298f --- /dev/null +++ b/.github/workflows/main_hgn-rest.yml @@ -0,0 +1,61 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Node.js app to Azure Web App - hgn-rest + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js version + uses: actions/setup-node@v3 + with: + node-version: 14 + + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + + - name: Zip artifact for deployment + run: zip release.zip ./* -r + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: node-app + path: release.zip + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: node-app + + - name: Unzip artifact for deployment + run: unzip release.zip + + - name: 'Deploy to Azure Web App' + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'hgn-rest' + slot-name: 'Production' + package: . + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_D375D726FC884C3C919531718B394AF9 }} diff --git a/package-lock.json b/package-lock.json index 8eff8a588..9e07004c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4797,6 +4797,11 @@ } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4923,6 +4928,33 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5076,7 +5108,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "colorette": { "version": "2.0.19", @@ -5321,6 +5353,23 @@ } } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -5788,7 +5837,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==" + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "8.47.0", @@ -7460,7 +7509,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==" + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-property-descriptors": { "version": "1.0.0", @@ -10799,7 +10848,7 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" }, "lodash.merge": { "version": "4.6.2", @@ -11511,6 +11560,14 @@ } } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -12330,6 +12387,23 @@ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -12338,7 +12412,7 @@ "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==" + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" }, "path-is-absolute": { "version": "1.0.1", diff --git a/package.json b/package.json index 8b88744dc..91e73f39a 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "babel-plugin-module-resolver": "^5.0.0", "bcryptjs": "^2.4.3", "body-parser": "^1.18.3", + "cheerio": "^1.0.0-rc.12", "cors": "^2.8.4", "cron": "^1.8.2", "dotenv": "^5.0.1", diff --git a/src/controllers/actionItemController.js b/src/controllers/actionItemController.js index 6d5864213..390794b31 100644 --- a/src/controllers/actionItemController.js +++ b/src/controllers/actionItemController.js @@ -1,128 +1,123 @@ -/** - * 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; +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/currentWarningsController.js b/src/controllers/currentWarningsController.js new file mode 100644 index 000000000..ec76d3326 --- /dev/null +++ b/src/controllers/currentWarningsController.js @@ -0,0 +1,153 @@ +/* eslint-disable */ +const mongoose = require('mongoose'); +const userProfile = require('../models/userProfile'); + +const currentWarningsController = function (currentWarnings) { + const checkForDuplicates = (currentWarning, warnings) => { + const duplicateFound = warnings.some( + (warning) => warning.warningTitle.toLowerCase() === currentWarning, + ); + + return duplicateFound; + }; + + const checkIfSpecialCharacter = (warning) => { + return !/^[a-zA-Z][a-zA-Z0-9]*(?: [a-zA-Z0-9]+)*$/.test(warning); + }; + const getCurrentWarnings = async (req, res) => { + try { + const response = await currentWarnings.find({}); + + if (response.length === 0) { + return res.status(400).send({ message: 'no valid records' }); + } + return res.status(200).send({ currentWarningDescriptions: response }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + + const postNewWarningDescription = async (req, res) => { + try { + const { newWarning, activeWarning, isPermanent } = req.body; + + const warnings = await currentWarnings.find({}); + + if (warnings.length === 0) { + return res.status(400).send({ error: 'no valid records' }); + } + + const testWarning = checkIfSpecialCharacter(newWarning); + if (testWarning) { + return res.status(200).send({ + error: 'Warning cannot have special characters as the first letter', + }); + } + + if (checkForDuplicates(newWarning, warnings)) { + return res.status(200).send({ error: 'warning already exists' }); + } + + const newWarningDescription = new currentWarnings(); + newWarningDescription.warningTitle = newWarning; + newWarningDescription.activeWarning = activeWarning; + newWarningDescription.isPermanent = isPermanent; + + warnings.push(newWarningDescription); + await newWarningDescription.save(); + + return res.status(201).send({ newWarnings: warnings }); + } catch (error) { + return res.status(401).send({ message: error.message }); + } + }; + + const editWarningDescription = async (req, res) => { + try { + const { editedWarning } = req.body; + + const id = editedWarning._id; + + const warnings = await currentWarnings.find({}); + + if (warnings.length === 0) { + return res.status(400).send({ message: 'no valid records' }); + } + + const lowerCaseWarning = editedWarning.warningTitle.toLowerCase(); + const testWarning = checkIfSpecialCharacter(lowerCaseWarning); + + if (testWarning) { + return res.status(200).send({ + error: 'Warning cannot have special characters as the first letter', + }); + } + + if (checkForDuplicates(lowerCaseWarning, warnings)) { + return res.status(200).send({ error: 'warning already exists try a different name' }); + } + + await currentWarnings.findOneAndUpdate( + { _id: id }, + [{ $set: { warningTitle: lowerCaseWarning.trim() } }], + { new: true }, + ); + + res.status(201).send({ message: 'warning description was updated' }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + const updateWarningDescription = async (req, res) => { + try { + const { warningDescriptionId } = req.params; + + await currentWarnings.findOneAndUpdate( + { _id: warningDescriptionId }, + [{ $set: { activeWarning: { $not: '$activeWarning' } } }], + { new: true }, + ); + + res.status(201).send({ message: 'warning description was updated' }); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + + const deleteWarningDescription = async (req, res) => { + try { + const { warningDescriptionId } = req.params; + const documentToDelete = await currentWarnings.findById(warningDescriptionId); + + await currentWarnings.deleteOne({ + _id: mongoose.Types.ObjectId(warningDescriptionId), + }); + + const deletedDescription = documentToDelete.warningTitle; + + await userProfile.updateMany( + { + 'warnings.description': deletedDescription, + }, + { + $pull: { + warnings: { description: deletedDescription }, + }, + }, + ); + + return res.status(200); + } catch (error) { + res.status(401).send({ message: error.message || error }); + } + }; + + return { + getCurrentWarnings, + postNewWarningDescription, + updateWarningDescription, + deleteWarningDescription, + editWarningDescription, + }; +}; +module.exports = currentWarningsController; diff --git a/src/controllers/emailController.js b/src/controllers/emailController.js index 523934448..15243ef5b 100644 --- a/src/controllers/emailController.js +++ b/src/controllers/emailController.js @@ -1,6 +1,6 @@ // emailController.js -const nodemailer = require('nodemailer'); const jwt = require('jsonwebtoken'); +const cheerio = require('cheerio'); const emailSender = require('../utilities/emailSender'); const { hasPermission } = require('../utilities/permissions'); const EmailSubcriptionList = require('../models/emailSubcriptionList'); @@ -9,6 +9,53 @@ const userProfile = require('../models/userProfile'); const frontEndUrl = process.env.FRONT_END_URL || 'http://localhost:3000'; const jwtSecret = process.env.JWT_SECRET || 'EmailSecret'; +const handleContentToOC = (htmlContent) => + ` + +
+ + + + ${htmlContent} + + `; + +const handleContentToNonOC = (htmlContent, email) => + ` + + + + + + ${htmlContent} +Thank you for subscribing to our email updates!
+If you would like to unsubscribe, please click here
+ + `; + +function extractImagesAndCreateAttachments(html) { + const $ = cheerio.load(html); + const attachments = []; + + $('img').each((i, img) => { + const src = $(img).attr('src'); + if (src.startsWith('data:image')) { + const base64Data = src.split(',')[1]; + const _cid = `image-${i}`; + attachments.push({ + filename: `image-${i}.png`, + content: Buffer.from(base64Data, 'base64'), + cid: _cid, + }); + $(img).attr('src', `cid:${_cid}`); + } + }); + return { + html: $.html(), + attachments, + }; +} + const sendEmail = async (req, res) => { const canSendEmail = await hasPermission(req.body.requestor, 'sendEmails'); if (!canSendEmail) { @@ -17,11 +64,36 @@ const sendEmail = async (req, res) => { } try { const { to, subject, html } = req.body; + // Validate required fields + if (!subject || !html || !to) { + const missingFields = []; + if (!subject) missingFields.push('Subject'); + if (!html) missingFields.push('HTML content'); + if (!to) missingFields.push('Recipient email'); + console.log('missingFields', missingFields); + return res + .status(400) + .send(`${missingFields.join(' and ')} ${missingFields.length > 1 ? 'are' : 'is'} required`); + } + + // Extract images and create attachments + const { html: processedHtml, attachments } = extractImagesAndCreateAttachments(html); + + // Log recipient for debugging + console.log('Recipient:', to); + + + await emailSender(to, subject, html) + .then(result => { + console.log('Email sent successfully:', result); + res.status(200).send(`Email sent successfully to ${to}`); + }) + .catch(error => { + console.error('Error sending email:', error); + res.status(500).send('Error sending email'); + }); - console.log('to', to); - emailSender(to, subject, html); - return res.status(200).send('Email sent successfully'); } catch (error) { console.error('Error sending email:', error); return res.status(500).send('Error sending email'); @@ -36,46 +108,44 @@ const sendEmailToAll = async (req, res) => { } try { const { subject, html } = req.body; + if (!subject || !html) { + return res.status(400).send('Subject and HTML content are required'); + } + + const { html: processedHtml, attachments } = extractImagesAndCreateAttachments(html); + const users = await userProfile.find({ - firstName: 'Haoji', + firstName: '', email: { $ne: null }, isActive: true, emailSubscriptions: true, }); - let to = ''; - const emailContent = ` - - - - + if (users.length === 0) { + return res.status(404).send('No users found'); + } + const recipientEmails = users.map((user) => user.email); + console.log('# sendEmailToAll to', recipientEmails.join(',')); + if (recipientEmails.length === 0) { + throw new Error('No recipients defined'); + } - - ${html} - - `; - users.forEach((user) => { - to += `${user.email},`; - }); - emailSender(to, subject, emailContent); - const emailList = await EmailSubcriptionList.find({ email: { $ne: null } }); - emailList.forEach((emailObject) => { - const { email } = emailObject; - const emailContent = ` - - - - + const emailContentToOCmembers = handleContentToOC(processedHtml); + await Promise.all( + recipientEmails.map((email) => + emailSender(email, subject, emailContentToOCmembers, attachments), + ), + ); + const emailSubscribers = await EmailSubcriptionList.find({ email: { $exists: true, $ne: '' } }); + console.log('# sendEmailToAll emailSubscribers', emailSubscribers.length); + await Promise.all( + emailSubscribers.map(({ email }) => { + const emailContentToNonOCmembers = handleContentToNonOC(processedHtml, email); + return emailSender(email, subject, emailContentToNonOCmembers, attachments); + }), + ); - - ${html} -Thank you for subscribing to our email updates!
-If you would like to unsubscribe, please click here
- - `; - emailSender(email, subject, emailContent); - }); return res.status(200).send('Email sent successfully'); } catch (error) { console.error('Error sending email:', error); @@ -113,13 +183,9 @@ const addNonHgnEmailSubscription = async (req, res) => { } const payload = { email }; - const token = jwt.sign( - payload, - jwtSecret, - { - expiresIn: 360, - }, - ); + const token = jwt.sign(payload, jwtSecret, { + expiresIn: 360, + }); const emailContent = ` diff --git a/src/controllers/jobsController.js b/src/controllers/jobsController.js new file mode 100644 index 000000000..65cfeafdc --- /dev/null +++ b/src/controllers/jobsController.js @@ -0,0 +1,131 @@ +const Job = require('../models/jobs'); // Import the Job model + +// Controller to fetch all jobs with pagination, search, and filtering +const getJobs = async (req, res) => { + const { page = 1, limit = 18, search = '', category = '' } = req.query; + + try { + // Validate query parameters + const pageNumber = Math.max(1, parseInt(page, 10)); // Ensure page is at least 1 + const limitNumber = Math.max(1, parseInt(limit, 10)); // Ensure limit is at least 1 + + // Build query object + const query = {}; + if (search) query.title = { $regex: search, $options: 'i' }; // Case-insensitive search + if (category) query.category = category; + + // Fetch total count for pagination metadata + const totalJobs = await Job.countDocuments(query); + + // Fetch paginated results + const jobs = await Job.find(query) + .skip((pageNumber - 1) * limitNumber) + .limit(limitNumber); + + // Prepare response + res.json({ + jobs, + pagination: { + totalJobs, + totalPages: Math.ceil(totalJobs / limitNumber), + currentPage: pageNumber, + limit: limitNumber, + hasNextPage: pageNumber < Math.ceil(totalJobs / limitNumber), + hasPreviousPage: pageNumber > 1, + }, + }); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch jobs', details: error.message }); + } +}; + +// Controller to fetch job details by ID +const getJobById = async (req, res) => { + const { id } = req.params; + + try { + const job = await Job.findById(id); + if (!job) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json(job); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch job', details: error.message }); + } +}; + +// Controller to create a new job +const createJob = async (req, res) => { + const { title, category, description, imageUrl, location, applyLink, jobDetailsLink } = req.body; + + try { + const newJob = new Job({ + title, + category, + description, + imageUrl, + location, + applyLink, + jobDetailsLink, + }); + + const savedJob = await newJob.save(); + res.status(201).json(savedJob); + } catch (error) { + res.status(500).json({ error: 'Failed to create job', details: error.message }); + } +}; + +// Controller to update an existing job by ID +const updateJob = async (req, res) => { + const { id } = req.params; + + try { + const updatedJob = await Job.findByIdAndUpdate(id, req.body, { new: true }); + if (!updatedJob) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json(updatedJob); + } catch (error) { + res.status(500).json({ error: 'Failed to update job', details: error.message }); + } +}; + +// Controller to delete a job by ID +const deleteJob = async (req, res) => { + const { id } = req.params; + + try { + const deletedJob = await Job.findByIdAndDelete(id); + if (!deletedJob) { + return res.status(404).json({ error: 'Job not found' }); + } + res.json({ message: 'Job deleted successfully' }); + } catch (error) { + res.status(500).json({ error: 'Failed to delete job', details: error.message }); + } +}; + +const getCategories = async (req, res) => { + try { + const categories = await Job.distinct('category', {}); + + // Sort categories alphabetically + categories.sort((a, b) => a.localeCompare(b)); + + res.status(200).json({ categories }); + } catch (error) { + console.error('Error fetching categories:', error); + res.status(500).json({ message: 'Failed to fetch categories' }); + } +}; + +// Export controllers as a plain object +module.exports = { + getJobs, + getJobById, + createJob, + updateJob, + deleteJob, + getCategories, +}; diff --git a/src/controllers/profileInitialSetupController.js b/src/controllers/profileInitialSetupController.js index 610d627a1..e56e7e406 100644 --- a/src/controllers/profileInitialSetupController.js +++ b/src/controllers/profileInitialSetupController.js @@ -172,13 +172,22 @@ const profileInitialSetupController = function ( const link = `${baseUrl}/ProfileInitialSetup/${savedToken.token}`; await session.commitTransaction(); - const acknowledgment = await sendEmailWithAcknowledgment( - email, - 'NEEDED: Complete your One Community profile setup', - sendLinkMessage(link), - ); - - return res.status(200).send(acknowledgment); + // Send response immediately without waiting for email acknowledgment + res.status(200).send({ message: 'Token created successfully, email is being sent.' }); + + // Asynchronously send the email acknowledgment + setImmediate(async () => { + try { + await sendEmailWithAcknowledgment( + email, + 'NEEDED: Complete your One Community profile setup', + sendLinkMessage(link), + ); + } catch (emailError) { + // Log email sending failure + LOGGER.logException(emailError, 'sendEmailWithAcknowledgment', JSON.stringify({ email, link }), null); + } + }); } catch (error) { await session.abortTransaction(); LOGGER.logException(error, 'getSetupToken', JSON.stringify(req.body), null); diff --git a/src/controllers/projectController.js b/src/controllers/projectController.js index 6d3cfc37b..3e48b3f42 100644 --- a/src/controllers/projectController.js +++ b/src/controllers/projectController.js @@ -300,12 +300,12 @@ const projectController = function (Project) { const updateTask = await hasPermission(req.body.requestor, 'updateTask'); const suggestTask = await hasPermission(req.body.requestor, 'suggestTask'); - const getId = (getProjMembers || postTask || updateTask || suggestTask); + const canGetId = (getProjMembers || postTask || updateTask || suggestTask); userProfile .find( { projects: projectId }, - { firstName: 1, lastName: 1, isActive: 1, profilePic: 1, _id: getId }, + { firstName: 1, lastName: 1, isActive: 1, profilePic: 1, _id: canGetId }, ) .sort({ firstName: 1, lastName: 1 }) .then((results) => { diff --git a/src/controllers/taskController.js b/src/controllers/taskController.js index 1e019ffa1..b4d71a5dd 100644 --- a/src/controllers/taskController.js +++ b/src/controllers/taskController.js @@ -683,7 +683,10 @@ const taskController = function (Task) { }; const updateTask = async (req, res) => { - if (!(await hasPermission(req.body.requestor, 'updateTask'))) { + if ( + !(await hasPermission(req.body.requestor, 'updateTask')) && + !(await hasPermission(req.body.requestor, 'removeUserFromTask')) + ) { res.status(403).send({ error: 'You are not authorized to update Task.' }); return; } diff --git a/src/controllers/teamController.js b/src/controllers/teamController.js index f8316095a..9ce719a8c 100644 --- a/src/controllers/teamController.js +++ b/src/controllers/teamController.js @@ -6,8 +6,66 @@ const Logger = require('../startup/logger'); const teamcontroller = function (Team) { const getAllTeams = function (req, res) { - Team.find({}) - .sort({ teamName: 1 }) + Team.aggregate([ + { + $unwind: '$members', + }, + { + $lookup: { + from: 'userProfiles', + localField: 'members.userId', + foreignField: '_id', + as: 'userProfile', + }, + }, + { + $unwind: '$userProfile', + }, + { + $match: { + isActive: true, + } + }, + { + $group: { + _id: { + teamId: '$_id', + teamCode: '$userProfile.teamCode', + }, + count: { $sum: 1 }, + teamName: { $first: '$teamName' }, + members: { + $push: { + _id: '$userProfile._id', + name: '$userProfile.name', + email: '$userProfile.email', + teamCode: '$userProfile.teamCode', + addDateTime: '$members.addDateTime', + }, + }, + createdDatetime: { $first: '$createdDatetime' }, + modifiedDatetime: { $first: '$modifiedDatetime' }, + isActive: { $first: '$isActive' }, + }, + }, + { + $sort: { count: -1 }, // Sort by the most frequent teamCode + }, + { + $group: { + _id: '$_id.teamId', + teamCode: { $first: '$_id.teamCode' }, // Get the most frequent teamCode + teamName: { $first: '$teamName' }, + members: { $first: '$members' }, + createdDatetime: { $first: '$createdDatetime' }, + modifiedDatetime: { $first: '$modifiedDatetime' }, + isActive: { $first: '$isActive' }, + }, + }, + { + $sort: { teamName: 1 }, // Sort teams by name + }, + ]) .then((results) => res.status(200).send(results)) .catch((error) => { Logger.logException(error); diff --git a/src/controllers/timeEntryController.js b/src/controllers/timeEntryController.js index 3908f8754..44a50fcfb 100644 --- a/src/controllers/timeEntryController.js +++ b/src/controllers/timeEntryController.js @@ -420,7 +420,7 @@ const addEditHistory = async (One Community
-ADMINISTRATIVE DETAILS:
Start Date: ${moment(userprofile.startDate).utc().format('M-D-YYYY')}
Role: ${userprofile.role}
@@ -594,7 +594,7 @@ const timeEntrycontroller = function (TimeEntry) { await timeEntry.save({ session }); if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session, validateModifiedOnly: true }); // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time removeOutdatedUserprofileCache(userprofile._id.toString()); } @@ -867,7 +867,7 @@ const timeEntrycontroller = function (TimeEntry) { } await timeEntry.save({ session }); if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session, validateModifiedOnly: true }); // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time removeOutdatedUserprofileCache(userprofile._id.toString()); @@ -940,7 +940,7 @@ const timeEntrycontroller = function (TimeEntry) { await timeEntry.remove({ session }); if (userprofile) { - await userprofile.save({ session }); + await userprofile.save({ session, validateModifiedOnly: true }); // since userprofile is updated, need to remove the cache so that the updated userprofile is fetched next time removeOutdatedUserprofileCache(userprofile._id.toString()); @@ -1069,8 +1069,8 @@ const timeEntrycontroller = function (TimeEntry) { const timeentryCache=cacheClosure(); const cacheData=timeentryCache.hasCache(cacheKey) if(cacheData){ - let data=timeentryCache.getCache(cacheKey); - return res.status(200).send(data); + const data = timeentryCache.getCache(cacheKey); + return res.status(200).send(data); } try { const results = await TimeEntry.find( @@ -1285,7 +1285,7 @@ const timeEntrycontroller = function (TimeEntry) { const getLostTimeEntriesForTeamList = function (req, res) { const { teams, fromDate, toDate } = req.body; const lostteamentryCache=cacheClosure() - const cacheKey='LostTeamEntry'+`_${fromDate}`+`_${toDate}`; + const cacheKey = `LostTeamEntry_${fromDate}_${toDate}`; const cacheData=lostteamentryCache.getCache(cacheKey) if(cacheData){ return res.status(200).send(cacheData) diff --git a/src/controllers/titleController.js b/src/controllers/titleController.js index 3ca3175f5..277842c43 100644 --- a/src/controllers/titleController.js +++ b/src/controllers/titleController.js @@ -1,4 +1,3 @@ -const Team = require('../models/team'); const Project = require('../models/project'); const cacheClosure = require('../utilities/nodeCache'); const userProfileController = require("./userProfileController"); @@ -6,11 +5,36 @@ const userProfile = require('../models/userProfile'); const project = require('../models/project'); const controller = userProfileController(userProfile, project); -const getAllTeamCodeHelper = controller.getAllTeamCodeHelper; +const { getAllTeamCodeHelper } = controller; const titlecontroller = function (Title) { const cache = cacheClosure(); + // Update: Confirmed with Jae. Team code is not related to the Team data model. But the team code field within the UserProfile data model. + async function checkTeamCodeExists(teamCode) { + try { + if (cache.getCache('teamCodes')) { + const teamCodes = JSON.parse(cache.getCache('teamCodes')); + return teamCodes.includes(teamCode); + } + const teamCodes = await getAllTeamCodeHelper(); + return teamCodes.includes(teamCode); + } catch (error) { + console.error('Error checking if team code exists:', error); + throw error; + } + } + + async function checkProjectExists(projectID) { + try { + const proj = await Project.findOne({ _id: projectID }).exec(); + return !!proj; + } catch (error) { + console.error('Error checking if project exists:', error); + throw error; + } + } + const getAllTitles = function (req, res) { Title.find({}) .then((results) => res.status(200).send(results)) @@ -37,7 +61,9 @@ const titlecontroller = function (Title) { const titleCodeRegex = /^[A-Za-z]+$/; if (!title.titleCode || !title.titleCode.trim()) { return res.status(400).send({ message: 'Title code cannot be empty.' }); - } else if (!titleCodeRegex.test(title.titleCode)) { + } + + if (!titleCodeRegex.test(title.titleCode)) { return res.status(400).send({ message: 'Title Code must contain only upper or lower case letters.' }); } @@ -172,30 +198,7 @@ const titlecontroller = function (Title) { res.status(500).send(error); }); }; - // Update: Confirmed with Jae. Team code is not related to the Team data model. But the team code field within the UserProfile data model. - async function checkTeamCodeExists(teamCode) { - try { - if (cache.getCache('teamCodes')) { - const teamCodes = JSON.parse(cache.getCache('teamCodes')); - return teamCodes.includes(teamCode); - } - const teamCodes = await getAllTeamCodeHelper(); - return teamCodes.includes(teamCode); - } 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; - } - } diff --git a/src/controllers/userProfileController.js b/src/controllers/userProfileController.js index d1a8e0cd1..71defbf58 100644 --- a/src/controllers/userProfileController.js +++ b/src/controllers/userProfileController.js @@ -1492,26 +1492,7 @@ const userProfileController = function (UserProfile, Project) { res.status(200).send({ refreshToken: currentRefreshToken }); }; - // 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'); @@ -1556,13 +1537,7 @@ const userProfileController = function (UserProfile, Project) { .catch((error) => res.status(500).send(error)); }; - // function escapeRegExp(string) { - // return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - // } - /** - * Authorizes user to be able to add Weekly Report Recipients - * - */ + const authorizeUser = async (req, res) => { try { let authorizedUser; @@ -1793,28 +1768,28 @@ const userProfileController = function (UserProfile, Project) { const query = match[1] ? { - $or: [ - { - firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, - }, - { - $and: [ - { firstName: { $regex: new RegExp(`${escapeRegExp(firstName)}`, 'i') } }, - { lastName: { $regex: new RegExp(`${escapeRegExp(lastName)}`, 'i') } }, - ], - }, - ], - } + $or: [ + { + firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + { + $and: [ + { firstName: { $regex: new RegExp(`${escapeRegExp(firstName)}`, 'i') } }, + { lastName: { $regex: new RegExp(`${escapeRegExp(lastName)}`, 'i') } }, + ], + }, + ], + } : { - $or: [ - { - firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, - }, - { - lastName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, - }, - ], - }; + $or: [ + { + firstName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + { + lastName: { $regex: new RegExp(`${escapeRegExp(name)}`, 'i') }, + }, + ], + }; const userProfile = await UserProfile.find(query); @@ -1862,13 +1837,49 @@ const userProfileController = function (UserProfile, Project) { } }; + const getUserByAutocomplete = (req, res) => { + const { searchText } = req.params; + + if (!searchText) { + return res.status(400).send({ message: 'Search text is required' }); + } + + const regex = new RegExp(searchText, 'i'); // Case-insensitive regex for partial matching + + UserProfile.find( + { + $or: [ + { firstName: { $regex: regex } }, + { lastName: { $regex: regex } }, + { + $expr: { + $regexMatch: { + input: { $concat: ['$firstName', ' ', '$lastName'] }, + regex: searchText, + options: 'i', + }, + }, + }, + ], + }, + '_id firstName lastName', // Projection to limit fields returned + ) + .limit(10) // Limit results for performance + .then((results) => { + res.status(200).send(results); + }) + .catch(() => { + res.status(500).send({ error: 'Internal Server Error' }); + }); + }; + const updateUserInformation = async function (req,res){ try { const data=req.body; data.map(async (e)=> { - let result = await UserProfile.findById(e.user_id); + const result = await UserProfile.findById(e.user_id); result[e.item]=e.value - let newdata=await result.save() + await result.save(); }) res.status(200).send({ message: 'Update successful'}); } catch (error) { @@ -1906,8 +1917,9 @@ const userProfileController = function (UserProfile, Project) { getProjectsByPerson, getAllTeamCode, getAllTeamCodeHelper, + getUserByAutocomplete, + getUserProfileBasicInfo, updateUserInformation, - getUserProfileBasicInfo }; }; diff --git a/src/controllers/warningsController.js b/src/controllers/warningsController.js index 381844883..08515c207 100644 --- a/src/controllers/warningsController.js +++ b/src/controllers/warningsController.js @@ -1,22 +1,45 @@ /* eslint-disable */ const mongoose = require('mongoose'); const userProfile = require('../models/userProfile'); +const currentWarnings = require('../models/currentWarnings'); +const emailSender = require('../utilities/emailSender'); +const userHelper = require('../helpers/userHelper')(); +let currentWarningDescriptions = null; +let currentUserName = null; +const emailTemplate = { + thirdWarning: { + subject: 'Third Warning', + body: `This is the 3rd time the Admin team has requested the same thing from you. Specifically <“tracked area”>. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.
+Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.
+With Gratitude,
+One Community
`, + }, + fourthWarning: { + subject: 'Fourth Warning', + body: `username !
+This is the 3rd time the Admin team has requested the same thing from you. Specifically <“tracked area”>. Please carefully review the communications you’ve gotten about this so you understand what is being requested. Ask questions if anything isn’t clear, the Admin team is here to help.
+Please also be sure to fix this from here on forward, asking for the same thing over and over requires administration that really shouldn’t be needed and will result in a blue square if it happens again.
+With Gratitude,
+One Community
`, + }, +}; +async function getWarningDescriptions() { + currentWarningDescriptions = await currentWarnings.find({}, { warningTitle: 1, _id: 0 }); +} -const descriptions = [ - 'Better Descriptions', - 'Log Time to Tasks', - 'Log Time as You Go', - 'Log Time to Action Items', - 'Intangible Time Log w/o Reason', -]; const warningsController = function (UserProfile) { const getWarningsByUserId = async function (req, res) { + currentWarningDescriptions = await currentWarnings.find({ + activeWarning: true, + }); + + currentWarningDescriptions = currentWarningDescriptions.map((a) => a.warningTitle); const { userId } = req.params; try { const { warnings } = await UserProfile.findById(userId); - const completedData = filterWarnings(warnings); + const { completedData } = filterWarnings(currentWarningDescriptions, warnings); if (!warnings) { return res.status(400).send({ message: 'no valiud records' }); @@ -32,13 +55,20 @@ const warningsController = function (UserProfile) { const { userId } = req.params; const { iconId, color, date, description } = req.body; + const { monitorData } = req.body; const record = await UserProfile.findById(userId); if (!record) { return res.status(400).send({ message: 'No valid records found' }); } - const updatedWarnings = await userProfile.findByIdAndUpdate( + const userAssignedWarning = { + firstName: record.firstName, + lastName: record.lastName, + email: record.email, + }; + + const updatedWarnings = await UserProfile.findByIdAndUpdate( { _id: userId, }, @@ -46,7 +76,24 @@ const warningsController = function (UserProfile) { { new: true, upsert: true }, ); - const completedData = filterWarnings(updatedWarnings.warnings); + const { completedData, sendEmail, size } = filterWarnings( + currentWarningDescriptions, + updatedWarnings.warnings, + iconId, + color, + ); + + const adminEmails = await getUserRoleByEmail(record); + if (sendEmail !== null) { + sendEmailToUser( + sendEmail, + description, + userAssignedWarning, + monitorData, + size, + adminEmails, + ); + } res.status(201).send({ message: 'success', warnings: completedData }); } catch (error) { @@ -69,8 +116,8 @@ const warningsController = function (UserProfile) { return res.status(400).send({ message: 'no valid records' }); } - const sortedWarnings = filterWarnings(warnings.warnings); - res.status(201).send({ message: 'succesfully deleted', warnings: sortedWarnings }); + const { completedData } = filterWarnings(currentWarningDescriptions, warnings.warnings); + res.status(201).send({ message: 'succesfully deleted', warnings: completedData }); } catch (error) { res.status(401).send({ message: error.message || error }); } @@ -83,19 +130,80 @@ const warningsController = function (UserProfile) { }; }; -// gests the dsecriptions key from the array -const getDescriptionKey = (val) => { - const descriptions = [ - 'Better Descriptions', - 'Log Time to Tasks', - 'Log Time as You Go', - 'Log Time to Action Items', - 'Intangible Time Log w/o Reason', - ]; - - return descriptions.indexOf(val); +//helper to get the team members admin emails +async function getUserRoleByEmail(user) { + //replacement for jae's email + const recipients = ['test@test.com']; + for (const teamId of user.teams) { + const managementEmails = await userHelper.getTeamManagementEmail(teamId); + if (Array.isArray(managementEmails) && managementEmails.length > 0) { + managementEmails.forEach((management) => { + recipients.push(management.email); + }); + } + } + + return [...new Set(recipients)]; +} + +//helper function to get the ordinal +function getOrdinal(n) { + const suffixes = ['th', 'st', 'nd', 'rd']; + const value = n % 100; + return n + (suffixes[(value - 20) % 10] || suffixes[value] || suffixes[0]); +} +const sendEmailToUser = ( + sendEmail, + warningDescription, + userAssignedWarning, + monitorData, + size, + adminEmails, +) => { + const ordinal = getOrdinal(size); + const subjectTitle = ordinal + ' Warning'; + + const currentUserName = `${userAssignedWarning.firstName} ${userAssignedWarning.lastName}`; + const emailTemplate = + sendEmail === 'issue warning' + ? `Hello ${currentUserName},
+This is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}. Please carefully review the previous communications you’ve received to fully understand what is being requested. If anything is unclear, don’t hesitate to ask questions—the Admin team is here to assist.
+Moving forward, please ensure this issue is resolved. Repeated requests for the same thing require unnecessary administrative attention and may result in a blue square being issued if it happens again.
+The Admin member who issued the warning is ${monitorData.firstName} ${monitorData.lastName} and their email is ${monitorData.email}. Please comment on your Google Doc and tag them using this email if you have any questions.
+With Gratitude,
+One Community
` + : `Hello ${currentUserName},
+A blue square has been issued because this is the ${ordinal} time the Admin team has requested the same thing from you. Specifically, ${warningDescription}.
+Moving forward, please ensure this is resolved. Repeated requests for the same thing require unnecessary administrative attention, will result in an additional blue square being issued, and could lead to termination.
+Please carefully review the previous communications you’ve received to fully understand what is being requested. If anything is unclear, feel free to ask questions—the Admin team is here to help.
+The Admin member who issued this blue square is ${monitorData.firstName} ${monitorData.lastName} and can be reached at ${monitorData.email}. If you have any questions, please comment on your Google Doc and tag them using this email.
+With Gratitude,
+One Community
`; + + if (sendEmail === 'issue warning') { + emailSender( + `${userAssignedWarning.email}`, + subjectTitle, + emailTemplate, + null, + adminEmails.toString(), + null, + ); + } else if (sendEmail === 'issue blue square') { + emailSender( + `${userAssignedWarning.email}`, + `Blue Square issued for ${warningDescription}`, + null, + emailTemplate, + adminEmails.toString(), + null, + ); + } }; +// gets the dsecriptions key from the array +const getDescriptionKey = (val) => currentWarningDescriptions.indexOf(val); + const sortKeysAlphabetically = (a, b) => getDescriptionKey(a) - getDescriptionKey(b); // method to see which color is first @@ -118,14 +226,29 @@ const sortByColorAndDate = (a, b) => { return colorComparison; }; -const filterWarnings = (warnings) => { +const filterWarnings = (currentWarningDescriptions, warnings, iconId = null, color = null) => { const warningsObject = {}; + let sendEmail = null; + let size = null; + warnings.forEach((warning) => { if (!warningsObject[warning.description]) { warningsObject[warning.description] = []; } warningsObject[warning.description].push(warning); + + if ( + warningsObject[warning.description].length >= 3 && + warning.iconId === iconId && + color === 'yellow' + ) { + sendEmail = 'issue warning'; + size = warningsObject[warning.description].length; + } else if (warning.iconId === iconId && color === 'red') { + sendEmail = 'issue blue square'; + size = warningsObject[warning.description].length; + } }); const warns = Object.keys(warningsObject) @@ -141,13 +264,13 @@ const filterWarnings = (warnings) => { const completedData = []; - for (const descrip of descriptions) { + for (const descrip of currentWarningDescriptions) { completedData.push({ title: descrip, warnings: warns[descrip] ? warns[descrip] : [], }); } - return completedData; + return { completedData, sendEmail, size }; }; module.exports = warningsController; diff --git a/src/cronjobs/userProfileJobs.js b/src/cronjobs/userProfileJobs.js index e7a8662a6..e773847d2 100644 --- a/src/cronjobs/userProfileJobs.js +++ b/src/cronjobs/userProfileJobs.js @@ -11,6 +11,7 @@ const userProfileJobs = () => { async () => { const SUNDAY = 0; // will change back to 0 after fix if (moment().tz('America/Los_Angeles').day() === SUNDAY) { + console.log('Running Cron Jobs'); await userhelper.assignBlueSquareForTimeNotMet(); await userhelper.applyMissedHourForCoreTeam(); await userhelper.emailWeeklySummariesForAllUsers(); diff --git a/src/helpers/dashboardhelper.js b/src/helpers/dashboardhelper.js index 08e0bc907..dddb7cca2 100644 --- a/src/helpers/dashboardhelper.js +++ b/src/helpers/dashboardhelper.js @@ -181,14 +181,11 @@ const dashboardhelper = function () { _myTeam.members.forEach((teamMember) => { if (teamMember.userId.equals(userid) && teamMember.visible) isUserVisible = true; }); - if(isUserVisible) - { + if (isUserVisible) { _myTeam.members.forEach((teamMember) => { - if (!teamMember.userId.equals(userid)) - teamMemberIds.push(teamMember.userId); - }); - } - + if (!teamMember.userId.equals(userid)) teamMemberIds.push(teamMember.userId); + }); + } }); teamMembers = await userProfile.find( @@ -204,8 +201,7 @@ const dashboardhelper = function () { timeOffTill: 1, endDate: 1, missedHours: 1, - } - + }, ); } else { // 'Core Team', 'Owner' //All users @@ -270,7 +266,7 @@ const dashboardhelper = function () { ? teamMember.weeklySummaries[0].summary !== '' : false, weeklycommittedHours: teamMember.weeklycommittedHours, - missedHours: (teamMember.missedHours ?? 0), + missedHours: teamMember.missedHours ?? 0, totaltangibletime_hrs: (timeEntryByPerson[teamMember._id.toString()]?.tangibleSeconds ?? 0) / 3600, totalintangibletime_hrs: @@ -311,255 +307,6 @@ const dashboardhelper = function () { console.log(error); return new Error(error); } - - // return myTeam.aggregate([ - // { - // $match: { - // _id: userid, - // }, - // }, - // { - // $unwind: '$myteam', - // }, - // { - // $project: { - // _id: 0, - // role: 1, - // personId: '$myteam._id', - // name: '$myteam.fullName', - // }, - // }, - // { - // $lookup: { - // from: 'userProfiles', - // localField: 'personId', - // foreignField: '_id', - // as: 'persondata', - // }, - // }, - // { - // $match: { - // // leaderboard user roles hierarchy - // $or: [ - // { - // role: { $in: ['Owner', 'Core Team'] }, - // }, - // { - // $and: [ - // { - // role: 'Administrator', - // }, - // { 'persondata.0.role': { $nin: ['Owner', 'Administrator'] } }, - // ], - // }, - // { - // $and: [ - // { - // role: { $in: ['Manager', 'Mentor'] }, - // }, - // { - // 'persondata.0.role': { - // $nin: ['Manager', 'Mentor', 'Core Team', 'Administrator', 'Owner'], - // }, - // }, - // ], - // }, - // { 'persondata.0._id': userId }, - // { 'persondata.0.role': 'Volunteer' }, - // { 'persondata.0.isVisible': true }, - // ], - // }, - // }, - // { - // $project: { - // personId: 1, - // name: 1, - // role: { - // $arrayElemAt: ['$persondata.role', 0], - // }, - // isVisible: { - // $arrayElemAt: ['$persondata.isVisible', 0], - // }, - // hasSummary: { - // $ne: [ - // { - // $arrayElemAt: [ - // { - // $arrayElemAt: ['$persondata.weeklySummaries.summary', 0], - // }, - // 0, - // ], - // }, - // '', - // ], - // }, - // weeklycommittedHours: { - // $sum: [ - // { - // $arrayElemAt: ['$persondata.weeklycommittedHours', 0], - // }, - // { - // $ifNull: [{ $arrayElemAt: ['$persondata.missedHours', 0] }, 0], - // }, - // ], - // }, - // }, - // }, - // { - // $lookup: { - // from: 'timeEntries', - // localField: 'personId', - // foreignField: 'personId', - // as: 'timeEntryData', - // }, - // }, - // { - // $project: { - // personId: 1, - // name: 1, - // role: 1, - // isVisible: 1, - // hasSummary: 1, - // weeklycommittedHours: 1, - // timeEntryData: { - // $filter: { - // input: '$timeEntryData', - // as: 'timeentry', - // cond: { - // $and: [ - // { - // $gte: ['$$timeentry.dateOfWork', pdtstart], - // }, - // { - // $lte: ['$$timeentry.dateOfWork', pdtend], - // }, - // ], - // }, - // }, - // }, - // }, - // }, - // { - // $unwind: { - // path: '$timeEntryData', - // preserveNullAndEmptyArrays: true, - // }, - // }, - // { - // $project: { - // personId: 1, - // name: 1, - // role: 1, - // isVisible: 1, - // hasSummary: 1, - // weeklycommittedHours: 1, - // totalSeconds: { - // $cond: [ - // { - // $gte: ['$timeEntryData.totalSeconds', 0], - // }, - // '$timeEntryData.totalSeconds', - // 0, - // ], - // }, - // isTangible: { - // $cond: [ - // { - // $gte: ['$timeEntryData.totalSeconds', 0], - // }, - // '$timeEntryData.isTangible', - // false, - // ], - // }, - // }, - // }, - // { - // $addFields: { - // tangibletime: { - // $cond: [ - // { - // $eq: ['$isTangible', true], - // }, - // '$totalSeconds', - // 0, - // ], - // }, - // intangibletime: { - // $cond: [ - // { - // $eq: ['$isTangible', false], - // }, - // '$totalSeconds', - // 0, - // ], - // }, - // }, - // }, - // { - // $group: { - // _id: { - // personId: '$personId', - // weeklycommittedHours: '$weeklycommittedHours', - // name: '$name', - // role: '$role', - // isVisible: '$isVisible', - // hasSummary: '$hasSummary', - // }, - // totalSeconds: { - // $sum: '$totalSeconds', - // }, - // tangibletime: { - // $sum: '$tangibletime', - // }, - // intangibletime: { - // $sum: '$intangibletime', - // }, - // }, - // }, - // { - // $project: { - // _id: 0, - // personId: '$_id.personId', - // name: '$_id.name', - // role: '$_id.role', - // isVisible: '$_id.isVisible', - // hasSummary: '$_id.hasSummary', - // weeklycommittedHours: '$_id.weeklycommittedHours', - // totaltime_hrs: { - // $divide: ['$totalSeconds', 3600], - // }, - // totaltangibletime_hrs: { - // $divide: ['$tangibletime', 3600], - // }, - // totalintangibletime_hrs: { - // $divide: ['$intangibletime', 3600], - // }, - // percentagespentintangible: { - // $cond: [ - // { - // $eq: ['$totalSeconds', 0], - // }, - // 0, - // { - // $multiply: [ - // { - // $divide: ['$tangibletime', '$totalSeconds'], - // }, - // 100, - // ], - // }, - // ], - // }, - // }, - // }, - // { - // $sort: { - // totaltangibletime_hrs: -1, - // name: 1, - // role: 1, - // }, - // }, - // ]); }; /** diff --git a/src/helpers/overviewReportHelper.js b/src/helpers/overviewReportHelper.js index 52d6a2ad0..cb1cd8edb 100644 --- a/src/helpers/overviewReportHelper.js +++ b/src/helpers/overviewReportHelper.js @@ -74,6 +74,8 @@ const overviewReportHelper = function () { _id: 1, firstName: 1, lastName: 1, + email: 1, + profilePic: 1, }, }, ]); diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 9ea062422..168d704b1 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -146,14 +146,14 @@ const userHelper = function () { .localeData() .ordinal( totalInfringements, - )} blue square of 5 and that means you have ${totalInfringements - 5} hour(s) added to your - requirement this week. This is in addition to any hours missed for last week: - ${weeklycommittedHours} hours commitment + ${remainHr} hours owed for last week + ${totalInfringements - 5} hours + )} blue square of 5 and that means you have ${totalInfringements - 5} hour(s) added to your + requirement this week. This is in addition to any hours missed for last week: + ${weeklycommittedHours} hours commitment + ${remainHr} hours owed for last week + ${totalInfringements - 5} hours owed for this being your ${moment .localeData() .ordinal( totalInfringements, - )} blue square = ${hrThisweek + totalInfringements - 5} hours required for this week. + )} blue square = ${hrThisweek + totalInfringements - 5} hours required for this week. .`; } // bold description for 'System auto-assigned infringement for two reasons ....' and 'not submitting a weekly summary' and logged hrs @@ -204,7 +204,7 @@ const userHelper = function () {One Community
-ADMINISTRATIVE DETAILS:
Start Date: ${administrativeContent.startDate}
Role: ${administrativeContent.role}
@@ -1049,7 +1049,7 @@ const userHelper = function () { }, ); - logger.logInfo(`Job deleting blue squares older than 1 year finished + logger.logInfo(`Job deleting blue squares older than 1 year finished at ${moment().tz('America/Los_Angeles').format()} \nReulst: ${JSON.stringify(results)}`); } catch (err) { logger.logException(err); @@ -1099,11 +1099,11 @@ const userHelper = function () { const emailBody = `Hi Admin!
This email is to let you know that ${person.firstName} ${person.lastName} has been made active again in the Highest Good Network application after being paused on ${endDate}.
- +If you need to communicate anything with them, this is their email from the system: ${person.email}.
- +Thanks!
- +The HGN A.I. (and One Community)
`; emailSender('onecommunityglobal@gmail.com', subject, emailBody, null, null, person.email); @@ -2066,74 +2066,70 @@ const userHelper = function () { sendThreeWeeks, followup, ) { - let subject; - let emailBody; - recipients.push('onecommunityglobal@gmail.com'); - recipients = recipients.toString(); + let subject; + let emailBody; + recipients.push('onecommunityglobal@gmail.com'); + recipients = recipients.toString(); if (reactivationDate) { subject = `IMPORTANT: ${firstName} ${lastName} has been PAUSED in the Highest Good Network`; emailBody = `Management,
Please note that ${firstName} ${lastName} has been PAUSED in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part until they return on ${moment(reactivationDate).format('M-D-YYYY')}.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); } else if (endDate && isSet && sendThreeWeeks) { const subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; const emailBody = `Management,
- +Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
This is more than 3 weeks from now, but you should still start confirming all your work is being wrapped up with this individual and nothing further will be needed on their part after this date.
An additional reminder email will be sent in their final 2 weeks.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); - } else if (endDate && isSet && followup) { subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; emailBody = `Management,
- +Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
This is coming up soon. For a smooth transition, please confirm all your work is wrapped up with this individual and nothing further will be needed on their part after this date.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); - - } else if (endDate && isSet ) { + } else if (endDate && isSet) { subject = `IMPORTANT: The last day for ${firstName} ${lastName} has been set in the Highest Good Network`; emailBody = `Management,
- +Please note that the final day for ${firstName} ${lastName} has been set in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); - - } else if(endDate){ + } else if (endDate) { subject = `IMPORTANT: ${firstName} ${lastName} has been deactivated in the Highest Good Network`; emailBody = `Management,
- +Please note that ${firstName} ${lastName} has been made inactive in the Highest Good Network as ${moment(endDate).format('M-D-YYYY')}.
For a smooth transition, Please confirm all your work with this individual has been wrapped up and nothing further is needed on their part.
- +With Gratitude,
- +One Community
`; emailSender(email, subject, emailBody, null, recipients, email); - }; - } - + }; + const deActivateUser = async () => { try { const emailReceivers = await userProfile.find( @@ -2149,14 +2145,18 @@ const userHelper = function () { const user = users[i]; const { endDate, finalEmailThreeWeeksSent } = user; endDate.setHours(endDate.getHours() + 7); - // notify reminder set final day before 2 weeks - if(finalEmailThreeWeeksSent && moment().isBefore(moment(endDate).subtract(2, 'weeks')) && moment().isAfter(moment(endDate).subtract(3, 'weeks'))){ + // notify reminder set final day before 2 weeks + if ( + finalEmailThreeWeeksSent && + moment().isBefore(moment(endDate).subtract(2, 'weeks')) && + moment().isAfter(moment(endDate).subtract(3, 'weeks')) + ) { const id = user._id; const person = await userProfile.findById(id); const lastDay = moment(person.endDate).format('YYYY-MM-DD'); logger.logInfo(`User with id: ${user._id}'s final Day is set at ${moment().format()}.`); person.teams.map(async (teamId) => { - const managementEmails = await userHelper.getTeamManagementEmail(teamId); + const managementEmails = await userHelper.getTeamManagementEmail(teamId); if (Array.isArray(managementEmails) && managementEmails.length > 0) { managementEmails.forEach((management) => { recipients.push(management.email); diff --git a/src/models/currentWarnings.js b/src/models/currentWarnings.js new file mode 100644 index 000000000..18a446199 --- /dev/null +++ b/src/models/currentWarnings.js @@ -0,0 +1,11 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const currentWarnings = new Schema({ + warningTitle: { type: String, required: true }, + activeWarning: { type: Boolean, required: true }, + isPermanent: { type: Boolean, required: true }, +}); + +module.exports = mongoose.model('currentWarning', currentWarnings, 'currentWarnings'); diff --git a/src/models/jobs.js b/src/models/jobs.js new file mode 100644 index 000000000..b1f98a34a --- /dev/null +++ b/src/models/jobs.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose'); + +const { Schema } = mongoose; + +const jobSchema = new Schema({ + title: { type: String, required: true }, // Job title + category: { type: String, required: true }, // General category (e.g., Engineering, Marketing) + description: { type: String, required: true }, // Detailed job description + imageUrl: { type: String, required: true }, // URL of the job-related image + location: { type: String, required: true }, // Job location (optional for remote jobs) + applyLink: { type: String, required: true }, // URL for the application form + featured: { type: Boolean, default: false }, // Whether the job should be featured prominently + datePosted: { type: Date, default: Date.now }, // Date the job was posted + jobDetailsLink: { type: String, required: true }, // Specific job details URL +}); + +module.exports = mongoose.model('Job', jobSchema); diff --git a/src/models/team.js b/src/models/team.js index a679140a6..109d93221 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -3,9 +3,9 @@ const mongoose = require('mongoose'); const { Schema } = mongoose; /** - * This schema represents a team in the system. - * - * Deprecated field: teamCode. Team code is no longer associated with a team. + * This schema represents a team in the system. + * + * Deprecated field: teamCode. Team code is no longer associated with a team. * Team code is used as a text string identifier in the user profile data model. */ const team = new Schema({ @@ -18,7 +18,7 @@ const team = new Schema({ userId: { type: mongoose.SchemaTypes.ObjectId, required: true, index : true }, addDateTime: { type: Date, default: Date.now(), ref: 'userProfile' }, visible: { type : 'Boolean', default:true}, - + }, ], // Deprecated field diff --git a/src/models/userProfile.js b/src/models/userProfile.js index 3a529294a..61e8e8f91 100644 --- a/src/models/userProfile.js +++ b/src/models/userProfile.js @@ -76,7 +76,7 @@ const userProfileSchema = new Schema({ startDate: { type: Date, required: true, - default () { + default() { return this.createdDate; }, }, @@ -128,6 +128,7 @@ const userProfileSchema = new Schema({ required: true, default: 'white', }, + iconId: { type: String, required: true }, }, ], location: { diff --git a/src/routes/curentWarningsRouter.js b/src/routes/curentWarningsRouter.js new file mode 100644 index 000000000..f1a004493 --- /dev/null +++ b/src/routes/curentWarningsRouter.js @@ -0,0 +1,23 @@ +const express = require('express'); + +const route = function (currentWarnings) { + const controller = require('../controllers/currentWarningsController')(currentWarnings); + + const currentWarningsRouter = express.Router(); + + currentWarningsRouter + .route('/currentWarnings') + .get(controller.getCurrentWarnings) + .post(controller.postNewWarningDescription); + + currentWarningsRouter.route('/currentWarnings/edit').put(controller.editWarningDescription); + + currentWarningsRouter + .route('/currentWarnings/:warningDescriptionId') + .delete(controller.deleteWarningDescription) + .put(controller.updateWarningDescription); + + return currentWarningsRouter; +}; + +module.exports = route; diff --git a/src/routes/jobsRouter.js b/src/routes/jobsRouter.js new file mode 100644 index 000000000..5b66735a6 --- /dev/null +++ b/src/routes/jobsRouter.js @@ -0,0 +1,14 @@ +const express = require('express'); +const jobsController = require('../controllers/jobsController'); // Adjust the path if needed + +const router = express.Router(); + +// Define routes +router.get('/', jobsController.getJobs); +router.get('/categories', jobsController.getCategories); +router.get('/:id', jobsController.getJobById); +router.post('/', jobsController.createJob); +router.put('/:id', jobsController.updateJob); +router.delete('/:id', jobsController.deleteJob); + +module.exports = router; diff --git a/src/routes/titleRouter.js b/src/routes/titleRouter.js index ce00e2279..1bced1e08 100644 --- a/src/routes/titleRouter.js +++ b/src/routes/titleRouter.js @@ -7,7 +7,7 @@ const router = function (title) { titleRouter.route('/title') .get(controller.getAllTitles) .post(controller.postTitle) - // .put(controller.putTitle); + // .put(controller.putTitle); titleRouter.route('/title/update').post(controller.updateTitle); diff --git a/src/routes/userProfileRouter.js b/src/routes/userProfileRouter.js index e622e729d..5babb9384 100644 --- a/src/routes/userProfileRouter.js +++ b/src/routes/userProfileRouter.js @@ -120,6 +120,10 @@ const routes = function (userProfile, project) { userProfileRouter.route('/userProfile/teamCode/list').get(controller.getAllTeamCode); + userProfileRouter + .route('/userProfile/autocomplete/:searchText') + .get(controller.getUserByAutocomplete); + return userProfileRouter; }; diff --git a/src/startup/middleware.js b/src/startup/middleware.js index 6304f1840..400f5af6c 100644 --- a/src/startup/middleware.js +++ b/src/startup/middleware.js @@ -39,6 +39,10 @@ module.exports = function (app) { next(); return; } + if (req.originalUrl.startsWith('/api/jobs') && req.method === 'GET') { + next(); + return; + } if (!req.header('Authorization')) { res.status(401).send({ 'error:': 'Unauthorized request' }); return; diff --git a/src/startup/routes.js b/src/startup/routes.js index 82a4155a8..8274188bc 100644 --- a/src/startup/routes.js +++ b/src/startup/routes.js @@ -4,7 +4,6 @@ const project = require('../models/project'); const information = require('../models/information'); const team = require('../models/team'); // const actionItem = require('../models/actionItem'); -const notification = require('../models/notification'); const wbs = require('../models/wbs'); const task = require('../models/task'); const popup = require('../models/popupEditor'); @@ -16,6 +15,8 @@ const inventoryItemType = require('../models/inventoryItemType'); const role = require('../models/role'); const rolePreset = require('../models/rolePreset'); const ownerMessage = require('../models/ownerMessage'); +const currentWarnings = require('../models/currentWarnings'); + // Title const title = require('../models/title'); const blueSquareEmailAssignment = require('../models/BlueSquareEmailAssignment'); @@ -49,12 +50,14 @@ const followUp = require('../models/followUp'); const userProfileRouter = require('../routes/userProfileRouter')(userProfile, project); const warningRouter = require('../routes/warningRouter')(userProfile); +const currentWarningsRouter = require('../routes/curentWarningsRouter')(currentWarnings); const badgeRouter = require('../routes/badgeRouter')(badge); const dashboardRouter = require('../routes/dashboardRouter')(weeklySummaryAIPrompt); 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 jobsRouter = require('../routes/jobsRouter'); // const actionItemRouter = require('../routes/actionItemRouter')(actionItem); const notificationRouter = require('../routes/notificationRouter')(); const loginRouter = require('../routes/loginRouter')(); @@ -158,10 +161,12 @@ module.exports = function (app) { app.use('/api', isEmailExistsRouter); app.use('/api', mapLocationRouter); app.use('/api', warningRouter); + app.use('/api', currentWarningsRouter); app.use('/api', titleRouter); app.use('/api', timeOffRequestRouter); app.use('/api', followUpRouter); app.use('/api', blueSquareEmailAssignmentRouter); + app.use('/api/jobs', jobsRouter); // bm dashboard app.use('/api/bm', bmLoginRouter); app.use('/api/bm', bmMaterialsRouter); diff --git a/src/utilities/createInitialPermissions.js b/src/utilities/createInitialPermissions.js index a530d17c3..7344c6016 100644 --- a/src/utilities/createInitialPermissions.js +++ b/src/utilities/createInitialPermissions.js @@ -259,7 +259,7 @@ const permissionsRoles = [ 'changeUserRehireableStatus', 'toggleInvisibility', 'manageAdminLinks', - + 'removeUserFromTask', 'editHeaderMessage', ], }, diff --git a/src/utilities/emailSender.js b/src/utilities/emailSender.js index eb8eca3de..b0fb40112 100644 --- a/src/utilities/emailSender.js +++ b/src/utilities/emailSender.js @@ -2,104 +2,113 @@ const nodemailer = require('nodemailer'); const { google } = require('googleapis'); const logger = require('../startup/logger'); -const closure = () => { - const queue = []; - - const CLIENT_EMAIL = process.env.REACT_APP_EMAIL; - const CLIENT_ID = process.env.REACT_APP_EMAIL_CLIENT_ID; - const CLIENT_SECRET = process.env.REACT_APP_EMAIL_CLIENT_SECRET; - const REDIRECT_URI = process.env.REACT_APP_EMAIL_CLIENT_REDIRECT_URI; - const REFRESH_TOKEN = process.env.REACT_APP_EMAIL_REFRESH_TOKEN; - // Create the email envelope (transport) - const transporter = nodemailer.createTransport({ - service: 'gmail', - auth: { - type: 'OAuth2', - user: CLIENT_EMAIL, - clientId: CLIENT_ID, - clientSecret: CLIENT_SECRET, - }, - }); +const config = { + email: process.env.REACT_APP_EMAIL, + clientId: process.env.REACT_APP_EMAIL_CLIENT_ID, + clientSecret: process.env.REACT_APP_EMAIL_CLIENT_SECRET, + redirectUri: process.env.REACT_APP_EMAIL_CLIENT_REDIRECT_URI, + refreshToken: process.env.REACT_APP_EMAIL_REFRESH_TOKEN, + batchSize: 50, + concurrency: 3, + rateLimitDelay: 1000, +}; - const OAuth2Client = new google.auth.OAuth2(CLIENT_ID, CLIENT_SECRET, REDIRECT_URI); +const OAuth2Client = new google.auth.OAuth2( + config.clientId, + config.clientSecret, + config.redirectUri, +); +OAuth2Client.setCredentials({ refresh_token: config.refreshToken }); - OAuth2Client.setCredentials({ refresh_token: REFRESH_TOKEN }); +// Create the email envelope (transport) +const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + type: 'OAuth2', + user: config.email, + clientId: config.clientId, + clientSecret: config.clientSecret, + }, +}); - setInterval(async () => { - const nextItem = queue.shift(); +const sendEmail = async (mailOptions) => { + try { + const { token } = await OAuth2Client.getAccessToken(); + mailOptions.auth = { + user: config.email, + refreshToken: config.refreshToken, + accessToken: token, + }; + const result = await transporter.sendMail(mailOptions); + if (process.env.NODE_ENV === 'local') { + logger.logInfo(`Email sent: ${JSON.stringify(result)}`); + } + return result; + } catch (error) { + logger.logException(error, `Error sending email: ${mailOptions.to}`); + throw error; + } +}; - if (!nextItem) return; +const queue = []; +let isProcessing = false; - const { recipient, subject, message, cc, bcc, replyTo, acknowledgingReceipt } = nextItem; +const processQueue = async () => { + if (isProcessing || queue.length === 0) return; - try { - // Generate the accessToken on the fly - const res = await OAuth2Client.getAccessToken(); - const ACCESSTOKEN = res.token; + isProcessing = true; + console.log('Processing email queue...'); - const mailOptions = { - from: CLIENT_EMAIL, - to: recipient, - cc, - bcc, - subject, - html: message, - replyTo, - auth: { - user: CLIENT_EMAIL, - refreshToken: REFRESH_TOKEN, - accessToken: ACCESSTOKEN, - }, - }; + const processBatch = async () => { + if (queue.length === 0) { + isProcessing = false; + return; + } - const result = await transporter.sendMail(mailOptions); - if (typeof acknowledgingReceipt === 'function') { - acknowledgingReceipt(null, result); - } - // Prevent logging email in production - // Why? - // 1. Could create a security risk - // 2. Could create heavy loads on the server if emails are sent to many people - // 3. Contain limited useful info: - // result format : {"accepted":["emailAddr"],"rejected":[],"envelopeTime":209,"messageTime":566,"messageSize":317,"response":"250 2.0.0 OK 17***69 p11-2***322qvd.85 - gsmtp","envelope":{"from":"emailAddr", "to":"emailAddr"}} - if (process.env.NODE_ENV === 'local') { - logger.logInfo(`Email sent: ${JSON.stringify(result)}`); - } + const batch = queue.shift(); + try { + console.log('Sending email...'); + await sendEmail(batch); } catch (error) { - if (typeof acknowledgingReceipt === 'function') { - acknowledgingReceipt(error, null); - } - logger.logException( - error, - `Error sending email: from ${CLIENT_EMAIL} to ${recipient} subject ${subject}`, - `Extra Data: cc ${cc} bcc ${bcc}`, - ); + logger.logException(error, 'Failed to send email batch'); } - }, process.env.MAIL_QUEUE_INTERVAL || 1000); - const emailSender = function ( - recipient, - subject, - message, - cc = null, - bcc = null, - replyTo = null, - acknowledgingReceipt = null, - ) { - if (process.env.sendEmail) { - queue.push({ - recipient, - subject, - message, - cc, - bcc, - replyTo, - acknowledgingReceipt, - }); - } + setTimeout(processBatch, config.rateLimitDelay); }; - return emailSender; + const concurrentProcesses = Array(config.concurrency).fill().map(processBatch); + + try { + await Promise.all(concurrentProcesses); + } finally { + isProcessing = false; + } +}; + +const emailSender = ( + recipients, + subject, + message, + attachments = null, + cc = null, + replyTo = null, +) => { + if (!process.env.sendEmail) return; + const recipientsArray = Array.isArray(recipients) ? recipients : [recipients]; + for (let i = 0; i < recipients.length; i += config.batchSize) { + const batchRecipients = recipientsArray.slice(i, i + config.batchSize); + queue.push({ + from: config.email, + bcc: batchRecipients.join(','), + subject, + html: message, + attachments, + cc, + replyTo, + }); + } + console.log('Emails queued:', queue.length); + setImmediate(processQueue); }; -module.exports = closure(); +module.exports = emailSender; diff --git a/src/utilities/permissions.js b/src/utilities/permissions.js index 2299e8812..75aee3b59 100644 --- a/src/utilities/permissions.js +++ b/src/utilities/permissions.js @@ -64,10 +64,10 @@ const canRequestorUpdateUser = async (requestorId, targetUserId) => { // Find out a list of protected email account ids and allowed email id allowedEmailAccountIds = query .filter(({ email }) => ALLOWED_EMAIL_ACCOUNT.includes(email)) - .map(({ _id }) => _id); + .map(({ _id }) => _id.toString()); protectedEmailAccountIds = query .filter(({ email }) => PROTECTED_EMAIL_ACCOUNT.includes(email)) - .map(({ _id }) => _id); + .map(({ _id }) => _id.toString()); serverCache.setCache('protectedEmailAccountIds', protectedEmailAccountIds); serverCache.setCache('allowedEmailAccountIds', allowedEmailAccountIds);