Email sending framework in place with nodemailer. Some rendering is happening dynamically, but i todesn't bring in most styles.

This commit is contained in:
Patrick Fic
2020-02-18 17:32:43 -08:00
parent dbf5c38752
commit 81e3ea622f
13 changed files with 277 additions and 16 deletions

View File

@@ -1,4 +1,4 @@
import { Input, Table, Button } from "antd";
import { Button, Input, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
@@ -123,8 +123,9 @@ export default function JobLinesComponent({
{record.allocations && record.allocations.length > 0
? record.allocations.map(item => (
<div
key={item.id}
>{`${item.employee.first_name} ${item.employee.last_name} (${item.hours})`}</div>
key={
item.id
}>{`${item.employee.first_name} ${item.employee.last_name} (${item.hours})`}</div>
))
: null}
<AllocationsAssignmentContainer
@@ -184,8 +185,7 @@ export default function JobLinesComponent({
/>
<Button
disabled={selectedLines.length > 0 ? false : true}
onClick={() => setPartsModalVisible(true)}
>
onClick={() => setPartsModalVisible(true)}>
{t("parts.actions.order")}
</Button>
</div>
@@ -193,7 +193,7 @@ export default function JobLinesComponent({
}}
{...formItemLayout}
loading={loading}
size="small"
size='small'
pagination={{ position: "bottom", defaultPageSize: 50 }}
rowSelection={{
// selectedRowKeys: selectedLines,
@@ -201,7 +201,7 @@ export default function JobLinesComponent({
setSelectedLines(selectedRows)
}}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
rowKey='id'
dataSource={jobLines}
onChange={handleTableChange}
/>

View File

@@ -56,6 +56,10 @@ export default connect(
data: linesToOrder.reduce((acc, value) => {
acc.push({
line_desc: value.line_desc,
oem_partno: value.oem_partno,
db_price: value.db_price,
act_price: value.act_price,
line_remarks: "Alalala",
joblineid: value.joblineid,
status:
bodyshop.md_order_statuses.default_ordered || "Ordered*"

View File

@@ -9,3 +9,47 @@ export const INSERT_NEW_PARTS_ORDERS = gql`
}
}
`;
export const REPORT_QUERY_PARTS_ORDER_BY_PK = gql`
query REPORT_QUERY_PARTS_ORDER_BY_PK($id: uuid!) {
parts_orders_by_pk(id: $id) {
job {
id
vehicle {
id
v_model_desc
v_make_desc
v_model_yr
v_vin
}
ro_number
est_number
}
id
deliver_by
parts_order_lines {
id
db_price
act_price
line_desc
line_remarks
oem_partno
status
}
status
user_email
}
bodyshops(where: { associations: { active: { _eq: true } } }) {
id
address1
address2
city
email
federal_tax_id
state
shopname
zip_post
logo_img_path
}
}
`;

View File

@@ -1,9 +1,42 @@
import React from 'react'
import { Button } from "antd";
import axios from "axios";
import React from "react";
import { useApolloClient } from "react-apollo";
import ReactDOMServer from "react-dom/server";
import { REPORT_QUERY_PARTS_ORDER_BY_PK } from "../../graphql/parts-orders.queries";
import ReportPartsOrder from "../../reports/pages/parts-order/parts-order.component";
export default function ManageRootPageComponent() {
return (
<div>
Temporary Home Page.
</div>
)
const client = useApolloClient();
return (
<div>
<Button
onClick={() => {
client
.query({
query: REPORT_QUERY_PARTS_ORDER_BY_PK,
variables: { id: "ebe0fb6b-6ec4-4ae0-8fdc-49bdf1e37ff3" },
fetchPolicy: "network-only"
})
.then(response => {
axios.post("/sendemail", {
from: { name: "Kavia Autobody" },
to: "snaptsoft@gmail.com",
replyTo: "patrickwf@gmail.com",
subject: "Sending Email using Node.js",
text: "Plaintext version of the message",
html: ReactDOMServer.renderToStaticMarkup(
<ReportPartsOrder
order={response.data.parts_orders_by_pk}
bodyshop={response.data.bodyshops[0]}
/>
)
});
});
}}>
Test Email
</Button>
<div>Testing Section-Report</div>
</div>
);
}

View File

@@ -0,0 +1,6 @@
import React from "react";
import { ApolloProvider } from "react-apollo";
export default function ReportEmailWrapper({ client, children }) {
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}

View File

@@ -0,0 +1,26 @@
import React from "react";
import { Row, Col, Typography } from "antd";
export default function RcShopHeaderComponent({ bodyshop }) {
if (!bodyshop) return null;
return (
<Row>
<Col span={8}>
{bodyshop.logo_img_path ? (
<img
alt='Shop Logo'
src={bodyshop.logo_img_path}
style={{ height: "80px" }}
/>
) : null}
</Col>
<Col span={16}>
<Row>
<Typography.Title>{`${bodyshop.shopname}`}</Typography.Title>
</Row>
<Row>{`${bodyshop.address1 || ""} ${bodyshop.address2 ||
""}, ${bodyshop.city || ""}, ${bodyshop.state ||
""} ${bodyshop.zip_post || ""}`}</Row>
</Col>
</Row>
);
}

View File

@@ -0,0 +1,73 @@
import React from "react";
import { Row, Col, Table } from "antd";
import CurrencyFormatter from "../../../utils/CurrencyFormatter";
import { useTranslation } from "react-i18next";
export default function ReportPartsOrderComponent({ order, bodyshop }) {
const { t } = useTranslation();
const columns = [
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc"
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
render: (text, record) => (
<span>
{record.oem_partno ? record.oem_partno : record.op_code_desc}
</span>
)
},
{
title: t("joblines.fields.line_remarks"),
dataIndex: "line_remarks",
key: "line_remarks"
},
{
title: t("joblines.fields.db_price"),
dataIndex: "db_price",
key: "db_price",
render: (text, record) => (
<CurrencyFormatter>{record.db_price}</CurrencyFormatter>
)
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
render: (text, record) => (
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
)
}
];
return (
<div>
<Row>
<Col span={4}>
<div>{`PO Number: ${order.po_number || ""}`}</div>
<div>{`Delivery By: ${order.deliver_by || ""}`}</div>
<div>{`Ordered By: ${order.user_email || ""}`}</div>
</Col>
<Col span={4}>
<div>{`Vehicle Description: ${order.job.vehicle.v_model_yr ||
""} ${order.job.vehicle.v_make_desc || ""} ${order.job.vehicle
.v_model_desc || ""}`}</div>
<div>{`VIN: ${order.job.vehicle.v_vin || ""}`}</div>
</Col>
</Row>
<Row>
<Table
size='small'
columns={columns.map(item => ({ ...item }))}
pagination={false}
rowKey='id'
dataSource={order.parts_order_lines ? order.parts_order_lines : null}
/>
</Row>
</div>
);
}

View File

@@ -0,0 +1,24 @@
import React from "react";
import { useQuery } from "react-apollo";
import AlertComponent from "../../../components/alert/alert.component";
import LoadingSpinner from "../../../components/loading-spinner/loading-spinner.component";
import { REPORT_QUERY_PARTS_ORDER_BY_PK } from "../../../graphql/parts-orders.queries";
import RcShopHeaderComponent from "../../components/shop-header/shop-header.component";
import ReportPartsOrderComponent from "./parts-order.component";
export default function ReportPartsOrderContainer({ orderId }) {
const { loading, error, data } = useQuery(REPORT_QUERY_PARTS_ORDER_BY_PK, {
variables: { id: orderId },
fetchPolicy: "network-only",
});
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type='error' />;
return (
<div>
<RcShopHeaderComponent bodyshop={data ? data.bodyshops[0] : null} />
<ReportPartsOrderComponent
order={data ? data.parts_orders_by_pk : null}
/>
</div>
);
}

View File

@@ -11196,8 +11196,6 @@ rxjs@^6.5.3:
version "6.5.4"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c"
integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==
dependencies:
tslib "^1.9.0"
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"

View File

@@ -24,7 +24,8 @@
"dotenv": "8.2.0",
"express": "^4.16.4",
"express-sslify": "^1.2.0",
"firebase-tools": "^7.12.1"
"firebase-tools": "^7.12.1",
"nodemailer": "^6.4.2"
},
"devDependencies": {
"concurrently": "^4.0.1",

45
sendemail.js Normal file
View File

@@ -0,0 +1,45 @@
var nodemailer = require("nodemailer");
require("dotenv").config();
var transporter = nodemailer.createTransport({
host: process.env.email_server,
port: 465,
secure: true, // upgrade later with STARTTLS
auth: {
user: process.env.email_api,
pass: process.env.email_secret
}
});
// verify connection configuration
transporter.verify(function(error, success) {
if (error) {
console.log(error);
} else {
console.log("[EMAIL] Succesfully connected to SMTP server.");
}
});
exports.sendEmail = (req, res) => {
if (process.env.NODE_ENV !== "production") {
console.log("[EMAIL] Incoming Message Body", req.body);
}
transporter.sendMail(
{
...req.body,
from: {
name: req.body.from.name || "No Reply @ Bodyshop.app",
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 });
}
}
);
};

View File

@@ -23,6 +23,8 @@ app.post("/sign_s3", s3upload.sign_s3);
app.get("/sign_s3", s3upload.get_s3);
app.post("/delete_s3", s3upload.delete_s3);
var sendEmail = require("./sendemail.js");
app.post("/sendemail", sendEmail.sendEmail);
// app.get("/test", function(req, res) {
// res.json({ success: true });
// });

View File

@@ -2872,6 +2872,11 @@ node-forge@^0.9.0:
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5"
integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ==
nodemailer@^6.4.2:
version "6.4.2"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.2.tgz#7147550e32cdc37453380ab78d2074533966090a"
integrity sha512-g0n4nH1ONGvqYo1v72uSWvF/MRNnnq1LzmSzXb/6EPF3LFb51akOhgG3K2+aETAsJx90/Q5eFNTntu4vBCwyQQ==
normalize-package-data@^2.3.2:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"