diff --git a/.circleci/config.yml b/.circleci/config.yml index 3ae3c51c0..763508b11 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1613'] + only: ['develop', 'migration-setup', 'pm-1611_1'] - deployProd: context : org-global filters: diff --git a/src/constants.js b/src/constants.js index b2987e65a..0dc8af26f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -314,6 +314,8 @@ export const TEMPLATE_IDS = { INFORM_PM_COPILOT_APPLICATION_ACCEPTED: 'd-b35d073e302b4279a1bd208fcfe96f58', COPILOT_ALREADY_PART_OF_PROJECT: 'd-003d41cdc9de4bbc9e14538e8f2e0585', COPILOT_APPLICATION_ACCEPTED: 'd-eef5e7568c644940b250e76d026ced5b', + COPILOT_OPPORTUNITY_COMPLETED: 'd-dc448919d11b4e7d8b4ba351c4b67b8b', + COPILOT_OPPORTUNITY_CANCELED: 'd-2a67ba71e82f4d70891fe6989c3522a3' } export const REGEX = { URL: /^(http(s?):\/\/)?(www\.)?[a-zA-Z0-9\.\-\_]+(\.[a-zA-Z]{2,15})+(\:[0-9]{2,5})?(\/[a-zA-Z0-9\_\-\s\.\/\?\%\#\&\=;]*)?$/, // eslint-disable-line diff --git a/src/models/copilotRequest.js b/src/models/copilotRequest.js index 7ddb924e1..c566073e6 100644 --- a/src/models/copilotRequest.js +++ b/src/models/copilotRequest.js @@ -2,7 +2,7 @@ import _ from 'lodash'; import { COPILOT_REQUEST_STATUS } from '../constants'; module.exports = function defineCopilotRequest(sequelize, DataTypes) { - const CopliotRequest = sequelize.define('CopilotRequest', { + const CopilotRequest = sequelize.define('CopilotRequest', { id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true }, status: { type: DataTypes.STRING(16), @@ -30,9 +30,10 @@ module.exports = function defineCopilotRequest(sequelize, DataTypes) { indexes: [], }); - CopliotRequest.associate = (models) => { - CopliotRequest.hasMany(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'copilotRequestId' }); + CopilotRequest.associate = (models) => { + CopilotRequest.hasMany(models.CopilotOpportunity, { as: 'copilotOpportunity', foreignKey: 'copilotRequestId' }); + CopilotRequest.belongsTo(models.Project, { as: 'project', foreignKey: 'projectId' }); }; - return CopliotRequest; + return CopilotRequest; }; diff --git a/src/routes/copilotOpportunity/assign.js b/src/routes/copilotOpportunity/assign.js index bf021bbb5..106ca9bb8 100644 --- a/src/routes/copilotOpportunity/assign.js +++ b/src/routes/copilotOpportunity/assign.js @@ -32,6 +32,34 @@ module.exports = [ return next(err); } + const sendEmailToAllApplicants = async (copilotRequest, allApplications) => { + + const userIds = allApplications.map(item => item.userId); + + const users = await util.getMemberDetailsByUserIds(userIds, req.log, req.id); + + users.forEach(async (user) => { + req.log.debug(`Sending email notification to copilots who are not accepted`); + const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; + const copilotPortalUrl = config.get('copilotPortalUrl'); + const requestData = copilotRequest.data; + createEvent(emailEventType, { + data: { + opportunity_details_url: copilotPortalUrl, + work_manager_url: config.get('workManagerUrl'), + opportunity_title: requestData.opportunityTitle, + user_name: user ? user.handle : "", + }, + sendgrid_template_id: TEMPLATE_IDS.COPILOT_OPPORTUNITY_COMPLETED, + recipients: [user.email], + version: 'v3', + }, req.log); + + req.log.debug(`Email sent to copilots who are not accepted`); + }); + + }; + return models.sequelize.transaction(async (t) => { const opportunity = await models.CopilotOpportunity.findOne({ where: { id: copilotOpportunityId }, @@ -238,6 +266,9 @@ module.exports = [ transaction: t, }); + // Send email to all applicants about opportunity completion + await sendEmailToAllApplicants(copilotRequest, otherApplications); + for (const otherApplication of otherApplications) { await otherApplication.update({ status: COPILOT_APPLICATION_STATUS.CANCELED, diff --git a/src/routes/copilotOpportunity/delete.js b/src/routes/copilotOpportunity/delete.js index 5336807f3..f825d7c7d 100644 --- a/src/routes/copilotOpportunity/delete.js +++ b/src/routes/copilotOpportunity/delete.js @@ -1,9 +1,11 @@ import _ from 'lodash'; import { Op } from 'sequelize'; +import config from 'config'; import models from '../../models'; import util from '../../util'; -import { COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, RESOURCES } from '../../constants'; +import { CONNECT_NOTIFICATION_EVENT, COPILOT_APPLICATION_STATUS, COPILOT_OPPORTUNITY_STATUS, COPILOT_REQUEST_STATUS, EVENT, INVITE_STATUS, RESOURCES, TEMPLATE_IDS } from '../../constants'; +import { createEvent } from '../../services/busApi'; import { PERMISSION } from '../../permissions/constants'; @@ -20,6 +22,32 @@ module.exports = [ // default values const opportunityId = _.parseInt(req.params.id); + const sendEmailToAllApplicants = async (copilotRequest, applications) => { + const userIds = applications.map(item => item.userId); + const users = await util.getMemberDetailsByUserIds(userIds, req.log, req.id); + + users.forEach(async (user) => { + req.log.debug(`Sending email notification to copilots who applied`); + const emailEventType = CONNECT_NOTIFICATION_EVENT.EXTERNAL_ACTION_EMAIL; + const copilotPortalUrl = config.get('copilotPortalUrl'); + const requestData = copilotRequest.data; + createEvent(emailEventType, { + data: { + opportunity_details_url: copilotPortalUrl, + work_manager_url: config.get('workManagerUrl'), + opportunity_title: requestData.opportunityTitle, + user_name: user ? user.handle : "", + }, + sendgrid_template_id: TEMPLATE_IDS.COPILOT_OPPORTUNITY_CANCELED, + recipients: [user.email], + version: 'v3', + }, req.log); + + req.log.debug(`Email sent to copilots who applied`); + }); + + }; + return models.sequelize.transaction(async (transaction) => { req.log.debug('Canceling Copilot opportunity transaction', opportunityId); const opportunity = await models.CopilotOpportunity.findOne({ @@ -93,6 +121,8 @@ module.exports = [ invite.toJSON()); } + await sendEmailToAllApplicants(copilotRequest, applications) + res.status(200).send({ id: opportunity.id }); }) diff --git a/src/routes/copilotOpportunity/get.js b/src/routes/copilotOpportunity/get.js index 9202a8458..e4bad5c8d 100644 --- a/src/routes/copilotOpportunity/get.js +++ b/src/routes/copilotOpportunity/get.js @@ -4,7 +4,6 @@ import util from '../../util'; module.exports = [ (req, res, next) => { const { id } = req.params; - if (!id || isNaN(id)) { return util.handleError('Invalid opportunity ID', null, req, next, 400); } diff --git a/src/routes/copilotRequest/list.js b/src/routes/copilotRequest/list.js index a36a3d7bf..ef36d26bc 100644 --- a/src/routes/copilotRequest/list.js +++ b/src/routes/copilotRequest/list.js @@ -1,8 +1,10 @@ import _ from 'lodash'; +import { Op, Sequelize } from 'sequelize'; import models from '../../models'; import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; +import { DEFAULT_PAGE_SIZE } from '../../constants'; module.exports = [ (req, res, next) => { @@ -15,33 +17,67 @@ module.exports = [ return next(err); } + const page = parseInt(req.query.page, 10) || 1; + const pageSize = parseInt(req.query.pageSize, 10) || DEFAULT_PAGE_SIZE; + const offset = (page - 1) * pageSize; + const projectId = _.parseInt(req.params.projectId); let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc'; if (sort.indexOf(' ') === -1) { sort += ' asc'; } - const sortableProps = ['createdAt asc', 'createdAt desc']; + const sortableProps = [ + 'createdAt asc', + 'createdAt desc', + 'projectName asc', + 'projectName desc', + 'opportunityTitle asc', + 'opportunityTitle desc', + 'projectType asc', + 'projectType desc', + 'status asc', + 'status desc', + ]; if (_.indexOf(sortableProps, sort) < 0) { return util.handleError('Invalid sort criteria', null, req, next); } - const sortParams = sort.split(' '); + let sortParams = sort.split(' '); + let order = [[sortParams[0], sortParams[1]]]; + const relationBasedSortParams = ['projectName']; + const jsonBasedSortParams = ['opportunityTitle', 'projectType']; + if (relationBasedSortParams.includes(sortParams[0])) { + order = [ + [{model: models.Project, as: 'project'}, 'name', sortParams[1]], + ['id', 'DESC'] + ] + } + + if (jsonBasedSortParams.includes(sortParams[0])) { + order = [ + [models.sequelize.literal(`("CopilotRequest"."data"->>'${sortParams[0]}')`), sortParams[1]], + ['id', 'DESC'], + ] + } const whereCondition = projectId ? { projectId } : {}; - return models.CopilotRequest.findAll({ + return models.CopilotRequest.findAndCountAll({ where: whereCondition, include: [ - { - model: models.CopilotOpportunity, - as: 'copilotOpportunity', - }, + { model: models.CopilotOpportunity, as: 'copilotOpportunity', required: false }, + { model: models.Project, as: 'project', required: false }, ], - order: [[sortParams[0], sortParams[1]]], - }) - .then(copilotRequests => res.json(copilotRequests)) - .catch((err) => { - util.handleError('Error fetching copilot requests', err, req, next); - }); + order, + limit: pageSize, + offset, + distinct: true, + subQuery: false, + }).then(({rows: copilotRequests, count}) => util.setPaginationHeaders(req, res, { + count: count, + rows: copilotRequests, + page, + pageSize, + })); }, ];