Merged in feature/IO-3166-Global-Notifications-Part-2 (pull request #2159)

Feature/IO-3166 Global Notifications Part 2
This commit is contained in:
Dave Richer
2025-03-06 21:06:15 +00:00
8 changed files with 132 additions and 108 deletions

View File

@@ -623,6 +623,7 @@ function Header({
key: "recent",
id: "header-recent",
icon: <ClockCircleFilled />,
label: t("menus.header.recent"),
children: recentItems.map((i, idx) => ({
key: idx,
id: `header-recent-${idx}`,
@@ -633,6 +634,7 @@ function Header({
key: "user",
id: "header-user",
icon: <UserOutlined />,
label: t("menus.currentuser.profile"),
children: [
{
key: "signout",

View File

@@ -140,7 +140,7 @@ export default function JobWatcherToggleComponent({
);
return (
<Popover content={popoverContent} trigger="click" open={open} onOpenChange={setOpen}>
<Popover placement="topRight" content={popoverContent} trigger="click" open={open} onOpenChange={setOpen}>
<Tooltip title={t("notifications.tooltips.job-watchers")}>
<Button
shape="circle"

View File

@@ -51,7 +51,7 @@ const NotificationCenterComponent = forwardRef(
<div className="notification-content">
<Title level={5} className="notification-title">
<span className="ro-number">
{t("notifications.labels.ro-number", { ro_number: notification.roNumber })}
{t("notifications.labels.ro-number", { ro_number: notification.roNumber || t("general.labels.na") })}
</span>
<Text type="secondary" className="relative-time" title={DateTimeFormat(notification.created_at)}>
{day(notification.created_at).fromNow()}

View File

@@ -299,7 +299,9 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNot
.catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`));
}}
>
{t("notifications.labels.notification-popup-title", { ro_number: jobRoNumber })}
{t("notifications.labels.notification-popup-title", {
ro_number: jobRoNumber || t("general.labels.na")
})}
</div>
),
description: (

View File

@@ -3303,7 +3303,7 @@
request_transform:
body:
action: transform
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body.event.session_variables.x-hasura-user-id}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"jobid\": {{$body.event.data.old.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.old.status}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"jobid\": {{$body.event.data.new.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.new.status}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_joblines\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"joblines\"\r\n }\r\n}\r\n"
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"jobid\": {{$body.event.data.old.jobid}},\r\n \"critical\": {{$body.event.data.old.critical}},\r\n \"status\": {{$body.event.data.old.status}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"jobid\": {{$body.event.data.new.jobid}},\r\n \"critical\": {{$body.event.data.new.critical}},\r\n \"status\": {{$body.event.data.new.status}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_joblines\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"joblines\"\r\n }\r\n}\r\n"
method: POST
query_params: {}
template_engine: Kriti
@@ -4573,7 +4573,7 @@
request_transform:
body:
action: transform
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body.event.session_variables.x-hasura-user-id}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
template: "{\r\n \"event\": {\r\n \"session_variables\": {\r\n \"x-hasura-user-id\": {{$body?.event?.session_variables?.x-hasura-user-id ?? \"Internal\"}}\r\n }, \r\n \"op\": \"UPDATE\",\r\n \"data\": {\r\n \"old\": {\r\n \"id\": {{$body.event.data.old.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.old.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.old.employee_prep}},\r\n \"clm_total\": {{$body.event.data.old.clm_total}},\r\n \"towin\": {{$body.event.data.old.towin}},\r\n \"employee_body\": {{$body.event.data.old.employee_body}},\r\n \"converted\": {{$body.event.data.old.converted}},\r\n \"scheduled_in\": {{$body.event.data.old.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.old.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.old.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.old.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.old.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.old.alt_transport}},\r\n \"date_exported\": {{$body.event.data.old.date_exported}},\r\n \"status\": {{$body.event.data.old.status}},\r\n \"employee_csr\": {{$body.event.data.old.employee_csr}},\r\n \"actual_in\": {{$body.event.data.old.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.old.deliverchecklist}},\r\n \"comment\": {{$body.event.data.old.comment}},\r\n \"employee_refinish\": {{$body.event.data.old.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.old.inproduction}},\r\n \"production_vars\": {{$body.event.data.old.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.old.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.old.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.old.date_invoiced}}\r\n },\r\n \"new\": {\r\n \"id\": {{$body.event.data.new.id}},\r\n \"ro_number\": {{$body.event.data.old.ro_number}},\r\n \"queued_for_parts\": {{$body.event.data.new.queued_for_parts}},\r\n \"employee_prep\": {{$body.event.data.new.employee_prep}},\r\n \"clm_total\": {{$body.event.data.new.clm_total}},\r\n \"towin\": {{$body.event.data.new.towin}},\r\n \"employee_body\": {{$body.event.data.new.employee_body}},\r\n \"converted\": {{$body.event.data.new.converted}},\r\n \"scheduled_in\": {{$body.event.data.new.scheduled_in}},\r\n \"scheduled_completion\": {{$body.event.data.new.scheduled_completion}},\r\n \"scheduled_delivery\": {{$body.event.data.new.scheduled_delivery}},\r\n \"actual_delivery\": {{$body.event.data.new.actual_delivery}},\r\n \"actual_completion\": {{$body.event.data.new.actual_completion}},\r\n \"alt_transport\": {{$body.event.data.new.alt_transport}},\r\n \"date_exported\": {{$body.event.data.new.date_exported}},\r\n \"status\": {{$body.event.data.new.status}},\r\n \"employee_csr\": {{$body.event.data.new.employee_csr}},\r\n \"actual_in\": {{$body.event.data.new.actual_in}},\r\n \"deliverchecklist\": {{$body.event.data.new.deliverchecklist}},\r\n \"comment\": {{$body.event.data.new.comment}},\r\n \"employee_refinish\": {{$body.event.data.new.employee_refinish}},\r\n \"inproduction\": {{$body.event.data.new.inproduction}},\r\n \"production_vars\": {{$body.event.data.new.production_vars}},\r\n \"intakechecklist\": {{$body.event.data.new.intakechecklist}},\r\n \"cieca_ttl\": {{$body.event.data.new.cieca_ttl}},\r\n \"date_invoiced\": {{$body.event.data.new.date_invoiced}}\r\n }\r\n }\r\n },\r\n \"trigger\": {\r\n \"name\": \"notifications_jobs\"\r\n },\r\n \"table\": {\r\n \"schema\": \"public\",\r\n \"name\": \"jobs\"\r\n }\r\n}\r\n"
method: POST
query_params: {}
template_engine: Kriti

View File

@@ -118,12 +118,12 @@ const loadEmailQueue = async ({ pubClient, logger }) => {
const details = await pubClient.hgetall(detailsKey);
const firstName = details.firstName || "User";
const multipleUpdateString = messages.length > 1 ? "Updates" : "Update";
const subject = `${multipleUpdateString} for job ${jobRoNumber} at ${bodyShopName}`;
const subject = `${multipleUpdateString} for job ${jobRoNumber || "N/A"} at ${bodyShopName}`;
const emailBody = generateEmailTemplate({
header: `${multipleUpdateString} for Job ${jobRoNumber}`,
header: `${multipleUpdateString} for Job ${jobRoNumber || "N/A"}`,
subHeader: `Dear ${firstName},`,
body: `
<p>There have been updates to job ${jobRoNumber} at ${bodyShopName}:</p><br/>
<p>There have been updates to job ${jobRoNumber || "N/A"} at ${bodyShopName}:</p><br/>
<ul>
${messages.map((msg) => `<li>${msg}</li>`).join("")}
</ul><br/><br/>
@@ -224,7 +224,7 @@ const dispatchEmailsToQueue = async ({ emailsToDispatch, logger }) => {
if (!jobId || !jobRoNumber || !bodyShopName || !body || !recipients.length) {
logger.logger.warn(
`Skipping email dispatch for jobId ${jobId} due to missing data: ` +
`jobRoNumber=${jobRoNumber}, bodyShopName=${bodyShopName}, body=${body}, recipients=${recipients.length}`
`jobRoNumber=${jobRoNumber || "N/A"}, bodyShopName=${bodyShopName}, body=${body}, recipients=${recipients.length}`
);
continue;
}

View File

@@ -1,6 +1,9 @@
const { getJobAssignmentType, formatTaskPriority } = require("./stringHelpers");
const moment = require("moment-timezone");
const { startCase } = require("lodash");
const Dinero = require("dinero.js");
Dinero.globalRoundingMode = "HALF_EVEN";
/**
* Populates the recipients for app, email, and FCM notifications based on scenario watchers.
@@ -26,7 +29,6 @@ const populateWatchers = (data, result) => {
/**
* Builds notification data for changes to alternate transport.
*/
// Verified
const alternateTransportChangedBuilder = (data) => {
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 = {
@@ -59,7 +61,6 @@ const alternateTransportChangedBuilder = (data) => {
/**
* Builds notification data for bill posted events.
*/
//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();
@@ -94,9 +95,10 @@ const billPostedHandler = (data) => {
/**
* Builds notification data for changes to critical parts status.
*/
// 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 = `A Critical Job Line status has been changed."`;
const result = {
app: {
jobId: data.jobId,
@@ -105,8 +107,7 @@ const criticalPartsStatusChangedBuilder = (data) => {
key: "notifications.job.criticalPartsStatusChanged",
body,
variables: {
queuedForParts: data?.data?.queued_for_parts,
oldQueuedForParts: data?.changedFields?.queued_for_parts?.old
joblineId: data?.data?.id // If we want to deeplink to the jobline
},
recipients: []
},
@@ -127,7 +128,6 @@ const criticalPartsStatusChangedBuilder = (data) => {
/**
* Builds notification data for completed intake or delivery checklists.
*/
// Verified
const intakeDeliveryChecklistCompletedBuilder = (data) => {
const checklistType = data?.changedFields?.intakechecklist ? "Intake" : "Delivery";
const body = `The ${checklistType.charAt(0).toUpperCase() + checklistType.slice(1)} checklist has been completed.`;
@@ -161,7 +161,6 @@ const intakeDeliveryChecklistCompletedBuilder = (data) => {
/**
* Builds notification data for job assignment events.
*/
// Verified
const jobAssignedToMeBuilder = (data) => {
const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}.`;
const result = {
@@ -193,7 +192,6 @@ const jobAssignedToMeBuilder = (data) => {
/**
* Builds notification data for jobs added to production.
*/
// Verified
const jobsAddedToProductionBuilder = (data) => {
const body = `Has been added to Production.`;
const result = {
@@ -223,7 +221,6 @@ const jobsAddedToProductionBuilder = (data) => {
/**
* Builds notification data for job status changes.
*/
// Verified
const jobStatusChangeBuilder = (data) => {
const body = `The status has changed from ${data?.changedFields?.status?.old || "unset"} to ${data?.changedFields?.status?.new || "unset"}`;
const result = {
@@ -256,7 +253,6 @@ const jobStatusChangeBuilder = (data) => {
/**
* Builds notification data for new media added or reassigned events.
*/
// Verified
const newMediaAddedReassignedBuilder = (data) => {
// Determine if it's an image or document
const mediaType = data?.data?.type?.startsWith("image") ? "Image" : "Document";
@@ -295,7 +291,6 @@ const newMediaAddedReassignedBuilder = (data) => {
/**
* Builds notification data for new notes added to a job.
*/
// verified
const newNoteAddedBuilder = (data) => {
const body = [
"A",
@@ -340,7 +335,6 @@ const newNoteAddedBuilder = (data) => {
/**
* Builds notification data for new time tickets posted.
*/
// Verified
const newTimeTicketPostedBuilder = (data) => {
const type = data?.data?.cost_center;
const body =
@@ -407,7 +401,22 @@ const partMarkedBackOrderedBuilder = (data) => {
* Builds notification data for payment collection events.
*/
const paymentCollectedCompletedBuilder = (data) => {
const body = `Payment of $${data.data.clm_total} has been collected.`;
const momentFormat = "MM/DD/YYYY";
const timezone = data.bodyShopTimezone;
// Format amount using Dinero.js
const amountDinero = Dinero({
amount: Math.round((data.data.amount || 0) * 100) // Convert to cents, default to 0 if missing
});
const amountFormatted = amountDinero.toFormat();
const payer = data.data.payer;
const paymentType = data.data.type;
const paymentDate = moment(data.data.date).tz(timezone).format(momentFormat);
const body = `Payment of ${amountFormatted} has been collected from ${payer} via ${paymentType} on ${paymentDate}`;
const result = {
app: {
jobId: data.jobId,
@@ -416,7 +425,10 @@ const paymentCollectedCompletedBuilder = (data) => {
key: "notifications.job.paymentCollected",
body,
variables: {
clmTotal: data.data.clm_total
amount: data.data.amount,
payer: data.data.payer,
type: data.data.type,
date: data.data.date
},
recipients: []
},
@@ -491,20 +503,94 @@ const scheduledDatesChangedBuilder = (data) => {
};
/**
* Builds notification data for supplement imported events.
* Builds notification data for tasks updated or created.
*/
const supplementImportedBuilder = (data) => {
const body = `A supplement of $${data.data.cieca_ttl?.data?.supp_amt || 0} has been imported.`;
const tasksUpdatedCreatedBuilder = (data) => {
const momentFormat = "MM/DD/YYYY hh:mm a";
const timezone = data.bodyShopTimezone;
const taskTitle = data?.data?.title ? `"${data.data.title}"` : "Unnamed Task";
let body;
let variables;
if (data.isNew) {
// Created case
const priority = formatTaskPriority(data?.data?.priority);
const createdBy = data?.data?.created_by || "Unknown"; // Fallback for undefined 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);
const oldTitle = changedFields.title ? `"${changedFields.title.old || "Unnamed Task"}"` : taskTitle;
// Special case: Only 'completed' changed
if (fieldNames.length === 1 && changedFields.completed) {
body = `Task ${oldTitle} 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.title) {
fieldMessages.push(`Task ${oldTitle} changed title to "${changedFields.title.new || "Unnamed Task"}"`);
}
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
? fieldMessages.length === 1 && changedFields.title
? fieldMessages[0] // If only title changed, use it standalone
: `Task ${oldTitle} updated: ${fieldMessages.join(", ")}`
: `Task ${oldTitle} has been updated.`;
variables = {
isNew: data.isNew,
roNumber: data.jobRoNumber,
title: data?.data?.title,
changedTitleOld: data?.changedFields?.title?.old,
changedTitleNew: data?.changedFields?.title?.new,
changedPriority: data?.changedFields?.priority?.new,
changedDueDate: data?.changedFields?.due_date?.new,
changedCompleted: data?.changedFields?.completed?.new
};
}
}
const result = {
app: {
jobId: data.jobId,
jobRoNumber: data.jobRoNumber,
bodyShopId: data.bodyShopId,
key: "notifications.job.supplementImported",
key: data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated",
body,
variables: {
suppAmt: data.data.cieca_ttl?.data?.supp_amt
},
variables,
recipients: []
},
email: {
@@ -522,86 +608,19 @@ const supplementImportedBuilder = (data) => {
};
/**
* Builds notification data for tasks updated or created.
* Builds notification data for supplement imported events.
* TODO: This is an advanced case and will be done later
*/
const tasksUpdatedCreatedBuilder = (data) => {
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 supplementImportedBuilder = (data) => {
const body = `A supplement has been imported.`;
const result = {
app: {
jobId: data.jobId,
jobRoNumber: data.jobRoNumber,
bodyShopId: data.bodyShopId,
key: data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated",
key: "notifications.job.supplementImported",
body,
variables,
variables: {},
recipients: []
},
email: {

View File

@@ -95,7 +95,7 @@ const notificationScenarios = [
builder: intakeDeliveryChecklistCompletedBuilder
},
{
key: "payment-added",
key: "payment-collected-completed",
table: "payments",
onNew: true,
builder: paymentCollectedCompletedBuilder
@@ -110,9 +110,10 @@ const notificationScenarios = [
{
key: "critical-parts-status-changed",
table: "joblines",
fields: ["critical"],
onlyTruthyValues: ["critical"],
builder: criticalPartsStatusChangedBuilder
fields: ["status"],
onlyTruthyValues: ["status"],
builder: criticalPartsStatusChangedBuilder,
filterCallback: ({ eventData }) => !eventData?.data?.critical
},
{
key: "part-marked-back-ordered",