Baseline email wrapper

This commit is contained in:
Patrick Fic
2020-02-19 11:01:05 -08:00
parent 81e3ea622f
commit 0b47fb166e
14 changed files with 286 additions and 213 deletions

View File

@@ -4,6 +4,8 @@
"private": true,
"proxy": "https://localhost:5000",
"dependencies": {
"@ckeditor/ckeditor5-build-classic": "^16.0.0",
"@ckeditor/ckeditor5-react": "^2.1.0",
"antd": "^3.26.8",
"apollo-boost": "^0.4.4",
"apollo-link-context": "^1.0.19",
@@ -23,6 +25,7 @@
"react-big-calendar": "^0.23.0",
"react-chartjs-2": "^2.9.0",
"react-dom": "^16.12.0",
"react-html-email": "^3.0.0",
"react-i18next": "^11.3.1",
"react-icons": "^3.9.0",
"react-image-file-resizer": "^0.2.1",

View File

@@ -0,0 +1,46 @@
import React from "react";
import { Input } from "antd";
import CKEditor from "@ckeditor/ckeditor5-react";
import ClassicEditor from "@ckeditor/ckeditor5-build-classic";
export default function SendEmailButtonComponent({
emailConfig,
handleConfigChange,
handleHtmlChange
}) {
return (
<div>
THis is where the text editing will happen To
<Input
defaultValue={emailConfig.to}
onChange={handleConfigChange}
name='to'
/>
CC
<Input
defaultValue={emailConfig.cc}
onChange={handleConfigChange}
name='cc'
/>
Subject
<Input
defaultValue={emailConfig.subject}
onChange={handleConfigChange}
name='subject'
/>
<CKEditor
editor={ClassicEditor}
data={emailConfig.html}
onInit={editor => {
// You can store the "editor" and use when it is needed.
console.log("Editor is ready to use!", editor);
}}
onChange={(event, editor) => {
const data = editor.getData();
console.log({ event, editor, data });
handleHtmlChange(data);
}}
/>
</div>
);
}

View File

@@ -0,0 +1,72 @@
import { Button, Modal } from "antd";
import axios from "axios";
import React, { useState } from "react";
import { useQuery } from "react-apollo";
//Message options has the to & from details
//Query Config is what can get popped into UseQuery(QUERY_NAME, {variables: {vars}, fetchonly})
//Template Which template should be used to send the email.
import ReactDOMServer from "react-dom/server";
import { renderEmail } from "react-html-email";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import SendEmailButtonComponent from "./send-email-button.component";
export default function SendEmail({
MessageOptions,
QueryConfig,
Template,
...otherProps
}) {
const [modalVisible, setModalVisible] = useState(false);
const [emailConfig, setEmailConfig] = useState({ ...MessageOptions });
const [gqlQuery, vars] = QueryConfig;
const { loading, data } = useQuery(gqlQuery, {
...vars,
fetchPolicy: "network-only",
skip: !modalVisible
});
if (data && !emailConfig.html) {
console.log(ReactDOMServer.renderToStaticMarkup(<Template data={data} />));
setEmailConfig({
...emailConfig,
//html: ReactDOMServer.renderToStaticMarkup(<Template data={data} />)
html: renderEmail(<Template data={data} />)
});
}
const handleConfigChange = event => {
const { name, value } = event.target;
setEmailConfig({ ...emailConfig, [name]: value });
};
const handleHtmlChange = text => {
setEmailConfig({ ...emailConfig, html: text });
};
const sendEmail = () => {
axios.post("/sendemail", emailConfig).then(response => {
alert(JSON.stringify(response));
});
};
return (
<div>
<Modal
destroyOnClose={true}
visible={modalVisible}
width={"80%"}
onOk={sendEmail}
onCancel={() => setModalVisible(false)}>
<LoadingSpinner loading={loading}>
<SendEmailButtonComponent
handleConfigChange={handleConfigChange}
emailConfig={emailConfig}
handleHtmlChange={handleHtmlChange}
/>
</LoadingSpinner>
</Modal>
<Button onClick={() => setModalVisible(true)}>
{otherProps.children}
</Button>
</div>
);
}

View File

@@ -0,0 +1,46 @@
import React from "react";
function Cell({ children }) {
return <td>{children}</td>;
}
function Row({ children }) {
return (
<tr>
{React.Children.map(children, el => {
if (el.type === Cell) return el;
return <td>{el}</td>;
})}
</tr>
);
}
function Grid({ children }) {
return (
<table>
<tbody>
{React.Children.map(children, el => {
if (!el) return;
if (el.type === Row) return el;
if (el.type === Cell) {
return <tr>{el}</tr>;
}
return (
<tr>
<td>{el}</td>
</tr>
);
})}
</tbody>
</table>
);
}
Grid.Row = Row;
Grid.Cell = Cell;
export default Grid;

View File

@@ -0,0 +1,30 @@
import React from "react";
import { A, Box, Email, Item, Span } from "react-html-email";
export default function PartsOrderEmail({ data }) {
console.log("Data", data);
return (
<Email title='Hello World!'>
<Box>
<Item align='center'>
<Span fontSize={20}>
This is an example email made with:
<A href='https://github.com/chromakode/react-html-email'>
react-html-email
</A>
.
</Span>
</Item>
<Item align='center'>
<Span fontSize={20}>
This is an example email made with:
<A href='https://github.com/chromakode/react-html-email'>
react-html-email
</A>
.
</Span>
</Item>
</Box>
</Email>
);
}

View File

@@ -0,0 +1,45 @@
import { gql } from "apollo-boost";
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

@@ -9,47 +9,3 @@ 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,42 +1,29 @@
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";
import SendEmailButton from "../../components/send-email-button/send-email-button.container";
import PartsOrderEmail from "../../emails/parts-order/parts-order.email";
import { REPORT_QUERY_PARTS_ORDER_BY_PK } from "../../emails/parts-order/parts-order.query";
export default function ManageRootPageComponent() {
const client = useApolloClient();
//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>
<SendEmailButton
MessageOptions={{
from: {
name: "Kavia"
},
to: "patrickwf@gmail.com",
replyTo: "patrickwf@gmail.com"
}}
Template={PartsOrderEmail}
QueryConfig={[
REPORT_QUERY_PARTS_ORDER_BY_PK,
{
variables: { id: "ebe0fb6b-6ec4-4ae0-8fdc-49bdf1e37ff3" }
}
]}>
Send an Email in new Window
</SendEmailButton>
</div>
);
}

View File

@@ -60,15 +60,13 @@ export default function Manage({ match }) {
</Header>
<Layout>
<Content
className="content-container"
style={{ padding: "0em 4em 4em" }}
>
className='content-container'
style={{ padding: "0em 4em 4em" }}>
<ErrorBoundary>
<Suspense
fallback={
<LoadingSpinner message={t("general.labels.loadingapp")} />
}
>
}>
<Route exact path={`${match.path}`} component={ManageRootPage} />
<Route exact path={`${match.path}/jobs`} component={JobsPage} />

View File

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

View File

@@ -1,26 +0,0 @@
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

@@ -1,73 +0,0 @@
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

@@ -1,24 +0,0 @@
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

@@ -967,6 +967,18 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@ckeditor/ckeditor5-build-classic@^16.0.0":
version "16.0.0"
resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-build-classic/-/ckeditor5-build-classic-16.0.0.tgz#9141e94ea6765eda4925aaf062448507410bbe70"
integrity sha512-gBfZqWg3hmCvhq6/wX5UJp4qwl6gB+NJPpOkya2Y3jWKA0HKf3UdlhIvaHq7dRaqhi4unmqaJZVEk5VpZ1vDOg==
"@ckeditor/ckeditor5-react@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@ckeditor/ckeditor5-react/-/ckeditor5-react-2.1.0.tgz#f612546a5a328899a436d71b72ffd632600049e8"
integrity sha512-rlHjRKhwP9tNK0Yj2UJShL14mRfxLPgJ+Pv6zTv/Mvmd4wrwGnJf+5ybOAGK92S02hP1cXH+9km+PRO1b4V+ng==
dependencies:
prop-types "^15.6.1"
"@cnakazawa/watch@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.3.tgz#099139eaec7ebf07a27c1786a3ff64f39464d2ef"
@@ -9755,7 +9767,7 @@ prop-types-exact@^1.2.0:
object.assign "^4.1.0"
reflect.ownkeys "^0.2.0"
prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -10499,6 +10511,13 @@ react-error-overlay@^6.0.5:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.5.tgz#55d59c2a3810e8b41922e0b4e5f85dcf239bd533"
integrity sha512-+DMR2k5c6BqMDSMF8hLH0vYKtKTeikiFW+fj0LClN+XZg4N9b8QUAdHC62CGWNLTi/gnuuemNcNcTFrCvK1f+A==
react-html-email@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/react-html-email/-/react-html-email-3.0.0.tgz#a5a51b78271a33daf9b14638e9e4c5df93f691aa"
integrity sha512-eIBmZjqoS1SLAOi/QYTHLnZSmBM5AhP/cScJGhVDHlBJFtMyiR4qBUQYQDCqRtNPDKHWGMbbS/+XHSgD+C2hng==
dependencies:
prop-types "^15.5.10"
react-i18next@^11.3.1:
version "11.3.1"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.3.1.tgz#9269282c3f566015f0bdf8fdbf46782bbe50f5a7"