IO-1914 Added inventory page.
This commit is contained in:
@@ -18,7 +18,6 @@ export const CalculateBillTotal = (invoice) => {
|
||||
amount: Math.round((i.actual_cost || 0) * 100),
|
||||
}).multiply(i.quantity || 1);
|
||||
|
||||
console.log(i, itemTotal.toFormat);
|
||||
subtotal = subtotal.add(itemTotal);
|
||||
if (i.applicable_taxes.federal) {
|
||||
federalTax = federalTax.add(
|
||||
|
||||
@@ -200,6 +200,13 @@ function Header({
|
||||
{t("menus.header.enterbills")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div4" />
|
||||
<Menu.Item
|
||||
key="inventory"
|
||||
icon={<Icon component={FaFileInvoiceDollar} />}
|
||||
>
|
||||
<Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div7" />
|
||||
<Menu.Item key="allpayments" icon={<BankFilled />}>
|
||||
<Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
</Menu.Item>
|
||||
@@ -216,7 +223,6 @@ function Header({
|
||||
{t("menus.header.enterpayment")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div5" />
|
||||
|
||||
<Menu.Item key="timetickets" icon={<FieldTimeOutlined />}>
|
||||
<Link to="/manage/timetickets">
|
||||
{t("menus.header.timetickets")}
|
||||
@@ -235,7 +241,6 @@ function Header({
|
||||
{t("menus.header.entertimeticket")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider key="div6" />
|
||||
|
||||
<Menu.SubMenu
|
||||
key="accountingexport"
|
||||
title={t("menus.header.export")}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Input, Space, Table, Typography } from "antd";
|
||||
import _ from "lodash";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import StartChatButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
|
||||
export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { page, sortcolumn, sortorder, invfilters } = search;
|
||||
const history = useHistory();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const columns = [
|
||||
{
|
||||
title: t("billlines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
|
||||
sorter: true, //(a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
sortOrder: sortcolumn === "line_desc" && sortorder,
|
||||
},
|
||||
{
|
||||
title: t("inventory.labels.frombillinvoicenumber"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
ellipsis: true,
|
||||
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
|
||||
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
render: (text, record) => record.billline?.bill?.invoice_number,
|
||||
},
|
||||
{
|
||||
title: t("inventory.labels.fromvendor"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
ellipsis: true,
|
||||
//sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
||||
|
||||
//sortOrder: sortcolumn === "ownr_ln" && sortorder,
|
||||
render: (text, record) => record.billline?.bill?.vendor?.name,
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.actual_price"),
|
||||
dataIndex: "actual_price",
|
||||
key: "actual_price",
|
||||
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.actual_price}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("billlines.fields.actual_cost"),
|
||||
dataIndex: "actual_cost",
|
||||
key: "actual_cost",
|
||||
|
||||
render: (text, record) => (
|
||||
<CurrencyFormatter>{record.actual_cost}</CurrencyFormatter>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("inventory.labels.consumedbyjob"),
|
||||
dataIndex: "consumedbyjob",
|
||||
key: "consumedbyjob",
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => record.bill?.job?.ro_number,
|
||||
},
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
search.page = pagination.current;
|
||||
search.sortcolumn = sorter.column && sorter.column.key;
|
||||
search.sortorder = sorter.order;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
{search.search && (
|
||||
<>
|
||||
<Typography.Title level={4}>
|
||||
{t("general.labels.searchresults", { search: search.search })}
|
||||
</Typography.Title>
|
||||
<Button
|
||||
onClick={() => {
|
||||
delete search.search;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
>
|
||||
{t("general.actions.clear")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (search.showall) delete search.showall;
|
||||
else {
|
||||
search.showall = true;
|
||||
}
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
>
|
||||
{search.showall
|
||||
? t("inventory.labels.showavailable")
|
||||
: t("inventory.labels.showall")}
|
||||
</Button>
|
||||
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<Input.Search
|
||||
placeholder={search.search || t("general.labels.search")}
|
||||
onSearch={(value) => {
|
||||
search.search = value;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}}
|
||||
enterButton
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
loading={loading}
|
||||
pagination={{
|
||||
position: "top",
|
||||
pageSize: 25,
|
||||
current: parseInt(page || 1),
|
||||
total: total,
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={jobs}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobsList);
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_INVENTORY_PAGINATED } from "../../graphql/inventory.queries";
|
||||
import {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import InventoryListPaginated from "./inventory-list.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//bodyshop: selectBodyshop,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
});
|
||||
|
||||
export function InventoryList({ setBreadcrumbs, setSelectedHeader }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { page, sortcolumn, sortorder, search, showall } = searchParams;
|
||||
|
||||
const { loading, error, data, refetch } = useQuery(
|
||||
QUERY_INVENTORY_PAGINATED,
|
||||
{
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
variables: {
|
||||
search: search || "",
|
||||
offset: page ? (page - 1) * 25 : 0,
|
||||
limit: 25,
|
||||
consumedIsNull: showall === "true" ? null : true,
|
||||
order: [
|
||||
{
|
||||
[sortcolumn || "created_at"]:
|
||||
sortorder && sortorder !== "false"
|
||||
? sortorder === "descend"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: "desc",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
return (
|
||||
<RbacWrapper action="jobs:list-all">
|
||||
<InventoryListPaginated
|
||||
refetch={refetch}
|
||||
loading={loading}
|
||||
searchParams={searchParams}
|
||||
total={data ? data.search_inventory_aggregate.aggregate.count : 0}
|
||||
jobs={data ? data.search_inventory : []}
|
||||
/>
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InventoryList);
|
||||
@@ -25,7 +25,7 @@ const ret = {
|
||||
"jobs:detail": 1,
|
||||
"jobs:partsqueue": 4,
|
||||
"jobs:checklist-view": 2,
|
||||
|
||||
"jobs:list-ready": 1,
|
||||
"bills:enter": 2,
|
||||
"bills:view": 2,
|
||||
"bills:list": 2,
|
||||
@@ -66,5 +66,7 @@ const ret = {
|
||||
"timetickets:shiftedit": 5,
|
||||
|
||||
"users:editaccess": 4,
|
||||
|
||||
"inventory:list": 1,
|
||||
};
|
||||
export default ret;
|
||||
|
||||
@@ -633,6 +633,18 @@ export default function ShopInfoRbacComponent({ form }) {
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.rbac.inventory.list")}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
//message: t("general.validation.required"),
|
||||
},
|
||||
]}
|
||||
name={["md_rbac", "inventory:list"]}
|
||||
>
|
||||
<InputNumber />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
</RbacWrapper>
|
||||
);
|
||||
|
||||
@@ -47,3 +47,53 @@ export const QUERY_OUTSTANDING_INVENTORY = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const QUERY_INVENTORY_PAGINATED = gql`
|
||||
query QUERY_INVENTORY_PAGINATED(
|
||||
$search: String
|
||||
$offset: Int
|
||||
$limit: Int
|
||||
$order: [inventory_order_by!]
|
||||
$consumedIsNull: Boolean
|
||||
) {
|
||||
search_inventory(
|
||||
args: { search: $search }
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
order_by: $order
|
||||
where: { consumedbybillid: { _is_null: $consumedIsNull } }
|
||||
) {
|
||||
id
|
||||
line_desc
|
||||
actual_price
|
||||
actual_cost
|
||||
bill {
|
||||
id
|
||||
invoice_number
|
||||
job {
|
||||
ro_number
|
||||
id
|
||||
}
|
||||
}
|
||||
billline {
|
||||
id
|
||||
bill {
|
||||
id
|
||||
invoice_number
|
||||
vendor {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
search_inventory_aggregate(
|
||||
args: { search: $search }
|
||||
where: { consumedbybillid: { _is_null: $consumedIsNull } }
|
||||
) {
|
||||
aggregate {
|
||||
count(distinct: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
32
client/src/pages/inventory/inventory.page.jsx
Normal file
32
client/src/pages/inventory/inventory.page.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
import {
|
||||
setBreadcrumbs,
|
||||
setSelectedHeader,
|
||||
} from "../../redux/application/application.actions";
|
||||
import InventoryList from "../../components/inventory-list/inventory-list.container";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||
});
|
||||
|
||||
export function InventoryPage({ setBreadcrumbs, setSelectedHeader }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("titles.inventory");
|
||||
setSelectedHeader("inventory");
|
||||
setBreadcrumbs([{ link: "/manage/jobs", label: t("titles.bc.inventory") }]);
|
||||
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||
|
||||
return (
|
||||
<RbacWrapper action="inventory:list">
|
||||
<InventoryList />
|
||||
</RbacWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(InventoryPage);
|
||||
@@ -34,6 +34,7 @@ const JobsPage = lazy(() => import("../jobs/jobs.page"));
|
||||
const JobsDetailPage = lazy(() =>
|
||||
import("../jobs-detail/jobs-detail.page.container")
|
||||
);
|
||||
const InventoryListPage = lazy(() => import("../inventory/inventory.page"));
|
||||
const ProfilePage = lazy(() => import("../profile/profile.container.page"));
|
||||
const JobsAvailablePage = lazy(() =>
|
||||
import("../jobs-available/jobs-available.page.container")
|
||||
@@ -250,6 +251,11 @@ export function Manage({ match, conflict, bodyshop }) {
|
||||
<Route path={`${match.path}/jobs/:jobId`} component={JobsDetailPage} />
|
||||
</Switch>
|
||||
<Route exact path={`${match.path}/temporarydocs/`} component={TempDocs} />
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/inventory/`}
|
||||
component={InventoryListPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/courtesycars/`}
|
||||
|
||||
@@ -1078,7 +1078,12 @@
|
||||
"inserting": "Error inserting inventory item. {{error}}"
|
||||
},
|
||||
"labels": {
|
||||
"inventory": "Inventory"
|
||||
"consumedbyjob": "Consumed by Job",
|
||||
"frombillinvoicenumber": "Original Bill Invoice Number",
|
||||
"fromvendor": "Original Bill Vendor",
|
||||
"inventory": "Inventory",
|
||||
"showall": "Show All Inventory",
|
||||
"showavailable": "Show Only Available Inventory"
|
||||
},
|
||||
"successes": {
|
||||
"inserted": "Added line to inventory."
|
||||
@@ -1769,6 +1774,7 @@
|
||||
"export-logs": "Export Logs",
|
||||
"help": "Help",
|
||||
"home": "Home",
|
||||
"inventory": "Inventory",
|
||||
"jobs": "Jobs",
|
||||
"newjob": "Create New Job",
|
||||
"owners": "Owners",
|
||||
@@ -2537,6 +2543,7 @@
|
||||
"dashboard": "Dashboard",
|
||||
"dms": "DMS Export",
|
||||
"export-logs": "Export Logs",
|
||||
"inventory": "Inventory",
|
||||
"jobs": "Jobs",
|
||||
"jobs-active": "Active Jobs",
|
||||
"jobs-admin": "Admin",
|
||||
@@ -2577,6 +2584,7 @@
|
||||
"dashboard": "Dashboard | $t(titles.app)",
|
||||
"dms": "DMS Export | $t(titles.app)",
|
||||
"export-logs": "Export Logs | $t(titles.app)",
|
||||
"inventory": "Inventory | $t(titles.app)",
|
||||
"jobs": "Active Jobs | $t(titles.app)",
|
||||
"jobs-admin": "Job {{ro_number}} - Admin | $t(titles.app)",
|
||||
"jobs-all": "All Jobs | $t(titles.app)",
|
||||
|
||||
@@ -1078,7 +1078,12 @@
|
||||
"inserting": ""
|
||||
},
|
||||
"labels": {
|
||||
"inventory": ""
|
||||
"consumedbyjob": "",
|
||||
"frombillinvoicenumber": "",
|
||||
"fromvendor": "",
|
||||
"inventory": "",
|
||||
"showall": "",
|
||||
"showavailable": ""
|
||||
},
|
||||
"successes": {
|
||||
"inserted": ""
|
||||
@@ -1769,6 +1774,7 @@
|
||||
"export-logs": "",
|
||||
"help": "",
|
||||
"home": "Casa",
|
||||
"inventory": "",
|
||||
"jobs": "Trabajos",
|
||||
"newjob": "",
|
||||
"owners": "propietarios",
|
||||
@@ -2537,6 +2543,7 @@
|
||||
"dashboard": "",
|
||||
"dms": "",
|
||||
"export-logs": "",
|
||||
"inventory": "",
|
||||
"jobs": "",
|
||||
"jobs-active": "",
|
||||
"jobs-admin": "",
|
||||
@@ -2577,6 +2584,7 @@
|
||||
"dashboard": "",
|
||||
"dms": "",
|
||||
"export-logs": "",
|
||||
"inventory": "",
|
||||
"jobs": "Todos los trabajos | $t(titles.app)",
|
||||
"jobs-admin": "",
|
||||
"jobs-all": "",
|
||||
|
||||
@@ -1078,7 +1078,12 @@
|
||||
"inserting": ""
|
||||
},
|
||||
"labels": {
|
||||
"inventory": ""
|
||||
"consumedbyjob": "",
|
||||
"frombillinvoicenumber": "",
|
||||
"fromvendor": "",
|
||||
"inventory": "",
|
||||
"showall": "",
|
||||
"showavailable": ""
|
||||
},
|
||||
"successes": {
|
||||
"inserted": ""
|
||||
@@ -1769,6 +1774,7 @@
|
||||
"export-logs": "",
|
||||
"help": "",
|
||||
"home": "Accueil",
|
||||
"inventory": "",
|
||||
"jobs": "Emplois",
|
||||
"newjob": "",
|
||||
"owners": "Propriétaires",
|
||||
@@ -2537,6 +2543,7 @@
|
||||
"dashboard": "",
|
||||
"dms": "",
|
||||
"export-logs": "",
|
||||
"inventory": "",
|
||||
"jobs": "",
|
||||
"jobs-active": "",
|
||||
"jobs-admin": "",
|
||||
@@ -2577,6 +2584,7 @@
|
||||
"dashboard": "",
|
||||
"dms": "",
|
||||
"export-logs": "",
|
||||
"inventory": "",
|
||||
"jobs": "Tous les emplois | $t(titles.app)",
|
||||
"jobs-admin": "",
|
||||
"jobs-all": "",
|
||||
|
||||
Reference in New Issue
Block a user