From 53580fbc784e602e59a14526a1cde57e5d9ce42c Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Thu, 6 Mar 2025 13:36:19 -0500 Subject: [PATCH] IO-3166-Global-Notifications-Part-2 - Checkpoint --- .../job-watcher-toggle.component.jsx | 9 +- hasura/metadata/tables.yaml | 2 + server/email/tasksEmails.js | 19 +- server/graphql-client/queries.js | 1 + server/notifications/queues/appQueue.js | 2 +- server/notifications/queues/emailQueue.js | 2 +- server/notifications/scenarioBuilders.js | 162 ++++++++++++++---- server/notifications/scenarioParser.js | 4 + server/notifications/stringHelpers.js | 15 +- 9 files changed, 166 insertions(+), 50 deletions(-) diff --git a/client/src/components/job-watcher-toggle/job-watcher-toggle.component.jsx b/client/src/components/job-watcher-toggle/job-watcher-toggle.component.jsx index 4a01ea0ca..605d75561 100644 --- a/client/src/components/job-watcher-toggle/job-watcher-toggle.component.jsx +++ b/client/src/components/job-watcher-toggle/job-watcher-toggle.component.jsx @@ -97,7 +97,11 @@ export default function JobWatcherToggleComponent({ {t("notifications.labels.add-watchers")} jobWatchers.every((w) => w.user_email !== e.user_email)) || []} + options={ + bodyshop?.employees?.filter((e) => + jobWatchers.every((w) => w.user_email !== e.user_email && e.active && e.user_email) + ) || [] + } placeholder={t("notifications.labels.employee-search")} value={selectedWatcher} onChange={(value) => { @@ -120,10 +124,9 @@ export default function JobWatcherToggleComponent({ const teamMembers = team.employee_team_members .map((member) => { const employee = bodyshop?.employees?.find((e) => e.id === member.employeeid); - return employee ? employee.user_email : null; + return employee?.user_email && employee?.active ? employee.user_email : null; }) .filter(Boolean); - return { value: JSON.stringify(teamMembers), label: team.name diff --git a/hasura/metadata/tables.yaml b/hasura/metadata/tables.yaml index 63a512bab..4a780b262 100644 --- a/hasura/metadata/tables.yaml +++ b/hasura/metadata/tables.yaml @@ -6290,10 +6290,12 @@ columns: - joblineid - assigned_to + - due_date - partsorderid - completed - description - billid + - title - priority retry_conf: interval_sec: 10 diff --git a/server/email/tasksEmails.js b/server/email/tasksEmails.js index 05811e5e2..b8c3c42a3 100644 --- a/server/email/tasksEmails.js +++ b/server/email/tasksEmails.js @@ -11,6 +11,7 @@ const moment = require("moment-timezone"); const { taskEmailQueue } = require("./tasksEmailsQueue"); const mailer = require("./mailer"); const { InstanceEndpoints } = require("../utils/instanceMgr"); +const { formatTaskPriority } = require("../notifications/stringHelpers"); // Initialize the Tasks Email Queue const tasksEmailQueue = taskEmailQueue(); @@ -62,16 +63,6 @@ const formatDate = (date) => { return date ? `| Due on: ${moment(date).format("MM/DD/YYYY")}` : ""; }; -const formatPriority = (priority) => { - if (priority === 1) { - return "High"; - } else if (priority === 3) { - return "Low"; - } else { - return "Medium"; - } -}; - /** * Generate the email template arguments. * @param title @@ -88,7 +79,7 @@ const generateTemplateArgs = (title, priority, description, dueDate, bodyshop, j const endPoints = InstanceEndpoints(); return { header: title, - subHeader: `Body Shop: ${bodyshop.shopname} | Priority: ${formatPriority(priority)} ${formatDate(dueDate)} | Created By: ${createdBy || "N/A"}`, + subHeader: `Body Shop: ${bodyshop.shopname} | Priority: ${formatTaskPriority(priority)} ${formatDate(dueDate)} | Created By: ${createdBy || "N/A"}`, body: `Reference: ${job.ro_number || "N/A"} | ${job.ownr_co_nm ? job.ownr_co_nm : `${job.ownr_fn || ""} ${job.ownr_ln || ""}`.trim()} | ${`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""}`.trim()}
${description ? description.concat("
") : ""}View this task.`, dateLine }; @@ -155,7 +146,7 @@ const taskAssignedEmail = async (req, res) => { sendMail( "assigned", tasks_by_pk.assigned_to_employee.user_email, - `A ${formatPriority(newTask.priority)} priority task has been ${dirty ? "reassigned to" : "created for"} you - ${newTask.title}`, + `A ${formatTaskPriority(newTask.priority)} priority task has been ${dirty ? "reassigned to" : "created for"} you - ${newTask.title}`, generateEmailTemplate( generateTemplateArgs( newTask.title, @@ -239,7 +230,7 @@ const tasksRemindEmail = async (req, res) => { const onlyTask = groupedTasks[recipient.email][0]; emailData.subject = - `New ${formatPriority(onlyTask.priority)} Priority Task Reminder - ${onlyTask.title} ${onlyTask.due_date ? `- ${formatDate(onlyTask.due_date)}` : ""}`.trim(); + `New ${formatTaskPriority(onlyTask.priority)} Priority Task Reminder - ${onlyTask.title} ${onlyTask.due_date ? `- ${formatDate(onlyTask.due_date)}` : ""}`.trim(); emailData.html = generateEmailTemplate( generateTemplateArgs( @@ -266,7 +257,7 @@ const tasksRemindEmail = async (req, res) => { body: `` diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js index b1b62cd36..cc50b3115 100644 --- a/server/graphql-client/queries.js +++ b/server/graphql-client/queries.js @@ -2726,6 +2726,7 @@ query GET_JOB_WATCHERS($jobid: uuid!) { bodyshop { id shopname + timezone } } } diff --git a/server/notifications/queues/appQueue.js b/server/notifications/queues/appQueue.js index 7403e807c..12f6f6fd8 100644 --- a/server/notifications/queues/appQueue.js +++ b/server/notifications/queues/appQueue.js @@ -7,7 +7,7 @@ const graphQLClient = require("../../graphql-client/graphql-client").client; const APP_CONSOLIDATION_DELAY_IN_MINS = (() => { const envValue = process.env?.APP_CONSOLIDATION_DELAY_IN_MINS; const parsedValue = envValue ? parseInt(envValue, 10) : NaN; - return isNaN(parsedValue) ? 1 : Math.max(1, parsedValue); // Default to 1, ensure at least 1 + return isNaN(parsedValue) ? 3 : Math.max(1, parsedValue); // Default to 3, ensure at least 1 })(); // Base time-related constant (in milliseconds) / DO NOT TOUCH diff --git a/server/notifications/queues/emailQueue.js b/server/notifications/queues/emailQueue.js index 8bb788ddd..fe25c99fa 100644 --- a/server/notifications/queues/emailQueue.js +++ b/server/notifications/queues/emailQueue.js @@ -7,7 +7,7 @@ const { registerCleanupTask } = require("../../utils/cleanupManager"); const EMAIL_CONSOLIDATION_DELAY_IN_MINS = (() => { const envValue = process.env?.EMAIL_CONSOLIDATION_DELAY_IN_MINS; const parsedValue = envValue ? parseInt(envValue, 10) : NaN; - return isNaN(parsedValue) ? 1 : Math.max(1, parsedValue); // Default to 1, ensure at least 1 + return isNaN(parsedValue) ? 3 : Math.max(1, parsedValue); // Default to 3, ensure at least 1 })(); // Base time-related constant (in milliseconds) / DO NOT TOUCH diff --git a/server/notifications/scenarioBuilders.js b/server/notifications/scenarioBuilders.js index 361d130a3..1355c437b 100644 --- a/server/notifications/scenarioBuilders.js +++ b/server/notifications/scenarioBuilders.js @@ -1,4 +1,6 @@ -const { getJobAssignmentType } = require("./stringHelpers"); +const { getJobAssignmentType, formatTaskPriority } = require("./stringHelpers"); +const moment = require("moment-timezone"); +const { startCase } = require("lodash"); /** * Populates the recipients for app, email, and FCM notifications based on scenario watchers. @@ -26,17 +28,17 @@ const populateWatchers = (data, result) => { */ // Verified const alternateTransportChangedBuilder = (data) => { - const body = `The Alternate Transport status has been updated to ${data?.data?.alt_transport}.`; + const body = `The Alternate Transport status has been updated from ${data.changedFields.alt_transport?.old || "unset"} to ${data?.changedFields?.alt_transport?.new || "unset"}.`; const result = { app: { jobId: data.jobId, bodyShopId: data.bodyShopId, jobRoNumber: data.jobRoNumber, key: "notifications.job.alternateTransportChanged", - body, // Same as email body + body, variables: { - alternateTransport: data.changedFields.alt_transport?.new, - oldAlternateTransport: data.changedFields.alt_transport?.old + alternateTransport: data?.changedFields?.alt_transport?.new, + oldAlternateTransport: data?.changedFields?.alt_transport?.old }, recipients: [] }, @@ -60,7 +62,6 @@ const alternateTransportChangedBuilder = (data) => { //verified const billPostedHandler = (data) => { const facing = data?.data?.isinhouse ? "In-House" : "External"; - const body = `An ${facing} Bill has been posted${data?.data?.is_credit_memo ? " (Credit Memo)" : ""}.`.trim(); const result = { @@ -71,8 +72,8 @@ const billPostedHandler = (data) => { key: "notifications.job.billPosted", body, variables: { - facing, - is_credit_memo: data?.data?.is_credit_memo + isInHouse: data?.data?.isinhouse, + isCreditMemo: data?.data?.is_credit_memo }, recipients: [] }, @@ -95,7 +96,7 @@ const billPostedHandler = (data) => { */ // TODO: Needs change const criticalPartsStatusChangedBuilder = (data) => { - const body = `The critical parts status has changed to ${data.data.queued_for_parts ? "queued" : "not queued"}.`; + const body = `The critical parts status has changed to ${data?.data?.queued_for_parts ? "queued" : "not queued"}.`; const result = { app: { jobId: data.jobId, @@ -104,8 +105,8 @@ const criticalPartsStatusChangedBuilder = (data) => { key: "notifications.job.criticalPartsStatusChanged", body, variables: { - queuedForParts: data.data.queued_for_parts, - oldQueuedForParts: data.changedFields.queued_for_parts?.old + queuedForParts: data?.data?.queued_for_parts, + oldQueuedForParts: data?.changedFields?.queued_for_parts?.old }, recipients: [] }, @@ -162,7 +163,7 @@ const intakeDeliveryChecklistCompletedBuilder = (data) => { */ // Verified const jobAssignedToMeBuilder = (data) => { - const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}`; + const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}.`; const result = { app: { jobId: data.jobId, @@ -224,7 +225,7 @@ const jobsAddedToProductionBuilder = (data) => { */ // Verified const jobStatusChangeBuilder = (data) => { - const body = `The status has changed from ${data.changedFields.status.old} to ${data.changedFields.status.new}`; + const body = `The status has changed from ${data?.changedFields?.status?.old || "unset"} to ${data?.changedFields?.status?.new || "unset"}`; const result = { app: { jobId: data.jobId, @@ -294,8 +295,19 @@ const newMediaAddedReassignedBuilder = (data) => { /** * Builds notification data for new notes added to a job. */ +// verified const newNoteAddedBuilder = (data) => { - const body = `An Note has been added: "${data.data.text}"`; + const body = [ + "A", + data?.data?.critical && "critical", + data?.data?.private && "private", + data?.data?.type, + "Note has been added by", + `${data.data.created_by}` + ] + .filter(Boolean) + .join(" "); + const result = { app: { jobId: data.jobId, @@ -304,7 +316,10 @@ const newNoteAddedBuilder = (data) => { key: "notifications.job.newNoteAdded", body, variables: { - text: data.data.text + createdBy: data?.data?.created_by, + critical: data?.data?.critical, + type: data?.data?.type, + private: data?.data?.private }, recipients: [] }, @@ -325,9 +340,11 @@ const newNoteAddedBuilder = (data) => { /** * Builds notification data for new time tickets posted. */ +// Verified const newTimeTicketPostedBuilder = (data) => { const type = data?.data?.cost_center; - const body = `An ${type} time ticket has been posted${data?.data?.flat_rate ? " (Flat Rate)" : ""}.`.trim(); + const body = + `A ${startCase(type.toLowerCase())} Time Ticket for ${data?.data?.date} has been posted${data?.data?.flat_rate ? " (Flat Rate)" : ""}.`.trim(); const result = { app: { @@ -337,7 +354,9 @@ const newTimeTicketPostedBuilder = (data) => { key: "notifications.job.newTimeTicketPosted", body, variables: { - type + type, + date: data?.data?.date, + flatRate: data?.data?.flat_rate }, recipients: [] }, @@ -419,7 +438,27 @@ const paymentCollectedCompletedBuilder = (data) => { * Builds notification data for changes to scheduled dates. */ const scheduledDatesChangedBuilder = (data) => { - const body = `Scheduled dates have been updated.`; + const momentFormat = "MM/DD/YYYY hh:mm a"; + const changedFields = data.changedFields; + + // Define field configurations + const fieldConfigs = { + scheduled_in: "Scheduled In", + scheduled_completion: "Scheduled Completion", + scheduled_delivery: "Scheduled Delivery" + }; + + // Build field messages dynamically + const fieldMessages = Object.entries(fieldConfigs) + .filter(([field]) => changedFields[field]) // Only include changed fields + .map(([field, label]) => { + const { old, new: newValue } = changedFields[field]; + const formatDate = (date) => (date ? moment(date).tz(data.bodyShopTimezone).format(momentFormat) : "unset"); + return `${label} changed from ${formatDate(old)} to ${formatDate(newValue)}`; + }); + + const body = fieldMessages.length > 0 ? fieldMessages.join(", ") + "." : "Scheduled dates have been updated."; + const result = { app: { jobId: data.jobId, @@ -428,12 +467,12 @@ const scheduledDatesChangedBuilder = (data) => { key: "notifications.job.scheduledDatesChanged", body, variables: { - scheduledIn: data.changedFields.scheduled_in?.new, - oldScheduledIn: data.changedFields.scheduled_in?.old, - scheduledCompletion: data.changedFields.scheduled_completion?.new, - oldScheduledCompletion: data.changedFields.scheduled_completion?.old, - scheduledDelivery: data.changedFields.scheduled_delivery?.new, - oldScheduledDelivery: data.changedFields.scheduled_delivery?.old + scheduledIn: changedFields.scheduled_in?.new, + oldScheduledIn: changedFields.scheduled_in?.old, + scheduledCompletion: changedFields.scheduled_completion?.new, + oldScheduledCompletion: changedFields.scheduled_completion?.old, + scheduledDelivery: changedFields.scheduled_delivery?.new, + oldScheduledDelivery: changedFields.scheduled_delivery?.old }, recipients: [] }, @@ -486,7 +525,75 @@ const supplementImportedBuilder = (data) => { * Builds notification data for tasks updated or created. */ const tasksUpdatedCreatedBuilder = (data) => { - const body = `Tasks have been ${data.isNew ? "created" : "updated"}.`; + const momentFormat = "MM/DD/YYYY hh:mm a"; + const timezone = data.bodyShopTimezone; + const taskTitle = data?.data?.title; + + let body; + let variables; + + if (data.isNew) { + // Created case + const priority = formatTaskPriority(data?.data?.priority); + const createdBy = data?.data?.created_by; + const dueDate = data.data.due_date ? ` due on ${moment(data.data.due_date).tz(timezone).format(momentFormat)}` : ""; + const completedOnCreation = data.data.completed === true; + body = `A ${priority} Task ${taskTitle} has been created${completedOnCreation ? " and marked completed" : ""} by ${createdBy}${dueDate}`; + variables = { + isNew: data.isNew, + roNumber: data.jobRoNumber, + title: data?.data?.title, + priority: data?.data?.priority, + createdBy: data?.data?.created_by, + dueDate: data?.data?.due_date, + completed: completedOnCreation ? data?.data?.completed : undefined // Only include if true + }; + } else { + // Updated case + const changedFields = data.changedFields; + const fieldNames = Object.keys(changedFields); + + // Special case: Only 'completed' changed + if (fieldNames.length === 1 && changedFields.completed) { + body = `Task ${taskTitle} was marked ${changedFields.completed.new ? "complete" : "incomplete"}`; + variables = { + isNew: data.isNew, + roNumber: data.jobRoNumber, + title: data?.data?.title, + changedCompleted: data?.changedFields?.completed?.new + }; + } else { + // General update case + const fieldMessages = []; + + if (changedFields.description) { + fieldMessages.push("Description Updated"); + } + if (changedFields.priority) { + fieldMessages.push(`Priority changed to ${formatTaskPriority(changedFields.priority.new)}`); + } + if (changedFields.due_date) { + fieldMessages.push(`Due date set to ${moment(changedFields.due_date.new).tz(timezone).format(momentFormat)}`); + } + if (changedFields.completed) { + fieldMessages.push(`Status changed to ${changedFields.completed.new ? "complete" : "incomplete"}`); + } + + body = + fieldMessages.length > 0 + ? `Task ${taskTitle} updated: ${fieldMessages.join(", ")}` + : `Task ${taskTitle} has been updated.`; + variables = { + isNew: data.isNew, + roNumber: data.jobRoNumber, + title: data?.data?.title, + changedPriority: data?.changedFields?.priority?.new, + changedDueDate: data?.changedFields?.due_date?.new, + changedCompleted: data?.changedFields?.completed?.new + }; + } + } + const result = { app: { jobId: data.jobId, @@ -494,10 +601,7 @@ const tasksUpdatedCreatedBuilder = (data) => { bodyShopId: data.bodyShopId, key: data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated", body, - variables: { - isNew: data.isNew, - roNumber: data.jobRoNumber - }, + variables, recipients: [] }, email: { diff --git a/server/notifications/scenarioParser.js b/server/notifications/scenarioParser.js index 1f6afea03..ac1db6aeb 100644 --- a/server/notifications/scenarioParser.js +++ b/server/notifications/scenarioParser.js @@ -110,6 +110,8 @@ const scenarioParser = async (req, jobIdField) => { const bodyShopId = watcherData?.job?.bodyshop?.id; const bodyShopName = watcherData?.job?.bodyshop?.shopname; + const bodyShopTimezone = watcherData?.job?.bodyshop?.timezone; + const jobRoNumber = watcherData?.job?.ro_number; const jobClaimNumber = watcherData?.job?.clm_no; @@ -147,6 +149,7 @@ const scenarioParser = async (req, jobIdField) => { jobWatchers, bodyShopId, bodyShopName, + bodyShopTimezone, matchingScenarios }; @@ -247,6 +250,7 @@ const scenarioParser = async (req, jobIdField) => { trigger: finalScenarioData.trigger.name, bodyShopId: finalScenarioData.bodyShopId, bodyShopName: finalScenarioData.bodyShopName, + bodyShopTimezone: finalScenarioData.bodyShopTimezone, scenarioKey: scenario.key, scenarioTable: scenario.table, scenarioFields: filteredScenarioFields, diff --git a/server/notifications/stringHelpers.js b/server/notifications/stringHelpers.js index 4e70670be..ce56063c9 100644 --- a/server/notifications/stringHelpers.js +++ b/server/notifications/stringHelpers.js @@ -26,6 +26,17 @@ const getJobAssignmentType = (data) => { } }; -module.exports = { - getJobAssignmentType +const formatTaskPriority = (priority) => { + if (priority === 1) { + return "High"; + } else if (priority === 3) { + return "Low"; + } else { + return "Medium"; + } +}; + +module.exports = { + getJobAssignmentType, + formatTaskPriority };