diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 9348a53a0..e4549e29b 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -8229,6 +8229,27 @@ + + qbo + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + rbac false diff --git a/client/src/assets/C2QB_composite_English.svg b/client/src/assets/C2QB_composite_English.svg new file mode 100644 index 000000000..d5e302253 --- /dev/null +++ b/client/src/assets/C2QB_composite_English.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/assets/C2QB_transparent_English.svg b/client/src/assets/C2QB_transparent_English.svg new file mode 100644 index 000000000..d5e302253 --- /dev/null +++ b/client/src/assets/C2QB_transparent_English.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/client/src/assets/qbo/C2QB_green_btn_med_default.svg b/client/src/assets/qbo/C2QB_green_btn_med_default.svg new file mode 100644 index 000000000..5777594bf --- /dev/null +++ b/client/src/assets/qbo/C2QB_green_btn_med_default.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/assets/qbo/C2QB_green_btn_med_hover.svg b/client/src/assets/qbo/C2QB_green_btn_med_hover.svg new file mode 100644 index 000000000..4495659ad --- /dev/null +++ b/client/src/assets/qbo/C2QB_green_btn_med_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/qbo/C2QB_green_btn_short_default.svg b/client/src/assets/qbo/C2QB_green_btn_short_default.svg new file mode 100644 index 000000000..f5e02d040 --- /dev/null +++ b/client/src/assets/qbo/C2QB_green_btn_short_default.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/assets/qbo/C2QB_green_btn_short_hover.svg b/client/src/assets/qbo/C2QB_green_btn_short_hover.svg new file mode 100644 index 000000000..41c42a246 --- /dev/null +++ b/client/src/assets/qbo/C2QB_green_btn_short_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/qbo/C2QB_green_btn_tall_default.svg b/client/src/assets/qbo/C2QB_green_btn_tall_default.svg new file mode 100644 index 000000000..d93a0e482 --- /dev/null +++ b/client/src/assets/qbo/C2QB_green_btn_tall_default.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/assets/qbo/C2QB_green_btn_tall_hover.svg b/client/src/assets/qbo/C2QB_green_btn_tall_hover.svg new file mode 100644 index 000000000..78e4f7670 --- /dev/null +++ b/client/src/assets/qbo/C2QB_green_btn_tall_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/qbo/C2QB_transparent_btn_med_default.svg b/client/src/assets/qbo/C2QB_transparent_btn_med_default.svg new file mode 100644 index 000000000..575057b1c --- /dev/null +++ b/client/src/assets/qbo/C2QB_transparent_btn_med_default.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/assets/qbo/C2QB_transparent_btn_med_hover.svg b/client/src/assets/qbo/C2QB_transparent_btn_med_hover.svg new file mode 100644 index 000000000..f4bab4f04 --- /dev/null +++ b/client/src/assets/qbo/C2QB_transparent_btn_med_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/qbo/C2QB_transparent_btn_short_default.svg b/client/src/assets/qbo/C2QB_transparent_btn_short_default.svg new file mode 100644 index 000000000..d1c15abeb --- /dev/null +++ b/client/src/assets/qbo/C2QB_transparent_btn_short_default.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/assets/qbo/C2QB_transparent_btn_short_hover.svg b/client/src/assets/qbo/C2QB_transparent_btn_short_hover.svg new file mode 100644 index 000000000..ab88da614 --- /dev/null +++ b/client/src/assets/qbo/C2QB_transparent_btn_short_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/assets/qbo/C2QB_transparent_btn_tall_default.svg b/client/src/assets/qbo/C2QB_transparent_btn_tall_default.svg new file mode 100644 index 000000000..66d56b3ff --- /dev/null +++ b/client/src/assets/qbo/C2QB_transparent_btn_tall_default.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/src/assets/qbo/C2QB_transparent_btn_tall_hover.svg b/client/src/assets/qbo/C2QB_transparent_btn_tall_hover.svg new file mode 100644 index 000000000..d8319ae93 --- /dev/null +++ b/client/src/assets/qbo/C2QB_transparent_btn_tall_hover.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx index d7f57388e..ea9e5517b 100644 --- a/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx +++ b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx @@ -11,6 +11,7 @@ import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-butto import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, }); @@ -206,6 +207,9 @@ export function AccountingReceivablesTableComponent({ completedCallback={setSelectedJobs} /> )} + {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && ( + + )} { + //Check if it's a CDK setup. if (bodyshop.cdk_dealerid) { history.push(`/manage/dms?jobId=${jobId}`); return; @@ -41,48 +42,58 @@ export function JobsCloseExportButton({ logImEXEvent("jobs_close_export"); setLoading(true); - let QbXmlResponse; - try { - QbXmlResponse = await axios.post( - "/accounting/qbxml/receivables", - { jobIds: [jobId] }, - { - headers: { - Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, - }, - } - ); - console.log("handle -> XML", QbXmlResponse); - } catch (error) { - console.log("Error getting QBXML from Server.", error); - notification["error"]({ - message: t("jobs.errors.exporting", { - error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), - }), - }); - setLoading(false); - return; - } - + //Check if it's a QBO Setup. let PartnerResponse; - try { - PartnerResponse = await axios.post( - "http://localhost:1337/qb/", - // "http://609feaeae986.ngrok.io/qb/", - QbXmlResponse.data, - { - headers: { - Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, - }, - } - ); - } catch (error) { - console.log("Error connecting to quickbooks or partner.", error); - notification["error"]({ - message: t("jobs.errors.exporting-partner"), + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + PartnerResponse = await axios.post(`/qbo/receivables`, { + withCredentials: true, + jobIds: [jobId], }); - setLoading(false); - return; + } else { + //Default is QBD + + let QbXmlResponse; + try { + QbXmlResponse = await axios.post( + "/accounting/qbxml/receivables", + { jobIds: [jobId] }, + { + headers: { + Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, + }, + } + ); + console.log("handle -> XML", QbXmlResponse); + } catch (error) { + console.log("Error getting QBXML from Server.", error); + notification["error"]({ + message: t("jobs.errors.exporting", { + error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), + }), + }); + setLoading(false); + return; + } + + try { + PartnerResponse = await axios.post( + "http://localhost:1337/qb/", + // "http://609feaeae986.ngrok.io/qb/", + QbXmlResponse.data, + { + headers: { + Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, + }, + } + ); + } catch (error) { + console.log("Error connecting to quickbooks or partner.", error); + notification["error"]({ + message: t("jobs.errors.exporting-partner"), + }); + setLoading(false); + return; + } } console.log("PartnerResponse", PartnerResponse); diff --git a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx index e0be5621c..69b15dcfc 100644 --- a/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx +++ b/client/src/components/jobs-export-all-button/jobs-export-all-button.component.jsx @@ -34,53 +34,61 @@ export function JobsExportAllButton({ const [loading, setLoading] = useState(false); const handleQbxml = async () => { logImEXEvent("jobs_export_all"); - - setLoading(true); - let QbXmlResponse; - try { - QbXmlResponse = await axios.post( - "/accounting/qbxml/receivables", - { jobIds: jobIds }, - { - headers: { - Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, - }, - } - ); - } catch (error) { - console.log("Error getting QBXML from Server.", error); - notification["error"]({ - message: t("jobs.errors.exporting", { - error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), - }), - }); - setLoading(false); - return; - } - let PartnerResponse; - try { - PartnerResponse = await axios.post( - "http://localhost:1337/qb/", - // "http://609feaeae986.ngrok.io/qb/", - QbXmlResponse.data, - { - headers: { - Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, - }, - } - ); - } catch (error) { - console.log("Error connecting to quickbooks or partner.", error); - notification["error"]({ - message: t("jobs.errors.exporting-partner"), + setLoading(true); + if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) { + PartnerResponse = await axios.post(`/qbo/receivables`, { + withCredentials: true, + jobIds: jobIds, }); - setLoading(false); - return; - } + } else { + let QbXmlResponse; + try { + QbXmlResponse = await axios.post( + "/accounting/qbxml/receivables", + { jobIds: jobIds }, + { + headers: { + Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, + }, + } + ); + } catch (error) { + console.log("Error getting QBXML from Server.", error); + notification["error"]({ + message: t("jobs.errors.exporting", { + error: "Unable to retrieve QBXML. " + JSON.stringify(error.message), + }), + }); + setLoading(false); + return; + } + try { + PartnerResponse = await axios.post( + "http://localhost:1337/qb/", + // "http://609feaeae986.ngrok.io/qb/", + QbXmlResponse.data, + { + headers: { + Authorization: `Bearer ${await auth.currentUser.getIdToken()}`, + }, + } + ); + } catch (error) { + console.log("Error connecting to quickbooks or partner.", error); + notification["error"]({ + message: t("jobs.errors.exporting-partner"), + }); + setLoading(false); + return; + } + } console.log("PartnerResponse", PartnerResponse); - const groupedData = _.groupBy(PartnerResponse.data, "id"); + const groupedData = _.groupBy( + PartnerResponse.data, + bodyshop.accountingconfig.qbo ? "jobid" : "id" + ); await Promise.all( Object.keys(groupedData).map(async (key) => { @@ -157,6 +165,7 @@ export function JobsExportAllButton({ if (!!completedCallback) completedCallback([]); if (!!loadingCallback) loadingCallback(false); + setLoading(false); }; diff --git a/client/src/components/qbo-authorize/qbo-authorize.component.jsx b/client/src/components/qbo-authorize/qbo-authorize.component.jsx index 839466f70..682e5ebe0 100644 --- a/client/src/components/qbo-authorize/qbo-authorize.component.jsx +++ b/client/src/components/qbo-authorize/qbo-authorize.component.jsx @@ -1,19 +1,19 @@ -import { Button, Space } from "antd"; +import { Space, Tag } from "antd"; import Axios from "axios"; -import React, { useEffect } from "react"; -//import QboImg from "./qbo_signin.png"; import queryString from "query-string"; -import { useLocation } from "react-router-dom"; +import React, { useEffect } from "react"; import { useCookies } from "react-cookie"; +import { useHistory, useLocation } from "react-router-dom"; +import QboSignIn from "../../assets/qbo/C2QB_green_btn_med_default.svg"; +import "./qbo-authorize.scss"; export default function QboAuthorizeComponent() { const location = useLocation(); - - const [, setCookie] = useCookies(["access_token", "refresh_token"]); + const history = useHistory(); + const [cookies, setCookie] = useCookies(["access_token", "refresh_token"]); const handleQbSignIn = async () => { const result = await Axios.post("/qbo/authorize"); - console.log("pushing to history", result.data); window.location.href = result.data; }; const qs = queryString.parse(location.search); @@ -35,42 +35,24 @@ export default function QboAuthorizeComponent() { path: "/", expires, }); + + history.push({ pathname: `/manage/accounting/receivables` }); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [qs, location, setCookie]); return ( -
- - - - - + + Sign In to QuickBooks Online + {!cookies.qbo_realmId && ( + No QuickBooks company has been connected. + )} {error && JSON.parse(decodeURIComponent(error)).error_description} -
+ ); } diff --git a/client/src/components/qbo-authorize/qbo-authorize.scss b/client/src/components/qbo-authorize/qbo-authorize.scss new file mode 100644 index 000000000..b4016ae80 --- /dev/null +++ b/client/src/components/qbo-authorize/qbo-authorize.scss @@ -0,0 +1,8 @@ +.qbo-sign-in { + background-image: url("../../assets/qbo/C2QB_green_btn_med_default.svg"); + width: 274px; + height: 48px; + &:hover { + background-image: url("../../assets/qbo/C2QB_green_btn_med_hover.svg"); + } +} diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index 6aa952218..1ab1e13f5 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -121,6 +121,13 @@ export default function ShopInfoGeneral({ form }) { + + + { Authorization: BearerToken, }, }); - logger.log("qbo-payable-create", "DEBUG", req.user.email, jobIds); + logger.log("qbo-receivable-create", "DEBUG", req.user.email, jobIds); const result = await client .setHeaders({ Authorization: BearerToken }) .request(queries.QUERY_JOBS_FOR_RECEIVABLES_EXPORT, { - ids: ["966dc7f9-2acd-44dc-9df5-d07c5578070a"], - //jobIds + ids: jobIds, }); const { jobs, bodyshops } = result; - - const job = jobs[0]; const bodyshop = bodyshops[0]; - const isThreeTier = bodyshop.accountingconfig.tiers === 3; - const twoTierPref = bodyshop.accountingconfig.twotierpref; - //Replace this with a for-each loop to check every single Job that's included in the list. + const ret = []; + for (const job of jobs) { + //const job = jobs[0]; + try { + const isThreeTier = bodyshop.accountingconfig.tiers === 3; + const twoTierPref = bodyshop.accountingconfig.twotierpref; - let insCoCustomerTier, ownerCustomerTier, jobTier; - if (isThreeTier || twoTierPref === "source") { - //Insert the insurance company tier. - //Query for top level customer, the insurance company name. - insCoCustomerTier = await QueryInsuranceCo(oauthClient, req, job); - if (!insCoCustomerTier) { - //Creating the Insurance Customer. - insCoCustomerTier = await InsertInsuranceCo( - oauthClient, - req, - job, - bodyshop - ); + //Replace this with a for-each loop to check every single Job that's included in the list. + + let insCoCustomerTier, ownerCustomerTier, jobTier; + if (isThreeTier || twoTierPref === "source") { + //Insert the insurance company tier. + //Query for top level customer, the insurance company name. + insCoCustomerTier = await QueryInsuranceCo(oauthClient, req, job); + if (!insCoCustomerTier) { + //Creating the Insurance Customer. + insCoCustomerTier = await InsertInsuranceCo( + oauthClient, + req, + job, + bodyshop + ); + } + } + console.log(insCoCustomerTier); + if (isThreeTier || twoTierPref === "name") { + //Insert the name/owner and account for whether the source should be the ins co in 3 tier.. + ownerCustomerTier = await QueryOwner(oauthClient, req, job); + //Query for the owner itself. + if (!ownerCustomerTier) { + ownerCustomerTier = await InsertOwner( + oauthClient, + req, + job, + isThreeTier, + insCoCustomerTier + ); + } + } + console.log(ownerCustomerTier); + //Query for the Job or Create it. + jobTier = await QueryJob(oauthClient, req, job); + + // Need to validate that the job tier is associated to the right individual? + + if (!jobTier) { + jobTier = await InsertJob( + oauthClient, + req, + job, + isThreeTier, + ownerCustomerTier + ); + } + console.log(jobTier); + await InsertInvoice(oauthClient, req, job, bodyshop, jobTier); + ret.push({ jobid: job.id, success: true }); + } catch (error) { + ret.push({ + jobid: job.id, + success: false, + errorMessage: error.message, + }); } } - if (isThreeTier || twoTierPref === "name") { - //Insert the name/owner and account for whether the source should be the ins co in 3 tier.. - ownerCustomerTier = await QueryOwner(oauthClient, req, job); - //Query for the owner itself. - if (!ownerCustomerTier) { - ownerCustomerTier = await InsertOwner( - oauthClient, - req, - job, - isThreeTier, - insCoCustomerTier - ); - } - } - //Query for the Job or Create it. - jobTier = await QueryJob(oauthClient, req, job); - - // Need to validate that the job tier is associated to the right individual? - - if (!jobTier) { - jobTier = await InsertJob( - oauthClient, - req, - job, - isThreeTier, - ownerCustomerTier - ); - } - await InsertInvoice(oauthClient, req, job, bodyshop, jobTier); - res.sendStatus(200); + res.status(200).json(ret); } catch (error) { console.log(error); - logger.log("qbo-payable-create-error", "ERROR", req.user.email, { error }); + logger.log("qbo-receivable-create-error", "ERROR", req.user.email, { + error, + }); res.status(400).json(error); } }; @@ -168,7 +182,7 @@ async function InsertInsuranceCo(oauthClient, req, job, bodyshop) { body: JSON.stringify(Customer), }); setNewRefreshToken(req.user.email, result); - return result && result.Customer; + return result && result.json.Customer; } catch (error) { logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, { error, @@ -230,7 +244,7 @@ async function InsertOwner(oauthClient, req, job, isThreeTier, parentTierRef) { body: JSON.stringify(Customer), }); setNewRefreshToken(req.user.email, result); - return result && result.Customer; + return result && result.json.Customer; } catch (error) { logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, { error, @@ -290,7 +304,7 @@ async function InsertJob(oauthClient, req, job, isThreeTier, parentTierRef) { body: JSON.stringify(Customer), }); setNewRefreshToken(req.user.email, result); - return result && result.Customer; + return result && result.json.Customer; } catch (error) { logger.log("qbo-receivables-error", "DEBUG", req.user.email, job.id, { error, @@ -319,14 +333,6 @@ async function QueryMetaData(oauthClient, req) { const taxCodeMapping = {}; - const accounts = await oauthClient.makeApiCall({ - url: urlBuilder(req.cookies.qbo_realmId, "query", `select * From Account`), - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); - taxCodes.json && taxCodes.json.QueryResponse && taxCodes.json.QueryResponse.TaxCode.forEach((t) => {