415 lines
17 KiB
JavaScript
415 lines
17 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";
|
|
|
|
/**
|
|
* Creates a standard notification object with app, email, and FCM properties and populates recipients.
|
|
* @param {Object} data - Input data containing jobId, jobRoNumber, bodyShopId, bodyShopName, and scenarioWatchers
|
|
* @param {string} key - Notification key for the app
|
|
* @param {string} body - Notification body text
|
|
* @param {Object} [variables={}] - Variables for the app notification
|
|
* @returns {Object} Notification object with populated recipients
|
|
*/
|
|
const buildNotification = (data, key, body, variables = {}) => {
|
|
const result = {
|
|
app: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopId: data.bodyShopId,
|
|
key,
|
|
body,
|
|
variables,
|
|
recipients: []
|
|
},
|
|
email: {
|
|
jobId: data.jobId,
|
|
jobRoNumber: data.jobRoNumber,
|
|
bodyShopName: data.bodyShopName,
|
|
bodyShopTimezone: data.bodyShopTimezone,
|
|
body,
|
|
recipients: []
|
|
},
|
|
fcm: { recipients: [] }
|
|
};
|
|
|
|
// Populate recipients from scenarioWatchers
|
|
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 });
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when the alternate transport is changed.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const alternateTransportChangedBuilder = (data) => {
|
|
const oldTransport = data?.changedFields?.alt_transport?.old;
|
|
const newTransport = data?.changedFields?.alt_transport?.new;
|
|
let body;
|
|
|
|
if (oldTransport && newTransport)
|
|
body = `The alternate transportation has been changed from ${oldTransport} to ${newTransport}.`;
|
|
else if (!oldTransport && newTransport) body = `The alternate transportation has been set to ${newTransport}.`;
|
|
else if (oldTransport && !newTransport)
|
|
body = `The alternate transportation has been canceled (previously ${oldTransport}).`;
|
|
else body = `The alternate transportation has been updated.`;
|
|
|
|
return buildNotification(data, "notifications.job.alternateTransportChanged", body, {
|
|
alternateTransport: newTransport,
|
|
oldAlternateTransport: oldTransport
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when a bill is posted.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const billPostedBuilder = (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();
|
|
|
|
return buildNotification(data, "notifications.job.billPosted", body, {
|
|
isInHouse: data?.data?.isinhouse,
|
|
isCreditMemo: data?.data?.is_credit_memo
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when the status of critical parts changes.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const criticalPartsStatusChangedBuilder = (data) => {
|
|
const lineDesc = data?.data?.line_desc;
|
|
const status = data?.data?.status;
|
|
const body = status
|
|
? `The status on a critical part line (${lineDesc}) has been set to ${status}.`
|
|
: `The status on a critical part line (${lineDesc}) has been cleared.`;
|
|
|
|
return buildNotification(data, "notifications.job.criticalPartsStatusChanged", body, {
|
|
joblineId: data?.data?.id,
|
|
status: data?.data?.status,
|
|
line_desc: lineDesc
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when the intake or delivery checklist is completed.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const intakeDeliveryChecklistCompletedBuilder = (data) => {
|
|
const checklistType = data?.changedFields?.intakechecklist ? "intake" : "delivery";
|
|
const body = `The ${checklistType.charAt(0).toUpperCase() + checklistType.slice(1)} checklist has been completed.`;
|
|
|
|
return buildNotification(data, "notifications.job.checklistCompleted", body, {
|
|
checklistType,
|
|
completed: true
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when a job is assigned to the user.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const jobAssignedToMeBuilder = (data) => {
|
|
const body = `You have been assigned to ${getJobAssignmentType(data.scenarioFields?.[0])}.`;
|
|
|
|
return buildNotification(data, "notifications.job.assigned", body, {
|
|
type: data.scenarioFields?.[0]
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when jobs are added to production.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const jobsAddedToProductionBuilder = (data) => {
|
|
const body = `Job is now in production.`;
|
|
return buildNotification(data, "notifications.job.addedToProduction", body);
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when the job status changes.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const jobStatusChangeBuilder = (data) => {
|
|
const oldStatus = data?.changedFields?.status?.old;
|
|
const newStatus = data?.changedFields?.status?.new;
|
|
let body;
|
|
|
|
if (oldStatus && newStatus) body = `The status has been changed from ${oldStatus} to ${newStatus}.`;
|
|
else if (!oldStatus && newStatus) body = `The status has been set to ${newStatus}.`;
|
|
else if (oldStatus && !newStatus) body = `The status has been cleared (previously ${oldStatus}).`;
|
|
else body = `The status has been updated.`;
|
|
|
|
return buildNotification(data, "notifications.job.statusChanged", body, {
|
|
status: newStatus,
|
|
oldStatus: oldStatus
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when new media is added or reassigned.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const newMediaAddedReassignedBuilder = (data) => {
|
|
const mediaType = data?.data?.type?.startsWith("image") ? "Image" : "Document";
|
|
const action = data?.data?._documentMoved
|
|
? "moved to another job"
|
|
: data.isNew
|
|
? "added"
|
|
: data.changedFields?.jobid && data.changedFields.jobid.old !== data.changedFields.jobid.new
|
|
? "moved to this job"
|
|
: "updated";
|
|
const body = `A ${mediaType} has been ${action}.`;
|
|
|
|
return buildNotification(data, "notifications.job.newMediaAdded", body, {
|
|
mediaType,
|
|
action,
|
|
movedToJob: data?.data?._movedToJob
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when a new note is added.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
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(" ");
|
|
|
|
return buildNotification(data, "notifications.job.newNoteAdded", body, {
|
|
createdBy: data?.data?.created_by,
|
|
critical: data?.data?.critical,
|
|
type: data?.data?.type,
|
|
private: data?.data?.private
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when a new time ticket is posted.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
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();
|
|
|
|
return buildNotification(data, "notifications.job.newTimeTicketPosted", body, {
|
|
type,
|
|
date: data?.data?.date
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when a part is marked as back-ordered.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const partMarkedBackOrderedBuilder = (data) => {
|
|
const body = `A part ${data?.data?.line_desc} has been marked as back-ordered.`;
|
|
|
|
return buildNotification(data, "notifications.job.partBackOrdered", body, {
|
|
line_desc: data?.data?.line_desc
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when payment is collected or completed.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const paymentCollectedCompletedBuilder = (data) => {
|
|
const momentFormat = "MM/DD/YYYY";
|
|
const amountDinero = Dinero({ amount: Math.round((data.data.amount || 0) * 100) });
|
|
const amountFormatted = amountDinero.toFormat();
|
|
const payer = data.data.payer;
|
|
const paymentType = data.data.type;
|
|
const paymentDate = moment(data.data.date).format(momentFormat);
|
|
const body = `Payment of ${amountFormatted} has been collected from ${payer} via ${paymentType} on ${paymentDate}`;
|
|
|
|
return buildNotification(data, "notifications.job.paymentCollected", body, {
|
|
amount: data.data.amount,
|
|
payer: data.data.payer,
|
|
type: data.data.type,
|
|
date: data.data.date
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when scheduled dates are changed.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const scheduledDatesChangedBuilder = (data) => {
|
|
const changedFields = data.changedFields;
|
|
const fieldConfigs = {
|
|
scheduled_in: "Scheduled In",
|
|
scheduled_completion: "Scheduled Completion",
|
|
scheduled_delivery: "Scheduled Delivery"
|
|
};
|
|
const formatDateTime = (date) => {
|
|
if (!date) return "(no date set)";
|
|
const formatted = moment(date).tz(data.bodyShopTimezone);
|
|
return `${formatted.format("MM/DD/YYYY")} at ${formatted.format("hh:mm a")}`;
|
|
};
|
|
|
|
const fieldMessages = Object.entries(fieldConfigs)
|
|
.filter(([field]) => changedFields[field])
|
|
.map(([field, label]) => {
|
|
const { old, new: newValue } = changedFields[field];
|
|
if (old && !newValue) return `${label} was cancelled (previously ${formatDateTime(old)}).`;
|
|
else if (!old && newValue) return `${label} was set to ${formatDateTime(newValue)}.`;
|
|
else if (old && newValue) return `${label} changed from ${formatDateTime(old)} to ${formatDateTime(newValue)}.`;
|
|
return "";
|
|
})
|
|
.filter(Boolean);
|
|
|
|
const body = fieldMessages.length > 0 ? fieldMessages.join(" ") : "Scheduled dates have been updated.";
|
|
|
|
return buildNotification(data, "notifications.job.scheduledDatesChanged", body, {
|
|
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
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when tasks are updated or created.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
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, variables;
|
|
if (data.isNew) {
|
|
const priority = formatTaskPriority(data?.data?.priority);
|
|
const createdBy = data?.data?.created_by || "Unknown";
|
|
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
|
|
};
|
|
} else {
|
|
const changedFields = data.changedFields;
|
|
const fieldNames = Object.keys(changedFields);
|
|
const oldTitle = changedFields.title ? `"${changedFields.title.old || "Unnamed Task"}"` : taskTitle;
|
|
|
|
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: changedFields.completed.new
|
|
};
|
|
} else {
|
|
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]
|
|
: `Task ${oldTitle} updated: ${fieldMessages.join(", ")}`
|
|
: `Task ${oldTitle} has been updated.`;
|
|
variables = {
|
|
isNew: data.isNew,
|
|
roNumber: data.jobRoNumber,
|
|
title: data?.data?.title,
|
|
changedTitleOld: changedFields.title?.old,
|
|
changedTitleNew: changedFields.title?.new,
|
|
changedPriority: changedFields.priority?.new,
|
|
changedDueDate: changedFields.due_date?.new,
|
|
changedCompleted: changedFields.completed?.new
|
|
};
|
|
}
|
|
}
|
|
|
|
return buildNotification(
|
|
data,
|
|
data.isNew ? "notifications.job.taskCreated" : "notifications.job.taskUpdated",
|
|
body,
|
|
variables
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Creates a notification for when a supplement is imported.
|
|
* @param data
|
|
* @returns {{app: {jobId, jobRoNumber: *, bodyShopId: *, key: string, body: string, variables: Object, recipients: *[]}, email: {jobId, jobRoNumber: *, bodyShopName: *, body: string, recipients: *[]}, fcm: {recipients: *[]}}}
|
|
*/
|
|
const supplementImportedBuilder = (data) => {
|
|
const body = `A supplement has been imported.`;
|
|
return buildNotification(data, "notifications.job.supplementImported", body);
|
|
};
|
|
|
|
module.exports = {
|
|
alternateTransportChangedBuilder,
|
|
billPostedBuilder,
|
|
criticalPartsStatusChangedBuilder,
|
|
intakeDeliveryChecklistCompletedBuilder,
|
|
jobAssignedToMeBuilder,
|
|
jobsAddedToProductionBuilder,
|
|
jobStatusChangeBuilder,
|
|
newMediaAddedReassignedBuilder,
|
|
newNoteAddedBuilder,
|
|
newTimeTicketPostedBuilder,
|
|
partMarkedBackOrderedBuilder,
|
|
paymentCollectedCompletedBuilder,
|
|
scheduledDatesChangedBuilder,
|
|
supplementImportedBuilder,
|
|
tasksUpdatedCreatedBuilder
|
|
};
|