From 979ba1c14246748e3647ed5b789e0bc1b49ee950 Mon Sep 17 00:00:00 2001 From: Patrick Fic <> Date: Mon, 7 Jun 2021 12:27:14 -0700 Subject: [PATCH] IO-557 Send documents in emails. --- bodyshop_translations.babel | 21 ++++ .../email-documents.component.jsx | 58 +++++++++ .../email-overlay/email-overlay.component.jsx | 63 +++++----- .../email-overlay/email-overlay.container.jsx | 37 +++--- .../schedule-event.component.jsx | 3 +- .../job-payments/job-payments.component.jsx | 1 + ...bs-detail-header-actions.csi.component.jsx | 2 + ...s-documents-gallery.external.component.jsx | 12 +- .../parts-order-list-table.component.jsx | 1 + .../parts-order-modal.container.jsx | 3 +- .../payment-modal/payment-modal.container.jsx | 3 +- .../payment-list-paginated.component.jsx | 1 + .../print-center-item.component.jsx | 3 +- .../print-wrapper/print-wrapper.component.jsx | 3 +- .../report-center-modal.component.jsx | 39 +++---- .../schedule-job-modal.container.jsx | 1 + client/src/redux/email/email.selectors.js | 10 +- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + client/src/utils/RenderTemplate.js | 8 +- server/email/sendemail.js | 110 +++++------------- 22 files changed, 218 insertions(+), 164 deletions(-) create mode 100644 client/src/components/email-documents/email-documents.component.jsx diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index d987d9ac9..b94370a28 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -11317,6 +11317,27 @@ + + documents + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + generatingemail false diff --git a/client/src/components/email-documents/email-documents.component.jsx b/client/src/components/email-documents/email-documents.component.jsx new file mode 100644 index 000000000..4769df3f6 --- /dev/null +++ b/client/src/components/email-documents/email-documents.component.jsx @@ -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 ( +
+ {loading && } + {error && } + {selectedMedia.filter((s) => s.isSelected).length >= 10 ? ( +
{t("messaging.labels.maxtenimages")}
+ ) : null} + {data && ( + + )} +
+ ); +} diff --git a/client/src/components/email-overlay/email-overlay.component.jsx b/client/src/components/email-overlay/email-overlay.component.jsx index 0e21bfbb4..3f6f8f72c 100644 --- a/client/src/components/email-overlay/email-overlay.component.jsx +++ b/client/src/components/email-overlay/email-overlay.component.jsx @@ -1,9 +1,10 @@ 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 { 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(); return (
@@ -52,34 +53,38 @@ export default function EmailOverlayComponent({ form }) { }} - - { - console.log("Upload event:", e); - if (Array.isArray(e)) { - return e; - } - return e && e.fileList; - }} - > - + + + + + { + if (Array.isArray(e)) { + return e; + } + return e && e.fileList; + }} > - <> -

- -

-

- Click or drag files to this area to upload. -

- -
-
-
+ + <> +

+ +

+

+ Click or drag files to this area to upload. +

+ +
+ + +
); } diff --git a/client/src/components/email-overlay/email-overlay.container.jsx b/client/src/components/email-overlay/email-overlay.container.jsx index 4010d68ee..e776a156e 100644 --- a/client/src/components/email-overlay/email-overlay.container.jsx +++ b/client/src/components/email-overlay/email-overlay.container.jsx @@ -43,6 +43,8 @@ export function EmailOverlayContainer({ const [loading, setLoading] = useState(false); const [sending, setSending] = useState(false); const [rawHtml, setRawHtml] = useState(""); + const [selectedMedia, setSelectedMedia] = useState([]); + const defaultEmailFrom = { from: { name: `${currentUser.displayName} @ ${bodyshop.shopname}`, @@ -56,17 +58,18 @@ export function EmailOverlayContainer({ const handleFinish = async (values) => { logImEXEvent("email_send_from_modal"); - console.log(`values`, values); + const attachments = []; - await asyncForEach(values.fileList, async (f) => { - const t = { - ContentType: f.type, - Filename: f.name, - Base64Content: (await toBase64(f.originFileObj)).split(",")[1], - }; - attachments.push(t); - }); + if (values.fileList) + await asyncForEach(values.fileList, async (f) => { + const t = { + ContentType: f.type, + Filename: f.name, + Base64Content: (await toBase64(f.originFileObj)).split(",")[1], + }; + attachments.push(t); + }); setSending(true); try { @@ -74,9 +77,12 @@ export function EmailOverlayContainer({ ...defaultEmailFrom, ...values, html: rawHtml, - attachments: await Promise.all( - values.fileList.map(async (f) => await toBase64(f.originFileObj)) - ), + attachments: + 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, }); notification["success"]({ message: t("emails.successes.sent") }); @@ -137,7 +143,12 @@ export function EmailOverlayContainer({ )} - {!loading && } + {!loading && ( + + )} ); diff --git a/client/src/components/job-at-change/schedule-event.component.jsx b/client/src/components/job-at-change/schedule-event.component.jsx index a36cc5b03..41bdb24d6 100644 --- a/client/src/components/job-at-change/schedule-event.component.jsx +++ b/client/src/components/job-at-change/schedule-event.component.jsx @@ -97,7 +97,8 @@ export function ScheduleEventComponent({ variables: { id: event.job.id }, }, { to: event.job && event.job.ownr_ea, subject: Template.subject }, - "e" + "e", + event.job && event.job.id ); }} disabled={event.arrived} diff --git a/client/src/components/job-payments/job-payments.component.jsx b/client/src/components/job-payments/job-payments.component.jsx index fd2558492..0e1a81d2b 100644 --- a/client/src/components/job-payments/job-payments.component.jsx +++ b/client/src/components/job-payments/job-payments.component.jsx @@ -123,6 +123,7 @@ export function JobPayments({ messageObject={{ to: job.ownr_ea, }} + id={job.id} /> ), }, diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx index b71e43919..a0e582b6c 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.csi.component.jsx @@ -97,6 +97,7 @@ export function JobsDetailHeaderCsi({ } if (e.key === "email") setEmailOptions({ + jobid: job.id, messageOptions: { to: [job.ownr_ea], replyTo: bodyshop.email, @@ -139,6 +140,7 @@ export function JobsDetailHeaderCsi({ } else { if (e.key === "email") setEmailOptions({ + jobid: job.id, messageOptions: { to: [job.ownr_ea], replyTo: bodyshop.email, diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.external.component.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.external.component.jsx index d22134ee6..278fe59d9 100644 --- a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.external.component.jsx +++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.external.component.jsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import Gallery from "react-grid-gallery"; import { useTranslation } from "react-i18next"; -import { DetermineFileType } from "../documents-upload/documents-upload.utility"; +import { GenerateSrcUrl, GenerateThumbUrl } from "./job-documents.utility"; function JobsDocumentGalleryExternal({ data, @@ -15,14 +15,8 @@ function JobsDocumentGalleryExternal({ let documents = data.reduce((acc, value) => { if (value.type.startsWith("image")) { acc.push({ - src: `${ - process.env.REACT_APP_CLOUDINARY_ENDPOINT - }/${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}`, + src: GenerateSrcUrl(value), + thumbnail: GenerateThumbUrl(value), thumbnailHeight: 225, thumbnailWidth: 225, isSelected: false, diff --git a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx index 436229c0c..fa7ff6247 100644 --- a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx +++ b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx @@ -183,6 +183,7 @@ export function PartsOrderListTableComponent({ ? Templates.parts_return_slip.subject : Templates.parts_order.subject, }} + id={job.id} /> ); diff --git a/client/src/components/parts-order-modal/parts-order-modal.container.jsx b/client/src/components/parts-order-modal/parts-order-modal.container.jsx index a565acc73..6bb66bc62 100644 --- a/client/src/components/parts-order-modal/parts-order-modal.container.jsx +++ b/client/src/components/parts-order-modal/parts-order-modal.container.jsx @@ -180,7 +180,8 @@ export function PartsOrderModalContainer({ ? Templates.parts_return_slip.subject : Templates.parts_order.subject, }, - "e" + "e", + jobId ); } else if (sendType === "p") { GenerateDocument( diff --git a/client/src/components/payment-modal/payment-modal.container.jsx b/client/src/components/payment-modal/payment-modal.container.jsx index e91e16d19..675af2664 100644 --- a/client/src/components/payment-modal/payment-modal.container.jsx +++ b/client/src/components/payment-modal/payment-modal.container.jsx @@ -133,7 +133,8 @@ function PaymentModalContainer({ replyTo: bodyshop.email, subject: Templates.payment_receipt.subject, }, - sendby === "email" ? "e" : "p" + sendby === "email" ? "e" : "p", + paymentObj.jobid ); } } else { diff --git a/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx b/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx index a72ad41d3..e79fc9a34 100644 --- a/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx +++ b/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx @@ -168,6 +168,7 @@ export function PaymentsListPaginated({ variables: { id: record.id }, }} messageObject={{ subject: Templates.payment_receipt.subject }} + id={record.job && record.job.id} /> ), diff --git a/client/src/components/print-center-item/print-center-item.component.jsx b/client/src/components/print-center-item/print-center-item.component.jsx index b592f8c41..d1c6b1b58 100644 --- a/client/src/components/print-center-item/print-center-item.component.jsx +++ b/client/src/components/print-center-item/print-center-item.component.jsx @@ -52,7 +52,8 @@ export function PrintCenterItemComponent({ variables: { id: id }, }, { to: context.job && context.job.ownr_ea, subject: item.subject }, - "e" + "e", + id ); }} /> diff --git a/client/src/components/print-wrapper/print-wrapper.component.jsx b/client/src/components/print-wrapper/print-wrapper.component.jsx index 68e808bb7..10b24669c 100644 --- a/client/src/components/print-wrapper/print-wrapper.component.jsx +++ b/client/src/components/print-wrapper/print-wrapper.component.jsx @@ -7,11 +7,12 @@ export default function PrintWrapperComponent({ templateObject, messageObject = {}, children, + id, }) { const [loading, setLoading] = useState(false); const handlePrint = async (type) => { setLoading(true); - await GenerateDocument(templateObject, messageObject, type); + await GenerateDocument(templateObject, messageObject, type, id); setLoading(false); }; diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index b7bb08703..51444f1e3 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -32,27 +32,23 @@ export function ReportCenterModalComponent({ reportCenterModal }) { const Templates = TemplateList("report_center"); const { visible } = reportCenterModal; - const [ - callVendorQuery, - { data: vendorData, called: vendorCalled }, - ] = useLazyQuery(QUERY_ALL_VENDORS, { - skip: !( - visible && - Templates[form.getFieldValue("key")] && - Templates[form.getFieldValue("key")].idtype - ), - }); + const [callVendorQuery, { data: vendorData, called: vendorCalled }] = + useLazyQuery(QUERY_ALL_VENDORS, { + skip: !( + visible && + Templates[form.getFieldValue("key")] && + Templates[form.getFieldValue("key")].idtype + ), + }); - const [ - callEmployeeQuery, - { data: employeeData, called: employeeCalled }, - ] = useLazyQuery(QUERY_ACTIVE_EMPLOYEES, { - skip: !( - visible && - Templates[form.getFieldValue("key")] && - Templates[form.getFieldValue("key")].idtype - ), - }); + const [callEmployeeQuery, { data: employeeData, called: employeeCalled }] = + useLazyQuery(QUERY_ACTIVE_EMPLOYEES, { + skip: !( + visible && + Templates[form.getFieldValue("key")] && + Templates[form.getFieldValue("key")].idtype + ), + }); const handleFinish = async (values) => { setLoading(true); @@ -73,7 +69,8 @@ export function ReportCenterModalComponent({ reportCenterModal }) { to: values.to, subject: Templates[values.key]?.subject, }, - values.sendby === "email" ? "e" : "p" + values.sendby === "email" ? "e" : "p", + id ); setLoading(false); }; diff --git a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx index 9a9b372ae..6e3bed49c 100644 --- a/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx +++ b/client/src/components/schedule-job-modal/schedule-job-modal.container.jsx @@ -148,6 +148,7 @@ export function ScheduleJobModalContainer({ toggleModalVisible(); if (values.notifyCustomer) { setEmailOptions({ + jobid: jobId, messageOptions: { to: [values.email], replyTo: bodyshop.email, diff --git a/client/src/redux/email/email.selectors.js b/client/src/redux/email/email.selectors.js index d691f742e..5730e36df 100644 --- a/client/src/redux/email/email.selectors.js +++ b/client/src/redux/email/email.selectors.js @@ -1,9 +1,6 @@ import { createSelector } from "reselect"; const selectEmail = (state) => state.email; -const selectEmailConfigMessageOptions = (state) => - state.email.emailConfig.messageOptions; -const selectEmailConfigTemplate = (state) => state.email.emailConfig.template; export const selectEmailVisible = createSelector( [selectEmail], @@ -11,9 +8,6 @@ export const selectEmailVisible = createSelector( ); export const selectEmailConfig = createSelector( - [selectEmailConfigMessageOptions, selectEmailConfigTemplate], - (messageOptions, template) => ({ - messageOptions, - template, - }) + [selectEmail], + (email) => email.emailConfig ); diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 38855c824..3a1b1b070 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -723,6 +723,7 @@ }, "labels": { "attachments": "Attachments", + "documents": "Documents", "generatingemail": "Generating email...", "preview": "Email Preview" }, diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index fbaf30da8..ef40ff1c5 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -723,6 +723,7 @@ }, "labels": { "attachments": "", + "documents": "", "generatingemail": "", "preview": "" }, diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index cc57c0222..2c2395e5a 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -723,6 +723,7 @@ }, "labels": { "attachments": "", + "documents": "", "generatingemail": "", "preview": "" }, diff --git a/client/src/utils/RenderTemplate.js b/client/src/utils/RenderTemplate.js index 6cdc4dbef..1e886fcdc 100644 --- a/client/src/utils/RenderTemplate.js +++ b/client/src/utils/RenderTemplate.js @@ -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; if (sendType === "e") { store.dispatch( setEmailOptions({ + jobid, messageOptions: { ...messageOptions, to: Array.isArray(messageOptions.to) diff --git a/server/email/sendemail.js b/server/email/sendemail.js index ff7374b01..6f54b00f1 100644 --- a/server/email/sendemail.js +++ b/server/email/sendemail.js @@ -5,12 +5,7 @@ require("dotenv").config({ `.env.${process.env.NODE_ENV || "development"}` ), }); - -const mailjet = require("node-mailjet").connect( - process.env.email_api, - process.env.email_secret -); - +const axios = require("axios"); let nodemailer = require("nodemailer"); let aws = require("aws-sdk"); @@ -28,21 +23,41 @@ exports.sendEmail = async (req, res) => { 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( { from: `${req.body.from.name} <${req.body.from.address}>`, replyTo: req.body.ReplyTo.Email, to: req.body.to, cc: req.body.cc, - subject: "Message", + subject: req.body.subject, attachments: - (req.body.attachments && - req.body.attachments.map((a) => { + [ + ...((req.body.attachments && + req.body.attachments.map((a) => { + return { + path: a, + }; + })) || + []), + ...downloadedMedia.map((a) => { return { path: a, }; - })) || - null, + }), + ] || null, html: req.body.html, ses: { // 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; +}