IO-557 Send documents in emails.

This commit is contained in:
Patrick Fic
2021-06-07 12:27:14 -07:00
parent 784c58e295
commit 979ba1c142
22 changed files with 218 additions and 164 deletions

View File

@@ -11317,6 +11317,27 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>documents</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>generatingemail</name> <name>generatingemail</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

View File

@@ -0,0 +1,58 @@
import { useQuery } from "@apollo/client";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_DOCUMENTS_BY_JOB } from "../../graphql/documents.queries";
import { selectEmailConfig } from "../../redux/email/email.selectors";
import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
emailConfig: selectEmailConfig,
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(EmailDocumentsComponent);
export function EmailDocumentsComponent({
emailConfig,
selectedMediaState,
}) {
const { t } = useTranslation();
const [selectedMedia, setSelectedMedia] = selectedMediaState;
const { loading, error, data } = useQuery(GET_DOCUMENTS_BY_JOB, {
variables: {
jobId: emailConfig.jobid,
},
skip: !emailConfig.jobid,
});
console.log(
"🚀 ~ file: email-documents.component.jsx ~ line 38 ~ emailConfig",
emailConfig
);
return (
<div>
{loading && <LoadingSpinner />}
{error && <AlertComponent message={error.message} type="error" />}
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div>
) : null}
{data && (
<JobDocumentsGalleryExternal
data={data ? data.documents : []}
externalMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
</div>
);
}

View File

@@ -1,9 +1,10 @@
import { UploadOutlined } from "@ant-design/icons"; import { UploadOutlined } from "@ant-design/icons";
import { Card, Divider, Form, Input, Select, Upload } from "antd"; import { Divider, Form, Input, Select, Tabs, Upload } from "antd";
import React from "react"; import React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import EmailDocumentsComponent from "../email-documents/email-documents.component";
export default function EmailOverlayComponent({ form }) { export default function EmailOverlayComponent({ form, selectedMediaState }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div> <div>
@@ -52,34 +53,38 @@ export default function EmailOverlayComponent({ form }) {
}} }}
</Form.Item> </Form.Item>
<Card title={t("emails.labels.attachments")}> <Tabs>
<Form.Item <Tabs.TabPane tab={t("emails.labels.documents")} key="documents">
name="fileList" <EmailDocumentsComponent selectedMediaState={selectedMediaState} />
valuePropName="fileList" </Tabs.TabPane>
getValueFromEvent={(e) => { <Tabs.TabPane tab={t("emails.labels.attachments")} key="attachments">
console.log("Upload event:", e); <Form.Item
if (Array.isArray(e)) { name="fileList"
return e; valuePropName="fileList"
} getValueFromEvent={(e) => {
return e && e.fileList; if (Array.isArray(e)) {
}} return e;
> }
<Upload.Dragger return e && e.fileList;
beforeUpload={Upload.LIST_IGNORE} }}
multiple
listType="picture-card"
> >
<> <Upload.Dragger
<p className="ant-upload-drag-icon"> beforeUpload={Upload.LIST_IGNORE}
<UploadOutlined /> multiple
</p> listType="picture-card"
<p className="ant-upload-text"> >
Click or drag files to this area to upload. <>
</p> <p className="ant-upload-drag-icon">
</> <UploadOutlined />
</Upload.Dragger> </p>
</Form.Item> <p className="ant-upload-text">
</Card> Click or drag files to this area to upload.
</p>
</>
</Upload.Dragger>
</Form.Item>
</Tabs.TabPane>
</Tabs>
</div> </div>
); );
} }

View File

@@ -43,6 +43,8 @@ export function EmailOverlayContainer({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [rawHtml, setRawHtml] = useState(""); const [rawHtml, setRawHtml] = useState("");
const [selectedMedia, setSelectedMedia] = useState([]);
const defaultEmailFrom = { const defaultEmailFrom = {
from: { from: {
name: `${currentUser.displayName} @ ${bodyshop.shopname}`, name: `${currentUser.displayName} @ ${bodyshop.shopname}`,
@@ -56,17 +58,18 @@ export function EmailOverlayContainer({
const handleFinish = async (values) => { const handleFinish = async (values) => {
logImEXEvent("email_send_from_modal"); logImEXEvent("email_send_from_modal");
console.log(`values`, values);
const attachments = []; const attachments = [];
await asyncForEach(values.fileList, async (f) => { if (values.fileList)
const t = { await asyncForEach(values.fileList, async (f) => {
ContentType: f.type, const t = {
Filename: f.name, ContentType: f.type,
Base64Content: (await toBase64(f.originFileObj)).split(",")[1], Filename: f.name,
}; Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
attachments.push(t); };
}); attachments.push(t);
});
setSending(true); setSending(true);
try { try {
@@ -74,9 +77,12 @@ export function EmailOverlayContainer({
...defaultEmailFrom, ...defaultEmailFrom,
...values, ...values,
html: rawHtml, html: rawHtml,
attachments: await Promise.all( attachments:
values.fileList.map(async (f) => await toBase64(f.originFileObj)) values.fileList &&
), (await Promise.all(
values.fileList.map(async (f) => await toBase64(f.originFileObj))
)),
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.src),
//attachments, //attachments,
}); });
notification["success"]({ message: t("emails.successes.sent") }); notification["success"]({ message: t("emails.successes.sent") });
@@ -137,7 +143,12 @@ export function EmailOverlayContainer({
<LoadingSpinner message={t("emails.labels.generatingemail")} /> <LoadingSpinner message={t("emails.labels.generatingemail")} />
</div> </div>
)} )}
{!loading && <EmailOverlayComponent form={form} />} {!loading && (
<EmailOverlayComponent
form={form}
selectedMediaState={[selectedMedia, setSelectedMedia]}
/>
)}
</Form> </Form>
</Modal> </Modal>
); );

View File

@@ -97,7 +97,8 @@ export function ScheduleEventComponent({
variables: { id: event.job.id }, variables: { id: event.job.id },
}, },
{ to: event.job && event.job.ownr_ea, subject: Template.subject }, { to: event.job && event.job.ownr_ea, subject: Template.subject },
"e" "e",
event.job && event.job.id
); );
}} }}
disabled={event.arrived} disabled={event.arrived}

View File

@@ -123,6 +123,7 @@ export function JobPayments({
messageObject={{ messageObject={{
to: job.ownr_ea, to: job.ownr_ea,
}} }}
id={job.id}
/> />
), ),
}, },

View File

@@ -97,6 +97,7 @@ export function JobsDetailHeaderCsi({
} }
if (e.key === "email") if (e.key === "email")
setEmailOptions({ setEmailOptions({
jobid: job.id,
messageOptions: { messageOptions: {
to: [job.ownr_ea], to: [job.ownr_ea],
replyTo: bodyshop.email, replyTo: bodyshop.email,
@@ -139,6 +140,7 @@ export function JobsDetailHeaderCsi({
} else { } else {
if (e.key === "email") if (e.key === "email")
setEmailOptions({ setEmailOptions({
jobid: job.id,
messageOptions: { messageOptions: {
to: [job.ownr_ea], to: [job.ownr_ea],
replyTo: bodyshop.email, replyTo: bodyshop.email,

View File

@@ -1,7 +1,7 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import Gallery from "react-grid-gallery"; import Gallery from "react-grid-gallery";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DetermineFileType } from "../documents-upload/documents-upload.utility"; import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility";
function JobsDocumentGalleryExternal({ function JobsDocumentGalleryExternal({
data, data,
@@ -15,14 +15,8 @@ function JobsDocumentGalleryExternal({
let documents = data.reduce((acc, value) => { let documents = data.reduce((acc, value) => {
if (value.type.startsWith("image")) { if (value.type.startsWith("image")) {
acc.push({ acc.push({
src: `${ src: GenerateSrcUrl(value),
process.env.REACT_APP_CLOUDINARY_ENDPOINT thumbnail: GenerateThumbUrl(value),
}/${DetermineFileType(value.type)}/upload/${value.key}`,
thumbnail: `${
process.env.REACT_APP_CLOUDINARY_ENDPOINT
}/${DetermineFileType(value.type)}/upload/${
process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS
}/${value.key}`,
thumbnailHeight: 225, thumbnailHeight: 225,
thumbnailWidth: 225, thumbnailWidth: 225,
isSelected: false, isSelected: false,

View File

@@ -183,6 +183,7 @@ export function PartsOrderListTableComponent({
? Templates.parts_return_slip.subject ? Templates.parts_return_slip.subject
: Templates.parts_order.subject, : Templates.parts_order.subject,
}} }}
id={job.id}
/> />
</Space> </Space>
); );

View File

@@ -180,7 +180,8 @@ export function PartsOrderModalContainer({
? Templates.parts_return_slip.subject ? Templates.parts_return_slip.subject
: Templates.parts_order.subject, : Templates.parts_order.subject,
}, },
"e" "e",
jobId
); );
} else if (sendType === "p") { } else if (sendType === "p") {
GenerateDocument( GenerateDocument(

View File

@@ -133,7 +133,8 @@ function PaymentModalContainer({
replyTo: bodyshop.email, replyTo: bodyshop.email,
subject: Templates.payment_receipt.subject, subject: Templates.payment_receipt.subject,
}, },
sendby === "email" ? "e" : "p" sendby === "email" ? "e" : "p",
paymentObj.jobid
); );
} }
} else { } else {

View File

@@ -168,6 +168,7 @@ export function PaymentsListPaginated({
variables: { id: record.id }, variables: { id: record.id },
}} }}
messageObject={{ subject: Templates.payment_receipt.subject }} messageObject={{ subject: Templates.payment_receipt.subject }}
id={record.job && record.job.id}
/> />
</Space> </Space>
), ),

View File

@@ -52,7 +52,8 @@ export function PrintCenterItemComponent({
variables: { id: id }, variables: { id: id },
}, },
{ to: context.job && context.job.ownr_ea, subject: item.subject }, { to: context.job && context.job.ownr_ea, subject: item.subject },
"e" "e",
id
); );
}} }}
/> />

View File

@@ -7,11 +7,12 @@ export default function PrintWrapperComponent({
templateObject, templateObject,
messageObject = {}, messageObject = {},
children, children,
id,
}) { }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handlePrint = async (type) => { const handlePrint = async (type) => {
setLoading(true); setLoading(true);
await GenerateDocument(templateObject, messageObject, type); await GenerateDocument(templateObject, messageObject, type, id);
setLoading(false); setLoading(false);
}; };

View File

@@ -32,27 +32,23 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
const Templates = TemplateList("report_center"); const Templates = TemplateList("report_center");
const { visible } = reportCenterModal; const { visible } = reportCenterModal;
const [ const [callVendorQuery, { data: vendorData, called: vendorCalled }] =
callVendorQuery, useLazyQuery(QUERY_ALL_VENDORS, {
{ data: vendorData, called: vendorCalled }, skip: !(
] = useLazyQuery(QUERY_ALL_VENDORS, { visible &&
skip: !( Templates[form.getFieldValue("key")] &&
visible && Templates[form.getFieldValue("key")].idtype
Templates[form.getFieldValue("key")] && ),
Templates[form.getFieldValue("key")].idtype });
),
});
const [ const [callEmployeeQuery, { data: employeeData, called: employeeCalled }] =
callEmployeeQuery, useLazyQuery(QUERY_ACTIVE_EMPLOYEES, {
{ data: employeeData, called: employeeCalled }, skip: !(
] = useLazyQuery(QUERY_ACTIVE_EMPLOYEES, { visible &&
skip: !( Templates[form.getFieldValue("key")] &&
visible && Templates[form.getFieldValue("key")].idtype
Templates[form.getFieldValue("key")] && ),
Templates[form.getFieldValue("key")].idtype });
),
});
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
@@ -73,7 +69,8 @@ export function ReportCenterModalComponent({ reportCenterModal }) {
to: values.to, to: values.to,
subject: Templates[values.key]?.subject, subject: Templates[values.key]?.subject,
}, },
values.sendby === "email" ? "e" : "p" values.sendby === "email" ? "e" : "p",
id
); );
setLoading(false); setLoading(false);
}; };

View File

@@ -148,6 +148,7 @@ export function ScheduleJobModalContainer({
toggleModalVisible(); toggleModalVisible();
if (values.notifyCustomer) { if (values.notifyCustomer) {
setEmailOptions({ setEmailOptions({
jobid: jobId,
messageOptions: { messageOptions: {
to: [values.email], to: [values.email],
replyTo: bodyshop.email, replyTo: bodyshop.email,

View File

@@ -1,9 +1,6 @@
import { createSelector } from "reselect"; import { createSelector } from "reselect";
const selectEmail = (state) => state.email; const selectEmail = (state) => state.email;
const selectEmailConfigMessageOptions = (state) =>
state.email.emailConfig.messageOptions;
const selectEmailConfigTemplate = (state) => state.email.emailConfig.template;
export const selectEmailVisible = createSelector( export const selectEmailVisible = createSelector(
[selectEmail], [selectEmail],
@@ -11,9 +8,6 @@ export const selectEmailVisible = createSelector(
); );
export const selectEmailConfig = createSelector( export const selectEmailConfig = createSelector(
[selectEmailConfigMessageOptions, selectEmailConfigTemplate], [selectEmail],
(messageOptions, template) => ({ (email) => email.emailConfig
messageOptions,
template,
})
); );

View File

@@ -723,6 +723,7 @@
}, },
"labels": { "labels": {
"attachments": "Attachments", "attachments": "Attachments",
"documents": "Documents",
"generatingemail": "Generating email...", "generatingemail": "Generating email...",
"preview": "Email Preview" "preview": "Email Preview"
}, },

View File

@@ -723,6 +723,7 @@
}, },
"labels": { "labels": {
"attachments": "", "attachments": "",
"documents": "",
"generatingemail": "", "generatingemail": "",
"preview": "" "preview": ""
}, },

View File

@@ -723,6 +723,7 @@
}, },
"labels": { "labels": {
"attachments": "", "attachments": "",
"documents": "",
"generatingemail": "", "generatingemail": "",
"preview": "" "preview": ""
}, },

View File

@@ -147,11 +147,17 @@ export async function RenderTemplates(
} }
} }
export const GenerateDocument = async (template, messageOptions, sendType) => { export const GenerateDocument = async (
template,
messageOptions,
sendType,
jobid
) => {
const bodyshop = store.getState().user.bodyshop; const bodyshop = store.getState().user.bodyshop;
if (sendType === "e") { if (sendType === "e") {
store.dispatch( store.dispatch(
setEmailOptions({ setEmailOptions({
jobid,
messageOptions: { messageOptions: {
...messageOptions, ...messageOptions,
to: Array.isArray(messageOptions.to) to: Array.isArray(messageOptions.to)

View File

@@ -5,12 +5,7 @@ require("dotenv").config({
`.env.${process.env.NODE_ENV || "development"}` `.env.${process.env.NODE_ENV || "development"}`
), ),
}); });
const axios = require("axios");
const mailjet = require("node-mailjet").connect(
process.env.email_api,
process.env.email_secret
);
let nodemailer = require("nodemailer"); let nodemailer = require("nodemailer");
let aws = require("aws-sdk"); let aws = require("aws-sdk");
@@ -28,21 +23,41 @@ exports.sendEmail = async (req, res) => {
console.log("[EMAIL] Incoming Message", req.body.from.name); console.log("[EMAIL] Incoming Message", req.body.from.name);
} }
let downloadedMedia = [];
if (req.body.media && req.body.media.length > 0) {
downloadedMedia = await Promise.all(
req.body.media.map((m) => {
try {
return getImage(m);
} catch (error) {
console.log(error);
}
})
);
}
transporter.sendMail( transporter.sendMail(
{ {
from: `${req.body.from.name} <${req.body.from.address}>`, from: `${req.body.from.name} <${req.body.from.address}>`,
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: "Message", subject: req.body.subject,
attachments: attachments:
(req.body.attachments && [
req.body.attachments.map((a) => { ...((req.body.attachments &&
req.body.attachments.map((a) => {
return {
path: a,
};
})) ||
[]),
...downloadedMedia.map((a) => {
return { return {
path: a, path: a,
}; };
})) || }),
null, ] || null,
html: req.body.html, html: req.body.html,
ses: { ses: {
// optional extra arguments for SendRawEmail // optional extra arguments for SendRawEmail
@@ -65,71 +80,10 @@ exports.sendEmail = async (req, res) => {
} }
} }
); );
// const inlinedCssHtml = await inlineCssTool(req.body.html, {
// url: "https://imex.online",
// });
// console.log("inlinedCssHtml", inlinedCssHtml);
// Create the promise and SES service object
// const request = mailjet.post("send", { version: "v3.1" }).request({
// Messages: [
// {
// From: {
// Email: req.body.from.address,
// Name: req.body.from.name,
// },
// To:
// req.body.to &&
// req.body.to.map((i) => {
// return { Email: i };
// }),
// CC:
// req.body.cc &&
// req.body.cc.map((i) => {
// return { Email: i };
// }),
// ReplyTo: {
// Email: req.body.ReplyTo.Email,
// Name: req.body.ReplyTo.Name,
// },
// Subject: req.body.subject,
// // TextPart:
// // "Dear passenger 1, welcome to Mailjet! May the delivery force be with you!",
// HTMLPart: req.body.html,
// Attachments: req.body.attachments || null,
// },
// ],
// });
// request
// .then((result) => {
// console.log("[EMAIL] Email sent: " + result);
// res.json({ success: true, response: result });
// })
// .catch((err) => {
// console.log("[EMAIL] Email send failed. ", err);
// res.json({ success: false, error: err.message });
// });
// transporter.sendMail(
// {
// ...req.body,
// from: {
// name: req.body.from.name ,
// address: "noreply@bodyshop.app",
// },
// },
// function (error, info) {
// if (error) {
// console.log("[EMAIL] Email send failed. ", error);
// res.json({ success: false, error: error });
// } else {
// console.log("[EMAIL] Email sent: " + info.response);
// res.json({ success: true, response: info.response });
// }
// }
// );
}; };
async function getImage(imageUrl) {
let image = await axios.get(imageUrl, { responseType: "arraybuffer" });
let raw = Buffer.from(image.data).toString("base64");
return "data:" + image.headers["content-type"] + ";base64," + raw;
}