From 92369fceba42d8dbb29418bf11954008d9cda323 Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 11 Jun 2025 10:29:58 -0700 Subject: [PATCH] IO-3255 Initial parts management changes. --- client/src/App/App.jsx | 22 +- client/src/App/App.styles.scss | 4 + .../job-detail-lines/job-lines.component.jsx | 91 +++--- .../job-detail-lines/job-lines.container.jsx | 4 +- .../simplified-parts-jobs-list.component.jsx | 251 +++++++++++++++++ .../simplified-parts-jobs-list.container.jsx | 53 ++++ client/src/graphql/jobs.queries.js | 45 +++ .../jobs-detail.page.component.jsx | 2 +- .../pages/manage/manage.page.component.jsx | 1 - .../src/pages/manage/manage.page.styles.scss | 8 - ...simplified-parts-jobs-detail.component.jsx | 214 +++++++++++++++ ...simplified-parts-jobs-detail.container.jsx | 103 +++++++ .../simplified-parts-jobs.page.jsx | 36 +++ .../simplified-parts.page.component.jsx | 258 ++++++++++++++++++ .../simplified-parts.page.container.jsx | 35 +++ 15 files changed, 1054 insertions(+), 73 deletions(-) create mode 100644 client/src/components/simplified-parts-jobs-list/simplified-parts-jobs-list.component.jsx create mode 100644 client/src/components/simplified-parts-jobs-list/simplified-parts-jobs-list.container.jsx delete mode 100644 client/src/pages/manage/manage.page.styles.scss create mode 100644 client/src/pages/simplified-parts-jobs-detail/simplified-parts-jobs-detail.component.jsx create mode 100644 client/src/pages/simplified-parts-jobs-detail/simplified-parts-jobs-detail.container.jsx create mode 100644 client/src/pages/simplified-parts-jobs/simplified-parts-jobs.page.jsx create mode 100644 client/src/pages/simplified-parts/simplified-parts.page.component.jsx create mode 100644 client/src/pages/simplified-parts/simplified-parts.page.container.jsx 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} + + + +
+
+ + + + + +
+ {`${InstanceRenderManager({ + imex: t("titles.imexonline"), + rome: t("titles.romeonline") + })} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`} +
+ +
+ + Disclaimer & Notices + +
+
+
+ + ); +} + +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);