@@ -266,6 +266,9 @@ export function TaskUpsertModalComponent({
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Form.Item name="remind_at_sent" hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("tasks.fields.remind_at")}
|
||||
name="remind_at"
|
||||
|
||||
@@ -168,30 +168,6 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
||||
});
|
||||
}
|
||||
|
||||
// if (isAssignedToDirty) {
|
||||
// // TODO This is being moved serverside
|
||||
// axios
|
||||
// .post("/sendemail", {
|
||||
// from: {
|
||||
// name: bodyshop.shopname,
|
||||
// address: bodyshop.email
|
||||
// },
|
||||
// ReplyTo: {
|
||||
// Email: "noreply@imex.online"
|
||||
// },
|
||||
// to: values.assigned_to,
|
||||
// subject: `A Task has been re-assigned to you on ${bodyshop.shopname} - ${values.title}`,
|
||||
// templateStrings: {
|
||||
// header: values.title,
|
||||
// subHeader: `Assigned by ${currentUser.email} ${values.due_at ? `| Due on ${dayjs(values.due_at).format("MM/DD/YYYY")}` : ""}`,
|
||||
// body: `<a href="${window.location.protocol}//${window.location.host}/manage/tasks/alltasks?taskid=${existingTask.id}">Please sign in to your account to view the task details.</a>`
|
||||
// }
|
||||
// })
|
||||
// .catch((e) =>
|
||||
// console.error(`Something went wrong sending email to Assigned party on Task creation. ${e.message || ""}`)
|
||||
// );
|
||||
// }
|
||||
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.updated")
|
||||
});
|
||||
@@ -233,30 +209,6 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
||||
form.resetFields();
|
||||
toggleModalVisible();
|
||||
|
||||
// send notification to the assigned user
|
||||
// TODO: This is being moved serverside
|
||||
// axios
|
||||
// .post("/sendemail", {
|
||||
// from: {
|
||||
// name: bodyshop.shopname,
|
||||
// address: bodyshop.email
|
||||
// },
|
||||
// replyTo: {
|
||||
// Email: "noreply@imex.online"
|
||||
// },
|
||||
// to: values.assigned_to,
|
||||
// subject: `A new Task has been assigned to you on ${bodyshop.shopname} - ${values.title}`,
|
||||
// templateName: "taskAssigned",
|
||||
// templateStrings: {
|
||||
// header: values.title,
|
||||
// subHeader: `Assigned by ${currentUser.email} ${values.due_at ? `| Due on ${dayjs(values.due_at).format("MM/DD/YYYY")}` : ""}`,
|
||||
// body: `<a href="${window.location.protocol}//${window.location.host}/manage/tasks/alltasks?taskid=${newTaskID}">Please sign to your account to view the task details.</a>`
|
||||
// }
|
||||
// })
|
||||
// .catch((e) =>
|
||||
// console.error(`Something went wrong sending email to Assigned party on Task edit. ${e.message || ""}`)
|
||||
// );
|
||||
|
||||
notification["success"]({
|
||||
message: t("tasks.successes.created")
|
||||
});
|
||||
@@ -276,6 +228,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
console.dir(dirtyValues?.remind_at_sent);
|
||||
try {
|
||||
await handleExistingTask(taskSource.id, taskSource.jobid, dirtyValues);
|
||||
} catch (e) {
|
||||
|
||||
@@ -15,6 +15,7 @@ export const PARTIAL_TASK_FIELDS = gql`
|
||||
completed
|
||||
completed_at
|
||||
remind_at
|
||||
remind_at_sent
|
||||
priority
|
||||
job {
|
||||
id
|
||||
@@ -63,10 +64,6 @@ export const PARTIAL_TASK_FIELDS = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export const PARTIAL_TASK_FIELDS_WRAPPER = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
`;
|
||||
|
||||
export const QUERY_GET_TASK_BY_ID = gql`
|
||||
${PARTIAL_TASK_FIELDS}
|
||||
query QUERY_GET_TASK_BY_ID($id: uuid!) {
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
[]
|
||||
- name: Task Reminders
|
||||
webhook: https://worktest.home.irony.online/tasks-remind-handler
|
||||
schedule: '*/1 * * * *'
|
||||
include_in_metadata: true
|
||||
payload: {}
|
||||
headers:
|
||||
- name: event-secret
|
||||
value: DevelopmentEventSecret
|
||||
|
||||
@@ -12,6 +12,7 @@ const client = require("../graphql-client/graphql-client").client;
|
||||
const queries = require("../graphql-client/queries");
|
||||
const { isObject } = require("lodash");
|
||||
const generateEmailTemplate = require("./generateTemplate");
|
||||
const moment = require("moment");
|
||||
|
||||
const ses = new aws.SES({
|
||||
// The key apiVersion is no longer supported in v3, and can be removed.
|
||||
@@ -23,12 +24,46 @@ const ses = new aws.SES({
|
||||
rome: "us-east-2"
|
||||
})
|
||||
});
|
||||
|
||||
let transporter = nodemailer.createTransport({
|
||||
SES: { ses, aws }
|
||||
});
|
||||
|
||||
exports.sendServerEmail = async function ({ subject, text }) {
|
||||
// Get the image from the URL and return it as a base64 string
|
||||
const getImage = async (imageUrl) => {
|
||||
let image = await axios.get(imageUrl, { responseType: "arraybuffer" });
|
||||
let raw = Buffer.from(image.data).toString("base64");
|
||||
return "data:" + image.headers["content-type"] + ";base64," + raw;
|
||||
};
|
||||
|
||||
// Log the email in the database
|
||||
const logEmail = async (req, email) => {
|
||||
try {
|
||||
const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, {
|
||||
email: {
|
||||
to: email.to,
|
||||
cc: email.cc,
|
||||
subject: email.subject,
|
||||
bodyshopid: req.body.bodyshopid,
|
||||
useremail: req.user.email,
|
||||
contents: req.body.html,
|
||||
jobid: req.body.jobid,
|
||||
sesmessageid: email.messageId,
|
||||
status: "Sent"
|
||||
}
|
||||
});
|
||||
console.log(insertresult);
|
||||
} catch (error) {
|
||||
logger.log("email-log-error", "error", req.user.email, null, {
|
||||
from: `${req.body.from.name} <${req.body.from.address}>`,
|
||||
to: req.body.to,
|
||||
cc: req.body.cc,
|
||||
subject: req.body.subject
|
||||
// info,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const sendServerEmail = async ({ subject, text }) => {
|
||||
if (process.env.NODE_ENV === undefined) return;
|
||||
try {
|
||||
transporter.sendMail(
|
||||
@@ -60,7 +95,8 @@ exports.sendServerEmail = async function ({ subject, text }) {
|
||||
logger.log("server-email-failure", "error", null, null, error);
|
||||
}
|
||||
};
|
||||
exports.sendTaskEmail = async function ({ to, subject, text, attachments }) {
|
||||
|
||||
const sendTaskEmail = async ({ to, subject, text, attachments }) => {
|
||||
try {
|
||||
transporter.sendMail(
|
||||
{
|
||||
@@ -84,19 +120,8 @@ exports.sendTaskEmail = async function ({ to, subject, text, attachments }) {
|
||||
}
|
||||
};
|
||||
|
||||
// This will be called by a Hasura event trigger
|
||||
exports.taskAssignedEmail = async function (req, res) {
|
||||
console.dir(req, { depth: null });
|
||||
return res.status(200).json(req);
|
||||
};
|
||||
|
||||
// This will be called by a Hasura event trigger
|
||||
exports.tasksRemindEmail = async function (req, res) {
|
||||
console.dir(req, { depth: null });
|
||||
return res.status(200).json(req);
|
||||
};
|
||||
|
||||
exports.sendEmail = async (req, res) => {
|
||||
// Send an email
|
||||
const sendEmail = async (req, res) => {
|
||||
logger.log("send-email", "DEBUG", req.user.email, null, {
|
||||
from: `${req.body.from.name} <${req.body.from.address}>`,
|
||||
replyTo: req.body.ReplyTo.Email,
|
||||
@@ -204,40 +229,8 @@ exports.sendEmail = async (req, res) => {
|
||||
);
|
||||
};
|
||||
|
||||
async function getImage(imageUrl) {
|
||||
let image = await axios.get(imageUrl, { responseType: "arraybuffer" });
|
||||
let raw = Buffer.from(image.data).toString("base64");
|
||||
return "data:" + image.headers["content-type"] + ";base64," + raw;
|
||||
}
|
||||
|
||||
async function logEmail(req, email) {
|
||||
try {
|
||||
const insertresult = await client.request(queries.INSERT_EMAIL_AUDIT, {
|
||||
email: {
|
||||
to: email.to,
|
||||
cc: email.cc,
|
||||
subject: email.subject,
|
||||
bodyshopid: req.body.bodyshopid,
|
||||
useremail: req.user.email,
|
||||
contents: req.body.html,
|
||||
jobid: req.body.jobid,
|
||||
sesmessageid: email.messageId,
|
||||
status: "Sent"
|
||||
}
|
||||
});
|
||||
console.log(insertresult);
|
||||
} catch (error) {
|
||||
logger.log("email-log-error", "error", req.user.email, null, {
|
||||
from: `${req.body.from.name} <${req.body.from.address}>`,
|
||||
to: req.body.to,
|
||||
cc: req.body.cc,
|
||||
subject: req.body.subject
|
||||
// info,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.emailBounce = async function (req, res) {
|
||||
// This will be called by an SNS event trigger
|
||||
const emailBounce = async (req, res) => {
|
||||
try {
|
||||
const body = JSON.parse(req.body);
|
||||
if (body.Type === "SubscriptionConfirmation") {
|
||||
@@ -311,3 +304,10 @@ ${body.bounce?.bouncedRecipients.map(
|
||||
}
|
||||
res.sendStatus(200);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sendEmail,
|
||||
sendServerEmail,
|
||||
sendTaskEmail,
|
||||
emailBounce
|
||||
};
|
||||
|
||||
223
server/email/tasksEmails.js
Normal file
223
server/email/tasksEmails.js
Normal file
@@ -0,0 +1,223 @@
|
||||
const path = require("path");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||
});
|
||||
let nodemailer = require("nodemailer");
|
||||
let aws = require("@aws-sdk/client-ses");
|
||||
let { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||
const InstanceManager = require("../utils/instanceMgr").default;
|
||||
const logger = require("../utils/logger");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
const queries = require("../graphql-client/queries");
|
||||
const generateEmailTemplate = require("./generateTemplate");
|
||||
const moment = require("moment");
|
||||
const { UPDATE_TASKS_REMIND_AT_SENT } = require("../graphql-client/queries");
|
||||
|
||||
const ses = new aws.SES({
|
||||
apiVersion: "latest",
|
||||
defaultProvider,
|
||||
region: InstanceManager({
|
||||
imex: "ca-central-1",
|
||||
rome: "us-east-2"
|
||||
})
|
||||
});
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
SES: { ses, aws },
|
||||
sendingRate: 40 // 40 emails per second.
|
||||
});
|
||||
|
||||
const fromEmails = InstanceManager({
|
||||
imex: `ImEX Online <noreply@imex.online>`,
|
||||
rome: `Rome Online <noreply@romeonline.io>`,
|
||||
promanager: `ProManager <noreply@promanager.web-est.com>`
|
||||
});
|
||||
|
||||
/**
|
||||
* Format the date for the email.
|
||||
* @param date
|
||||
* @returns {string|string}
|
||||
*/
|
||||
const formatDate = (date) => {
|
||||
return date ? `| Due on ${moment(date).format("MM/DD/YYYY")}` : "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the email template arguments.
|
||||
* @param title
|
||||
* @param createdBy
|
||||
* @param dueDate
|
||||
* @param taskId
|
||||
* @returns {{header, body: string, subHeader: string}}
|
||||
*/
|
||||
const generateTemplateArgs = (title, createdBy, dueDate, taskId) => {
|
||||
return {
|
||||
header: title,
|
||||
subHeader: `Assigned by ${createdBy} ${formatDate(dueDate)}`,
|
||||
body: `<a href="${fromEmails}/manage/tasks/alltasks?taskid=${taskId}">Please sign in to your account to view the Task details.</a>`
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Send the email.
|
||||
* @param type
|
||||
* @param to
|
||||
* @param subject
|
||||
* @param html
|
||||
* @param taskIds
|
||||
* @param successCallback
|
||||
*/
|
||||
const sendMail = (type, to, subject, html, taskIds, successCallback) => {
|
||||
// Push next messages to Nodemailer
|
||||
transporter.once("idle", () => {
|
||||
if (transporter.isIdle()) {
|
||||
transporter.sendMail(
|
||||
{
|
||||
from: fromEmails,
|
||||
to,
|
||||
subject,
|
||||
html
|
||||
},
|
||||
(error, info) => {
|
||||
if (info) {
|
||||
if (typeof successCallback === "function" && taskIds && taskIds.length) {
|
||||
successCallback(taskIds);
|
||||
}
|
||||
} else {
|
||||
logger.log(`task-${type}-email-failure`, "error", null, null, error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Send an email to the assigned user.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const taskAssignedEmail = async (req, res) => {
|
||||
// We have no event Data, bail
|
||||
if (!req?.payload?.event?.data?.new) {
|
||||
return res.status(400).json({ message: "No data in the event payload" });
|
||||
}
|
||||
|
||||
const { new: newTask } = req.payload.event.data;
|
||||
|
||||
// This is not a new task, but a reassignment.
|
||||
const dirty = req.payload.event.data?.old && req.payload.event.data?.old?.assigned_to;
|
||||
|
||||
sendMail(
|
||||
"assigned",
|
||||
newTask.assigned_to,
|
||||
`A Task has been ${dirty ? "reassigned" : "created"} for you - ${newTask.title}`,
|
||||
generateEmailTemplate(generateTemplateArgs(newTask.title, newTask.created_by, newTask.due_date, newTask.id))
|
||||
);
|
||||
|
||||
// We return success regardless because we don't want to block the event trigger.
|
||||
res.status(200).json({ success: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Send an email to remind the user of their tasks.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const tasksRemindEmail = async (req, res) => {
|
||||
try {
|
||||
const tasksRequest = await client.request(queries.QUERY_REMIND_TASKS, {
|
||||
time: moment().add(1, "minutes").toISOString()
|
||||
});
|
||||
|
||||
// No tasks present in the database, bail.
|
||||
if (!tasksRequest?.tasks || !tasksRequest?.tasks.length) {
|
||||
return res.status(200).json({ message: "No tasks to remind" });
|
||||
}
|
||||
|
||||
// Group tasks by assigned_to, to avoid sending multiple emails to the same recipient.
|
||||
const groupedTasks = tasksRequest.tasks.reduce((acc, task) => {
|
||||
const key = task.assigned_to;
|
||||
if (!acc[key]) {
|
||||
acc[key] = [];
|
||||
}
|
||||
acc[key].push(task);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// No grouped tasks, bail.
|
||||
if (Object.keys(groupedTasks).length === 0) {
|
||||
return res.status(200).json({ message: "No tasks to remind" });
|
||||
}
|
||||
|
||||
// Build an aggregate data object containing email and the count of tasks assigned to them.
|
||||
const recipientCounts = Object.keys(groupedTasks).map((key) => {
|
||||
return {
|
||||
email: key,
|
||||
count: groupedTasks[key].length
|
||||
};
|
||||
});
|
||||
|
||||
// Iterate over all recipients and send the email.
|
||||
recipientCounts.forEach((recipient) => {
|
||||
const emailData = {
|
||||
from: fromEmails,
|
||||
to: recipient.email
|
||||
};
|
||||
|
||||
const taskIds = groupedTasks[recipient.email].map((task) => task.id);
|
||||
|
||||
// There is only the one email to send to this author.
|
||||
if (recipient.count === 1) {
|
||||
const onlyTask = groupedTasks[recipient.email][0];
|
||||
|
||||
emailData.subject = `New Task Reminder - ${onlyTask.title} - ${formatDate(onlyTask.due_date)}`;
|
||||
|
||||
emailData.html = generateEmailTemplate(
|
||||
generateTemplateArgs(onlyTask.title, onlyTask.created_by, onlyTask.due_date, onlyTask.id)
|
||||
);
|
||||
}
|
||||
// There are multiple emails to send to this author.
|
||||
else {
|
||||
const allTasks = groupedTasks[recipient.email];
|
||||
emailData.subject = `New Task Reminder - ${allTasks.length} Tasks require your attention`;
|
||||
emailData.html = generateEmailTemplate({
|
||||
header: `${allTasks.length} Tasks require your attention`,
|
||||
subHeader: `Please sign in to your account to view the Task details.`,
|
||||
body: `<ul>
|
||||
${allTasks
|
||||
.map(
|
||||
(task) =>
|
||||
`<li><a href="${fromEmails}/manage/tasks/alltasks?taskid=${task.id}">${task.title} - ${formatDate(task.due_date)}</a></li>`
|
||||
)
|
||||
.join("")}
|
||||
</ul>`
|
||||
});
|
||||
}
|
||||
|
||||
if (emailData?.subject && emailData?.html) {
|
||||
// Send Email
|
||||
sendMail("remind", emailData.to, emailData.subject, emailData.html, taskIds, (taskIds) => {});
|
||||
client.request(UPDATE_TASKS_REMIND_AT_SENT, {
|
||||
taskIds,
|
||||
now: moment().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sixth step would be to set the remind_at_sent to the current time.
|
||||
res.status(200).json({ status: "success" });
|
||||
} catch (err) {
|
||||
res.status(500).json({
|
||||
status: "error",
|
||||
message: `Something went wrong sending Task Reminders: ${err.message || "An error occurred"}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
taskAssignedEmail,
|
||||
tasksRemindEmail
|
||||
};
|
||||
@@ -2393,3 +2393,37 @@ exports.COMPLETE_SURVEY = `mutation COMPLETE_SURVEY($surveyId: uuid!, $survey: c
|
||||
affected_rows
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.QUERY_REMIND_TASKS = `
|
||||
query QUERY_REMIND_TASKS($time: timestamptz!) {
|
||||
tasks(
|
||||
where: {
|
||||
_and: [
|
||||
{ remind_at: { _is_null: false } }
|
||||
{ remind_at: { _lte: $time } }
|
||||
{ remind_at_sent: { _is_null: true } }
|
||||
]
|
||||
}
|
||||
) {
|
||||
id
|
||||
title
|
||||
due_date
|
||||
created_by
|
||||
assigned_to
|
||||
remind_at
|
||||
remind_at_sent
|
||||
priority
|
||||
job {
|
||||
id
|
||||
ro_number
|
||||
}
|
||||
jobid
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
exports.UPDATE_TASKS_REMIND_AT_SENT = `mutation UPDATE_TASK_REMIND_AT_SENT($taskIds: [uuid!]!, $now: timestamptz!) {
|
||||
update_tasks_many(updates: {where: {id: {_in: $taskIds}}, _set: {remind_at_sent: $now}}) {
|
||||
affected_rows
|
||||
}
|
||||
}`;
|
||||
|
||||
@@ -10,6 +10,7 @@ const os = require("../opensearch/os-handler");
|
||||
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
|
||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
|
||||
const { taskAssignedEmail, tasksRemindEmail } = require("../email/tasksEmails");
|
||||
|
||||
//Test route to ensure Express is responding.
|
||||
router.get("/test", async function (req, res) {
|
||||
@@ -41,8 +42,8 @@ router.post("/sendemail", validateFirebaseIdTokenMiddleware, sendEmail.sendEmail
|
||||
router.post("/emailbounce", bodyParser.text(), sendEmail.emailBounce);
|
||||
|
||||
// Tasks Email Handler
|
||||
router.post("/tasks-assigned-handler", eventAuthorizationMiddleware, sendEmail.taskAssignedEmail);
|
||||
router.post("/tasks-remind-handler", eventAuthorizationMiddleware, sendEmail.tasksRemindEmail);
|
||||
router.post("/tasks-assigned-handler", eventAuthorizationMiddleware, taskAssignedEmail);
|
||||
router.post("/tasks-remind-handler", eventAuthorizationMiddleware, tasksRemindEmail);
|
||||
|
||||
// Handlers
|
||||
router.post("/record-handler/arms", data.arms);
|
||||
|
||||
Reference in New Issue
Block a user