From a70933f03c4989b78d3e805803e920980b9b3448 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Tue, 2 Jun 2020 11:43:08 -0700 Subject: [PATCH] Added error handling for receivables screen BOD-138 --- bodyshop_translations.babel | 126 +++++++++++ ...accounting-receivables-table.component.jsx | 197 ++++++++++++++++++ .../components/header/header.component.jsx | 142 +++++++------ .../jobs-close-export-button.component.jsx | 101 +++++++-- .../jobs-close-save-button.component.jsx | 13 -- client/src/graphql/accounting.queries.js | 29 +++ client/src/graphql/jobs.queries.js | 6 +- .../accounting-receivables.container.jsx | 51 +++++ .../pages/manage/manage.page.component.jsx | 16 +- client/src/translations/en_us/common.json | 6 + client/src/translations/es/common.json | 6 + client/src/translations/fr/common.json | 6 + 12 files changed, 596 insertions(+), 103 deletions(-) create mode 100644 client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx create mode 100644 client/src/graphql/accounting.queries.js create mode 100644 client/src/pages/accounting-receivables/accounting-receivables.container.jsx diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index dea70f85a..d5d1f8cd0 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -7610,6 +7610,48 @@ + + exporting + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + exporting-partner + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + invoicing false @@ -11604,6 +11646,27 @@ + + exported + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + invoiced false @@ -11787,6 +11850,27 @@ + + accounting-receivables + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + activejobs false @@ -14834,6 +14918,27 @@ titles + + accounting-receivables + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + app false @@ -14858,6 +14963,27 @@ bc + + accounting-receivables + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + availablejobs false 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 new file mode 100644 index 000000000..6af18698b --- /dev/null +++ b/client/src/components/accounting-receivables-table/accounting-receivables-table.component.jsx @@ -0,0 +1,197 @@ +import { Input, Table } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import { alphaSort } from "../../utils/sorters"; +import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component"; + +export default function AccountingReceivablesTableComponent({ loading, jobs }) { + const { t } = useTranslation(); + + const [state, setState] = useState({ + sortedInfo: {}, + search: "", + }); + + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; + + const columns = [ + { + title: t("jobs.fields.ro_number"), + dataIndex: "ro_number", + key: "ro_number", + sorter: (a, b) => alphaSort(a.ro_number, b.ro_number), + sortOrder: + state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order, + render: (text, record) => ( + {record.ro_number} + ), + }, + { + title: t("jobs.fields.est_number"), + dataIndex: "est_number", + key: "est_number", + sorter: (a, b) => a.est_number - b.est_number, + sortOrder: + state.sortedInfo.columnKey === "est_number" && state.sortedInfo.order, + render: (text, record) => ( + {record.est_number} + ), + }, + { + title: t("jobs.fields.status"), + dataIndex: "status", + key: "status", + sorter: (a, b) => a.status - b.status, + sortOrder: + state.sortedInfo.columnKey === "status" && state.sortedInfo.order, + }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln), + sortOrder: + state.sortedInfo.columnKey === "owner" && state.sortedInfo.order, + render: (text, record) => { + return record.owner ? ( + + {`${record.ownr_fn || ""} ${record.ownr_ln || ""}`} + + ) : ( + {`${record.ownr_fn || ""} ${record.ownr_ln || ""}`} + ); + }, + }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + render: (text, record) => { + return record.vehicleid ? ( + + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + + ) : ( + {`${record.v_model_yr || ""} ${record.v_make_desc || ""} ${ + record.v_model_desc || "" + }`} + ); + }, + }, + { + title: t("jobs.fields.clm_no"), + dataIndex: "clm_no", + key: "clm_no", + ellipsis: true, + sorter: (a, b) => alphaSort(a.clm_no, b.clm_no), + sortOrder: + state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order, + render: (text, record) => { + return record.clm_no ? ( + {record.clm_no} + ) : ( + t("general.labels.unknown") + ); + }, + }, + { + title: t("jobs.fields.clm_total"), + dataIndex: "clm_total", + key: "clm_total", + sorter: (a, b) => a.clm_total - b.clm_total, + sortOrder: + state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order, + render: (text, record) => { + return record.clm_total ? ( + {record.clm_total} + ) : ( + t("general.labels.unknown") + ); + }, + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + sorter: (a, b) => a.clm_total - b.clm_total, + + render: (text, record) => ( +
+ + + {JSON.stringify(record.date_exported)} +
+ ), + }, + ]; + + const handleSearch = (e) => { + setState({ ...state, search: e.target.value }); + }; + + const dataSource = state.search + ? jobs.filter( + (v) => + (v.ro_number || "") + .toString() + .toLowerCase() + .includes(state.search.toLowerCase()) || + (v.est_number || "") + .toString() + .toLowerCase() + .includes(state.search.toLowerCase()) || + (v.ownr_fn || "") + .toLowerCase() + .includes(state.search.toLowerCase()) || + (v.ownr_ln || "") + .toLowerCase() + .includes(state.search.toLowerCase()) || + (v.ownr_co_nm || "") + .toLowerCase() + .includes(state.search.toLowerCase()) || + (v.v_model_desc || "") + .toLowerCase() + .includes(state.search.toLowerCase()) || + (v.v_make_desc || "") + .toLowerCase() + .includes(state.search.toLowerCase()) || + (v.clm_no || "").toLowerCase().includes(state.search.toLowerCase()) + ) + : jobs; + + return ( +
+ { + return ( +
+ +
+ ); + }} + dataSource={dataSource} + size="small" + pagination={{ position: "top" }} + columns={columns} + rowKey="id" + onChange={handleTableChange} + /> + + ); +} diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index 68a7dca9e..85ae5c631 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -42,28 +42,29 @@ function Header({ //TODO Add return ( - + {logo ? ( - Shop Logo + Shop Logo ) : null} {landingHeader ? ( + mode="horizontal" + onClick={handleMenuClick} + > {currentUser.displayName || t("general.labels.unknown")} - }> + } + > signOutStart()}> {t("user.actions.signout")} - + {t("menus.currentuser.profile")} @@ -86,14 +88,15 @@ function Header({ {t("menus.currentuser.languageselector")} - }> - + } + > + {t("general.languages.english")} - + {t("general.languages.french")} - + {t("general.languages.spanish")} @@ -101,13 +104,14 @@ function Header({ ) : ( - - + mode="horizontal" + onClick={handleMenuClick} + > + + {t("menus.header.home")} @@ -118,40 +122,41 @@ function Header({ {t("menus.header.jobs")} - }> - - + } + > + + {t("menus.header.schedule")} - - + + {t("menus.header.productionlist")} - - {t("menus.header.activejobs")} + + {t("menus.header.activejobs")} - - {t("menus.header.alljobs")} + + {t("menus.header.alljobs")} - - + + {t("menus.header.availablejobs")} - - + + {t("menus.header.owners")} - - + + {t("menus.header.vehicles")} @@ -164,21 +169,22 @@ function Header({ {t("menus.header.courtesycars")} - }> - - + } + > + + {t("menus.header.courtesycars-all")} - - + + {t("menus.header.courtesycars-contracts")} - - + + {t("menus.header.courtesycars-newcontract")} @@ -191,43 +197,51 @@ function Header({ {t("menus.header.accounting")} - }> + } + > { setInvoiceEnterContext({ actions: {}, context: {}, }); - }}> + }} + > {t("menus.header.enterinvoices")} - - {t("menus.header.invoices")} + + {t("menus.header.invoices")} { setTimeTicketContext({ actions: {}, context: {}, }); - }}> + }} + > {t("menus.header.entertimeticket")} + + + {t("menus.header.accounting-receivables")} + + - - {t("menus.header.shop_config")} + + {t("menus.header.shop_config")} - - + + {t("menus.header.shop_templates")} - - + + {t("menus.header.shop_vendors")} @@ -237,8 +251,8 @@ function Header({ title={
{currentUser.displayName || t("general.labels.unknown")}
- }> + } + > signOutStart()}> {t("user.actions.signout")} - + {t("menus.currentuser.profile")} @@ -261,14 +276,15 @@ function Header({ {t("menus.currentuser.languageselector")} - }> - + } + > + {t("general.languages.english")} - + {t("general.languages.french")} - + {t("general.languages.spanish")}
diff --git a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx index b9e423963..e63d4a318 100644 --- a/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx +++ b/client/src/components/jobs-close-export-button/jobs-close-export-button.component.jsx @@ -1,43 +1,102 @@ -import { Button } from "antd"; +import { useMutation } from "@apollo/react-hooks"; +import { Button, notification } from "antd"; import axios from "axios"; -import React from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; import { auth } from "../../firebase/firebase.utils"; +import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); -export default function JobsCloseExportButton({ jobId, disabled }) { +export function JobsCloseExportButton({ bodyshop, jobId, disabled }) { const { t } = useTranslation(); - + const [updateJob] = useMutation(UPDATE_JOB); + const [loading, setLoading] = useState(false); const handleQbxml = async () => { - const response = await axios.post( - "/accounting/qbxml/receivables", - { jobId: jobId }, - { - headers: { - Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`, - }, - } - ); - console.log("handle -> XML", response); - + setLoading(true); + let QbXmlResponse; try { - const response2 = await axios.post( - "http://e9c5a8ed9079.ngrok.io/qb/receivables", - response.data, + QbXmlResponse = await axios.post( + "/accounting/qbxml/receivables", + { jobId: jobId }, { headers: { Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`, }, } ); - console.log("handle -> result", response2); + console.log("handle -> XML", QbXmlResponse); } catch (error) { - console.log("error", error, JSON.stringify(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://e9c5a8ed9079.ngrok.io/qb/receivables", + QbXmlResponse.data, + { + headers: { + Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`, + }, + } + ); + } 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 jobUpdateResponse = await updateJob({ + variables: { + jobId: jobId, + job: { + status: bodyshop.md_ro_statuses.default_exported || "Exported*", + date_exported: new Date(), + }, + }, + }); + + if (!!!jobUpdateResponse.errors) { + notification["success"]({ + message: t("jobs.successes.exported"), + }); + } else { + notification["error"]({ + message: t("jobs.errors.exporting", { + error: JSON.stringify(jobUpdateResponse.error), + }), + }); + } + + setLoading(false); }; return ( - ); } + +export default connect(mapStateToProps, null)(JobsCloseExportButton); diff --git a/client/src/components/jobs-close-save-button/jobs-close-save-button.component.jsx b/client/src/components/jobs-close-save-button/jobs-close-save-button.component.jsx index 73c00cba4..9b8895163 100644 --- a/client/src/components/jobs-close-save-button/jobs-close-save-button.component.jsx +++ b/client/src/components/jobs-close-save-button/jobs-close-save-button.component.jsx @@ -40,19 +40,6 @@ export function JobsCloseSaveButton({ }, }, }, - optimisticResponse: { - update_jobs: { - returning: { - id: jobId, - date_invoiced: new Date(), - status: bodyshop.md_ro_statuses.default_invoiced || "Invoiced*", - invoice_allocation: { - labMatAllocations, - partsAllocations, - }, - }, - }, - }, }); if (!!!result.errors) { notification["success"]({ message: t("jobs.successes.invoiced") }); diff --git a/client/src/graphql/accounting.queries.js b/client/src/graphql/accounting.queries.js new file mode 100644 index 000000000..61b210363 --- /dev/null +++ b/client/src/graphql/accounting.queries.js @@ -0,0 +1,29 @@ +import gql from "graphql-tag"; + +export const QUERY_JOBS_FOR_EXPORT = gql` + query QUERY_JOBS_FOR_EXPORT($invoicedStatus: String!) { + jobs( + where: { + date_exported: { _is_null: true } + status: { _eq: $invoicedStatus } + } + ) { + id + ro_number + ownr_fn + ownr_ln + ownr_co_nm + date_invoiced + date_exported + status + v_model_desc + v_make_desc + v_model_yr + v_color + est_number + clm_total + clm_no + ins_co_nm + } + } +`; diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index ce51fa17e..d3a24ff53 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -366,8 +366,8 @@ export const UPDATE_JOB = gql` update_jobs(where: { id: { _eq: $jobId } }, _set: $job) { returning { id - est_ph1 - est_ea + date_exported + status } } } @@ -824,7 +824,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql` rate_mapa rate_mash rate_matd - status + status owner_owing joblines { id diff --git a/client/src/pages/accounting-receivables/accounting-receivables.container.jsx b/client/src/pages/accounting-receivables/accounting-receivables.container.jsx new file mode 100644 index 000000000..e3e4bde14 --- /dev/null +++ b/client/src/pages/accounting-receivables/accounting-receivables.container.jsx @@ -0,0 +1,51 @@ +import { useQuery } from "@apollo/react-hooks"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import AccountingReceivablesTable from "../../components/accounting-receivables-table/accounting-receivables-table.component"; +import AlertComponent from "../../components/alert/alert.component"; +import { QUERY_JOBS_FOR_EXPORT } from "../../graphql/accounting.queries"; +import { setBreadcrumbs } from "../../redux/application/application.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); + +const mapDispatchToProps = (dispatch) => ({ + setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)), +}); +export function AccountingReceivablesContainer({ bodyshop, setBreadcrumbs }) { + const { t } = useTranslation(); + + useEffect(() => { + document.title = t("titles.accounting-receivables"); + setBreadcrumbs([ + { + link: "/manage/accounting/receivables", + label: t("titles.bc.accounting-receivables"), + }, + ]); + }, [t, setBreadcrumbs]); + + const { loading, error, data } = useQuery(QUERY_JOBS_FOR_EXPORT, { + variables: { + invoicedStatus: bodyshop.md_ro_statuses.default_invoiced || "Invoiced*", + }, + }); + + if (error) return ; + return ( +
+ +
+ ); +} +export default connect( + mapStateToProps, + mapDispatchToProps +)(AccountingReceivablesContainer); diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index a357ee254..e123393d6 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -91,6 +91,9 @@ const ShopTemplates = lazy(() => const JobIntake = lazy(() => import("../jobs-intake/jobs-intake.page.container") ); +const AccountingReceivables = lazy(() => + import("../accounting-receivables/accounting-receivables.container") +); const AllJobs = lazy(() => import("../jobs-all/jobs-all.container")); const JobsClose = lazy(() => import("../jobs-close/jobs-close.container")); @@ -118,8 +121,9 @@ export function Manage({ match, conflict }) { + className="content-container" + style={{ padding: "0em 4em 4em" }} + > {conflict ? ( @@ -128,7 +132,8 @@ export function Manage({ match, conflict }) { - }> + } + > @@ -267,6 +272,11 @@ export function Manage({ match, conflict }) { path={`${match.path}/shop/vendors`} component={ShopVendorPageContainer} /> + )} diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index ccd2310cb..041eef326 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -513,6 +513,8 @@ "addingtoproduction": "Error adding to production. {{error}}", "creating": "Error encountered while creating job. {{error}}", "deleted": "Error deleting job.", + "exporting": "Error exporting job. {{error}}", + "exporting-partner": "Unable to connect to ImEX Partner. Please ensure it is running and logged in.", "invoicing": "Error invoicing job. {{error}}", "noaccess": "This job does not exist or you do not have access to it.", "nodamage": "No damage points on estimate.", @@ -712,6 +714,7 @@ "created_subtitle": "Estimate Number {{est_number}} has been created.", "creatednoclick": "Job created successfully. ", "deleted": "Job deleted successfully.", + "exported": "Job exported successfully. ", "invoiced": "Job closed and invoiced successfully.", "save": "Job saved successfully.", "savetitle": "Record saved successfully.", @@ -726,6 +729,7 @@ }, "header": { "accounting": "Accounting", + "accounting-receivables": "Receivables", "activejobs": "Active Jobs", "alljobs": "All Jobs", "availablejobs": "Available Jobs", @@ -946,8 +950,10 @@ } }, "titles": { + "accounting-receivables": "Receivables | $t(titles.app)", "app": "ImEX Online", "bc": { + "accounting-receivables": "Receivables", "availablejobs": "Available Jobs", "contracts": "Contracts", "contracts-create": "New Contract", diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index e8a79db05..f1a38e61f 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -513,6 +513,8 @@ "addingtoproduction": "", "creating": "", "deleted": "Error al eliminar el trabajo.", + "exporting": "", + "exporting-partner": "", "invoicing": "", "noaccess": "Este trabajo no existe o no tiene acceso a él.", "nodamage": "", @@ -712,6 +714,7 @@ "created_subtitle": "", "creatednoclick": "", "deleted": "Trabajo eliminado con éxito.", + "exported": "", "invoiced": "", "save": "Trabajo guardado con éxito.", "savetitle": "Registro guardado con éxito.", @@ -726,6 +729,7 @@ }, "header": { "accounting": "", + "accounting-receivables": "", "activejobs": "Empleos activos", "alljobs": "", "availablejobs": "Trabajos disponibles", @@ -946,8 +950,10 @@ } }, "titles": { + "accounting-receivables": "", "app": "ImEX Online", "bc": { + "accounting-receivables": "", "availablejobs": "", "contracts": "", "contracts-create": "", diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 2ae245920..609bd4282 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -513,6 +513,8 @@ "addingtoproduction": "", "creating": "", "deleted": "Erreur lors de la suppression du travail.", + "exporting": "", + "exporting-partner": "", "invoicing": "", "noaccess": "Ce travail n'existe pas ou vous n'y avez pas accès.", "nodamage": "", @@ -712,6 +714,7 @@ "created_subtitle": "", "creatednoclick": "", "deleted": "Le travail a bien été supprimé.", + "exported": "", "invoiced": "", "save": "Le travail a été enregistré avec succès.", "savetitle": "Enregistrement enregistré avec succès.", @@ -726,6 +729,7 @@ }, "header": { "accounting": "", + "accounting-receivables": "", "activejobs": "Emplois actifs", "alljobs": "", "availablejobs": "Emplois disponibles", @@ -946,8 +950,10 @@ } }, "titles": { + "accounting-receivables": "", "app": "ImEX Online", "bc": { + "accounting-receivables": "", "availablejobs": "", "contracts": "", "contracts-create": "",