diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel
index ec94be6a6..eac8b6536 100644
--- a/bodyshop_translations.babel
+++ b/bodyshop_translations.babel
@@ -17292,6 +17292,69 @@
labels
+
+ consumedbyjob
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ frombillinvoicenumber
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ fromvendor
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
inventory
false
@@ -17313,6 +17376,48 @@
+
+ showall
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
+
+ showavailable
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
@@ -30064,6 +30169,27 @@
+
+ inventory
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
jobs
false
@@ -42492,6 +42618,27 @@
+
+ inventory
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
jobs
false
@@ -43313,6 +43460,27 @@
+
+ inventory
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
jobs
false
diff --git a/client/src/components/bill-form/bill-form.totals.utility.js b/client/src/components/bill-form/bill-form.totals.utility.js
index 05396e66c..df84ec86e 100644
--- a/client/src/components/bill-form/bill-form.totals.utility.js
+++ b/client/src/components/bill-form/bill-form.totals.utility.js
@@ -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(
diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx
index 2a4e9698e..63af81751 100644
--- a/client/src/components/header/header.component.jsx
+++ b/client/src/components/header/header.component.jsx
@@ -200,6 +200,13 @@ function Header({
{t("menus.header.enterbills")}
+ }
+ >
+ {t("menus.header.inventory")}
+
+
}>
{t("menus.header.allpayments")}
@@ -216,7 +223,6 @@ function Header({
{t("menus.header.enterpayment")}
-
}>
{t("menus.header.timetickets")}
@@ -235,7 +241,6 @@ function Header({
{t("menus.header.entertimeticket")}
-
({
+ //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) => (
+ {record.actual_price}
+ ),
+ },
+ {
+ title: t("billlines.fields.actual_cost"),
+ dataIndex: "actual_cost",
+ key: "actual_cost",
+
+ render: (text, record) => (
+ {record.actual_cost}
+ ),
+ },
+ {
+ 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 (
+
+ {search.search && (
+ <>
+
+ {t("general.labels.searchresults", { search: search.search })}
+
+
+ >
+ )}
+
+
+
+
+ {
+ search.search = value;
+ history.push({ search: queryString.stringify(search) });
+ }}
+ enterButton
+ />
+
+ }
+ >
+
+
+ );
+}
+export default connect(mapStateToProps, mapDispatchToProps)(JobsList);
diff --git a/client/src/components/inventory-list/inventory-list.container.jsx b/client/src/components/inventory-list/inventory-list.container.jsx
new file mode 100644
index 000000000..568b3162a
--- /dev/null
+++ b/client/src/components/inventory-list/inventory-list.container.jsx
@@ -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 ;
+ return (
+
+
+
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(InventoryList);
diff --git a/client/src/components/rbac-wrapper/rbac-defaults.js b/client/src/components/rbac-wrapper/rbac-defaults.js
index 089e8954e..2f83443ac 100644
--- a/client/src/components/rbac-wrapper/rbac-defaults.js
+++ b/client/src/components/rbac-wrapper/rbac-defaults.js
@@ -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;
diff --git a/client/src/components/shop-info/shop-info.rbac.component.jsx b/client/src/components/shop-info/shop-info.rbac.component.jsx
index 3c0f75c88..4c425ff69 100644
--- a/client/src/components/shop-info/shop-info.rbac.component.jsx
+++ b/client/src/components/shop-info/shop-info.rbac.component.jsx
@@ -633,6 +633,18 @@ export default function ShopInfoRbacComponent({ form }) {
>
+
+
+
);
diff --git a/client/src/graphql/inventory.queries.js b/client/src/graphql/inventory.queries.js
index 8c5bb7a4b..a2c36f6c1 100644
--- a/client/src/graphql/inventory.queries.js
+++ b/client/src/graphql/inventory.queries.js
@@ -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)
+ }
+ }
+ }
+`;
diff --git a/client/src/pages/inventory/inventory.page.jsx b/client/src/pages/inventory/inventory.page.jsx
new file mode 100644
index 000000000..26cc2f2dc
--- /dev/null
+++ b/client/src/pages/inventory/inventory.page.jsx
@@ -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 (
+
+
+
+ );
+}
+
+export default connect(null, mapDispatchToProps)(InventoryPage);
diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx
index 0fc635598..82017ffae 100644
--- a/client/src/pages/manage/manage.page.component.jsx
+++ b/client/src/pages/manage/manage.page.component.jsx
@@ -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 }) {
+