From c08713bfbeda0731e18cd0d84de7b76ae47fb582 Mon Sep 17 00:00:00 2001
From: Patrick Fic <>
Date: Mon, 18 Apr 2022 13:07:52 -0700
Subject: [PATCH] IO-1650 Added ready jobs screen.
---
bodyshop_translations.babel | 105 ++++++
.../components/header/header.component.jsx | 4 +
.../jobs-ready-list.component.jsx | 355 ++++++++++++++++++
.../shop-info/shop-info.rbac.component.jsx | 12 +
.../shop-info.rostatus.component.jsx | 19 +
client/src/landing/data.source.js | 2 +-
.../src/pages/jobs-ready/jobs-ready.page.jsx | 36 ++
.../pages/manage/manage.page.component.jsx | 2 +
client/src/translations/en_us/common.json | 7 +-
client/src/translations/es/common.json | 7 +-
client/src/translations/fr/common.json | 7 +-
11 files changed, 552 insertions(+), 4 deletions(-)
create mode 100644 client/src/components/jobs-ready-list/jobs-ready-list.component.jsx
create mode 100644 client/src/pages/jobs-ready/jobs-ready.page.jsx
diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel
index 56e402f25..ce8c30b78 100644
--- a/bodyshop_translations.babel
+++ b/bodyshop_translations.babel
@@ -5736,6 +5736,27 @@
+
+ list-ready
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
partsqueue
false
@@ -8028,6 +8049,27 @@
+
+ ready_statuses
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
@@ -29640,6 +29682,27 @@
+
+ readyjobs
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
recent
false
@@ -41811,6 +41874,27 @@
+
+ jobs-ready
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
owner-detail
false
@@ -42821,6 +42905,27 @@
+
+ readyjobs
+ false
+
+
+
+
+
+ en-US
+ false
+
+
+ es-MX
+ false
+
+
+ fr-CA
+ false
+
+
+
resetpassword
false
diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx
index 46cdd6063..2a4e9698e 100644
--- a/client/src/components/header/header.component.jsx
+++ b/client/src/components/header/header.component.jsx
@@ -3,6 +3,7 @@ import Icon, {
BarChartOutlined,
CarFilled,
ClockCircleFilled,
+ CheckCircleOutlined,
DashboardFilled,
DollarCircleFilled,
ExportOutlined,
@@ -108,6 +109,9 @@ function Header({
}>
{t("menus.header.activejobs")}
+ }>
+ {t("menus.header.readyjobs")}
+
}>
{t("menus.header.parts-queue")}
diff --git a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx
new file mode 100644
index 000000000..e1a598562
--- /dev/null
+++ b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx
@@ -0,0 +1,355 @@
+import {
+ ExclamationCircleFilled,
+ PauseCircleOutlined,
+ SyncOutlined,
+} from "@ant-design/icons";
+import { useQuery } from "@apollo/client";
+import { Button, Card, Grid, Input, Space, Table } from "antd";
+import queryString from "query-string";
+import React, { useMemo, useState } 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 { QUERY_ALL_ACTIVE_JOBS } from "../../graphql/jobs.queries";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { onlyUnique } from "../../utils/arrayHelper";
+import CurrencyFormatter from "../../utils/CurrencyFormatter";
+import { alphaSort } from "../../utils/sorters";
+import AlertComponent from "../alert/alert.component";
+import ChatOpenButton from "../chat-open-button/chat-open-button.component";
+import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+});
+
+export function JobsReadyList({ bodyshop }) {
+ const searchParams = queryString.parse(useLocation().search);
+ const { selected } = searchParams;
+ const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
+ .filter((screen) => !!screen[1])
+ .slice(-1)[0];
+
+ const readyStatuses = useMemo(() => {
+ if (bodyshop.md_ro_statuses.ready_statuses)
+ return bodyshop.md_ro_statuses.ready_statuses;
+
+ return bodyshop.md_ro_statuses.post_production_statuses.filter(
+ (s) =>
+ s !== bodyshop.md_ro_statuses.default_invoiced &&
+ s !== bodyshop.md_ro_statuses.default_exported
+ );
+ }, [bodyshop.md_ro_statuses]);
+
+ const { loading, error, data, refetch } = useQuery(QUERY_ALL_ACTIVE_JOBS, {
+ variables: {
+ statuses: readyStatuses,
+ },
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only",
+ });
+
+ const [state, setState] = useState({
+ sortedInfo: {},
+ filteredInfo: { text: "" },
+ });
+
+ const { t } = useTranslation();
+ const history = useHistory();
+ const [searchText, setSearchText] = useState("");
+
+ if (error) return ;
+
+ const jobs = data
+ ? searchText === ""
+ ? data.jobs
+ : data.jobs.filter(
+ (j) =>
+ (j.ro_number || "")
+ .toString()
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.ownr_co_nm || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.comments || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.ownr_fn || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.ownr_ln || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.clm_no || "").toLowerCase().includes(searchText.toLowerCase()) ||
+ (j.plate_no || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.v_model_desc || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase()) ||
+ (j.v_make_desc || "")
+ .toLowerCase()
+ .includes(searchText.toLowerCase())
+ )
+ : [];
+
+ const handleTableChange = (pagination, filters, sorter) => {
+ setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
+ };
+
+ 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"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ sorter: (a, b) => alphaSort(a.ro_number, b.ro_number),
+ sortOrder:
+ state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+
+ render: (text, record) => (
+ e.stopPropagation()}
+ >
+
+ {record.ro_number || t("general.labels.na")}
+ {record.production_vars && record.production_vars.alert ? (
+
+ ) : null}
+ {record.suspended && (
+
+ )}
+
+
+ ),
+ },
+ {
+ title: t("jobs.fields.owner"),
+ dataIndex: "owner",
+ key: "owner",
+ ellipsis: true,
+
+ responsive: ["md"],
+ sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
+ sortOrder:
+ state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
+ render: (text, record) => {
+ return record.owner ? (
+ e.stopPropagation()}
+ >
+
+
+ ) : (
+
+
+
+ );
+ },
+ },
+ {
+ title: t("jobs.fields.ownr_ph1"),
+ dataIndex: "ownr_ph1",
+ key: "ownr_ph1",
+ ellipsis: true,
+ responsive: ["md"],
+ render: (text, record) => (
+
+ ),
+ },
+ {
+ title: t("jobs.fields.ownr_ph2"),
+ dataIndex: "ownr_ph2",
+ key: "ownr_ph2",
+ ellipsis: true,
+ responsive: ["md"],
+ render: (text, record) => (
+
+ ),
+ },
+
+ {
+ title: t("jobs.fields.status"),
+ dataIndex: "status",
+ key: "status",
+ ellipsis: true,
+
+ sorter: (a, b) => alphaSort(a.status, b.status),
+ sortOrder:
+ state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
+ filters:
+ (jobs &&
+ jobs
+ .map((j) => j.status)
+ .filter(onlyUnique)
+ .map((s) => {
+ return {
+ text: s || "No Status*",
+ value: [s],
+ };
+ })) ||
+ [],
+ onFilter: (value, record) => value.includes(record.status),
+ },
+
+ {
+ title: t("jobs.fields.vehicle"),
+ dataIndex: "vehicle",
+ key: "vehicle",
+ ellipsis: true,
+ render: (text, record) => {
+ return record.vehicleid ? (
+ e.stopPropagation()}
+ >
+ {`${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("vehicles.fields.plate_no"),
+ dataIndex: "plate_no",
+ key: "plate_no",
+ ellipsis: true,
+
+ responsive: ["md"],
+ sorter: (a, b) => alphaSort(a.plate_no, b.plate_no),
+ sortOrder:
+ state.sortedInfo.columnKey === "plate_no" && state.sortedInfo.order,
+ },
+ {
+ title: t("jobs.fields.clm_no"),
+ dataIndex: "clm_no",
+ key: "clm_no",
+ ellipsis: true,
+ responsive: ["md"],
+ sorter: (a, b) => alphaSort(a.clm_no, b.clm_no),
+ sortOrder:
+ state.sortedInfo.columnKey === "clm_no" && state.sortedInfo.order,
+ render: (text, record) =>
+ `${record.clm_no || ""}${
+ record.po_number ? ` (PO: ${record.po_number})` : ""
+ }`,
+ },
+ {
+ title: t("jobs.fields.ins_co_nm"),
+ dataIndex: "ins_co_nm",
+ key: "ins_co_nm",
+ ellipsis: true,
+ responsive: ["md"],
+ },
+ {
+ title: t("jobs.fields.clm_total"),
+ dataIndex: "clm_total",
+ key: "clm_total",
+ responsive: ["md"],
+ ellipsis: true,
+
+ sorter: (a, b) => a.clm_total - b.clm_total,
+ sortOrder:
+ state.sortedInfo.columnKey === "clm_total" && state.sortedInfo.order,
+ render: (text, record) => (
+ {record.clm_total}
+ ),
+ },
+ {
+ title: t("jobs.fields.comment"),
+ dataIndex: "comment",
+ key: "comment",
+ ellipsis: true,
+ responsive: ["md"],
+ },
+ // {
+ // title: t("jobs.fields.owner_owing"),
+ // dataIndex: "owner_owing",
+ // key: "owner_owing",
+ // responsive: ["md"],
+ // render: (text, record) => (
+ // {record.owner_owing}
+ // ),
+ // },
+ ];
+
+ const scrollMapper = {
+ xs: true,
+ sm: true,
+ md: true,
+ lg: "100%",
+ xl: "100%",
+ xxl: "100%",
+ };
+
+ return (
+
+ ({readyStatuses && readyStatuses.join(", ")})
+
+ {
+ setSearchText(e.target.value);
+ }}
+ value={searchText}
+ enterButton
+ />
+
+ }
+ >
+ {
+ handleOnRowClick(record);
+ },
+ selectedRowKeys: [selected],
+ type: "radio",
+ }}
+ onChange={handleTableChange}
+ onRow={(record, rowIndex) => {
+ return {
+ onClick: (event) => {
+ handleOnRowClick(record);
+ },
+ };
+ }}
+ />
+
+ );
+}
+
+export default connect(mapStateToProps, null)(JobsReadyList);
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 1f6363e3a..3c0f75c88 100644
--- a/client/src/components/shop-info/shop-info.rbac.component.jsx
+++ b/client/src/components/shop-info/shop-info.rbac.component.jsx
@@ -165,6 +165,18 @@ export default function ShopInfoRbacComponent({ form }) {
>
+
+
+
+
+
+
({
+ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
+ setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
+});
+
+export function JobsReadyPage({ setBreadcrumbs, setSelectedHeader }) {
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ document.title = t("titles.readyjobs");
+ setSelectedHeader("readyjobs");
+ setBreadcrumbs([
+ { link: "/manage/jobs", label: t("titles.bc.jobs-ready") },
+ ]);
+ }, [t, setBreadcrumbs, setSelectedHeader]);
+
+ return (
+
+
+
+
+ );
+}
+
+export default connect(null, mapDispatchToProps)(JobsReadyPage);
diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx
index 836b73f6d..0fc635598 100644
--- a/client/src/pages/manage/manage.page.component.jsx
+++ b/client/src/pages/manage/manage.page.component.jsx
@@ -127,6 +127,7 @@ const AccountingPayments = lazy(() =>
import("../accounting-payments/accounting-payments.container")
);
const AllJobs = lazy(() => import("../jobs-all/jobs-all.container"));
+const ReadyJobs = lazy(() => import("../jobs-ready/jobs-ready.page"));
const JobsClose = lazy(() => import("../jobs-close/jobs-close.container"));
const JobsAdmin = lazy(() => import("../jobs-admin/jobs-admin.page"));
const TempDocs = lazy(() =>
@@ -240,6 +241,7 @@ export function Manage({ match, conflict, bodyshop }) {
component={JobsAdmin}
/>
+
Intake",
"list-active": "Jobs -> List Active",
"list-all": "Jobs -> List All",
+ "list-ready": "Jobs -> List Ready",
"partsqueue": "Jobs -> Parts Queue"
},
"owners": {
@@ -494,7 +495,8 @@
"post_production_statuses": "Post-Production Statuses",
"pre_production_statuses": "Pre-Production Statuses",
"production_colors": "Production Status Colors",
- "production_statuses": "Production Statuses"
+ "production_statuses": "Production Statuses",
+ "ready_statuses": "Ready Statuses"
},
"target_touchtime": "Target Touch Time",
"timezone": "Timezone",
@@ -1740,6 +1742,7 @@
"phonebook": "Phonebook",
"productionboard": "Production Board - Visual",
"productionlist": "Production Board - List",
+ "readyjobs": "Ready Jobs",
"recent": "Recent Items",
"reportcenter": "Report Center",
"rescueme": "Rescue me!",
@@ -2494,6 +2497,7 @@
"jobs-detail": "Job {{number}}",
"jobs-intake": "Intake",
"jobs-new": "Create a New Job",
+ "jobs-ready": "Ready Jobs",
"owner-detail": "{{name}}",
"owners": "Owners",
"parts-queue": "Parts Queue",
@@ -2543,6 +2547,7 @@
"productionboard": "Production - Board",
"productionlist": "Production Board - List | $t(titles.app)",
"profile": "My Profile | $t(titles.app)",
+ "readyjobs": "Ready Jobs | $t(titles.app)",
"resetpassword": "Reset Password",
"resetpasswordvalidate": "Enter New Password",
"schedule": "Schedule | $t(titles.app)",
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 5e67fd462..7edccfb60 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -355,6 +355,7 @@
"intake": "",
"list-active": "",
"list-all": "",
+ "list-ready": "",
"partsqueue": ""
},
"owners": {
@@ -494,7 +495,8 @@
"post_production_statuses": "",
"pre_production_statuses": "",
"production_colors": "",
- "production_statuses": ""
+ "production_statuses": "",
+ "ready_statuses": ""
},
"target_touchtime": "",
"timezone": "",
@@ -1740,6 +1742,7 @@
"phonebook": "",
"productionboard": "",
"productionlist": "",
+ "readyjobs": "",
"recent": "",
"reportcenter": "",
"rescueme": "",
@@ -2494,6 +2497,7 @@
"jobs-detail": "",
"jobs-intake": "",
"jobs-new": "",
+ "jobs-ready": "",
"owner-detail": "",
"owners": "",
"parts-queue": "",
@@ -2543,6 +2547,7 @@
"productionboard": "",
"productionlist": "",
"profile": "Mi perfil | $t(titles.app)",
+ "readyjobs": "",
"resetpassword": "",
"resetpasswordvalidate": "",
"schedule": "Horario | $t(titles.app)",
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index fb982cf6d..f7e49304c 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -355,6 +355,7 @@
"intake": "",
"list-active": "",
"list-all": "",
+ "list-ready": "",
"partsqueue": ""
},
"owners": {
@@ -494,7 +495,8 @@
"post_production_statuses": "",
"pre_production_statuses": "",
"production_colors": "",
- "production_statuses": ""
+ "production_statuses": "",
+ "ready_statuses": ""
},
"target_touchtime": "",
"timezone": "",
@@ -1740,6 +1742,7 @@
"phonebook": "",
"productionboard": "",
"productionlist": "",
+ "readyjobs": "",
"recent": "",
"reportcenter": "",
"rescueme": "",
@@ -2494,6 +2497,7 @@
"jobs-detail": "",
"jobs-intake": "",
"jobs-new": "",
+ "jobs-ready": "",
"owner-detail": "",
"owners": "",
"parts-queue": "",
@@ -2543,6 +2547,7 @@
"productionboard": "",
"productionlist": "",
"profile": "Mon profil | $t(titles.app)",
+ "readyjobs": "",
"resetpassword": "",
"resetpasswordvalidate": "",
"schedule": "Horaire | $t(titles.app)",