From 224029a1e87a20e5966009733eb7784465c7a73d Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Thu, 9 Apr 2020 16:30:18 -0700 Subject: [PATCH] Added invoice and invoice detail pages. Begin work on paginated invoices page. --- bodyshop_translations.babel | 42 ++++ .../components/header/header.component.jsx | 25 ++- .../job-totals-table/job-totals.utility.js | 24 +-- client/src/graphql/invoices.queries.js | 26 +++ .../invoice-detail.page.component.jsx | 5 + .../invoice-detail.page.container.jsx | 13 ++ .../invoices/invoices.page.component.jsx | 188 ++++++++++++++++++ .../invoices/invoices.page.container.jsx | 57 ++++++ .../pages/manage/manage.page.component.jsx | 19 ++ client/src/translations/en_us/common.json | 2 + client/src/translations/es/common.json | 2 + client/src/translations/fr/common.json | 2 + 12 files changed, 391 insertions(+), 14 deletions(-) create mode 100644 client/src/pages/invoice-detail/invoice-detail.page.component.jsx create mode 100644 client/src/pages/invoice-detail/invoice-detail.page.container.jsx create mode 100644 client/src/pages/invoices/invoices.page.component.jsx create mode 100644 client/src/pages/invoices/invoices.page.container.jsx diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index a803a43d4..1b11839b1 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -8949,6 +8949,27 @@ header + + accounting + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + activejobs false @@ -9117,6 +9138,27 @@ + + invoices + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobs false diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index b00ea95ea..bf8bca271 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -1,4 +1,12 @@ -import Icon, { CarFilled, FileAddFilled, FileFilled, GlobalOutlined, HomeFilled, TeamOutlined } from "@ant-design/icons"; +import Icon, { + CarFilled, + FileAddFilled, + FileFilled, + GlobalOutlined, + HomeFilled, + TeamOutlined, + DollarCircleFilled, +} from "@ant-design/icons"; import { Avatar, Col, Menu, Row } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; @@ -13,7 +21,7 @@ export default ({ logo, handleMenuClick, currentUser, - signOutStart + signOutStart, }) => { const { t } = useTranslation(); //TODO Add @@ -159,6 +167,19 @@ export default ({ + + + {t("menus.header.accounting")} + + } + > + + {t("menus.header.invoices")} + + + {t("menus.header.shop_config")} diff --git a/client/src/components/job-totals-table/job-totals.utility.js b/client/src/components/job-totals-table/job-totals.utility.js index dd08f32ca..a3c327997 100644 --- a/client/src/components/job-totals-table/job-totals.utility.js +++ b/client/src/components/job-totals-table/job-totals.utility.js @@ -33,18 +33,18 @@ function CalculateTaxesTotals(job, otherTotals) { } }, 0); - console.log("otherTotals", otherTotals); - console.log("job", job); - console.log("parts pst", statePartsTax); - console.log( - "pst on labor", - otherTotals.rates.rates_subtotal * (job.tax_lbr_rt || 0) - ); - console.log( - "pst on mat", - (otherTotals.rates.paint_mat.total + otherTotals.rates.shop_mat.total) * - (job.tax_paint_mat_rt || 0) - ); + // console.log("otherTotals", otherTotals); + // console.log("job", job); + // console.log("parts pst", statePartsTax); + // console.log( + // "pst on labor", + // otherTotals.rates.rates_subtotal * (job.tax_lbr_rt || 0) + // ); + // console.log( + // "pst on mat", + // (otherTotals.rates.paint_mat.total + otherTotals.rates.shop_mat.total) * + // (job.tax_paint_mat_rt || 0) + // ); let ret = { subtotal: subtotal, diff --git a/client/src/graphql/invoices.queries.js b/client/src/graphql/invoices.queries.js index c33c01679..efb7d7bf6 100644 --- a/client/src/graphql/invoices.queries.js +++ b/client/src/graphql/invoices.queries.js @@ -10,6 +10,32 @@ export const INSERT_NEW_INVOICE = gql` } `; +export const QUERY_ALL_INVOICES_PAGINATED = gql` + query QUERY_ALL_INVOICES_PAGINATED($offset: Int, $limit: Int) { + invoices(offset: $offset, limit: $limit, order_by: { date: desc }) { + id + vendor { + id + name + } + total + invoice_number + date + job { + id + ro_number + } + invoicelines { + actual_price + actual_cost + cost_center + id + line_desc + } + } + } +`; + export const QUERY_INVOICES_BY_JOBID = gql` query QUERY_INVOICES_BY_JOBID($jobid: uuid!) { invoices(where: { jobid: { _eq: $jobid } }, order_by: { date: desc }) { diff --git a/client/src/pages/invoice-detail/invoice-detail.page.component.jsx b/client/src/pages/invoice-detail/invoice-detail.page.component.jsx new file mode 100644 index 000000000..6a3885aba --- /dev/null +++ b/client/src/pages/invoice-detail/invoice-detail.page.component.jsx @@ -0,0 +1,5 @@ +import React from "react"; + +export default function InvoiceDetailPageComponent() { + return
Invoice Detail Page Component
; +} diff --git a/client/src/pages/invoice-detail/invoice-detail.page.container.jsx b/client/src/pages/invoice-detail/invoice-detail.page.container.jsx new file mode 100644 index 000000000..1b5a53ed3 --- /dev/null +++ b/client/src/pages/invoice-detail/invoice-detail.page.container.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { useParams } from "react-router-dom"; +import InvoiceDetailPageComponent from "./invoice-detail.page.component"; + +export default function InvoiceDetailPageContainer() { + const { invoiceId } = useParams(); + + return ( +
+ +
+ ); +} diff --git a/client/src/pages/invoices/invoices.page.component.jsx b/client/src/pages/invoices/invoices.page.component.jsx new file mode 100644 index 000000000..2a0ebad5b --- /dev/null +++ b/client/src/pages/invoices/invoices.page.component.jsx @@ -0,0 +1,188 @@ +import { Button, Descriptions, 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 { DateFormatter } from "../../utils/DateFormatter"; +import { alphaSort } from "../../utils/sorters"; + +export default function InvoicesPageComponent({ + loading, + invoices, + selectedInvoice, + handleFetchMore, + handleOnRowClick, +}) { + const { t } = useTranslation(); + + const [state, setState] = useState({ + sortedInfo: {}, + }); + + const columns = [ + { + title: t("invoices.fields.vendorname"), + dataIndex: "vendorname", + key: "vendorname", + sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name), + sortOrder: + state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order, + render: (text, record) => {record.vendor.name}, + }, + { + title: t("invoices.fields.invoice_number"), + dataIndex: "invoice_number", + key: "invoice_number", + sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), + sortOrder: + state.sortedInfo.columnKey === "invoice_number" && + state.sortedInfo.order, + }, + { + title: t("invoices.fields.date"), + dataIndex: "date", + key: "date", + + sorter: (a, b) => a.date - b.date, + sortOrder: + state.sortedInfo.columnKey === "date" && state.sortedInfo.order, + render: (text, record) => {record.date}, + }, + { + title: t("invoices.fields.total"), + dataIndex: "total", + key: "total", + + sorter: (a, b) => a.total - b.total, + sortOrder: + state.sortedInfo.columnKey === "total" && state.sortedInfo.order, + render: (text, record) => ( + {record.total} + ), + }, + { + title: t("general.labels.actions"), + dataIndex: "actions", + key: "actions", + render: (text, record) => ( + + + + ), + }, + ]; + + const handleTableChange = (pagination, filters, sorter) => { + setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); + }; + + const rowExpander = (record) => { + const columns = [ + { + title: t("invoicelines.fields.line_desc"), + dataIndex: "line_desc", + key: "line_desc", + sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), + sortOrder: + state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order, + }, + { + title: t("invoicelines.fields.retail"), + dataIndex: "actual_price", + key: "actual_price", + sorter: (a, b) => a.actual_price - b.actual_price, + sortOrder: + state.sortedInfo.columnKey === "actual_price" && + state.sortedInfo.order, + render: (text, record) => ( + {record.actual_price} + ), + }, + { + title: t("invoicelines.fields.actual_cost"), + dataIndex: "actual_cost", + key: "actual_cost", + sorter: (a, b) => a.actual_cost - b.actual_cost, + sortOrder: + state.sortedInfo.columnKey === "actual_cost" && + state.sortedInfo.order, + render: (text, record) => ( + {record.actual_cost} + ), + }, + { + title: t("invoicelines.fields.cost_center"), + dataIndex: "cost_center", + key: "cost_center", + sorter: (a, b) => alphaSort(a.cost_center, b.cost_center), + sortOrder: + state.sortedInfo.columnKey === "cost_center" && + state.sortedInfo.order, + }, + ]; + + return ( +
+ + Zhou Maomao + 1810000000 + Hangzhou, Zhejiang + empty + + No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China + + + ({ ...item }))} + rowKey="id" + dataSource={record.invoicelines} + /> + + ); + }; + + return ( +
{ + handleOnRowClick(page * pageSize); + }, + }} + columns={columns.map((item) => ({ ...item }))} + rowKey="id" + dataSource={invoices} + onChange={handleTableChange} + expandable={{ + expandedRowKeys: [selectedInvoice], + onExpand: (expanded, record) => { + handleOnRowClick(expanded ? record : null); + }, + }} + rowSelection={{ + onSelect: (record) => { + handleOnRowClick(record); + }, + selectedRowKeys: [selectedInvoice], + type: "radio", + }} + onRow={(record, rowIndex) => { + return { + onClick: (event) => { + handleOnRowClick(record); + }, // click row + onDoubleClick: (event) => {}, // double click row + onContextMenu: (event) => {}, // right button click row + onMouseEnter: (event) => {}, // mouse enter row + onMouseLeave: (event) => {}, // mouse leave row + }; + }} + /> + ); +} diff --git a/client/src/pages/invoices/invoices.page.container.jsx b/client/src/pages/invoices/invoices.page.container.jsx new file mode 100644 index 000000000..78a2e4ee3 --- /dev/null +++ b/client/src/pages/invoices/invoices.page.container.jsx @@ -0,0 +1,57 @@ +import { useQuery } from "@apollo/react-hooks"; +import React from "react"; +import { QUERY_ALL_INVOICES_PAGINATED } from "../../graphql/invoices.queries"; +import InvoicesPageComponent from "./invoices.page.component"; +import AlertComponent from "../../components/alert/alert.component"; +import queryString from "query-string"; +import { useHistory, useLocation } from "react-router-dom"; + +export default function InvoicesPageContainer() { + const { loading, error, data, fetchMore } = useQuery( + QUERY_ALL_INVOICES_PAGINATED, + { + variables: { offset: 0, limit: 1 }, + } + ); + const search = queryString.parse(useLocation().search); + const history = useHistory(); + + const handleOnRowClick = (record) => { + if (record) { + if (record.id) { + search.invoiceid = record.id; + history.push({ search: queryString.stringify(search) }); + } + } else { + delete search.invoiceid; + history.push({ search: queryString.stringify(search) }); + } + }; + + const handleFetchMore = (offset) => { + fetchMore({ + variables: { + offset: offset, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return Object.assign({}, prev, { + invoices: [...prev.invoices, ...fetchMoreResult.invoices], + }); + }, + }); + }; + + if (error) return ; + return ( +
+ +
+ ); +} diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx index fed64173c..dc34419a4 100644 --- a/client/src/pages/manage/manage.page.component.jsx +++ b/client/src/pages/manage/manage.page.component.jsx @@ -64,6 +64,13 @@ const ContractDetailPage = lazy(() => const ContractsList = lazy(() => import("../contracts/contracts.page.container") ); +const InvoicesListPage = lazy(() => + import("../invoices/invoices.page.container") +); +const InvoiceDetailPage = lazy(() => + import("../invoice-detail/invoice-detail.page.container") +); + const { Header, Content, Footer } = Layout; export default function Manage({ match }) { @@ -154,6 +161,18 @@ export default function Manage({ match }) { path={`${match.path}/vehicles/:vehId`} component={VehiclesDetailContainer} /> + + + +