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)",