- Progress Commit (Emailzzz)
Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
@@ -19,11 +19,9 @@ import {FaTasks} from "react-icons/fa";
|
|||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
|
||||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||||
setReconciliationContext: (context) => dispatch(setModalContext({
|
setReconciliationContext: (context) => dispatch(setModalContext({
|
||||||
context: context,
|
context: context,
|
||||||
@@ -33,17 +31,15 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function BillsListTableComponent({
|
export function BillsListTableComponent({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
jobRO,
|
jobRO,
|
||||||
job,
|
job,
|
||||||
billsQuery,
|
billsQuery,
|
||||||
handleOnRowClick,
|
handleOnRowClick,
|
||||||
setPartsOrderContext,
|
setBillEnterContext,
|
||||||
setBillEnterContext,
|
|
||||||
setReconciliationContext,
|
setReconciliationContext,
|
||||||
setTaskUpsertContext,
|
setTaskUpsertContext,
|
||||||
currentUser,
|
}) {
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export function TaskUpsertModalComponent({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<JobSearchSelectComponent placeholder={t('tasks.placeholders.jobid')}
|
<JobSearchSelectComponent placeholder={t('tasks.placeholders.jobid')}
|
||||||
onSelect={changeJobId} onClear={changeJobId}/>
|
onSelect={changeJobId} onClear={changeJobId} autoFocus={false}/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ import {selectBodyshop, selectCurrentUser} from "../../redux/user/user.selectors
|
|||||||
import TaskUpsertModalComponent from "./task-upsert-modal.component";
|
import TaskUpsertModalComponent from "./task-upsert-modal.component";
|
||||||
import {replaceUndefinedWithNull} from "../../utils/undefinedtonull.js";
|
import {replaceUndefinedWithNull} from "../../utils/undefinedtonull.js";
|
||||||
import {useNavigate} from "react-router-dom";
|
import {useNavigate} from "react-router-dom";
|
||||||
|
import axios from "axios";
|
||||||
|
import dayjs from '../../utils/day';
|
||||||
|
import {insertAuditTrail} from "../../redux/application/application.actions.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -24,6 +27,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("taskUpsert")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("taskUpsert")),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||||
});
|
});
|
||||||
|
|
||||||
export function TaskUpsertModalContainer({
|
export function TaskUpsertModalContainer({
|
||||||
@@ -31,6 +35,7 @@ export function TaskUpsertModalContainer({
|
|||||||
currentUser,
|
currentUser,
|
||||||
taskUpsert,
|
taskUpsert,
|
||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
|
insertAuditTrail
|
||||||
}) {
|
}) {
|
||||||
const {t} = useTranslation();
|
const {t} = useTranslation();
|
||||||
const history = useNavigate();
|
const history = useNavigate();
|
||||||
@@ -62,7 +67,7 @@ export function TaskUpsertModalContainer({
|
|||||||
skip: !taskId,
|
skip: !taskId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// This takes care of the ability to deep link a task from the URL (Fills in form fields)
|
// Use Effect to hydrate existing task if only a taskid is provided
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!taskLoading && !taskError && taskData && taskData?.tasks_by_pk) {
|
if (!taskLoading && !taskError && taskData && taskData?.tasks_by_pk) {
|
||||||
form.setFieldsValue(taskData.tasks_by_pk);
|
form.setFieldsValue(taskData.tasks_by_pk);
|
||||||
@@ -70,22 +75,25 @@ export function TaskUpsertModalContainer({
|
|||||||
}
|
}
|
||||||
}, [taskLoading, taskError, taskData]);
|
}, [taskLoading, taskError, taskData]);
|
||||||
|
|
||||||
/**
|
// Use Effect to hydrate selected job details
|
||||||
* Set the selected job id when the modal is opened and jobId is passed as a prop or when an existing task is passed as a prop
|
useEffect(() => {
|
||||||
*/
|
if (!loading && !error && data) {
|
||||||
|
setSelectedJobDetails(data.jobs_by_pk);
|
||||||
|
}
|
||||||
|
}, [loading, error, data]);
|
||||||
|
|
||||||
|
// Use Effect to toggle to set jobid state
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedJobId) {
|
||||||
|
setJobIdState(selectedJobId);
|
||||||
|
}
|
||||||
|
}, [selectedJobId]);
|
||||||
|
|
||||||
|
// Use Effect to hydrate form fields
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (jobid || existingTask?.id) {
|
if (jobid || existingTask?.id) {
|
||||||
setSelectedJobId(jobid || existingTask.jobid);
|
setSelectedJobId(jobid || existingTask.jobid);
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
setSelectedJobId(null);
|
|
||||||
};
|
|
||||||
}, [jobid, existingTask]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the form values when the modal is opened and an existing task is passed as a prop
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (existingTask && open) {
|
if (existingTask && open) {
|
||||||
form.setFieldsValue(existingTask);
|
form.setFieldsValue(existingTask);
|
||||||
} else if (!existingTask && open) {
|
} else if (!existingTask && open) {
|
||||||
@@ -94,34 +102,131 @@ export function TaskUpsertModalContainer({
|
|||||||
if (billid) form.setFieldsValue({billid});
|
if (billid) form.setFieldsValue({billid});
|
||||||
if (partsorderid) form.setFieldsValue({partsorderid});
|
if (partsorderid) form.setFieldsValue({partsorderid});
|
||||||
}
|
}
|
||||||
}, [existingTask, form, open, joblineid, billid, partsorderid]);
|
return () => {
|
||||||
|
setSelectedJobId(null);
|
||||||
|
};
|
||||||
|
}, [jobid, existingTask, form, open, joblineid, billid, partsorderid]);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the job id state when the selected job id changes
|
* Remove the taskid from the URL
|
||||||
*/
|
*/
|
||||||
useEffect(() => {
|
|
||||||
if (selectedJobId) {
|
|
||||||
// Update the state variable instead of calling useQuery
|
|
||||||
setJobIdState(selectedJobId);
|
|
||||||
}
|
|
||||||
}, [selectedJobId]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the selected job details when the job details query is successful
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loading && !error && data) {
|
|
||||||
setSelectedJobDetails(data.jobs_by_pk);
|
|
||||||
}
|
|
||||||
}, [loading, error, data]);
|
|
||||||
|
|
||||||
|
|
||||||
const removeTaskIdFromUrl = () => {
|
const removeTaskIdFromUrl = () => {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (!urlParams.has('taskid')) return;
|
||||||
urlParams.delete('taskid');
|
urlParams.delete('taskid');
|
||||||
history(`${window.location.pathname}?${urlParams}`);
|
history(`${window.location.pathname}?${urlParams}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle existing task
|
||||||
|
* @param values
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const handleExistingTask = async (values) => {
|
||||||
|
const isAssignedToDirty = values.assigned_to !== existingTask.assigned_to;
|
||||||
|
|
||||||
|
await updateTask({
|
||||||
|
variables: {
|
||||||
|
taskId: existingTask.id,
|
||||||
|
task: replaceUndefinedWithNull(values)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isAssignedToDirty) {
|
||||||
|
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 sing 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 || ''}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('taskUpdated', {
|
||||||
|
detail: {message: 'A task has been created or edited.'},
|
||||||
|
}));
|
||||||
|
|
||||||
|
notification["success"]({
|
||||||
|
message: t("tasks.successes.updated"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (refetch) await refetch();
|
||||||
|
|
||||||
|
toggleModalVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewTask = async (values) => {
|
||||||
|
const newTaskID = (await insertTask({
|
||||||
|
variables: {
|
||||||
|
taskInput: [
|
||||||
|
{
|
||||||
|
...values,
|
||||||
|
created_by: currentUser.email,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// TODO: Consult Patrick, because this fails on relationship data, and an event emitter is just much easier to use
|
||||||
|
// update(cache) {
|
||||||
|
// cache.modify({
|
||||||
|
// fields: {
|
||||||
|
// tasks(existingTasks) {
|
||||||
|
// return [{
|
||||||
|
// ...values,
|
||||||
|
// jobid: selectedJobId || values.jobid,
|
||||||
|
// created_by: currentUser.email,
|
||||||
|
// bodyshopid: bodyshop.id
|
||||||
|
// }, ...existingTasks]
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
})).data.insert_tasks.returning[0].id;
|
||||||
|
|
||||||
|
if (refetch) await refetch();
|
||||||
|
|
||||||
|
form.resetFields();
|
||||||
|
|
||||||
|
toggleModalVisible();
|
||||||
|
|
||||||
|
// send notification to the assigned user
|
||||||
|
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 sing in 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 || ''}`));
|
||||||
|
|
||||||
|
window.dispatchEvent(new CustomEvent('taskUpdated', {
|
||||||
|
detail: {message: 'A task has been created or edited.'},
|
||||||
|
}));
|
||||||
|
|
||||||
|
notification["success"]({
|
||||||
|
message: t("tasks.successes.created"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle the form submit
|
* Handle the form submit
|
||||||
* @param formValues
|
* @param formValues
|
||||||
@@ -130,58 +235,9 @@ export function TaskUpsertModalContainer({
|
|||||||
const handleFinish = async (formValues) => {
|
const handleFinish = async (formValues) => {
|
||||||
const {...values} = formValues;
|
const {...values} = formValues;
|
||||||
if (existingTask) {
|
if (existingTask) {
|
||||||
await updateTask({
|
await handleExistingTask(values);
|
||||||
variables: {
|
|
||||||
taskId: existingTask.id,
|
|
||||||
task: replaceUndefinedWithNull(values)
|
|
||||||
},
|
|
||||||
});
|
|
||||||
window.dispatchEvent(new CustomEvent('taskUpdated', {
|
|
||||||
detail: {message: 'A task has been created or edited.'},
|
|
||||||
}));
|
|
||||||
notification["success"]({
|
|
||||||
message: t("tasks.successes.updated"),
|
|
||||||
});
|
|
||||||
if (refetch) await refetch();
|
|
||||||
toggleModalVisible();
|
|
||||||
} else {
|
} else {
|
||||||
await insertTask({
|
await handleNewTask(values);
|
||||||
variables: {
|
|
||||||
taskInput: [
|
|
||||||
{
|
|
||||||
...values,
|
|
||||||
created_by: currentUser.email,
|
|
||||||
bodyshopid: bodyshop.id
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// TODO: Consult Patrick, because this fails on relationship data, and an event emitter is just much easier to use
|
|
||||||
// update(cache) {
|
|
||||||
// cache.modify({
|
|
||||||
// fields: {
|
|
||||||
// tasks(existingTasks) {
|
|
||||||
// return [{
|
|
||||||
// ...values,
|
|
||||||
// jobid: selectedJobId || values.jobid,
|
|
||||||
// created_by: currentUser.email,
|
|
||||||
// bodyshopid: bodyshop.id
|
|
||||||
// }, ...existingTasks]
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// },
|
|
||||||
|
|
||||||
});
|
|
||||||
if (refetch) await refetch();
|
|
||||||
form.resetFields();
|
|
||||||
toggleModalVisible();
|
|
||||||
window.dispatchEvent(new CustomEvent('taskUpdated', {
|
|
||||||
detail: {message: 'A task has been created or edited.'},
|
|
||||||
}));
|
|
||||||
notification["success"]({
|
|
||||||
message: t("tasks.successes.created"),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -199,7 +255,6 @@ export function TaskUpsertModalContainer({
|
|||||||
removeTaskIdFromUrl();
|
removeTaskIdFromUrl();
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
|
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ export function Manage({ conflict, bodyshop, enableJoyRide, joyRideSteps, setJoy
|
|||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
This
|
|
||||||
>
|
>
|
||||||
<PaymentModalContainer />
|
<PaymentModalContainer />
|
||||||
|
|
||||||
|
|||||||
2668
server/email/generateTemplate.js
Normal file
2668
server/email/generateTemplate.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ const InstanceManager = require("../utils/instanceMgr").default;
|
|||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
|
const {isObject} = require("lodash");
|
||||||
|
const generateEmailTemplate = require('./generateTemplate');
|
||||||
|
|
||||||
const ses = new aws.SES({
|
const ses = new aws.SES({
|
||||||
// The key apiVersion is no longer supported in v3, and can be removed.
|
// The key apiVersion is no longer supported in v3, and can be removed.
|
||||||
@@ -88,7 +90,8 @@ exports.sendEmail = async (req, res) => {
|
|||||||
replyTo: req.body.ReplyTo.Email,
|
replyTo: req.body.ReplyTo.Email,
|
||||||
to: req.body.to,
|
to: req.body.to,
|
||||||
cc: req.body.cc,
|
cc: req.body.cc,
|
||||||
subject: req.body.subject
|
subject: req.body.subject,
|
||||||
|
templateStrings: req.body.templateStrings
|
||||||
});
|
});
|
||||||
|
|
||||||
let downloadedMedia = [];
|
let downloadedMedia = [];
|
||||||
@@ -104,6 +107,7 @@ exports.sendEmail = async (req, res) => {
|
|||||||
to: req.body.to,
|
to: req.body.to,
|
||||||
cc: req.body.cc,
|
cc: req.body.cc,
|
||||||
subject: req.body.subject,
|
subject: req.body.subject,
|
||||||
|
templateStrings: req.body.templateStrings,
|
||||||
error
|
error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -134,7 +138,7 @@ exports.sendEmail = async (req, res) => {
|
|||||||
};
|
};
|
||||||
})
|
})
|
||||||
] || null,
|
] || null,
|
||||||
html: req.body.html,
|
html: isObject(req.body?.templateStrings) ? generateEmailTemplate(req.body.templateStrings) : req.body.html,
|
||||||
ses: {
|
ses: {
|
||||||
// optional extra arguments for SendRawEmail
|
// optional extra arguments for SendRawEmail
|
||||||
Tags: [
|
Tags: [
|
||||||
@@ -153,7 +157,8 @@ exports.sendEmail = async (req, res) => {
|
|||||||
replyTo: req.body.ReplyTo.Email,
|
replyTo: req.body.ReplyTo.Email,
|
||||||
to: req.body.to,
|
to: req.body.to,
|
||||||
cc: req.body.cc,
|
cc: req.body.cc,
|
||||||
subject: req.body.subject
|
subject: req.body.subject,
|
||||||
|
templateStrings: req.body.templateStrings
|
||||||
// info,
|
// info,
|
||||||
});
|
});
|
||||||
logEmail(req, {
|
logEmail(req, {
|
||||||
@@ -172,6 +177,7 @@ exports.sendEmail = async (req, res) => {
|
|||||||
to: req.body.to,
|
to: req.body.to,
|
||||||
cc: req.body.cc,
|
cc: req.body.cc,
|
||||||
subject: req.body.subject,
|
subject: req.body.subject,
|
||||||
|
templateStrings: req.body.templateStrings,
|
||||||
error: err
|
error: err
|
||||||
});
|
});
|
||||||
logEmail(req, {
|
logEmail(req, {
|
||||||
|
|||||||
Reference in New Issue
Block a user