diff --git a/.circleci/config.yml b/.circleci/config.yml
index 437f570d9..5d6f8f506 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -42,7 +42,7 @@ jobs:
app-build:
docker:
- image: cimg/node:16.15.0
-
+ resource_class: large
working_directory: ~/repo/client
steps:
@@ -83,7 +83,7 @@ jobs:
test-app-build:
docker:
- image: cimg/node:16.15.0
-
+ resource_class: large
working_directory: ~/repo/client
steps:
@@ -159,4 +159,4 @@ workflows:
#- admin-app-build:
#filters:
#branches:
- #only: master
\ No newline at end of file
+ #only: master
diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
index 617b9e603..c106a8c4c 100644
--- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
+++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx
@@ -94,6 +94,7 @@ function BillEnterModalContainer({
location,
outstanding_returns,
inventory,
+ federal_tax_exempt,
...remainingValues
} = values;
diff --git a/client/src/components/csi-response-form/csi-response-form.container.jsx b/client/src/components/csi-response-form/csi-response-form.container.jsx
index a1882b09a..d7a565608 100644
--- a/client/src/components/csi-response-form/csi-response-form.container.jsx
+++ b/client/src/components/csi-response-form/csi-response-form.container.jsx
@@ -5,6 +5,7 @@ import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { QUERY_CSI_RESPONSE_BY_PK } from "../../graphql/csi.queries";
+import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import ConfigFormComponents from "../config-form-components/config-form-components.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
@@ -44,6 +45,13 @@ export default function CsiResponseFormContainer() {
readOnly
componentList={data.csi_by_pk.csiquestion.config}
/>
+ {data.csi_by_pk.validuntil ? (
+ <>
+ {t("csi.fields.validuntil")}
+ {": "}
+ {data.csi_by_pk.validuntil}
+ >
+ ) : null}
);
diff --git a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx
index f73e35512..3d1a3b864 100644
--- a/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx
+++ b/client/src/components/csi-response-list-paginated/csi-response-list-paginated.component.jsx
@@ -5,9 +5,11 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useLocation } from "react-router-dom";
import { DateFormatter } from "../../utils/DateFormatter";
-import { alphaSort } from "../../utils/sorters";
-import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
-import {pageLimit} from "../../utils/config";
+import { pageLimit } from "../../utils/config";
+import { alphaSort, dateSort } from "../../utils/sorters";
+import OwnerNameDisplay, {
+ OwnerNameDisplayFunction,
+} from "../owner-name-display/owner-name-display.component";
export default function CsiResponseListPaginated({
refetch,
@@ -16,23 +18,23 @@ export default function CsiResponseListPaginated({
total,
}) {
const search = queryString.parse(useLocation().search);
- const { responseid, page, sortcolumn, sortorder } = search;
+ const { responseid } = search;
const history = useHistory();
+ const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
filteredInfo: { text: "" },
+ page: "",
});
- const { t } = useTranslation();
const columns = [
{
title: t("jobs.fields.ro_number"),
dataIndex: "ro_number",
key: "ro_number",
- width: "8%",
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
- sortOrder: sortcolumn === "ro_number" && sortorder,
-
+ sortOrder:
+ state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
render: (text, record) => (
{record.job.ro_number || t("general.labels.na")}
@@ -41,15 +43,18 @@ export default function CsiResponseListPaginated({
},
{
title: t("jobs.fields.owner"),
- dataIndex: "owner",
- key: "owner",
- ellipsis: true,
- sorter: (a, b) => alphaSort(a.job.ownr_ln, b.job.ownr_ln),
- width: "25%",
- sortOrder: sortcolumn === "owner" && sortorder,
+ dataIndex: "owner_name",
+ key: "owner_name",
+ sorter: (a, b) =>
+ alphaSort(
+ OwnerNameDisplayFunction(a.job),
+ OwnerNameDisplayFunction(b.job)
+ ),
+ sortOrder:
+ state.sortedInfo.columnKey === "owner_name" && state.sortedInfo.order,
render: (text, record) => {
- return record.job.owner ? (
-
+ return record.job.ownerid ? (
+
) : (
@@ -64,9 +69,9 @@ export default function CsiResponseListPaginated({
dataIndex: "completedon",
key: "completedon",
ellipsis: true,
- sorter: (a, b) => a.completedon - b.completedon,
- width: "25%",
- sortOrder: sortcolumn === "completedon" && sortorder,
+ sorter: (a, b) => dateSort(a.completedon, b.completedon),
+ sortOrder:
+ state.sortedInfo.columnKey === "completedon" && state.sortedInfo.order,
render: (text, record) => {
return record.completedon ? (
{record.completedon}
@@ -76,11 +81,12 @@ export default function CsiResponseListPaginated({
];
const handleTableChange = (pagination, filters, sorter) => {
- setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
- search.page = pagination.current;
- search.sortcolumn = sorter.columnKey;
- search.sortorder = sorter.order;
- history.push({ search: queryString.stringify(search) });
+ setState({
+ ...state,
+ filteredInfo: filters,
+ sortedInfo: sorter,
+ page: pagination.current,
+ });
};
const handleOnRowClick = (record) => {
@@ -108,7 +114,7 @@ export default function CsiResponseListPaginated({
pagination={{
position: "top",
pageSize: pageLimit,
- current: parseInt(page || 1),
+ current: parseInt(state.page || 1),
total: total,
}}
columns={columns}
@@ -122,13 +128,6 @@ export default function CsiResponseListPaginated({
selectedRowKeys: [responseid],
type: "radio",
}}
- onRow={(record, rowIndex) => {
- return {
- onClick: (event) => {
- handleOnRowClick(record);
- }, // click row
- };
- }}
/>
);
diff --git a/client/src/components/parts-queue-card/parts-queue-card.component.jsx b/client/src/components/parts-queue-card/parts-queue-card.component.jsx
new file mode 100644
index 000000000..c703b7616
--- /dev/null
+++ b/client/src/components/parts-queue-card/parts-queue-card.component.jsx
@@ -0,0 +1,77 @@
+import { useQuery } from "@apollo/client";
+import { Card, Divider, Drawer, Grid } from "antd";
+import queryString from "query-string";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Link, useHistory, useLocation } from "react-router-dom";
+import { QUERY_PARTS_QUEUE_CARD_DETAILS } from "../../graphql/jobs.queries";
+import AlertComponent from "../alert/alert.component";
+import JobsDetailHeader from "../jobs-detail-header/jobs-detail-header.component";
+import LoadingSpinner from "../loading-spinner/loading-spinner.component";
+import PartsQueueJobLinesComponent from "./parts-queue-job-lines.component";
+
+export default function PartsQueueDetailCard() {
+ const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
+ .filter((screen) => !!screen[1])
+ .slice(-1)[0];
+
+ const bpoints = {
+ xs: "100%",
+ sm: "100%",
+ md: "100%",
+ lg: "75%",
+ xl: "75%",
+ xxl: "60%",
+ };
+ const drawerPercentage = selectedBreakpoint
+ ? bpoints[selectedBreakpoint[0]]
+ : "100%";
+
+ const searchParams = queryString.parse(useLocation().search);
+ const { selected } = searchParams;
+ const history = useHistory();
+ const { loading, error, data } = useQuery(QUERY_PARTS_QUEUE_CARD_DETAILS, {
+ variables: { id: selected },
+ skip: !selected,
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only",
+ });
+
+ const { t } = useTranslation();
+ const handleDrawerClose = () => {
+ delete searchParams.selected;
+ history.push({
+ search: queryString.stringify({
+ ...searchParams,
+ }),
+ });
+ };
+
+ return (
+
+ {loading ? : null}
+ {error ? : null}
+ {data ? (
+
+ {data.jobs_by_pk.ro_number || t("general.labels.na")}
+
+ }
+ >
+
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx b/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx
new file mode 100644
index 000000000..d680c1bef
--- /dev/null
+++ b/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx
@@ -0,0 +1,208 @@
+import { Card, Table } from "antd";
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectJobReadOnly } from "../../redux/application/application.selectors";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import CurrencyFormatter from "../../utils/CurrencyFormatter";
+import { onlyUnique } from "../../utils/arrayHelper";
+import { alphaSort } from "../../utils/sorters";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+ jobRO: selectJobReadOnly,
+});
+
+const mapDispatchToProps = (dispatch) => ({});
+
+export function PartsQueueJobLinesComponent({ jobRO, loading, jobLines }) {
+ const [state, setState] = useState({
+ sortedInfo: {},
+ filteredInfo: {},
+ });
+ const { t } = useTranslation();
+
+ const columns = [
+ {
+ title: "#",
+ dataIndex: "line_no",
+ key: "line_no",
+ sorter: (a, b) => a.line_no - b.line_no,
+ sortOrder:
+ state.sortedInfo.columnKey === "line_no" && state.sortedInfo.order,
+ },
+ {
+ title: t("joblines.fields.line_desc"),
+ dataIndex: "line_desc",
+ key: "line_desc",
+ sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
+ onCell: (record) => ({
+ className: record.manual_line && "job-line-manual",
+ style: {
+ ...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}),
+ },
+ }),
+ sortOrder:
+ state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
+ ellipsis: true,
+ },
+ {
+ title: t("joblines.fields.oem_partno"),
+ dataIndex: "oem_partno",
+ key: "oem_partno",
+ sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
+ sortOrder:
+ state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
+ ellipsis: true,
+ render: (text, record) =>
+ `${record.oem_partno || ""} ${
+ record.alt_partno ? `(${record.alt_partno})` : ""
+ }`.trim(),
+ },
+ {
+ title: t("joblines.fields.part_type"),
+ dataIndex: "part_type",
+ key: "part_type",
+ filteredValue: state.filteredInfo.part_type || null,
+ sorter: (a, b) => alphaSort(a.part_type, b.part_type),
+ sortOrder:
+ state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order,
+ filters: [
+ {
+ text: t("jobs.labels.partsfilter"),
+ value: [
+ "PAN",
+ "PAC",
+ "PAR",
+ "PAL",
+ "PAA",
+ "PAM",
+ "PAP",
+ "PAS",
+ "PASL",
+ "PAG",
+ ],
+ },
+ {
+ text: t("joblines.fields.part_types.PAN"),
+ value: ["PAN"],
+ },
+ {
+ text: t("joblines.fields.part_types.PAP"),
+ value: ["PAP"],
+ },
+ {
+ text: t("joblines.fields.part_types.PAL"),
+ value: ["PAL"],
+ },
+ {
+ text: t("joblines.fields.part_types.PAA"),
+ value: ["PAA"],
+ },
+ {
+ text: t("joblines.fields.part_types.PAG"),
+ value: ["PAG"],
+ },
+ {
+ text: t("joblines.fields.part_types.PAS"),
+ value: ["PAS"],
+ },
+ {
+ text: t("joblines.fields.part_types.PASL"),
+ value: ["PASL"],
+ },
+ {
+ text: t("joblines.fields.part_types.PAC"),
+ value: ["PAC"],
+ },
+ {
+ text: t("joblines.fields.part_types.PAR"),
+ value: ["PAR"],
+ },
+ {
+ text: t("joblines.fields.part_types.PAM"),
+ value: ["PAM"],
+ },
+ ],
+ onFilter: (value, record) => value.includes(record.part_type),
+ render: (text, record) =>
+ record.part_type
+ ? t(`joblines.fields.part_types.${record.part_type}`)
+ : null,
+ },
+ {
+ title: t("joblines.fields.part_qty"),
+ dataIndex: "part_qty",
+ key: "part_qty",
+ },
+ {
+ title: t("joblines.fields.act_price"),
+ dataIndex: "act_price",
+ key: "act_price",
+ sorter: (a, b) => a.act_price - b.act_price,
+ sortOrder:
+ state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
+ ellipsis: true,
+ render: (text, record) => (
+
+ {record.db_ref === "900510" || record.db_ref === "900511"
+ ? record.prt_dsmk_m
+ : record.act_price}
+
+ ),
+ },
+ {
+ title: t("joblines.fields.location"),
+ dataIndex: "location",
+ key: "location",
+ },
+ {
+ title: t("joblines.fields.status"),
+ dataIndex: "status",
+ key: "status",
+ sorter: (a, b) => alphaSort(a.status, b.status),
+ sortOrder:
+ state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
+ filteredValue: state.filteredInfo.status || null,
+ filters:
+ (jobLines &&
+ jobLines
+ .map((l) => l.status)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Status*",
+ value: [s],
+ };
+ })) ||
+ [],
+ onFilter: (value, record) => value.includes(record.status),
+ },
+ ];
+
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState((state) => ({
+ ...state,
+ filteredInfo: filters,
+ sortedInfo: sorter,
+ }));
+ };
+
+ return (
+
+
+
+ );
+}
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(PartsQueueJobLinesComponent);
diff --git a/client/src/pages/parts-queue/parts-queue.page.component.jsx b/client/src/components/parts-queue-list/parts-queue.list.component.jsx
similarity index 76%
rename from client/src/pages/parts-queue/parts-queue.page.component.jsx
rename to client/src/components/parts-queue-list/parts-queue.list.component.jsx
index 62970d788..44d5368ce 100644
--- a/client/src/pages/parts-queue/parts-queue.page.component.jsx
+++ b/client/src/components/parts-queue-list/parts-queue.list.component.jsx
@@ -8,30 +8,28 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useHistory, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
-import AlertComponent from "../../components/alert/alert.component";
-import JobPartsQueueCount from "../../components/job-parts-queue-count/job-parts-queue-count.component";
-import JobRemoveFromPartsQueue from "../../components/job-remove-from-parst-queue/job-remove-from-parts-queue.component";
-import OwnerNameDisplay from "../../components/owner-name-display/owner-name-display.component";
-import ProductionListColumnComment from "../../components/production-list-columns/production-list-columns.comment.component";
import { QUERY_PARTS_QUEUE } from "../../graphql/jobs.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateTimeFormatter, TimeAgoFormatter } from "../../utils/DateFormatter";
+import { onlyUnique } from "../../utils/arrayHelper";
+import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
-import {pageLimit} from "../../utils/config";
+import AlertComponent from "../alert/alert.component";
+import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
+import JobRemoveFromPartsQueue from "../job-remove-from-parst-queue/job-remove-from-parts-queue.component";
+import OwnerNameDisplay, {
+ OwnerNameDisplayFunction,
+} from "../owner-name-display/owner-name-display.component";
+import ProductionListColumnComment from "../production-list-columns/production-list-columns.comment.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
-export function PartsQueuePageComponent({ bodyshop }) {
+export function PartsQueueListComponent({ bodyshop }) {
const searchParams = queryString.parse(useLocation().search);
- const {
- //page,
- sortcolumn,
- sortorder,
- statusFilters,
- } = searchParams;
+ const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
const history = useHistory();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
@@ -39,19 +37,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
variables: {
- // offset: page ? (page - 1) * 25 : 0,
- // limit: 25,
statuses: (statusFilters && JSON.parse(statusFilters)) ||
bodyshop.md_ro_statuses.active_statuses || ["Open", "Open*"],
- order: [
- {
- [sortcolumn || "ro_number"]: sortorder
- ? sortorder === "descend"
- ? "desc"
- : "asc"
- : "desc",
- },
- ],
},
});
@@ -107,6 +94,19 @@ export function PartsQueuePageComponent({ bodyshop }) {
history.push({ search: queryString.stringify(searchParams) });
};
+ const handleOnRowClick = (record) => {
+ if (record) {
+ if (record.id) {
+ history.push({
+ search: queryString.stringify({
+ ...searchParams,
+ selected: record.id,
+ }),
+ });
+ }
+ }
+ };
+
const columns = [
{
title: t("jobs.fields.ro_number"),
@@ -125,7 +125,8 @@ export function PartsQueuePageComponent({ bodyshop }) {
title: t("jobs.fields.owner"),
dataIndex: "ownr_ln",
key: "ownr_ln",
- sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
+ sorter: (a, b) =>
+ alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
sortOrder: sortcolumn === "ownr_ln" && sortorder,
render: (text, record) => {
return record.ownerid ? (
@@ -139,6 +140,56 @@ export function PartsQueuePageComponent({ bodyshop }) {
);
},
},
+ {
+ title: t("jobs.fields.vehicle"),
+ dataIndex: "vehicle",
+ key: "vehicle",
+ ellipsis: true,
+ sorter: (a, b) =>
+ alphaSort(
+ `${a.v_model_yr || ""} ${a.v_make_desc || ""} ${
+ a.v_model_desc || ""
+ }`,
+ `${b.v_model_yr || ""} ${b.v_make_desc || ""} ${b.v_model_desc || ""}`
+ ),
+ sortOrder: sortcolumn === "vehicle" && sortorder,
+ 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.ins_co_nm_short"),
+ dataIndex: "ins_co_nm",
+ key: "ins_co_nm",
+ ellipsis: true,
+ sorter: (a, b) => alphaSort(a.ins_co_nm, b.ins_co_nm),
+ sortOrder: sortcolumn === "ins_co_nm" && sortorder,
+ filteredValue: filter?.ins_co_nm || null,
+ filters:
+ (jobs &&
+ jobs
+ .map((j) => j.ins_co_nm)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Ins. Co.*",
+ value: [s],
+ };
+ })
+ .sort((a, b) => alphaSort(a.text, b.text))) ||
+ [],
+ onFilter: (value, record) => value.includes(record.ins_co_nm),
+ },
{
title: t("jobs.fields.status"),
dataIndex: "status",
@@ -170,23 +221,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
),
},
{
- title: t("jobs.fields.vehicle"),
- dataIndex: "vehicle",
- key: "vehicle",
+ title: t("jobs.fields.scheduled_completion"),
+ dataIndex: "scheduled_completion",
+ key: "scheduled_completion",
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 || ""
- }`}
- );
- },
+ sorter: (a, b) =>
+ dateSort(a.scheduled_completion, b.scheduled_completion),
+ sortOrder: sortcolumn === "scheduled_completion" && sortorder,
+ render: (text, record) => (
+ {record.scheduled_completion}
+ ),
},
// {
// title: t("vehicles.fields.plate_no"),
@@ -198,14 +242,6 @@ export function PartsQueuePageComponent({ bodyshop }) {
// return record.plate_no ? record.plate_no : "";
// },
// },
- {
- 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: sortcolumn === "clm_no" && sortorder,
- },
// {
// title: t("jobs.fields.clm_total"),
// dataIndex: "clm_total",
@@ -307,9 +343,16 @@ export function PartsQueuePageComponent({ bodyshop }) {
style={{ height: "100%" }}
scroll={{ x: true }}
onChange={handleTableChange}
+ rowSelection={{
+ onSelect: (record) => {
+ handleOnRowClick(record);
+ },
+ selectedRowKeys: [selected],
+ type: "radio",
+ }}
/>
);
}
-export default connect(mapStateToProps, null)(PartsQueuePageComponent);
+export default connect(mapStateToProps, null)(PartsQueueListComponent);
diff --git a/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx b/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx
index 4ed0c6033..9818bf896 100644
--- a/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx
+++ b/client/src/components/shop-csi-config-form/shop-csi-config-form.component.jsx
@@ -1,5 +1,5 @@
-import React from "react";
import { Form } from "antd";
+import React from "react";
import ConfigFormComponents from "../config-form-components/config-form-components.component";
export default function ShopCsiConfigForm({ selectedCsi }) {
@@ -9,7 +9,7 @@ export default function ShopCsiConfigForm({ selectedCsi }) {
return (
- The Config Form {readOnly}
+ {readOnly}
{selectedCsi && (