diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx
index 3cda7fe8e..cbbaf4432 100644
--- a/client/src/App/App.jsx
+++ b/client/src/App/App.jsx
@@ -12,6 +12,7 @@ import LoadingSpinner from "../components/loading-spinner/loading-spinner.compon
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
import LandingPage from "../pages/landing/landing.page";
import TechPageContainer from "../pages/tech/tech.page.container";
+import SimplifiedPartsPageContainer from "../pages/simplified-parts/simplified-parts.page.container.jsx";
import { setOnline } from "../redux/application/application.actions";
import { selectOnline } from "../redux/application/application.selectors";
import { checkUserSession } from "../redux/user/user.actions";
@@ -28,7 +29,6 @@ const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
-const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -188,14 +188,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
}
/>
-
-
-
- }
- />
} />
+
+
+
+
+
+ }
+ >
+ } />
+
}>
} />
diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss
index 9ea0a8d24..898228880 100644
--- a/client/src/App/App.styles.scss
+++ b/client/src/App/App.styles.scss
@@ -190,3 +190,7 @@
margin-right: 0;
}
}
+
+.content-container {
+ padding: 1rem;
+}
diff --git a/client/src/components/job-detail-lines/job-lines.component.jsx b/client/src/components/job-detail-lines/job-lines.component.jsx
index 455caefd6..41eb5f21a 100644
--- a/client/src/components/job-detail-lines/job-lines.component.jsx
+++ b/client/src/components/job-detail-lines/job-lines.component.jsx
@@ -78,7 +78,8 @@ export function JobLinesComponent({
billsQuery,
handleBillOnRowClick,
handlePartsOrderOnRowClick,
- handlePartsDispatchOnRowClick
+ handlePartsDispatchOnRowClick,
+ simple
}) {
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
const {
@@ -220,59 +221,43 @@ export function JobLinesComponent({
sorter: (a, b) => a.part_qty - b.part_qty,
sortOrder: state.sortedInfo.columnKey === "part_qty" && state.sortedInfo.order
},
- // {
- // title: t('joblines.fields.tax_part'),
- // dataIndex: 'tax_part',
- // key: 'tax_part',
- // render: (text, record) => ,
- // },
- // {
- // title: t("joblines.fields.total"),
- // dataIndex: "total",
- // key: "total",
- // sorter: (a, b) => a.act_price * a.part_qty - b.act_price * b.part_qty,
- // sortOrder:
- // state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
- // ellipsis: true,
- // render: (text, record) => (
- //
- // {record.act_price * record.part_qty}
- //
- // ),
- // },
- {
- title: t("joblines.fields.mod_lbr_ty"),
- dataIndex: "mod_lbr_ty",
- key: "mod_lbr_ty",
-
- sorter: (a, b) => alphaSort(a.mod_lbr_ty, b.mod_lbr_ty),
- sortOrder: state.sortedInfo.columnKey === "mod_lbr_ty" && state.sortedInfo.order,
- render: (text, record) => (record.mod_lbr_ty ? t(`joblines.fields.lbr_types.${record.mod_lbr_ty}`) : null)
- },
- {
- title: t("joblines.fields.mod_lb_hrs"),
- dataIndex: "mod_lb_hrs",
- key: "mod_lb_hrs",
-
- sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
- sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order
- },
- {
- title: t("joblines.fields.line_ind"),
- dataIndex: "line_ind",
- key: "line_ind",
- sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
- sortOrder: state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
- responsive: ["md"]
- },
- ...(Enhanced_Payroll.treatment === "on"
+ ...(!simple
? [
{
- title: t("joblines.fields.assigned_team"),
- dataIndex: "assigned_team",
- key: "assigned_team",
- render: (text, record) =>
- }
+ title: t("joblines.fields.mod_lbr_ty"),
+ dataIndex: "mod_lbr_ty",
+ key: "mod_lbr_ty",
+
+ sorter: (a, b) => alphaSort(a.mod_lbr_ty, b.mod_lbr_ty),
+ sortOrder: state.sortedInfo.columnKey === "mod_lbr_ty" && state.sortedInfo.order,
+ render: (text, record) => (record.mod_lbr_ty ? t(`joblines.fields.lbr_types.${record.mod_lbr_ty}`) : null)
+ },
+ {
+ title: t("joblines.fields.mod_lb_hrs"),
+ dataIndex: "mod_lb_hrs",
+ key: "mod_lb_hrs",
+
+ sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
+ sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order
+ },
+ {
+ title: t("joblines.fields.line_ind"),
+ dataIndex: "line_ind",
+ key: "line_ind",
+ sorter: (a, b) => alphaSort(a.line_ind, b.line_ind),
+ sortOrder: state.sortedInfo.columnKey === "line_ind" && state.sortedInfo.order,
+ responsive: ["md"]
+ },
+ ...(Enhanced_Payroll.treatment === "on"
+ ? [
+ {
+ title: t("joblines.fields.assigned_team"),
+ dataIndex: "assigned_team",
+ key: "assigned_team",
+ render: (text, record) =>
+ }
+ ]
+ : [])
]
: []),
@@ -288,7 +273,7 @@ export function JobLinesComponent({
key: "location",
render: (text, record) =>
},
- ...(HasFeatureAccess({ featureName: "bills", bodyshop })
+ ...(!simple && HasFeatureAccess({ featureName: "bills", bodyshop })
? [
{
title: t("joblines.labels.billref"),
diff --git a/client/src/components/job-detail-lines/job-lines.container.jsx b/client/src/components/job-detail-lines/job-lines.container.jsx
index 8ed70dbdc..5d4567418 100644
--- a/client/src/components/job-detail-lines/job-lines.container.jsx
+++ b/client/src/components/job-detail-lines/job-lines.container.jsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useState } from "react";
+import { useMemo, useState } from "react";
import JobLinesComponent from "./job-lines.component";
function JobLinesContainer({
@@ -10,6 +10,7 @@ function JobLinesContainer({
handlePartsDispatchOnRowClick,
refetch,
form,
+ simple = false,
...rest
}) {
const [searchText, setSearchText] = useState("");
@@ -43,6 +44,7 @@ function JobLinesContainer({
setSearchText={setSearchText}
job={job}
form={form}
+ simple={simple}
/>
);
diff --git a/client/src/components/simplified-parts-jobs-list/simplified-parts-jobs-list.component.jsx b/client/src/components/simplified-parts-jobs-list/simplified-parts-jobs-list.component.jsx
new file mode 100644
index 000000000..db57f62b5
--- /dev/null
+++ b/client/src/components/simplified-parts-jobs-list/simplified-parts-jobs-list.component.jsx
@@ -0,0 +1,251 @@
+import { SyncOutlined } from "@ant-design/icons";
+import { Button, Card, Input, Space, Table, Typography } from "antd";
+import axios from "axios";
+import _ from "lodash";
+import queryString from "query-string";
+import { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { Link, useLocation, useNavigate } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import CurrencyFormatter from "../../utils/CurrencyFormatter";
+import { pageLimit } from "../../utils/config";
+import { alphaSort, statusSort } from "../../utils/sorters";
+import useLocalStorage from "../../utils/useLocalStorage";
+import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
+import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
+
+const mapStateToProps = createStructuredSelector({
+ //currentUser: selectCurrentUser
+ bodyshop: selectBodyshop
+});
+const mapDispatchToProps = (dispatch) => ({
+ //setUserLanguage: language => dispatch(setUserLanguage(language))
+});
+
+export function SimplifiedPartsJobsListComponent({ bodyshop, refetch, loading, jobs, total }) {
+ const search = queryString.parse(useLocation().search);
+ const [openSearchResults, setOpenSearchResults] = useState([]);
+ const [searchLoading, setSearchLoading] = useState(false);
+ const [filter, setFilter] = useLocalStorage("filter_jobs_all", null);
+ const { page, sortcolumn, sortorder } = search;
+ const history = useNavigate();
+
+ const { t } = useTranslation();
+ const columns = [
+ {
+ title: t("jobs.fields.ro_number"),
+ dataIndex: "ro_number",
+ key: "ro_number",
+ sorter: search?.search
+ ? (a, b) =>
+ parseInt((a.ro_number || "0").replace(/\D/g, "")) - parseInt((b.ro_number || "0").replace(/\D/g, ""))
+ : true,
+ sortOrder: sortcolumn === "ro_number" && sortorder,
+ render: (text, record) => (
+ {record.ro_number || t("general.labels.na")}
+ )
+ },
+ {
+ title: t("jobs.fields.owner"),
+ dataIndex: "ownr_ln",
+ key: "ownr_ln",
+ ellipsis: true,
+ //sorter: true, // (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
+ //sortOrder: sortcolumn === "ownr_ln" && sortorder,
+ render: (text, record) => {
+ return record.ownerid ? (
+
+
+
+ ) : (
+
+
+
+ );
+ }
+ },
+
+ {
+ title: t("jobs.fields.status"),
+ dataIndex: "status",
+ key: "status",
+
+ ellipsis: true,
+ sorter: search?.search ? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses) : true,
+ sortOrder: sortcolumn === "status" && sortorder,
+ render: (text, record) => {
+ return record.status || t("general.labels.na");
+ },
+ filteredValue: filter?.status || null,
+ filters: bodyshop.md_ro_statuses.statuses.map((s) => {
+ return { text: s, 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 ? (
+
+ {`${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,
+ sorter: search?.search ? (a, b) => alphaSort(a.plate_no, b.plate_no) : true,
+ sortOrder: sortcolumn === "plate_no" && sortorder,
+ render: (text, record) => {
+ return record.plate_no ? record.plate_no : "";
+ }
+ },
+ {
+ title: t("jobs.fields.clm_no"),
+ dataIndex: "clm_no",
+ key: "clm_no",
+ ellipsis: true,
+ sorter: search?.search ? (a, b) => alphaSort(a.clm_no, b.clm_no) : true,
+ sortOrder: sortcolumn === "clm_no" && sortorder,
+ 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
+ },
+ {
+ title: t("jobs.fields.clm_total"),
+ dataIndex: "clm_total",
+ key: "clm_total",
+ sorter: search?.search ? (a, b) => a.clm_total - b.clm_total : true,
+ sortOrder: sortcolumn === "clm_total" && sortorder,
+ render: (text, record) => {
+ return record.clm_total ? (
+ {record.clm_total}
+ ) : (
+ t("general.labels.unknown")
+ );
+ }
+ },
+ {
+ title: t("jobs.fields.partsstatus"),
+ dataIndex: "partsstatus",
+ key: "partsstatus",
+ render: (text, record) =>
+ },
+ {
+ title: t("jobs.fields.comment"),
+ dataIndex: "comment",
+ key: "comment",
+ ellipsis: true
+ }
+ ];
+
+ const handleTableChange = (pagination, filters, sorter) => {
+ search.page = pagination.current;
+ search.sortcolumn = sorter.column && sorter.column.key;
+ search.sortorder = sorter.order;
+ if (filters.status) {
+ search.statusFilters = JSON.stringify(_.flattenDeep(filters.status));
+ } else {
+ delete search.statusFilters;
+ }
+ setFilter(filters);
+ history({ search: queryString.stringify(search) });
+ };
+
+ useEffect(() => {
+ if (search.search && search.search.trim() !== "") {
+ searchJobs();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ async function searchJobs(value) {
+ try {
+ setSearchLoading(true);
+ const searchData = await axios.post("/search", {
+ search: value || search.search,
+ index: "jobs"
+ });
+ setOpenSearchResults(searchData.data.hits.hits.map((s) => s._source));
+ } catch (error) {
+ console.log("Error while fetching search results", error);
+ } finally {
+ setSearchLoading(false);
+ }
+ }
+
+ return (
+
+ {search.search && (
+ <>
+
+ {t("general.labels.searchresults", { search: search.search })}
+
+
+ >
+ )}
+
+ {
+ search.search = value;
+ history({ search: queryString.stringify(search) });
+ searchJobs(value);
+ }}
+ loading={loading || searchLoading}
+ enterButton
+ />
+
+ }
+ >
+
+
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SimplifiedPartsJobsListComponent);
diff --git a/client/src/components/simplified-parts-jobs-list/simplified-parts-jobs-list.container.jsx b/client/src/components/simplified-parts-jobs-list/simplified-parts-jobs-list.container.jsx
new file mode 100644
index 000000000..916108a80
--- /dev/null
+++ b/client/src/components/simplified-parts-jobs-list/simplified-parts-jobs-list.container.jsx
@@ -0,0 +1,53 @@
+import { useQuery } from "@apollo/client";
+import queryString from "query-string";
+import { connect } from "react-redux";
+import { useLocation } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import AlertComponent from "../../components/alert/alert.component";
+import { QUERY_SIMPLIFIED_PARTS_PAGINATED_STATUS_FILTERED } from "../../graphql/jobs.queries";
+import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
+import { pageLimit } from "../../utils/config";
+import SimplifiedPartsJobsListComponent from "./simplified-parts-jobs-list.component";
+
+const mapStateToProps = createStructuredSelector({
+ //bodyshop: selectBodyshop,
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
+ setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
+});
+
+export function SimplifiedPartsJobsListContainer({ setBreadcrumbs, setSelectedHeader }) {
+ const searchParams = queryString.parse(useLocation().search);
+ const { page, sortcolumn, sortorder, statusFilters } = searchParams;
+
+ const { loading, error, data, refetch } = useQuery(QUERY_SIMPLIFIED_PARTS_PAGINATED_STATUS_FILTERED, {
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only",
+ variables: {
+ offset: page ? (page - 1) * pageLimit : 0,
+ limit: pageLimit,
+ ...(statusFilters ? { statusList: JSON.parse(statusFilters) } : {}),
+ order: [
+ {
+ [sortcolumn || "ro_number"]:
+ sortorder && sortorder !== "false" ? (sortorder === "descend" ? "desc" : "asc") : "desc"
+ }
+ ]
+ }
+ });
+
+ if (error) return ;
+ return (
+
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SimplifiedPartsJobsListContainer);
diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js
index 68c8adfbe..119263c01 100644
--- a/client/src/graphql/jobs.queries.js
+++ b/client/src/graphql/jobs.queries.js
@@ -1836,6 +1836,51 @@ export const QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED = gql`
}
}
`;
+export const QUERY_SIMPLIFIED_PARTS_PAGINATED_STATUS_FILTERED = gql`
+ query QUERY_ALL_JOBS_PAGINATED_STATUS_FILTERED(
+ $offset: Int
+ $limit: Int
+ $order: [jobs_order_by!]
+ $statusList: [String!]
+ ) {
+ jobs(offset: $offset, limit: $limit, order_by: $order, where: { status: { _in: $statusList } }) {
+ comment
+ ownr_fn
+ ownr_ln
+ ownr_co_nm
+ ownr_ph1
+ ownr_ph2
+ plate_no
+ plate_st
+ v_vin
+ v_model_yr
+ v_model_desc
+ v_make_desc
+ v_color
+ vehicleid
+ id
+ ins_co_nm
+ clm_no
+ clm_total
+ owner_owing
+ ro_number
+ po_number
+ status
+ updated_at
+ ded_amt
+ joblines_status {
+ count
+ part_type
+ status
+ }
+ }
+ jobs_aggregate(where: { status: { _in: $statusList } }) {
+ aggregate {
+ count(distinct: true)
+ }
+ }
+ }
+`;
export const QUERY_JOB_CLOSE_DETAILS = gql`
query QUERY_JOB_CLOSE_DETAILS($id: uuid!) {
diff --git a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx
index 6afe3ba15..7b797ee31 100644
--- a/client/src/pages/jobs-detail/jobs-detail.page.component.jsx
+++ b/client/src/pages/jobs-detail/jobs-detail.page.component.jsx
@@ -487,7 +487,7 @@ export function JobsDetailPage({
export default connect(mapStateToProps, mapDispatchToProps)(JobsDetailPage);
-const transformJobToForm = (job) => {
+export const transformJobToForm = (job) => {
const transformedJob = { ...job };
transformedJob.parts_tax_rates = Object.keys(transformedJob.parts_tax_rates).reduce((acc, parttype) => {
diff --git a/client/src/pages/manage/manage.page.component.jsx b/client/src/pages/manage/manage.page.component.jsx
index d8a9b4b84..bd2657818 100644
--- a/client/src/pages/manage/manage.page.component.jsx
+++ b/client/src/pages/manage/manage.page.component.jsx
@@ -23,7 +23,6 @@ import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-st
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
import UpdateAlert from "../../components/update-alert/update-alert.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
-import "./manage.page.styles.scss";
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
import { selectAlerts } from "../../redux/application/application.selectors.js";
import { addAlerts } from "../../redux/application/application.actions.js";
diff --git a/client/src/pages/manage/manage.page.styles.scss b/client/src/pages/manage/manage.page.styles.scss
deleted file mode 100644
index 9084e96bb..000000000
--- a/client/src/pages/manage/manage.page.styles.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.content-container {
- padding: 1rem;
-}
-
-.layout-container {
- // height: 100vh;
- // overflow-y: auto;
-}
diff --git a/client/src/pages/simplified-parts-jobs-detail/simplified-parts-jobs-detail.component.jsx b/client/src/pages/simplified-parts-jobs-detail/simplified-parts-jobs-detail.component.jsx
new file mode 100644
index 000000000..faff65ce0
--- /dev/null
+++ b/client/src/pages/simplified-parts-jobs-detail/simplified-parts-jobs-detail.component.jsx
@@ -0,0 +1,214 @@
+import { BarsOutlined, PrinterFilled, SyncOutlined, ToolFilled } from "@ant-design/icons";
+import { PageHeader } from "@ant-design/pro-layout";
+import { useQuery } from "@apollo/client";
+import { Button, Divider, Form, Space, Tabs } from "antd";
+import Axios from "axios";
+import _ from "lodash";
+import queryString from "query-string";
+import { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { useLocation, useNavigate } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component.jsx";
+import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container.jsx";
+import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx";
+import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component.jsx";
+import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component.jsx";
+import JobsDetailHeaderActions from "../../components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx";
+import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component.jsx";
+import JobsDetailPliContainer from "../../components/jobs-detail-pli/jobs-detail-pli.container.jsx";
+import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
+import { useSocket } from "../../contexts/SocketIO/useSocket.js";
+import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries.js";
+import { insertAuditTrail } from "../../redux/application/application.actions.js";
+import { selectJobReadOnly } from "../../redux/application/application.selectors.js";
+import { setModalContext } from "../../redux/modals/modals.actions.js";
+import { selectBodyshop } from "../../redux/user/user.selectors.js";
+import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
+import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
+import dayjs from "../../utils/day.js";
+import UndefinedToNull from "../../utils/undefinedtonull.js";
+import { transformJobToForm } from "../jobs-detail/jobs-detail.page.component.jsx";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop,
+ jobRO: selectJobReadOnly
+});
+const mapDispatchToProps = (dispatch) => ({
+ setPrintCenterContext: (context) =>
+ dispatch(
+ setModalContext({
+ context: context,
+ modal: "printCenter"
+ })
+ ),
+ insertAuditTrail: ({ jobid, operation, type }) =>
+ dispatch(
+ insertAuditTrail({
+ jobid,
+ operation,
+ type
+ })
+ )
+});
+
+export function SimplifiedPartsJobDetailComponent({
+ bodyshop,
+ setPrintCenterContext,
+ jobRO,
+ job,
+ mutationUpdateJob,
+ insertAuditTrail,
+ refetch
+}) {
+ const { t } = useTranslation();
+ const [form] = Form.useForm();
+ const history = useNavigate();
+ const [loading, setLoading] = useState(false);
+ const search = queryString.parse(useLocation().search);
+ const formItemLayout = {
+ layout: "vertical"
+ };
+ const billsQuery = useQuery(QUERY_PARTS_BILLS_BY_JOBID, {
+ variables: { jobid: job.id },
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
+ const notification = useNotification();
+ const { scenarioNotificationsOn } = useSocket();
+
+ useEffect(() => {
+ //form.setFieldsValue(transormJobToForm(job));
+ form.resetFields();
+ }, [form, job]);
+
+ const handleBillOnRowClick = (record) => {
+ if (record) {
+ if (record.id) {
+ search.billid = record.id;
+ history({ search: queryString.stringify(search) });
+ }
+ } else {
+ delete search.billid;
+ history({ search: queryString.stringify(search) });
+ }
+ };
+
+ const handlePartsOrderOnRowClick = (record) => {
+ if (record) {
+ if (record.id) {
+ search.partsorderid = record.id;
+ history({ search: queryString.stringify(search) });
+ }
+ } else {
+ delete search.partsorderid;
+ history({ search: queryString.stringify(search) });
+ }
+ };
+
+ const handlePartsDispatchOnRowClick = (record) => {
+ if (record) {
+ if (record.id) {
+ search.partsdispatchid = record.id;
+ history.push({ search: queryString.stringify(search) });
+ }
+ } else {
+ delete search.partsdispatchid;
+ history.push({ search: queryString.stringify(search) });
+ }
+ };
+
+ const menuExtra = (
+
+
+
+
+
+
+
+
+ );
+
+ return (
+
+
+
+
{job.ro_number || t("general.labels.na")}} extra={menuExtra} />
+
+
+
+
+ history({ search: `?tab=${key}` })}
+ tabBarStyle={{ fontWeight: "bold", borderBottom: "10px" }}
+ items={[
+ {
+ key: "repairdata",
+ icon: ,
+ id: "job-details-repairdata",
+ label: t("menus.jobsdetail.repairdata"),
+ forceRender: true,
+ children: (
+
+ )
+ },
+
+ {
+ key: "partssublet",
+ id: "job-details-partssublet",
+ icon: ,
+ label: t("menus.jobsdetail.partssublet"),
+ children: (
+
+ )
+ }
+ ]}
+ />
+
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SimplifiedPartsJobDetailComponent);
diff --git a/client/src/pages/simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx b/client/src/pages/simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx
new file mode 100644
index 000000000..2728118d0
--- /dev/null
+++ b/client/src/pages/simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx
@@ -0,0 +1,103 @@
+import { useQuery } from "@apollo/client";
+import { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { useParams } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import AlertComponent from "../../components/alert/alert.component";
+import SpinComponent from "../../components/loading-spinner/loading-spinner.component";
+import NotFound from "../../components/not-found/not-found.component";
+import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
+import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
+import { GET_JOB_BY_PK } from "../../graphql/jobs.queries";
+import {
+ addRecentItem,
+ setBreadcrumbs,
+ setJobReadOnly,
+ setSelectedHeader
+} from "../../redux/application/application.actions";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { CreateRecentItem } from "../../utils/create-recent-item";
+import InstanceRenderManager from "../../utils/instanceRenderMgr";
+import IsJobReadOnly from "../../utils/jobReadOnly";
+import SimplifiedPartsJobsDetailComponent from "./simplified-parts-jobs-detail.component";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
+ addRecentItem: (item) => dispatch(addRecentItem(item)),
+ setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
+ setJobReadOnly: (bool) => dispatch(setJobReadOnly(bool))
+});
+
+function SimplifiedPartsJobsDetailContainer({ setBreadcrumbs, addRecentItem, setSelectedHeader, setJobReadOnly }) {
+ const { jobId } = useParams();
+ const { t } = useTranslation();
+
+ const { loading, error, data, refetch } = useQuery(GET_JOB_BY_PK, {
+ variables: { id: jobId },
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
+
+ useEffect(() => {
+ setSelectedHeader("activejobs");
+ document.title = loading
+ ? InstanceRenderManager({
+ imex: t("titles.imexonline"),
+ rome: t("titles.romeonline")
+ })
+ : error
+ ? InstanceRenderManager({
+ imex: t("titles.imexonline"),
+ rome: t("titles.romeonline")
+ })
+ : t("titles.jobsdetail", {
+ app: InstanceRenderManager({
+ imex: "$t(titles.imexonline)",
+ rome: "$t(titles.romeonline)"
+ }),
+ ro_number: (data.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na")
+ });
+ setBreadcrumbs([
+ { link: "/parts/jobs", label: t("titles.bc.jobs") },
+ {
+ link: `/parts/jobs/${jobId}`,
+ label: t("titles.bc.jobs-detail", {
+ number: (data && data.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na")
+ })
+ }
+ ]);
+
+ if (data && data.jobs_by_pk) {
+ setJobReadOnly(IsJobReadOnly(data.jobs_by_pk));
+
+ addRecentItem(
+ CreateRecentItem(
+ jobId,
+ "job",
+
+ `${data.jobs_by_pk.ro_number || t("general.labels.na")} | ${OwnerNameDisplayFunction(data.jobs_by_pk)}`,
+ `/manage/jobs/${jobId}`
+ )
+ );
+ }
+ }, [loading, data, t, error, setBreadcrumbs, jobId, addRecentItem, setSelectedHeader, setJobReadOnly]);
+
+ if (loading) return ;
+ if (error) return ;
+ if (!data.jobs_by_pk) return ;
+
+ return data.jobs_by_pk ? (
+
+
+
+ ) : (
+
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SimplifiedPartsJobsDetailContainer);
diff --git a/client/src/pages/simplified-parts-jobs/simplified-parts-jobs.page.jsx b/client/src/pages/simplified-parts-jobs/simplified-parts-jobs.page.jsx
new file mode 100644
index 000000000..ed7215e69
--- /dev/null
+++ b/client/src/pages/simplified-parts-jobs/simplified-parts-jobs.page.jsx
@@ -0,0 +1,36 @@
+import { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
+import SimplifiedPartsJobsListContainer from "../../components/simplified-parts-jobs-list/simplified-parts-jobs-list.container";
+import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
+import InstanceRenderManager from "../../utils/instanceRenderMgr";
+
+const mapDispatchToProps = (dispatch) => ({
+ setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
+ setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
+});
+
+export function SimplifiedPartsJobsPage({ setBreadcrumbs, setSelectedHeader }) {
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ document.title = t("titles.simplified-parts-jobs", {
+ app: InstanceRenderManager({
+ imex: "$t(titles.imexonline)",
+ rome: "$t(titles.romeonline)"
+ })
+ });
+ setSelectedHeader("parts-queue");
+ setBreadcrumbs([{ link: "/parts", label: t("titles.bc.simplified-parts-jobs") }]);
+ }, [setBreadcrumbs, t, setSelectedHeader]);
+
+ return (
+
+
+ {/* */}
+
+ );
+}
+
+export default connect(null, mapDispatchToProps)(SimplifiedPartsJobsPage);
diff --git a/client/src/pages/simplified-parts/simplified-parts.page.component.jsx b/client/src/pages/simplified-parts/simplified-parts.page.component.jsx
new file mode 100644
index 000000000..a7a30c198
--- /dev/null
+++ b/client/src/pages/simplified-parts/simplified-parts.page.component.jsx
@@ -0,0 +1,258 @@
+import { AlertOutlined, BulbOutlined } from "@ant-design/icons";
+import * as Sentry from "@sentry/react";
+import { Button, FloatButton, Layout, Space, Spin } from "antd";
+import { lazy, Suspense, useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import { Link, Route, Routes } from "react-router-dom";
+import { createStructuredSelector } from "reselect";
+import BreadCrumbs from "../../components/breadcrumbs/breadcrumbs.component.jsx";
+import ConflictComponent from "../../components/conflict/conflict.component.jsx";
+import ErrorBoundary from "../../components/error-boundary/error-boundary.component.jsx";
+import HeaderContainer from "../../components/header/header.container.jsx";
+import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component.jsx";
+import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container.jsx";
+import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component.jsx";
+import UpdateAlert from "../../components/update-alert/update-alert.component.jsx";
+import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
+import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
+import { useSocket } from "../../contexts/SocketIO/useSocket.js";
+import { addAlerts } from "../../redux/application/application.actions.js";
+import { selectAlerts } from "../../redux/application/application.selectors.js";
+import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors.js";
+import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
+
+const SimplifiedPartsJobsPage = lazy(() => import("../simplified-parts-jobs/simplified-parts-jobs.page.jsx"));
+const SimplifiedPartsJobsDetailPage = lazy(
+ () => import("../simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx")
+);
+const ShopPage = lazy(() => import("../shop/shop.page.component.jsx"));
+const ShopVendorPageContainer = lazy(() => import("../shop-vendor/shop-vendor.page.container.jsx"));
+const EmailOverlayContainer = lazy(() => import("../../components/email-overlay/email-overlay.container.jsx"));
+const FeatureRequestPage = lazy(() => import("../feature-request/feature-request.page.jsx"));
+const JobCostingModal = lazy(() => import("../../components/job-costing-modal/job-costing-modal.container.jsx"));
+const ReportCenterModal = lazy(() => import("../../components/report-center-modal/report-center-modal.container.jsx"));
+const BillEnterModalContainer = lazy(() => import("../../components/bill-enter-modal/bill-enter-modal.container.jsx"));
+const Help = lazy(() => import("../help/help.page.jsx"));
+
+const { Content, Footer } = Layout;
+
+const mapStateToProps = createStructuredSelector({
+ conflict: selectInstanceConflict,
+ bodyshop: selectBodyshop,
+ alerts: selectAlerts
+});
+
+const ALERT_FILE_URL = InstanceRenderManager({
+ imex: "https://images.imex.online/alerts/alerts-imex.json",
+ rome: "https://images.imex.online/alerts/alerts-rome.json"
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ setAlerts: (alerts) => dispatch(addAlerts(alerts))
+});
+
+export function SimplifiedPartsPage({ conflict, bodyshop, alerts, setAlerts }) {
+ const { t } = useTranslation();
+ const { socket, clientId } = useSocket();
+ const notification = useNotification();
+
+ // State to track displayed alerts
+ const [displayedAlertIds, setDisplayedAlertIds] = useState([]);
+
+ // Fetch displayed alerts from localStorage on mount
+ useEffect(() => {
+ const displayedAlerts = JSON.parse(localStorage.getItem("displayedAlerts") || "[]");
+ setDisplayedAlertIds(displayedAlerts);
+ }, []);
+
+ // Fetch alerts from the JSON file and dispatch to Redux store
+ useEffect(() => {
+ const fetchAlerts = async () => {
+ try {
+ const response = await fetch(ALERT_FILE_URL);
+ const fetchedAlerts = await response.json();
+ setAlerts(fetchedAlerts);
+ } catch (error) {
+ console.warn("Error fetching alerts:", error.message);
+ }
+ };
+
+ fetchAlerts().catch((err) => `Error fetching Bodyshop Alerts: ${err?.message || ""}`);
+ }, [setAlerts]);
+
+ // Use useEffect to watch for new alerts
+ useEffect(() => {
+ if (alerts && Object.keys(alerts).length > 0) {
+ // Convert the alerts object into an array
+ const alertArray = Object.values(alerts);
+
+ // Filter out alerts that have already been dismissed
+ const newAlerts = alertArray.filter((alert) => !displayedAlertIds.includes(alert.id));
+
+ newAlerts.forEach((alert) => {
+ // Display the notification
+ notification.open({
+ key: "notification-alerts-" + alert.id,
+ message: alert.message,
+ description: alert.description,
+ type: alert.type || "info",
+ duration: 0,
+ closable: true,
+ onClose: () => {
+ // When the notification is closed, update displayed alerts state and localStorage
+ setDisplayedAlertIds((prevIds) => {
+ const updatedIds = [...prevIds, alert.id];
+ localStorage.setItem("displayedAlerts", JSON.stringify(updatedIds));
+ return updatedIds;
+ });
+ }
+ });
+ });
+ }
+ }, [alerts, displayedAlertIds, notification]);
+
+ useEffect(() => {
+ window.Canny("initChangelog", {
+ appID: "680bd2c7ee501290377f6686",
+ position: "top",
+ align: "left",
+ theme: "light" // options: light [default], dark, auto
+ });
+ }, []);
+
+ useEffect(() => {
+ document.title = InstanceRenderManager({
+ imex: t("titles.imexonline"),
+ rome: t("titles.romeonline")
+ });
+ }, [t]);
+
+ const AppRouteTable = (
+
+ }
+ >
+
+
+
+
+
+
+
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ }>
+
+
+ }
+ />
+ } />
+
+ }>
+
+
+ }
+ />
+
+
+ );
+
+ let PageContent;
+
+ if (conflict) PageContent = ;
+ else if (bodyshop && bodyshop.sub_status !== "active") PageContent = ;
+ else PageContent = AppRouteTable;
+
+ const broadcastMessage = () => {
+ if (socket && bodyshop && bodyshop.id) {
+ console.log(`Broadcasting message to bodyshop ${bodyshop.id}:`);
+ socket.emit("broadcast-to-bodyshop", bodyshop.id, `Hello from ${clientId}`);
+ }
+ };
+
+ return (
+ <>
+
+
+ {/* */}
+
+ } showDialog>
+ {PageContent}
+
+
+
+
+
+ >
+ );
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(SimplifiedPartsPage);
diff --git a/client/src/pages/simplified-parts/simplified-parts.page.container.jsx b/client/src/pages/simplified-parts/simplified-parts.page.container.jsx
new file mode 100644
index 000000000..b76323e10
--- /dev/null
+++ b/client/src/pages/simplified-parts/simplified-parts.page.container.jsx
@@ -0,0 +1,35 @@
+import { useQuery } from "@apollo/client";
+import { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { connect } from "react-redux";
+import AlertComponent from "../../components/alert/alert.component";
+import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
+import { QUERY_BODYSHOP } from "../../graphql/bodyshop.queries";
+import { setBodyshop } from "../../redux/user/user.actions";
+import SimplifiedPartsPage from "./simplified-parts.page.component";
+
+const mapDispatchToProps = (dispatch) => ({
+ setBodyshop: (bs) => dispatch(setBodyshop(bs))
+});
+
+function SimplifiedPartsPageContainer({ setBodyshop }) {
+ const { loading, error, data } = useQuery(QUERY_BODYSHOP, {
+ fetchPolicy: "network-only",
+ nextFetchPolicy: "network-only"
+ });
+
+ const { t } = useTranslation();
+
+ useEffect(() => {
+ if (data) {
+ setBodyshop(data.bodyshops[0] || { notfound: true });
+ }
+ }, [data, setBodyshop]);
+
+ if (loading) return ;
+ if (error) return ;
+
+ return ;
+}
+
+export default connect(null, mapDispatchToProps)(SimplifiedPartsPageContainer);