- Progress

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-04-16 15:36:27 -04:00
parent 34d773bcd8
commit bb205af019
8 changed files with 323 additions and 105 deletions

View File

@@ -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"

View File

@@ -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) {

View File

@@ -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!) {

View File

@@ -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

View File

@@ -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
View 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
};

View File

@@ -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
}
}`;

View File

@@ -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);