680 lines
19 KiB
JavaScript
680 lines
19 KiB
JavaScript
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.
|
|
*
|
|
* @param {Object} data - The data object containing scenarioWatchers and bodyShopId.
|
|
* @param {Object} result - The result object to populate with recipients for app, email, and FCM notifications.
|
|
*/
|
|
const populateWatchers = (data, result) => {
|
|
data.scenarioWatchers.forEach((recipients) => {
|
|
const { user, app, fcm, email, firstName, lastName, employeeId, associationId } = recipients;
|
|
if (app === true)
|
|
result.app.recipients.push({
|
|
user,
|
|
bodyShopId: data.bodyShopId,
|
|
employeeId,
|
|
associationId
|
|
});
|
|
if (fcm === true) result.fcm.recipients.push(user);
|
|
if (email === true) result.email.recipients.push({ user, firstName, lastName });
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for changes to alternate transport.
|
|
*/
|
|
const alternateTransportChangedBuilder = (data) => {
|
|
const body = `The alternate transportation has been changed 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,
|
|
variables: {
|
|
alternateTransport: data?.changedFields?.alt_transport?.new,
|
|
oldAlternateTransport: data?.changedFields?.alt_transport?.old
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for bill posted events.
|
|
*/
|
|
const billPostedHandler = (data) => {
|
|
const facing = data?.data?.isinhouse ? "in-house" : "vendor";
|
|
const body = `An ${facing} ${data?.data?.is_credit_memo ? "credit memo" : "bill"} has been posted.`.trim();
|
|
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.billPosted",
|
|
body,
|
|
variables: {
|
|
isInHouse: data?.data?.isinhouse,
|
|
isCreditMemo: data?.data?.is_credit_memo
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for changes to critical parts status.
|
|
*/
|
|
//
|
|
const criticalPartsStatusChangedBuilder = (data) => {
|
|
const body = `The status on a critical part line (${data?.data?.line_desc}) has changed to ${data?.data?.status || "unset"}.`;
|
|
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
bodyShopId: data.bodyShopId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
key: "notifications.job.criticalPartsStatusChanged",
|
|
body,
|
|
variables: {
|
|
joblineId: data?.data?.id, // If we want to deeplink to the jobline
|
|
status: data?.data?.status,
|
|
line_desc: data?.data?.line_desc
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for completed intake or delivery checklists.
|
|
*/
|
|
const intakeDeliveryChecklistCompletedBuilder = (data) => {
|
|
const checklistType = data?.changedFields?.intakechecklist ? "intake" : "delivery";
|
|
const body = `The ${checklistType.charAt(0).toUpperCase() + checklistType.slice(1)} checklist has been completed.`;
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.checklistCompleted",
|
|
body,
|
|
variables: {
|
|
checklistType,
|
|
completed: true
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for job assignment events.
|
|
*/
|
|
const jobAssignedToMeBuilder = (data) => {
|
|
const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}.`;
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.assigned",
|
|
body,
|
|
variables: {
|
|
type: data.scenarioFields?.[0]
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for jobs added to production.
|
|
*/
|
|
const jobsAddedToProductionBuilder = (data) => {
|
|
const body = `Job is now in production.`;
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.addedToProduction",
|
|
body,
|
|
variables: {},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for job status changes.
|
|
*/
|
|
const jobStatusChangeBuilder = (data) => {
|
|
const body = `The status has changed from ${data?.changedFields?.status?.old || "unset"} to ${data?.changedFields?.status?.new || "unset"}`;
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.statusChanged",
|
|
body,
|
|
variables: {
|
|
status: data.changedFields.status.new,
|
|
oldStatus: data.changedFields.status.old
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for new media added or reassigned events.
|
|
*/
|
|
const newMediaAddedReassignedBuilder = (data) => {
|
|
// Determine if it's an image or document
|
|
const mediaType = data?.data?.type?.startsWith("image") ? "Image" : "Document";
|
|
// Determine if it's added or updated
|
|
const action = data.isNew ? "added" : "updated";
|
|
// Construct the body string
|
|
const body = `An ${mediaType} has been ${action}.`;
|
|
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.newMediaAdded",
|
|
body,
|
|
variables: {
|
|
mediaType,
|
|
action
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for new notes added to a job.
|
|
*/
|
|
const newNoteAddedBuilder = (data) => {
|
|
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,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.newNoteAdded",
|
|
body,
|
|
variables: {
|
|
createdBy: data?.data?.created_by,
|
|
critical: data?.data?.critical,
|
|
type: data?.data?.type,
|
|
private: data?.data?.private
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for new time tickets posted.
|
|
*/
|
|
const newTimeTicketPostedBuilder = (data) => {
|
|
const type = data?.data?.cost_center;
|
|
const body = `A ${startCase(type.toLowerCase())} time ticket for ${data?.data?.date} has been posted.`.trim();
|
|
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.newTimeTicketPosted",
|
|
body,
|
|
variables: {
|
|
type,
|
|
date: data?.data?.date
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for parts marked as back-ordered.
|
|
*/
|
|
const partMarkedBackOrderedBuilder = (data) => {
|
|
const body = `A part ${data?.data?.line_desc} has been marked as back-ordered.`;
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.partBackOrdered",
|
|
body,
|
|
variables: {
|
|
line_desc: data?.data?.line_desc
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for payment collection events.
|
|
*/
|
|
const paymentCollectedCompletedBuilder = (data) => {
|
|
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,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.paymentCollected",
|
|
body,
|
|
variables: {
|
|
amount: data.data.amount,
|
|
payer: data.data.payer,
|
|
type: data.data.type,
|
|
date: data.data.date
|
|
},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for changes to scheduled dates.
|
|
*/
|
|
const scheduledDatesChangedBuilder = (data) => {
|
|
const changedFields = data.changedFields;
|
|
|
|
// Define field configurations
|
|
const fieldConfigs = {
|
|
scheduled_in: "Scheduled In",
|
|
scheduled_completion: "Scheduled Completion",
|
|
scheduled_delivery: "Scheduled Delivery"
|
|
};
|
|
|
|
// Helper function to format date and time with "at"
|
|
const formatDateTime = (date) => {
|
|
if (!date) return "unset";
|
|
const formatted = moment(date).tz(data.bodyShopTimezone);
|
|
const datePart = formatted.format("MM/DD/YYYY");
|
|
const timePart = formatted.format("hh:mm a");
|
|
return `${datePart} at ${timePart}`;
|
|
};
|
|
|
|
// 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];
|
|
|
|
// Case 1: Scheduled date cancelled (from value to null)
|
|
if (old && !newValue) {
|
|
return `${label} was cancelled (previously ${formatDateTime(old)}).`;
|
|
}
|
|
// Case 2: Scheduled date set (from null to value)
|
|
else if (!old && newValue) {
|
|
return `${label} was set to ${formatDateTime(newValue)}.`;
|
|
}
|
|
// Case 3: Scheduled date changed (from value to value)
|
|
else if (old && newValue) {
|
|
return `${label} changed from ${formatDateTime(old)} to ${formatDateTime(newValue)}.`;
|
|
}
|
|
return ""; // Fallback, though this shouldn't happen with the filter
|
|
})
|
|
.filter(Boolean); // Remove any empty strings
|
|
|
|
const body = fieldMessages.length > 0 ? fieldMessages.join(" ") : "Scheduled dates have been updated.";
|
|
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.scheduledDatesChanged",
|
|
body,
|
|
variables: {
|
|
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: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for tasks updated or created.
|
|
*/
|
|
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: data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated",
|
|
body,
|
|
variables,
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Builds notification data for supplement imported events.
|
|
* TODO: This is an advanced case and will be done later
|
|
*/
|
|
const supplementImportedBuilder = (data) => {
|
|
const body = `A supplement has been imported.`;
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key: "notifications.job.supplementImported",
|
|
body,
|
|
variables: {},
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
populateWatchers(data, result);
|
|
return result;
|
|
};
|
|
|
|
module.exports = {
|
|
alternateTransportChangedBuilder,
|
|
billPostedHandler,
|
|
criticalPartsStatusChangedBuilder,
|
|
intakeDeliveryChecklistCompletedBuilder,
|
|
jobAssignedToMeBuilder,
|
|
jobsAddedToProductionBuilder,
|
|
jobStatusChangeBuilder,
|
|
newMediaAddedReassignedBuilder,
|
|
newNoteAddedBuilder,
|
|
newTimeTicketPostedBuilder,
|
|
partMarkedBackOrderedBuilder,
|
|
paymentCollectedCompletedBuilder,
|
|
scheduledDatesChangedBuilder,
|
|
supplementImportedBuilder,
|
|
tasksUpdatedCreatedBuilder
|
|
};
|