Added invoice and invoice detail pages. Begin work on paginated invoices page.
This commit is contained in:
@@ -8949,6 +8949,27 @@
|
||||
<folder_node>
|
||||
<name>header</name>
|
||||
<children>
|
||||
<concept_node>
|
||||
<name>accounting</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>activejobs</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
@@ -9117,6 +9138,27 @@
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>invoices</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
<description></description>
|
||||
<comment></comment>
|
||||
<default_text></default_text>
|
||||
<translations>
|
||||
<translation>
|
||||
<language>en-US</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>es-MX</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
<translation>
|
||||
<language>fr-CA</language>
|
||||
<approved>false</approved>
|
||||
</translation>
|
||||
</translations>
|
||||
</concept_node>
|
||||
<concept_node>
|
||||
<name>jobs</name>
|
||||
<definition_loaded>false</definition_loaded>
|
||||
|
||||
@@ -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 ({
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
|
||||
<Menu.SubMenu
|
||||
title={
|
||||
<span>
|
||||
<DollarCircleFilled />
|
||||
<span>{t("menus.header.accounting")}</span>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<Menu.Item key="invoices">
|
||||
<Link to="/manage/invoices">{t("menus.header.invoices")}</Link>
|
||||
</Menu.Item>
|
||||
</Menu.SubMenu>
|
||||
|
||||
<Menu.SubMenu title={t("menus.header.shop")}>
|
||||
<Menu.Item key="shop">
|
||||
<Link to="/manage/shop">{t("menus.header.shop_config")}</Link>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
export default function InvoiceDetailPageComponent() {
|
||||
return <div>Invoice Detail Page Component</div>;
|
||||
}
|
||||
@@ -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 (
|
||||
<div>
|
||||
<InvoiceDetailPageComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
188
client/src/pages/invoices/invoices.page.component.jsx
Normal file
188
client/src/pages/invoices/invoices.page.component.jsx
Normal file
@@ -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) => <span>{record.vendor.name}</span>,
|
||||
},
|
||||
{
|
||||
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) => <DateFormatter>{record.date}</DateFormatter>,
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<CurrencyFormatter>{record.total}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Link to={`/manage/invoices/${record.id}`}>
|
||||
<Button>{t("invoices.actions.edit")}</Button>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
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) => (
|
||||
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
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) => (
|
||||
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<div>
|
||||
<Descriptions title="User Info">
|
||||
<Descriptions.Item label="UserName">Zhou Maomao</Descriptions.Item>
|
||||
<Descriptions.Item label="Telephone">1810000000</Descriptions.Item>
|
||||
<Descriptions.Item label="Live">Hangzhou, Zhejiang</Descriptions.Item>
|
||||
<Descriptions.Item label="Remark">empty</Descriptions.Item>
|
||||
<Descriptions.Item label="Address">
|
||||
No. 18, Wantang Road, Xihu District, Hangzhou, Zhejiang, China
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Table
|
||||
size="small"
|
||||
pagination={{ position: "top", defaultPageSize: 25 }}
|
||||
columns={columns.map((item) => ({ ...item }))}
|
||||
rowKey="id"
|
||||
dataSource={record.invoicelines}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
loading={loading}
|
||||
size="small"
|
||||
expandedRowRender={rowExpander}
|
||||
pagination={{
|
||||
position: "top",
|
||||
defaultPageSize: 1,
|
||||
onChange: (page, pageSize) => {
|
||||
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
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
57
client/src/pages/invoices/invoices.page.container.jsx
Normal file
57
client/src/pages/invoices/invoices.page.container.jsx
Normal file
@@ -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 <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<div>
|
||||
<InvoicesPageComponent
|
||||
loading={loading}
|
||||
invoices={data ? data.invoices : null}
|
||||
selectedInvoice={search.invoiceid}
|
||||
handleFetchMore={handleFetchMore}
|
||||
handleOnRowClick={handleOnRowClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/invoices`}
|
||||
component={InvoicesListPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/invoices/:invoiceId`}
|
||||
component={InvoiceDetailPage}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/owners`}
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"profile": "Profile"
|
||||
},
|
||||
"header": {
|
||||
"accounting": "Accounting",
|
||||
"activejobs": "Active Jobs",
|
||||
"availablejobs": "Available Jobs",
|
||||
"courtesycars": "Courtesy Cars",
|
||||
@@ -583,6 +584,7 @@
|
||||
"courtesycars-newcontract": "New Contract",
|
||||
"customers": "Customers",
|
||||
"home": "Home",
|
||||
"invoices": "Invoices",
|
||||
"jobs": "Jobs",
|
||||
"owners": "Owners",
|
||||
"schedule": "Schedule",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"profile": "Perfil"
|
||||
},
|
||||
"header": {
|
||||
"accounting": "",
|
||||
"activejobs": "Empleos activos",
|
||||
"availablejobs": "Trabajos disponibles",
|
||||
"courtesycars": "",
|
||||
@@ -583,6 +584,7 @@
|
||||
"courtesycars-newcontract": "",
|
||||
"customers": "Clientes",
|
||||
"home": "Casa",
|
||||
"invoices": "",
|
||||
"jobs": "Trabajos",
|
||||
"owners": "propietarios",
|
||||
"schedule": "Programar",
|
||||
|
||||
@@ -575,6 +575,7 @@
|
||||
"profile": "Profil"
|
||||
},
|
||||
"header": {
|
||||
"accounting": "",
|
||||
"activejobs": "Emplois actifs",
|
||||
"availablejobs": "Emplois disponibles",
|
||||
"courtesycars": "",
|
||||
@@ -583,6 +584,7 @@
|
||||
"courtesycars-newcontract": "",
|
||||
"customers": "Les clients",
|
||||
"home": "Accueil",
|
||||
"invoices": "",
|
||||
"jobs": "Emplois",
|
||||
"owners": "Propriétaires",
|
||||
"schedule": "Programme",
|
||||
|
||||
Reference in New Issue
Block a user