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 90%
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 a9d66b363..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,13 +8,6 @@ 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, {
- OwnerNameDisplayFunction,
-} 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";
@@ -22,14 +15,21 @@ import { onlyUnique } from "../../utils/arrayHelper";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
import useLocalStorage from "../../utils/useLocalStorage";
+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 { sortcolumn, sortorder, statusFilters } = searchParams;
+ const { selected, sortcolumn, sortorder, statusFilters } = searchParams;
const history = useHistory();
const [filter, setFilter] = useLocalStorage("filter_parts_queue", null);
@@ -94,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"),
@@ -330,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/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js
index 72ecd6897..ee4c49dcc 100644
--- a/client/src/graphql/jobs.queries.js
+++ b/client/src/graphql/jobs.queries.js
@@ -108,11 +108,7 @@ export const QUERY_ALL_ACTIVE_JOBS = gql`
`;
export const QUERY_PARTS_QUEUE = gql`
- query QUERY_PARTS_QUEUE(
- $statuses: [String!]!
- $offset: Int
- $limit: Int
- ) {
+ query QUERY_PARTS_QUEUE($statuses: [String!]!, $offset: Int, $limit: Int) {
jobs_aggregate(where: { _and: [{ status: { _in: $statuses } }] }) {
aggregate {
count(distinct: true)
@@ -2339,3 +2335,166 @@ export const MARK_JOB_AS_UNINVOICED = gql`
}
}
`;
+
+export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
+ query QUERY_JOB_CARD_DETAILS($id: uuid!) {
+ jobs_by_pk(id: $id) {
+ actual_completion
+ actual_delivery
+ actual_in
+ alt_transport
+ available_jobs {
+ id
+ }
+ area_of_damage
+ ca_gst_registrant
+ cccontracts {
+ agreementnumber
+ courtesycar {
+ id
+ make
+ model
+ year
+ plate
+ fleetnumber
+ }
+ id
+ scheduledreturn
+ start
+ status
+ }
+ clm_no
+ clm_total
+ comment
+ date_estimated
+ date_exported
+ date_invoiced
+ date_last_contacted
+ date_next_contact
+ date_open
+ date_repairstarted
+ date_scheduled
+ ded_amt
+ employee_body
+ employee_body_rel {
+ id
+ first_name
+ last_name
+ }
+ employee_csr
+ employee_csr_rel {
+ id
+ first_name
+ last_name
+ }
+ employee_prep
+ employee_prep_rel {
+ id
+ first_name
+ last_name
+ }
+ employee_refinish
+ employee_refinish_rel {
+ id
+ first_name
+ last_name
+ }
+ est_co_nm
+ est_ct_fn
+ est_ct_ln
+ est_ea
+ est_ph1
+ id
+ ins_co_nm
+ ins_ct_fn
+ ins_ct_ln
+ ins_ea
+ ins_ph1
+ inproduction
+ job_totals
+ joblines(
+ order_by: { line_no: asc }
+ where: {
+ act_price: { _neq: 0 }
+ part_type: {
+ _in: [
+ "PAN"
+ "PAC"
+ "PAR"
+ "PAL"
+ "PAA"
+ "PAM"
+ "PAP"
+ "PAS"
+ "PASL"
+ "PAG"
+ ]
+ }
+ removed: { _eq: false }
+ }
+ ) {
+ act_price
+ alt_partno
+ db_ref
+ id
+ line_desc
+ line_no
+ location
+ mod_lbr_ty
+ mod_lb_hrs
+ oem_partno
+ part_qty
+ part_type
+ prt_dsmk_m
+ status
+ }
+ lbr_adjustments
+ ownr_co_nm
+ ownr_ea
+ ownr_fn
+ ownr_ln
+ ownr_ph1
+ ownr_ph2
+ owner {
+ id
+ allow_text_message
+ preferred_contact
+ tax_number
+ }
+ owner_owing
+ plate_no
+ plate_st
+ po_number
+ production_vars
+ ro_number
+ scheduled_completion
+ scheduled_delivery
+ scheduled_in
+ special_coverage_policy
+ status
+ suspended
+ updated_at
+ vehicle {
+ id
+ jobs {
+ id
+ clm_no
+ ro_number
+ }
+ notes
+ plate_no
+ v_color
+ v_make_desc
+ v_model_desc
+ v_model_yr
+ }
+ vehicleid
+ v_color
+ v_make_desc
+ v_model_desc
+ v_model_yr
+ v_vin
+ voided
+ }
+ }
+`;
diff --git a/client/src/pages/parts-queue/parts-queue.page.container.jsx b/client/src/pages/parts-queue/parts-queue.page.container.jsx
index 5f5d8d617..3f78e9b92 100644
--- a/client/src/pages/parts-queue/parts-queue.page.container.jsx
+++ b/client/src/pages/parts-queue/parts-queue.page.container.jsx
@@ -1,12 +1,13 @@
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
+import PartsQueueDetailCard from "../../components/parts-queue-card/parts-queue-card.component";
+import PartsQueueList from "../../components/parts-queue-list/parts-queue.list.component";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import {
setBreadcrumbs,
setSelectedHeader,
} from "../../redux/application/application.actions";
-import PartsQueuePage from "./parts-queue.page.component";
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -26,7 +27,8 @@ export function PartsQueuePageContainer({ setBreadcrumbs, setSelectedHeader }) {
return (
-
+
+
);
}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 7679ffeed..f1e514b90 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -1822,6 +1822,7 @@
"override_header": "Override estimate header on import?",
"ownerassociation": "Owner Association",
"parts": "Parts",
+ "parts_lines": "Parts Lines",
"parts_received": "Parts Rec.",
"parts_tax_rates": "Parts Tax rates",
"partsfilter": "Parts Only",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 0b7722676..eac477911 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -1822,6 +1822,7 @@
"override_header": "¿Anular encabezado estimado al importar?",
"ownerassociation": "",
"parts": "Partes",
+ "parts_lines": "",
"parts_received": "",
"parts_tax_rates": "",
"partsfilter": "",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 86ebd85ec..a70920b18 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -1822,6 +1822,7 @@
"override_header": "Remplacer l'en-tête d'estimation à l'importation?",
"ownerassociation": "",
"parts": "les pièces",
+ "parts_lines": "",
"parts_received": "",
"parts_tax_rates": "",
"partsfilter": "",