sendmaterialscosting
false
diff --git a/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx b/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx
index 0c950fba9..1cd3281f7 100644
--- a/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx
+++ b/client/src/components/dms-allocations-summary/dms-allocations-summary.component.jsx
@@ -113,6 +113,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
rowKey="center"
dataSource={allocationsSummary}
locale={{ emptyText: t("dms.labels.refreshallocations") }}
+ scroll={{ x: true }}
summary={() => {
const totals =
allocationsSummary &&
diff --git a/client/src/components/job-detail-cards/job-detail-cards.parts.component.jsx b/client/src/components/job-detail-cards/job-detail-cards.parts.component.jsx
index 9f3c32820..949d42eae 100644
--- a/client/src/components/job-detail-cards/job-detail-cards.parts.component.jsx
+++ b/client/src/components/job-detail-cards/job-detail-cards.parts.component.jsx
@@ -90,7 +90,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
.filter(onlyUnique)
.map((s) => {
return {
- text: s || "No Status*",
+ text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||
@@ -103,7 +103,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
);
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 2cadcf4e8..02dd2cda6 100644
--- a/client/src/components/job-detail-lines/job-lines.component.jsx
+++ b/client/src/components/job-detail-lines/job-lines.component.jsx
@@ -318,7 +318,7 @@ export function JobLinesComponent({
.filter(onlyUnique)
.map((s) => {
return {
- text: s || "No Status*",
+ text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||
diff --git a/client/src/components/job-parts-queue-count/job-parts-queue-count.component.jsx b/client/src/components/job-parts-queue-count/job-parts-queue-count.component.jsx
index 8fbee411c..c7cc3a115 100644
--- a/client/src/components/job-parts-queue-count/job-parts-queue-count.component.jsx
+++ b/client/src/components/job-parts-queue-count/job-parts-queue-count.component.jsx
@@ -1,8 +1,9 @@
import { useMemo } from "react";
-import { Col, Row, Tag, Tooltip } from "antd";
+import { Tag, Tooltip } from "antd";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
+import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -11,65 +12,67 @@ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
-export const DEFAULT_COL_LAYOUT = { xs: 24, sm: 24, md: 8, lg: 4, xl: 4, xxl: 4 };
-
export default connect(mapStateToProps, mapDispatchToProps)(JobPartsQueueCount);
-export function JobPartsQueueCount({ bodyshop, parts, defaultColLayout = DEFAULT_COL_LAYOUT }) {
+export function JobPartsQueueCount({ bodyshop, parts }) {
+ const { t } = useTranslation();
const partsStatus = useMemo(() => {
if (!parts) return null;
+ const statusKeys = ["default_bo", "default_ordered", "default_received", "default_returned"];
return parts.reduce(
(acc, val) => {
if (val.part_type === "PAS" || val.part_type === "PASL") return acc;
acc.total = acc.total + val.count;
acc[val.status] = acc[val.status] + val.count;
-
return acc;
},
{
total: 0,
null: 0,
- [bodyshop.md_order_statuses.default_bo]: 0,
- [bodyshop.md_order_statuses.default_ordered]: 0,
- [bodyshop.md_order_statuses.default_received]: 0,
- [bodyshop.md_order_statuses.default_returned]: 0
+ ...Object.fromEntries(statusKeys.map((key) => [bodyshop.md_order_statuses[key], 0]))
}
);
}, [bodyshop, parts]);
if (!parts) return null;
return (
-
-
-
- {partsStatus.total}
-
-
-
-
- {partsStatus["null"]}
-
-
-
-
- {partsStatus[bodyshop.md_order_statuses.default_ordered]}
-
-
-
-
- {partsStatus[bodyshop.md_order_statuses.default_received]}
-
-
-
-
- {partsStatus[bodyshop.md_order_statuses.default_returned]}
-
-
-
-
- {partsStatus[bodyshop.md_order_statuses.default_bo]}
-
-
-
+
+
+ {partsStatus.total}
+
+
+
+ {partsStatus["null"]}
+
+
+
+
+ {partsStatus[bodyshop.md_order_statuses.default_bo]}
+
+
+
+
+ {partsStatus[bodyshop.md_order_statuses.default_ordered]}
+
+
+
+
+ {partsStatus[bodyshop.md_order_statuses.default_received]}
+
+
+
+
+ {partsStatus[bodyshop.md_order_statuses.default_returned]}
+
+
+
);
}
diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx
index 49439fa00..bde88d161 100644
--- a/client/src/components/jobs-list/jobs-list.component.jsx
+++ b/client/src/components/jobs-list/jobs-list.component.jsx
@@ -166,7 +166,7 @@ export function JobsList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
- text: s || "No Status*",
+ text: s || t("dashboard.errors.status"),
value: [s]
};
})
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
index b88b713c2..b218b46f4 100644
--- a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx
+++ b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx
@@ -165,7 +165,7 @@ export function JobsReadyList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
- text: s || "No Status*",
+ text: s || t("dashboard.errors.status"),
value: [s]
};
})
diff --git a/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx b/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx
index 7d108954c..066e937e8 100644
--- a/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx
+++ b/client/src/components/parts-queue-card/parts-queue-job-lines.component.jsx
@@ -145,7 +145,7 @@ export function PartsQueueJobLinesComponent({ loading, jobLines }) {
.filter(onlyUnique)
.map((s) => {
return {
- text: s || "No Status*",
+ text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||
diff --git a/client/src/components/parts-queue-list/parts-queue.list.component.jsx b/client/src/components/parts-queue-list/parts-queue.list.component.jsx
index bb4b72b79..a947eaefa 100644
--- a/client/src/components/parts-queue-list/parts-queue.list.component.jsx
+++ b/client/src/components/parts-queue-list/parts-queue.list.component.jsx
@@ -171,7 +171,7 @@ export function PartsQueueListComponent({ bodyshop }) {
filters:
bodyshop.md_ro_statuses.active_statuses.map((s) => {
return {
- text: s || "No Status*",
+ text: s || t("dashboard.errors.status"),
value: [s]
};
}) || [],
diff --git a/client/src/components/production-list-columns/production-list-columns.data.jsx b/client/src/components/production-list-columns/production-list-columns.data.jsx
index f26733c36..165e15c1d 100644
--- a/client/src/components/production-list-columns/production-list-columns.data.jsx
+++ b/client/src/components/production-list-columns/production-list-columns.data.jsx
@@ -34,8 +34,9 @@ const getEmployeeName = (employeeId, employees) => {
return employee ? `${employee.first_name} ${employee.last_name}` : "";
};
-const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
+const productionListColumnsData = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
const { Enhanced_Payroll } = treatments;
+
return [
{
title: i18n.t("jobs.actions.viewdetail"),
@@ -313,7 +314,7 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
activeStatuses
?.map((s) => {
return {
- text: s || "No Status*",
+ text: s || i18n.t("dashboard.errors.status"),
value: [s]
};
})
@@ -584,4 +585,4 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
}
];
};
-export default r;
+export default productionListColumnsData;
diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx
index 165e133a2..acf553bb5 100644
--- a/client/src/components/shop-info/shop-info.general.component.jsx
+++ b/client/src/components/shop-info/shop-info.general.component.jsx
@@ -425,7 +425,15 @@ export function ShopInfoGeneral({ form, bodyshop }) {
]
: [])
]
- : [])
+ : []),
+
+
+
]}
null}>
diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx
index d0aae4ff3..53f780c6b 100644
--- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx
+++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx
@@ -138,6 +138,15 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
)}
+ {bodyshop.pbs_serialnumber && (
+
+
+
+ )}
{bodyshop.pbs_serialnumber && (
{
+ filters: bodyshop?.md_ro_statuses?.parts_statuses?.map((s) => {
return { text: s, value: [s] };
}),
onFilter: (value, record) => value.includes(record.status)
diff --git a/client/src/components/tech-lookup-jobs-list/tech-lookup-jobs-list.component.jsx b/client/src/components/tech-lookup-jobs-list/tech-lookup-jobs-list.component.jsx
index 0febbb24d..dfba5a133 100644
--- a/client/src/components/tech-lookup-jobs-list/tech-lookup-jobs-list.component.jsx
+++ b/client/src/components/tech-lookup-jobs-list/tech-lookup-jobs-list.component.jsx
@@ -111,7 +111,7 @@ export function TechLookupJobsList({ bodyshop }) {
.filter(onlyUnique)
.map((s) => {
return {
- text: s || "No Status*",
+ text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||
diff --git a/client/src/firebase/firebase.utils.js b/client/src/firebase/firebase.utils.js
index d3964eaac..8f1518504 100644
--- a/client/src/firebase/firebase.utils.js
+++ b/client/src/firebase/firebase.utils.js
@@ -4,7 +4,7 @@ import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
import { getFirestore } from "@firebase/firestore";
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
import { store } from "../redux/store";
-import * as amplitude from '@amplitude/analytics-browser';
+//import * as amplitude from '@amplitude/analytics-browser';
import posthog from 'posthog-js'
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
@@ -91,14 +91,14 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
// dbevent: false,
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
// });
- console.log(
- "%c[Analytics]",
- "background-color: green ;font-weight:bold;",
- eventName,
- eventParams
- );
+ // console.log(
+ // "%c[Analytics]",
+ // "background-color: green ;font-weight:bold;",
+ // eventName,
+ // eventParams
+ // );
logEvent(analytics, eventName, eventParams);
- amplitude.track(eventName, eventParams);
+ //amplitude.track(eventName, eventParams);
posthog.capture(eventName, eventParams);
} finally {
diff --git a/client/src/graphql/bills.queries.js b/client/src/graphql/bills.queries.js
index a9c3433fa..03be22e61 100644
--- a/client/src/graphql/bills.queries.js
+++ b/client/src/graphql/bills.queries.js
@@ -42,7 +42,7 @@ export const QUERY_ALL_BILLS_PAGINATED = gql`
ro_number
}
}
- bills_aggregate {
+ bills_aggregate(where: $where) {
aggregate {
count(distinct: true)
}
diff --git a/client/src/index.jsx b/client/src/index.jsx
index 610aa8aad..557ac9c43 100644
--- a/client/src/index.jsx
+++ b/client/src/index.jsx
@@ -14,7 +14,7 @@ import { persistor, store } from "./redux/store";
import reportWebVitals from "./reportWebVitals";
import "./translations/i18n";
import "./utils/CleanAxios";
-import * as amplitude from "@amplitude/analytics-browser";
+//import * as amplitude from "@amplitude/analytics-browser";
import { PostHogProvider } from "posthog-js/react";
import posthog from "posthog-js";
@@ -26,23 +26,23 @@ registerSW({ immediate: true });
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
-amplitude.init(import.meta.env.VITE_APP_AMP_KEY, {
- defaultTracking: true,
- serverUrl: import.meta.env.VITE_APP_AMP_URL
- // {
- // attribution: {
- // excludeReferrers: true,
- // initialEmptyValue: true,
- // resetSessionOnNewCampaign: true,
- // },
- // fileDownloads: true,
- // formInteractions: true,
- // pageViews: {
- // trackHistoryChanges: 'all'
- // },
- // sessions: true
- // }
-});
+// amplitude.init(import.meta.env.VITE_APP_AMP_KEY, {
+// defaultTracking: true,
+// serverUrl: import.meta.env.VITE_APP_AMP_URL
+// // {
+// // attribution: {
+// // excludeReferrers: true,
+// // initialEmptyValue: true,
+// // resetSessionOnNewCampaign: true,
+// // },
+// // fileDownloads: true,
+// // formInteractions: true,
+// // pageViews: {
+// // trackHistoryChanges: 'all'
+// // },
+// // sessions: true
+// // }
+// });
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
autocapture: false,
diff --git a/client/src/pages/bills/bills.page.component.jsx b/client/src/pages/bills/bills.page.component.jsx
index e3d2f55b0..0fb17a978 100644
--- a/client/src/pages/bills/bills.page.component.jsx
+++ b/client/src/pages/bills/bills.page.component.jsx
@@ -16,7 +16,6 @@ import { DateFormatter } from "../../utils/DateFormatter";
import { TemplateList } from "../../utils/TemplateConstants";
import { pageLimit } from "../../utils/config";
import { alphaSort, dateSort } from "../../utils/sorters";
-import useLocalStorage from "../../utils/useLocalStorage";
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
import { logImEXEvent } from "../../firebase/firebase.utils";
@@ -24,16 +23,12 @@ const mapDispatchToProps = (dispatch) => ({
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" }))
});
-export function BillsListPage({ loading, data, refetch, total, setBillEnterContext }) {
+export function BillsListPage({ loading, data, refetch, total, setBillEnterContext, handleTableChange, sortedInfo }) {
const search = queryString.parse(useLocation().search);
const [openSearchResults, setOpenSearchResults] = useState([]);
const [searchLoading, setSearchLoading] = useState(false);
const { page } = search;
const history = useNavigate();
- const [state, setState] = useLocalStorage("bills_list_sort", {
- sortedInfo: {},
- filteredInfo: { vendorname: [] }
- });
const Templates = TemplateList("bill");
const { t } = useTranslation();
@@ -50,7 +45,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
}),
filters: (vendorsData?.vendors || []).map((v) => ({ text: v.name, value: v.id })),
filteredValue: search.vendorIds ? search.vendorIds.split(",") : null,
- sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "vendorname" && sortedInfo.order,
render: (text, record) => {record.vendor.name}
},
{
@@ -58,7 +53,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "invoice_number",
key: "invoice_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
- sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order
+ sortOrder: sortedInfo.columnKey === "invoice_number" && sortedInfo.order
},
{
title: t("jobs.fields.ro_number"),
@@ -68,7 +63,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
sortObject: (order) => ({
job: { ro_number: order === "descend" ? "desc" : "asc" }
}),
- sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "ro_number" && sortedInfo.order,
render: (text, record) => record.job && {record.job.ro_number}
},
{
@@ -76,7 +71,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "date",
key: "date",
sorter: (a, b) => dateSort(a.date, b.date),
- sortOrder: state.sortedInfo.columnKey === "date" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "date" && sortedInfo.order,
render: (text, record) => {record.date}
},
{
@@ -84,7 +79,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "total",
key: "total",
sorter: (a, b) => a.total - b.total,
- sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "total" && sortedInfo.order,
render: (text, record) => {record.total}
},
{
@@ -92,7 +87,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "is_credit_memo",
key: "is_credit_memo",
sorter: (a, b) => a.is_credit_memo - b.is_credit_memo,
- sortOrder: state.sortedInfo.columnKey === "is_credit_memo" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "is_credit_memo" && sortedInfo.order,
render: (text, record) =>
},
{
@@ -100,7 +95,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
dataIndex: "exported",
key: "exported",
sorter: (a, b) => a.exported - b.exported,
- sortOrder: state.sortedInfo.columnKey === "exported" && state.sortedInfo.order,
+ sortOrder: sortedInfo.columnKey === "exported" && sortedInfo.order,
render: (text, record) =>
},
{
@@ -164,37 +159,7 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
}
];
- const handleTableChange = (pagination, filters, sorter) => {
- setState({
- sortedInfo: sorter,
- filteredInfo: { ...state.filteredInfo, vendorname: filters.vendorname || [] }
- });
-
- search.page = pagination.current;
- if (filters.vendorname && filters.vendorname.length) {
- search.vendorIds = filters.vendorname.join(",");
- } else {
- delete search.vendorIds;
- }
- if (sorter && sorter.column && sorter.column.sortObject) {
- search.searchObj = JSON.stringify(sorter.column.sortObject(sorter.order));
- delete search.sortcolumn;
- delete search.sortorder;
- } else {
- delete search.searchObj;
- search.sortcolumn = sorter.order ? sorter.columnKey : null;
- search.sortorder = sorter.order;
- }
- history({ search: queryString.stringify(search) });
- logImEXEvent("bills_list_sort_filter", { pagination, filters, sorter });
- };
-
- useEffect(() => {
- if (!search.vendorIds && state.filteredInfo.vendorname && state.filteredInfo.vendorname.length) {
- search.vendorIds = state.filteredInfo.vendorname.join(",");
- history({ search: queryString.stringify(search) });
- }
- }, []);
+ // (State & URL handling moved to container - Option C)
useEffect(() => {
if (search.search && search.search.trim() !== "") {
diff --git a/client/src/pages/bills/bills.page.container.jsx b/client/src/pages/bills/bills.page.container.jsx
index 68c5d4c01..f4a7a1859 100644
--- a/client/src/pages/bills/bills.page.container.jsx
+++ b/client/src/pages/bills/bills.page.container.jsx
@@ -3,13 +3,14 @@ import queryString from "query-string";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
-import { useLocation } from "react-router-dom";
+import { useLocation, useNavigate } from "react-router-dom";
import AlertComponent from "../../components/alert/alert.component";
import BillDetailEditContainer from "../../components/bill-detail-edit/bill-detail-edit.container";
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
import { QUERY_ALL_BILLS_PAGINATED } from "../../graphql/bills.queries";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import BillsPageComponent from "./bills.page.component";
+import useLocalStorage from "../../utils/useLocalStorage";
import { pageLimit } from "../../utils/config";
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
@@ -23,7 +24,9 @@ const mapDispatchToProps = (dispatch) => ({
export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
- const searchParams = queryString.parse(useLocation().search);
+ const location = useLocation();
+ const history = useNavigate();
+ const searchParams = queryString.parse(location.search);
const { page, sortcolumn, sortorder, searchObj } = searchParams;
useEffect(() => {
@@ -37,6 +40,12 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
setBreadcrumbs([{ link: "/manage/bills", label: t("titles.bc.bills-list") }]);
}, [t, setBreadcrumbs, setSelectedHeader]);
+ // Persisted table state (sorting & filtering)
+ const [persistState, setPersistState] = useLocalStorage("bills_list_sort", {
+ sortedInfo: {},
+ filteredInfo: { vendorname: [] }
+ });
+
const { loading, error, data, refetch } = useQuery(QUERY_ALL_BILLS_PAGINATED, {
fetchPolicy: "network-only",
nextFetchPolicy: "network-only",
@@ -54,6 +63,90 @@ export function BillsPageContainer({ setBreadcrumbs, setSelectedHeader }) {
}
});
+ const handleTableChange = (pagination, filters, sorter) => {
+ const search = queryString.parse(location.search);
+
+ const vendorArr = filters?.vendorname ?? [];
+ const newVendorIds = vendorArr.length ? vendorArr.join(",") : undefined;
+ const vendorFilterChanged = search.vendorIds !== newVendorIds;
+
+ search.page = vendorFilterChanged || !search.page ? 1 : pagination.current;
+ newVendorIds ? (search.vendorIds = newVendorIds) : delete search.vendorIds;
+
+ const { columnKey, order, column } = sorter || {};
+ if (column?.sortObject) {
+ search.searchObj = JSON.stringify(column.sortObject(order));
+ delete search.sortcolumn;
+ delete search.sortorder;
+ } else {
+ delete search.searchObj;
+ search.sortcolumn = order ? columnKey : null;
+ search.sortorder = order ?? null; // keep explicit null to mirror prior behavior
+ }
+
+ setPersistState({
+ sortedInfo: sorter || {},
+ filteredInfo: { vendorname: vendorArr }
+ });
+
+ history({ search: queryString.stringify(search) });
+ };
+
+ useEffect(() => {
+ const search = queryString.parse(location.search);
+ let changed = false;
+
+ const vendorPersisted = persistState.filteredInfo.vendorname || [];
+ if (!search.vendorIds && vendorPersisted.length) {
+ search.vendorIds = vendorPersisted.join(",");
+ search.page = 1; // reset page when injecting filter
+ changed = true;
+ }
+
+ const { sortedInfo } = persistState;
+ if (!search.searchObj && !search.sortcolumn && sortedInfo?.order) {
+ const { columnKey, order } = sortedInfo;
+ if (columnKey) {
+ const dir = order === "descend" ? "desc" : "asc";
+ if (columnKey === "vendorname") {
+ search.searchObj = JSON.stringify({ vendor: { name: dir } });
+ } else if (columnKey === "ro_number") {
+ search.searchObj = JSON.stringify({ job: { ro_number: dir } });
+ } else {
+ search.sortcolumn = columnKey;
+ search.sortorder = order;
+ }
+ changed = true;
+ }
+ }
+
+ if (changed) {
+ history({ search: queryString.stringify(search) });
+ return;
+ }
+
+ const hasPersistSort = !!sortedInfo?.order;
+ const hasUrlSort = !!(search.searchObj || (search.sortcolumn && search.sortorder));
+ if (!hasPersistSort && hasUrlSort) {
+ let derived = {};
+ if (search.searchObj) {
+ try {
+ const o = JSON.parse(search.searchObj);
+ if (o.vendor?.name) {
+ derived = { columnKey: "vendorname", order: o.vendor.name === "desc" ? "descend" : "ascend" };
+ } else if (o.job?.ro_number) {
+ derived = { columnKey: "ro_number", order: o.job.ro_number === "desc" ? "descend" : "ascend" };
+ }
+ } catch {
+ /* ignore parse errors */
+ }
+ } else {
+ derived = { columnKey: search.sortcolumn, order: search.sortorder };
+ }
+ if (derived.order) setPersistState((prev) => ({ ...prev, sortedInfo: derived }));
+ }
+ }, [location.search]);
+
if (error) return ;
return (
diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx
index 7b68952a7..496ea81f2 100644
--- a/client/src/pages/dms/dms.container.jsx
+++ b/client/src/pages/dms/dms.container.jsx
@@ -50,7 +50,7 @@ export const socket = SocketIO(
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
const { t } = useTranslation();
- const [logLevel, setLogLevel] = useState("DEBUG");
+ const [logLevel, setLogLevel] = useState(determineDmsType(bodyshop) === "pbs" ? "INFO" : "DEBUG");
const history = useNavigate();
const [logs, setLogs] = useState([]);
const search = queryString.parse(useLocation().search);
diff --git a/client/src/pages/tech-assigned-prod-jobs/tech-assigned-prod-jobs.component.jsx b/client/src/pages/tech-assigned-prod-jobs/tech-assigned-prod-jobs.component.jsx
index 1849b9b63..5cc1f308d 100644
--- a/client/src/pages/tech-assigned-prod-jobs/tech-assigned-prod-jobs.component.jsx
+++ b/client/src/pages/tech-assigned-prod-jobs/tech-assigned-prod-jobs.component.jsx
@@ -97,7 +97,7 @@ export function TechAssignedProdJobs({ setTimeTicketTaskContext, technician, bod
.filter(onlyUnique)
.map((s) => {
return {
- text: s || "No Status*",
+ text: s || t("dashboard.errors.status"),
value: [s]
};
})) ||
diff --git a/client/src/redux/user/user.sagas.js b/client/src/redux/user/user.sagas.js
index 1c50357cc..c508bf96e 100644
--- a/client/src/redux/user/user.sagas.js
+++ b/client/src/redux/user/user.sagas.js
@@ -49,7 +49,7 @@ import {
validatePasswordResetSuccess
} from "./user.actions";
import UserActionTypes from "./user.types";
-import * as amplitude from '@amplitude/analytics-browser';
+//import * as amplitude from '@amplitude/analytics-browser';
import posthog from 'posthog-js';
const fpPromise = FingerprintJS.load();
@@ -92,7 +92,7 @@ export function* isUserAuthenticated() {
}
LogRocket.identify(user.email);
- amplitude.setUserId(user.email);
+ //amplitude.setUserId(user.email);
posthog.identify(user.email);
const eulaQuery = yield client.query({
@@ -139,7 +139,7 @@ export function* signOutStart() {
imexshopid: state.user.bodyshop.imexshopid,
type: "messaging"
});
- amplitude.reset();
+ //amplitude.reset();
} catch {
console.log("No FCM token. Skipping unsubscribe.");
}
@@ -365,7 +365,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
}
try {
- amplitude.setGroup('Shop', payload.shopname);
+ //amplitude.setGroup('Shop', payload.shopname);
window.$crisp.push(["set", "user:company", [payload.shopname]]);
if (authRecord[0] && authRecord[0].user.validemail) {
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index aca3da3e2..05219fd5e 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -281,6 +281,7 @@
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
+ "accumulatePayableLines": "Accumulate Payable Lines",
"address1": "Address 1",
"address2": "Address 2",
"appt_alt_transport": "Appointment Alternative Transportation Options",
@@ -321,6 +322,7 @@
"itc_local": "Local Tax is ITC?",
"itc_state": "State Tax is ITC?",
"mappingname": "DMS Mapping Name",
+ "ro_posting": "Create $0 RO?",
"sendmaterialscosting": "Materials Cost as % of Sale",
"srcco": "Source Company #/Dealer #"
},
@@ -996,6 +998,7 @@
"insco": "No Ins. Co.*",
"refreshrequired": "You must refresh the dashboard data to see this component.",
"status": "No Status*",
+ "status_normal": "No Status",
"updatinglayout": "Error saving updated layout {{message}}"
},
"labels": {
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 345712670..20610391c 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -281,6 +281,7 @@
},
"fields": {
"ReceivableCustomField": "",
+ "accumulatePayableLines": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -321,6 +322,7 @@
"itc_local": "",
"itc_state": "",
"mappingname": "",
+ "ro_posting": "",
"sendmaterialscosting": "",
"srcco": ""
},
@@ -996,6 +998,7 @@
"insco": "",
"refreshrequired": "",
"status": "",
+ "status_normal": "",
"updatinglayout": ""
},
"labels": {
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index c3f680059..76d5bbd1d 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -281,6 +281,7 @@
},
"fields": {
"ReceivableCustomField": "",
+ "accumulatePayableLines": "",
"address1": "",
"address2": "",
"appt_alt_transport": "",
@@ -321,6 +322,7 @@
"itc_local": "",
"itc_state": "",
"mappingname": "",
+ "ro_posting": "",
"sendmaterialscosting": "",
"srcco": ""
},
@@ -996,6 +998,7 @@
"insco": "",
"refreshrequired": "",
"status": "",
+ "status_normal": "",
"updatinglayout": ""
},
"labels": {
diff --git a/docker-compose-cluster.yml b/docker-compose-cluster.yml
index 86defaf50..bbce31dd4 100644
--- a/docker-compose-cluster.yml
+++ b/docker-compose-cluster.yml
@@ -207,6 +207,9 @@ services:
aws --endpoint-url=http://localstack:4566 secretsmanager create-secret --name CHATTER_PRIVATE_KEY --secret-string file:///tmp/certs/io-ftp-test.key
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
+ aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
+ aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
+ aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
"
networks:
diff --git a/docker-compose.yml b/docker-compose.yml
index ad805e272..261076522 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -120,6 +120,8 @@ services:
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
+ aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rome-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
+ aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket rps-carfax-uploads --create-bucket-configuration LocationConstraint=ca-central-1
"
# Node App: The Main IMEX API
node-app:
diff --git a/hasura/metadata/cron_triggers.yaml b/hasura/metadata/cron_triggers.yaml
index a9bcc3361..2c8c4c91d 100644
--- a/hasura/metadata/cron_triggers.yaml
+++ b/hasura/metadata/cron_triggers.yaml
@@ -15,6 +15,15 @@
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH
comment: Project Mexico
+- name: CARFAX RPS Data Pump
+ webhook: '{{HASURA_API_URL}}/data/carfaxrps'
+ schedule: 15 7 * * 0
+ include_in_metadata: true
+ payload: {}
+ headers:
+ - name: x-imex-auth
+ value_from_env: DATAPUMP_AUTH
+ comment: Project Mexico
- name: Chatter Data Pump
webhook: '{{HASURA_API_URL}}/data/chatter'
schedule: 45 5 * * *
diff --git a/server/accounting/pbs/pbs-ap-allocations.js b/server/accounting/pbs/pbs-ap-allocations.js
index 0fc2d0c4d..56fe0e78a 100644
--- a/server/accounting/pbs/pbs-ap-allocations.js
+++ b/server/accounting/pbs/pbs-ap-allocations.js
@@ -177,11 +177,9 @@ exports.PbsCalculateAllocationsAp = PbsCalculateAllocationsAp;
async function QueryBillData(socket, billids) {
WsLogger.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
- const currentToken =
- (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
const result = await client
- .setHeaders({ Authorization: `Bearer ${currentToken}` })
+ .setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.GET_PBS_AP_ALLOCATIONS, { billids: billids });
WsLogger.createLogEvent(socket, "SILLY", `Bill data query result ${JSON.stringify(result, null, 2)}`);
@@ -201,11 +199,9 @@ function getCostAccount(billline, respcenters) {
async function MarkApExported(socket, billids) {
WsLogger.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
- const currentToken =
- (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
const result = await client
- .setHeaders({ Authorization: `Bearer ${currentToken}` })
+ .setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_BILLS_EXPORTED, {
billids,
bill: {
diff --git a/server/accounting/pbs/pbs-constants.js b/server/accounting/pbs/pbs-constants.js
index c8a6d54f3..7c54f4d0e 100644
--- a/server/accounting/pbs/pbs-constants.js
+++ b/server/accounting/pbs/pbs-constants.js
@@ -6,10 +6,6 @@ const PBS_CREDENTIALS = {
};
exports.PBS_CREDENTIALS = PBS_CREDENTIALS;
-// const cdkDomain =
-// process.env.NODE_ENV === "production"
-// ? "https://3pa.dmotorworks.com"
-// : "https://uat-3pa.dmotorworks.com";
const pbsDomain = `https://partnerhub.pbsdealers.com/json/reply`;
exports.PBS_ENDPOINTS = {
@@ -18,5 +14,9 @@ exports.PBS_ENDPOINTS = {
VehicleGet: `${pbsDomain}/VehicleGet`,
AccountingPostingChange: `${pbsDomain}/AccountingPostingChange`,
ContactChange: `${pbsDomain}/ContactChange`,
- VehicleChange: `${pbsDomain}/VehicleChange`
+ VehicleChange: `${pbsDomain}/VehicleChange`,
+ RepairOrderChange: `${pbsDomain}/RepairOrderChange`, //TODO: Verify that this is correct. Docs have /reply/ in path.
+ RepairOrderGet: `${pbsDomain}/RepairOrderGet`,
+ RepairOrderContactVehicleGet: `${pbsDomain}/RepairOrderContactVehicleGet`,
+ RepairOrderContactVehicleChange: `${pbsDomain}/RepairOrderContactVehicleChange`,
};
diff --git a/server/accounting/pbs/pbs-job-export.js b/server/accounting/pbs/pbs-job-export.js
index 04ee907d0..911382e15 100644
--- a/server/accounting/pbs/pbs-job-export.js
+++ b/server/accounting/pbs/pbs-job-export.js
@@ -3,6 +3,8 @@ const AxiosLib = require("axios").default;
const queries = require("../../graphql-client/queries");
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
const WsLogger = require("../../web-sockets/createLogEvent");
+const fs = require("fs");
+const path = require("path");
//const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
const CalculateAllocations = require("../../cdk/cdk-calculate-allocations").default;
@@ -22,9 +24,9 @@ axios.interceptors.request.use((x) => {
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
- //console.log(printable);
+ //logRequestToFile(printable);
- WsLogger.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
+ WsLogger.createJsonEvent(socket, "DEBUG", `Raw Request: ${printable}`, x.data);
return x;
});
@@ -32,23 +34,36 @@ axios.interceptors.request.use((x) => {
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
- const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
- //console.log(printable);
- WsLogger.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
+ const printable = `${new Date()} | Response: ${x.status} ${x.statusText} |${JSON.stringify(x.data)}`;
+ //logRequestToFile(printable);
+ WsLogger.createJsonEvent(socket, "DEBUG", `Raw Response: ${printable}`, x.data);
return x;
});
+function logRequestToFile(printable) {
+ try {
+ const logDir = path.join(process.cwd(), "logs");
+ if (!fs.existsSync(logDir)) {
+ fs.mkdirSync(logDir, { recursive: true });
+ }
+ const logFile = path.join(logDir, "pbs-http.log");
+ fs.appendFileSync(logFile, `${printable}\n`);
+ } catch (err) {
+ console.error("Unexpected error in logRequestToFile:", err);
+ }
+}
+
const defaultHandler = async (socket, { txEnvelope, jobid }) => {
socket.logEvents = [];
socket.recordid = jobid;
socket.txEnvelope = txEnvelope;
try {
- WsLogger.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
+ WsLogger.createLogEvent(socket, "INFO", `Received Job export request for id ${jobid}`);
const JobData = await QueryJobData(socket, jobid);
socket.JobData = JobData;
- WsLogger.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
+ WsLogger.createLogEvent(socket, "INFO", `Querying the DMS for the Vehicle Record.`);
//Query for the Vehicle record to get the associated customer.
socket.DmsVeh = await QueryVehicleFromDms(socket);
//Todo: Need to validate the lines and methods below.
@@ -71,42 +86,53 @@ exports.default = defaultHandler;
exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selectedCustomerId) {
try {
- if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
- WsLogger.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
+ socket.selectedCustomerId = selectedCustomerId;
+ if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle !== true) {
+ WsLogger.createLogEvent(socket, "INFO", `User selected customer ${selectedCustomerId || "NEW"}`);
//Upsert the contact information as per Wafaa's Email.
WsLogger.createLogEvent(
socket,
- "DEBUG",
+ "INFO",
`Upserting contact information to DMS for ${
socket.JobData.ownr_fn || ""
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
);
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
+ socket.ownerRef = ownerRef;
- WsLogger.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
- await UpsertVehicleData(socket, ownerRef.ReferenceId);
+ WsLogger.createLogEvent(socket, "INFO", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
+ const vehicleRef = await UpsertVehicleData(socket, ownerRef.ReferenceId);
+ socket.vehicleRef = vehicleRef;
} else {
WsLogger.createLogEvent(
socket,
- "DEBUG",
- `Contact and Vehicle updates disabled. Skipping to accounting data insert.`
+ "INFO",
+ `Contact and Vehicle updates disabled. Querying data and skipping to accounting data insert.`
);
+ //Must query for records to insert $0 RO.
+ if (!socket.ownerRef) {
+ const ownerRef = (await QueryCustomerBycodeFromDms(socket, selectedCustomerId))?.[0];
+ socket.ownerRef = ownerRef;
+ }
+ const vehicleRef = await GetVehicleData(socket, socket.ownerRef?.ReferenceId || socket.selectedCustomerId);
+ socket.vehicleRef = vehicleRef;
}
- WsLogger.createLogEvent(socket, "DEBUG", `Inserting account data.`);
- WsLogger.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
+ WsLogger.createLogEvent(socket, "INFO", `Inserting account posting data...`);
const insertResponse = await InsertAccountPostingData(socket);
if (insertResponse.WasSuccessful) {
- WsLogger.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
+ if (socket.JobData.bodyshop.pbs_configuration.ro_posting) {
+ await CreateRepairOrderInPBS(socket, socket.ownerRef, socket.vehicleRef);
+ }
+ WsLogger.createLogEvent(socket, "INFO", `Marking job as exported.`);
await MarkJobExported(socket, socket.JobData.id);
-
socket.emit("export-success", socket.JobData.id);
} else {
WsLogger.createLogEvent(socket, "ERROR", `Export was not successful.`);
}
} catch (error) {
- WsLogger.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
+ WsLogger.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
await InsertFailedExportLog(socket, error);
}
};
@@ -114,26 +140,24 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
// Was Successful
async function CheckForErrors(socket, response) {
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
- WsLogger.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
+ WsLogger.createLogEvent(socket, "INFO", `Successful response from DMS. ${response.Message || ""}`);
} else {
WsLogger.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
- WsLogger.createLogEvent(socket, "SILLY", `Error received from DMS: ${JSON.stringify(response)}`);
+ WsLogger.createLogEvent(socket, "DEBUG", `Error received from DMS: ${JSON.stringify(response)}`);
}
}
exports.CheckForErrors = CheckForErrors;
async function QueryJobData(socket, jobid) {
- WsLogger.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
+ WsLogger.createLogEvent(socket, "INFO", `Querying job data for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
- const currentToken =
- (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
const result = await client
- .setHeaders({ Authorization: `Bearer ${currentToken}` })
+ .setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.QUERY_JOBS_FOR_PBS_EXPORT, { id: jobid });
- WsLogger.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
+ WsLogger.createLogEvent(socket, "DEBUG", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
@@ -338,7 +362,7 @@ async function UpsertVehicleData(socket, ownerRef) {
//FleetNumber: "String",
//Status: "String",
OwnerRef: ownerRef, // "00000000000000000000000000000000",
- ModelNumber: socket.JobData.vehicle?.v_makecode,
+ // ModelNumber: socket.JobData.vehicle?.v_makecode,
Make: socket.JobData.v_make_desc,
Model: socket.JobData.v_model_desc,
Trim: socket.JobData.vehicle?.v_trimcode,
@@ -346,7 +370,7 @@ async function UpsertVehicleData(socket, ownerRef) {
Year: socket.JobData.v_model_yr,
Odometer: socket.JobData.kmout,
ExteriorColor: {
- Code: socket.JobData.v_color,
+ // Code: socket.JobData.v_color,
Description: socket.JobData.v_color
}
// InteriorColor: { Code: "String", Description: "String" },
@@ -476,6 +500,56 @@ async function UpsertVehicleData(socket, ownerRef) {
}
}
+async function GetVehicleData(socket, ownerRef) {
+ try {
+ const {
+ data: { Vehicles }
+ } = await axios.post(
+ PBS_ENDPOINTS.VehicleGet,
+ {
+ SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
+ // "VehicleId": "00000000000000000000000000000000",
+ // "Year": "String",
+ // "YearFrom": "String",
+ // "YearTo": "String",
+ // "Make": "String",
+ // "Model": "String",
+ // "Trim": "String",
+ // "ModelNumber": "String",
+ // "StockNumber": "String",
+ VIN: socket.JobData.v_vin
+ // "LicenseNumber": "String",
+ // "Lot": "String",
+ // "Status": "String",
+ // "StatusList": ["String"],
+ // "OwnerRef": "00000000000000000000000000000000",
+ // "ModifiedSince": "0001-01-01T00:00:00.0000000Z",
+ // "ModifiedUntil": "0001-01-01T00:00:00.0000000Z",
+ // "LastSaleSince": "0001-01-01T00:00:00.0000000Z",
+ // "VehicleIDList": ["00000000000000000000000000000000"],
+ // "IncludeInactive": false,
+ // "IncludeBuildVehicles": false,
+ // "IncludeBlankLot": false,
+ // "ShortVIN": "String",
+ // "ResultLimit": 0,
+ // "LotAccessDivisions": [0],
+ // "OdometerTo": 0,
+ // "OdometerFrom": 0
+ },
+ { auth: PBS_CREDENTIALS, socket }
+ );
+ CheckForErrors(socket, Vehicles);
+ if (Vehicles.length === 1) {
+ return Vehicles[0];
+ } else {
+ WsLogger.createLogEvent(socket, "ERROR", `Error in Getting Vehicle Data - ${Vehicles.length} vehicle(s) found`);
+ }
+ } catch (error) {
+ WsLogger.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
+ throw new Error(error);
+ }
+}
+
async function InsertAccountPostingData(socket) {
try {
const allocations = await CalculateAllocations(socket, socket.JobData.id);
@@ -579,13 +653,11 @@ async function InsertAccountPostingData(socket) {
}
async function MarkJobExported(socket, jobid) {
- WsLogger.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
+ WsLogger.createLogEvent(socket, "INFO", `Marking job as exported for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
- const currentToken =
- (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
const result = await client
- .setHeaders({ Authorization: `Bearer ${currentToken}` })
+ .setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_JOB_EXPORTED, {
jobId: jobid,
job: {
@@ -611,11 +683,9 @@ async function MarkJobExported(socket, jobid) {
async function InsertFailedExportLog(socket, error) {
try {
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
- const currentToken =
- (socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
const result = await client
- .setHeaders({ Authorization: `Bearer ${currentToken}` })
+ .setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.INSERT_EXPORT_LOG, {
log: {
bodyshopid: socket.JobData.bodyshop.id,
@@ -631,3 +701,161 @@ async function InsertFailedExportLog(socket, error) {
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
}
}
+
+async function CreateRepairOrderInPBS(socket) {
+ try {
+ const { RepairOrders } = await RepairOrderGet(socket);
+ if (RepairOrders.length === 0) {
+ const InsertedRepairOrder = await RepairOrderChange(socket);
+ socket.InsertedRepairOrder = InsertedRepairOrder;
+ WsLogger.createLogEvent(socket, "INFO", `No repair orders found for vehicle. Inserting record.`);
+ } else if (RepairOrders.length > 0) {
+ //Find out if it's a matching RO.
+ //This logic is used because the integration will simply add another line to an open RO if it exists.
+ const matchingRo = RepairOrders.find((ro) =>
+ ro.Memo?.toLowerCase()?.includes(socket.JobData.ro_number.toLowerCase())
+ );
+ if (!matchingRo) {
+ WsLogger.createLogEvent(socket, "INFO", `ROs found for vehicle, but none match. Inserting record.`);
+ const InsertedRepairOrder = await RepairOrderChange(socket);
+ socket.InsertedRepairOrder = InsertedRepairOrder;
+ } else {
+ WsLogger.createLogEvent(
+ socket,
+ "WARN",
+ `Repair order appears to already exist in PBS. ${matchingRo.RepairOrderNumber}`
+ );
+ }
+ }
+ } catch (error) {
+ WsLogger.createLogEvent(socket, "ERROR", `Error in CreateRepairOrderInPBS - ${error} - ${JSON.stringify(error)}`);
+ }
+}
+
+async function RepairOrderGet(socket) {
+ try {
+ const { data: RepairOrderGet } = await axios.post(
+ PBS_ENDPOINTS.RepairOrderGet,
+ {
+ SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
+ //"RepairOrderId": "374728766",
+ //"RepairOrderNumber": "4" || socket.JobData.ro_number,
+ //"RawRepairOrderNumber": socket.JobData.ro_number,
+ // "Tag": "String",
+ //"ContactRef": socket.contactRef,
+ // "ContactRefList": ["00000000000000000000000000000000"],
+ VehicleRef: socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId
+ // "VehicleRefList": ["00000000000000000000000000000000"],
+ // "Status": "String",
+ // "CashieredSince": "0001-01-01T00:00:00.0000000Z",
+ // "CashieredUntil": "0001-01-01T00:00:00.0000000Z",
+ // "OpenDateSince": "0001-01-01T00:00:00.0000000Z",
+ // "OpenDateUntil": "0001-01-01T00:00:00.0000000Z",
+ //"ModifiedSince": "2025-01-01T00:00:00.0000000Z",
+ // "ModifiedUntil": "0001-01-01T00:00:00.0000000Z",
+ // "Shop": "String"
+ },
+ { auth: PBS_CREDENTIALS, socket }
+ );
+ CheckForErrors(socket, RepairOrderGet);
+ return RepairOrderGet;
+ } catch (error) {
+ WsLogger.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
+ throw new Error(error);
+ }
+}
+
+async function RepairOrderChange(socket) {
+ try {
+ const { data: RepairOrderChangeResponse } = await axios.post(
+ PBS_ENDPOINTS.RepairOrderChange,
+ {
+ //Additional details at https://partnerhub.pbsdealers.com/json/metadata?op=RepairOrderChange
+ RepairOrderInfo: {
+ //"Id": "string/00000000-0000-0000-0000-000000000000",
+ //"RepairOrderId": "00000000000000000000000000000000",
+ SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
+ RepairOrderNumber: "00000000000000000000000000000000", //This helps force a new RO.
+ RawRepairOrderNumber: "00000000000000000000000000000000",
+ // "RepairOrderNumber": socket.JobData.ro_number, //These 2 values are ignored as confirmed by PBS.
+ // "RawRepairOrderNumber": socket.JobData.ro_number,
+ DateOpened: moment(),
+ // "DateOpenedUTC": "0001-01-01T00:00:00.0000000Z",
+ // "DateCashiered": "0001-01-01T00:00:00.0000000Z",
+ // "DateCashieredUTC": "0001-01-01T00:00:00.0000000Z",
+ DatePromised: socket.JobData.scheduled_completion,
+ // "DatePromisedUTC": "0001-01-01T00:00:00.0000000Z",
+ DateVehicleCompleted: socket.JobData.actual_completion,
+ // "DateCustomerNotified": "0001-01-01T00:00:00.0000000Z",
+ // "CSR": "String",
+ // "CSRRef": "00000000000000000000000000000000",
+ // "BookingUser": "String",
+ // "BookingUserRef": "00000000000000000000000000000000",
+ ContactRef: socket.ownerRef?.ReferenceId || socket.ownerRef?.ContactId,
+ VehicleRef: socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId,
+ MileageIn: socket.JobData.km_in,
+ Tag: "BODYSHOP",
+ //"Status": "CLOSED", //Values here do not impact the status. Confirmed by PBS support.
+ Requests: [
+ {
+ // "RepairOrderRequestRef": "b1842ecad62c4279bbc2fef4f6bf6cde",
+ // "RepairOrderRequestId": 1,
+ // "CSR": "PBS",
+ // "CSRRef": "1ce12ac692564e94bda955d529ee911a",
+ // "Skill": "GEN",
+ RequestCode: "MISC",
+ RequestDescription: `VEHICLE REPAIRED AT BODYSHOP. PLEASE REFERENCE IMEX SHOP MANAGEMENT SYSTEM. ${socket.txEnvelope.story}`,
+ Status: "Completed",
+ // "TechRef": "00000000000000000000000000000000",
+ AllowedHours: 0,
+ EstimateLabour: 0,
+ EstimateParts: 0,
+ ComeBack: false,
+ AddedOperation: true,
+ PartLines: [],
+ PartRequestLines: [],
+ LabourLines: [],
+ SubletLines: [],
+ TimePunches: [],
+ Summary: {
+ Labour: 0,
+ Parts: 0,
+ OilGas: 0,
+ SubletTow: 0,
+ Misc: 0,
+ Environment: 0,
+ ShopSupplies: 0,
+ Freight: 0,
+ WarrantyDeductible: 0,
+ Discount: 0,
+ SubTotal: 0,
+ Tax1: 0,
+ Tax2: 0,
+ InvoiceTotal: 0,
+ CustomerDeductible: 0,
+ GrandTotal: 0,
+ LabourDiscount: 0,
+ PartDiscount: 0,
+ ServiceFeeTotal: 0,
+ OEMDiscount: 0
+ },
+ LineType: "RequestLine"
+ }
+ ],
+
+ Memo: socket.txEnvelope.story
+ },
+ IsAsynchronous: false
+ // "UserRequest": "String",
+ // "UserRef": "00000000000000000000000000000000"
+ },
+
+ { auth: PBS_CREDENTIALS, socket }
+ );
+ CheckForErrors(socket, RepairOrderChangeResponse);
+ return RepairOrderChangeResponse;
+ } catch (error) {
+ WsLogger.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
+ throw new Error(error);
+ }
+}
diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js
index 9f0aacc42..a21e8ebc0 100644
--- a/server/accounting/qbo/qbo-payables.js
+++ b/server/accounting/qbo/qbo-payables.js
@@ -205,21 +205,49 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) {
const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, bill.job.shopid);
+ let lines;
+ if (bodyshop.accountingconfig.accumulatePayableLines === true) {
+ lines = Object.values(
+ bill.billlines.reduce((acc, il) => {
+ const { cost_center, actual_cost, quantity = 1 } = il;
- const lines = bill.billlines.map((il) =>
- generateBillLine(
- il,
- accounts,
- bill.job.class,
- bodyshop.md_responsibility_centers.sales_tax_codes,
- classes,
- taxCodes,
- bodyshop.md_responsibility_centers.costs,
- bodyshop.accountingconfig,
- bodyshop.region_config
- )
- );
+ if (!acc[cost_center]) {
+ acc[cost_center] = { ...il, actual_cost: 0, quantity: 1 };
+ }
+ acc[cost_center].actual_cost += Math.round(actual_cost * quantity * 100);
+
+ return acc;
+ }, {})
+ ).map((il) => {
+ il.actual_cost /= 100;
+ return generateBillLine(
+ il,
+ accounts,
+ bill.job.class,
+ bodyshop.md_responsibility_centers.sales_tax_codes,
+ classes,
+ taxCodes,
+ bodyshop.md_responsibility_centers.costs,
+ bodyshop.accountingconfig,
+ bodyshop.region_config
+ );
+ });
+ } else {
+ lines = bill.billlines.map((il) =>
+ generateBillLine(
+ il,
+ accounts,
+ bill.job.class,
+ bodyshop.md_responsibility_centers.sales_tax_codes,
+ classes,
+ taxCodes,
+ bodyshop.md_responsibility_centers.costs,
+ bodyshop.accountingconfig,
+ bodyshop.region_config
+ )
+ );
+ }
//QB USA with GST
//This was required for the No. 1 Collision Group.
if (
@@ -241,7 +269,7 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
Amount: Dinero({
amount: Math.round(
bill.billlines.reduce((acc, val) => {
- return acc + (val.applicable_taxes?.federal ? (val.actual_cost * val.quantity ?? 0) : 0);
+ return acc + (val.applicable_taxes?.federal ? val.actual_cost * val.quantity || 0 : 0);
}, 0) * 100
)
})
diff --git a/server/accounting/qbxml/qbxml-payables.js b/server/accounting/qbxml/qbxml-payables.js
index 9e658ecea..2b5efaf18 100644
--- a/server/accounting/qbxml/qbxml-payables.js
+++ b/server/accounting/qbxml/qbxml-payables.js
@@ -46,6 +46,28 @@ exports.default = async (req, res) => {
};
const generateBill = (bill, bodyshop) => {
+ let lines;
+ if (bodyshop.accountingconfig.accumulatePayableLines === true) {
+ lines = Object.values(
+ bill.billlines.reduce((acc, il) => {
+ const { cost_center, actual_cost, quantity = 1 } = il;
+
+ if (!acc[cost_center]) {
+ acc[cost_center] = { ...il, actual_cost: 0, quantity: 1 };
+ }
+
+ acc[cost_center].actual_cost += Math.round(actual_cost * quantity * 100);
+
+ return acc;
+ }, {})
+ ).map((il) => {
+ il.actual_cost /= 100;
+ return generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class);
+ });
+ } else {
+ lines = bill.billlines.map((il) => generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class));
+ }
+
const billQbxmlObj = {
QBXML: {
QBXMLMsgsRq: {
@@ -67,9 +89,7 @@ const generateBill = (bill, bodyshop) => {
}),
RefNumber: bill.invoice_number,
Memo: `RO ${bill.job.ro_number || ""}`,
- ExpenseLineAdd: bill.billlines.map((il) =>
- generateBillLine(il, bodyshop.md_responsibility_centers, bill.job.class)
- )
+ ExpenseLineAdd: lines
}
}
}
diff --git a/server/data/carfax-rps.js b/server/data/carfax-rps.js
new file mode 100644
index 000000000..d6065df57
--- /dev/null
+++ b/server/data/carfax-rps.js
@@ -0,0 +1,441 @@
+const queries = require("../graphql-client/queries");
+const moment = require("moment-timezone");
+const logger = require("../utils/logger");
+const fs = require("fs");
+const client = require("../graphql-client/graphql-client").rpsClient;
+const { sendServerEmail, sendMexicoBillingEmail } = require("../email/sendemail");
+const crypto = require("crypto");
+const { ftpSetup, uploadToS3 } = require("./carfax");
+let Client = require("ssh2-sftp-client");
+
+const AHDateFormat = "YYYY-MM-DD";
+
+const NON_ASCII_REGEX = /[^\x20-\x7E]/g;
+
+const S3_BUCKET_NAME = "rps-carfax-uploads";
+
+const carfaxExportRps = async (req, res) => {
+ // Only process if in production environment.
+ if (process.env.NODE_ENV !== "production") {
+ return res.sendStatus(403);
+ }
+ // Only process if the appropriate token is provided.
+ if (req.headers["x-imex-auth"] !== process.env.AUTOHOUSE_AUTH_TOKEN) {
+ return res.sendStatus(401);
+ }
+
+ // Send immediate response and continue processing.
+ res.status(202).json({
+ success: true,
+ message: "Processing request ...",
+ timestamp: new Date().toISOString()
+ });
+
+ try {
+ logger.log("CARFAX-RPS-start", "DEBUG", "api", null, null);
+ const allJSONResults = [];
+ const allErrors = [];
+
+ const { bodyshops } = await client.request(queries.GET_CARFAX_RPS_SHOPS); //Query for the List of Bodyshop Clients.
+ const specificShopIds = req.body.bodyshopIds; // ['uuid];
+ const { start, end, skipUpload, ignoreDateFilter } = req.body; //YYYY-MM-DD
+
+ const shopsToProcess =
+ specificShopIds?.length > 0 ? bodyshops.filter((shop) => specificShopIds.includes(shop.id)) : bodyshops;
+ logger.log("CARFAX-RPS-shopsToProcess-generated", "DEBUG", "api", null, null);
+
+ if (shopsToProcess.length === 0) {
+ logger.log("CARFAX-RPS-shopsToProcess-empty", "DEBUG", "api", null, null);
+ return;
+ }
+
+ await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors);
+
+ await sendServerEmail({
+ subject: `Project Mexico RPS Report ${moment().format("MM-DD-YY")}`,
+ text: `Total Count: ${allJSONResults.reduce((a, v) => a + v.count, 0)}\nErrors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
+ allJSONResults.map((x) => ({
+ imexshopid: x.imexshopid,
+ filename: x.filename,
+ count: x.count,
+ result: x.result
+ })),
+ null,
+ 2
+ )}`,
+ to: ["bradley.rhoades@convenient-brands.com"]
+ });
+
+ logger.log("CARFAX-RPS-end", "DEBUG", "api", null, null);
+ } catch (error) {
+ logger.log("CARFAX-RPS-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
+ }
+};
+
+async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors) {
+ for (const bodyshop of shopsToProcess) {
+ const shopid = bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
+ const erroredJobs = [];
+ try {
+ logger.log("CARFAX-RPS-start-shop-extract", "DEBUG", "api", bodyshop.id, {
+ shopname: bodyshop.shopname
+ });
+
+ const { jobs, bodyshops_by_pk } = await client.request(queries.CARFAX_RPS_QUERY, {
+ bodyshopid: bodyshop.id,
+ ...(ignoreDateFilter
+ ? {}
+ : {
+ starttz: start ? moment(start).startOf("day") : moment().subtract(7, "days").startOf("day"),
+ ...(end && { endtz: moment(end).endOf("day") }),
+ start: start
+ ? moment(start).startOf("day").format(AHDateFormat)
+ : moment().subtract(7, "days").startOf("day").format(AHDateFormat),
+ ...(end && { endtz: moment(end).endOf("day").format(AHDateFormat) })
+ })
+ });
+
+ const carfaxObject = {
+ shopid: shopid,
+ shop_name: bodyshop.shopname,
+ job: jobs.map((j) =>
+ CreateRepairOrderTag({ ...j, bodyshop: bodyshops_by_pk }, function ({ job, error }) {
+ erroredJobs.push({ job: job, error: error.toString() });
+ })
+ )
+ };
+
+ if (erroredJobs.length > 0) {
+ logger.log("CARFAX-RPS-failed-jobs", "ERROR", "api", bodyshop.id, {
+ count: erroredJobs.length,
+ jobs: JSON.stringify(erroredJobs.map((j) => j.job.id))
+ });
+ }
+
+ const jsonObj = {
+ bodyshopid: bodyshop.id,
+ imexshopid: shopid,
+ json: JSON.stringify(carfaxObject, null, 2),
+ filename: `${shopid}_${moment().format("DDMMYYYY_HHMMss")}.json`,
+ count: carfaxObject.job.length
+ };
+
+ if (skipUpload) {
+ fs.writeFileSync(`./logs/${jsonObj.filename}`, jsonObj.json);
+ uploadToS3(jsonObj, S3_BUCKET_NAME);
+ } else {
+ await uploadViaSFTP(jsonObj);
+
+ await sendMexicoBillingEmail({
+ subject: `${shopid.replace(/_/g, "").toUpperCase()}_MexicoRPS_${moment().format("MMDDYYYY")} ROs ${jsonObj.count} Error ${errorCode(jsonObj)}`,
+ text: `Errors:\n${JSON.stringify(
+ erroredJobs.map((ej) => ({
+ jobid: ej.job?.id,
+ error: ej.error
+ })),
+ null,
+ 2
+ )}\n\nUploaded:\n${JSON.stringify(
+ {
+ bodyshopid: bodyshop.id,
+ imexshopid: shopid,
+ count: jsonObj.count,
+ filename: jsonObj.filename,
+ result: jsonObj.result
+ },
+ null,
+ 2
+ )}`
+ });
+ }
+
+ allJSONResults.push({
+ bodyshopid: bodyshop.id,
+ imexshopid: shopid,
+ count: jsonObj.count,
+ filename: jsonObj.filename,
+ result: jsonObj.result
+ });
+
+ logger.log("CARFAX-RPS-end-shop-extract", "DEBUG", "api", bodyshop.id, {
+ shopname: bodyshop.shopname
+ });
+ } catch (error) {
+ //Error at the shop level.
+ logger.log("CARFAX-RPS-error-shop", "ERROR", "api", bodyshop.id, { error: error.message, stack: error.stack });
+
+ allErrors.push({
+ bodyshopid: bodyshop.id,
+ imexshopid: shopid,
+ CARFAXid: bodyshop.CARFAXid,
+ fatal: true,
+ errors: [error.toString()]
+ });
+ } finally {
+ allErrors.push({
+ bodyshopid: bodyshop.id,
+ imexshopid: shopid,
+ CARFAXid: bodyshop.CARFAXid,
+ errors: erroredJobs.map((ej) => ({
+ jobid: ej.job?.id,
+ error: ej.error
+ }))
+ });
+ }
+ }
+}
+
+async function uploadViaSFTP(jsonObj) {
+ const sftp = new Client();
+ sftp.on("error", (errors) =>
+ logger.log("CARFAX-RPS-sftp-connection-error", "ERROR", "api", jsonObj.bodyshopid, {
+ error: errors.message,
+ stack: errors.stack
+ })
+ );
+ try {
+ // Upload to S3 first.
+ uploadToS3(jsonObj, S3_BUCKET_NAME);
+
+ //Connect to the FTP and upload all.
+ await sftp.connect(ftpSetup);
+
+ try {
+ jsonObj.result = await sftp.put(Buffer.from(jsonObj.json), `${jsonObj.filename}`);
+ logger.log("CARFAX-RPS-sftp-upload", "DEBUG", "api", jsonObj.bodyshopid, {
+ imexshopid: jsonObj.imexshopid,
+ filename: jsonObj.filename,
+ result: jsonObj.result
+ });
+ } catch (error) {
+ logger.log("CARFAX-RPS-sftp-upload-error", "ERROR", "api", jsonObj.bodyshopid, {
+ filename: jsonObj.filename,
+ error: error.message,
+ stack: error.stack
+ });
+ throw error;
+ }
+ } catch (error) {
+ logger.log("CARFAX-RPS-sftp-error", "ERROR", "api", jsonObj.bodyshopid, {
+ error: error.message,
+ stack: error.stack
+ });
+ throw error;
+ } finally {
+ sftp.end();
+ }
+}
+
+const CreateRepairOrderTag = (job, errorCallback) => {
+ try {
+ const subtotalEntry = job.totals.find((total) => total.TTL_TYPECD === "");
+ const subtotal = subtotalEntry ? subtotalEntry.T_AMT : 0;
+
+ const ret = {
+ ro_number: crypto.createHash("md5").update(job.id, "utf8").digest("hex"),
+ v_vin: job.v_vin || "",
+ v_year: job.v_model_yr
+ ? parseInt(job.v_model_yr.match(/\d/g))
+ ? parseInt(job.v_model_yr.match(/\d/g).join(""), 10)
+ : ""
+ : "",
+ v_make: job.v_makedesc || "",
+ v_model: job.v_model || "",
+
+ date_estimated: moment(job.created_at).tz("America/Winnipeg").format(AHDateFormat) || "",
+ data_opened: moment(job.created_at).tz("America/Winnipeg").format(AHDateFormat) || "",
+ date_invoiced: [job.close_date, job.created_at].find((date) => date)
+ ? moment([job.close_date, job.created_at].find((date) => date))
+ .tz("America/Winnipeg")
+ .format(AHDateFormat)
+ : "",
+ loss_date: job.loss_date ? moment(job.loss_date).format(AHDateFormat) : "",
+
+ ins_co_nm: job.ins_co_nm || "",
+ loss_desc: job.loss_desc || "",
+ theft_ind: job.theft_ind,
+ tloss_ind: job.tlos_ind,
+
+ subtotal: subtotal,
+
+ areaofdamage: {
+ impact1: generateAreaOfDamage(job.impact_1 || ""),
+ impact2: generateAreaOfDamage(job.impact_2 || "")
+ },
+
+ jobLines: job.joblines.length > 0 ? job.joblines.map((jl) => GenerateDetailLines(jl)) : [generateNullDetailLine()]
+ };
+ return ret;
+ } catch (error) {
+ logger.log("CARFAX-RPS-job-data-error", "ERROR", "api", null, { error: error.message, stack: error.stack });
+ errorCallback({ jobid: job.id, error });
+ }
+};
+
+const GenerateDetailLines = (line) => {
+ const ret = {
+ line_desc: line.line_desc ? line.line_desc.replace(NON_ASCII_REGEX, "") : null,
+ oem_partno: line.oem_partno ? line.oem_partno.replace(NON_ASCII_REGEX, "") : null,
+ alt_partno: line.alt_partno ? line.alt_partno.replace(NON_ASCII_REGEX, "") : null,
+ op_code_desc: generateOpCodeDescription(line.lbr_op),
+ lbr_ty: generateLaborType(line.mod_lbr_ty),
+ lbr_hrs: line.mod_lb_hrs || 0,
+ part_qty: line.part_qty || 0,
+ part_type: generatePartType(line.part_type),
+ act_price: line.act_price || 0
+ };
+ return ret;
+};
+
+const generateNullDetailLine = () => {
+ return {
+ line_desc: null,
+ oem_partno: null,
+ alt_partno: null,
+ lbr_ty: null,
+ part_qty: 0,
+ part_type: null,
+ act_price: 0
+ };
+};
+
+const generateAreaOfDamage = (loc) => {
+ const areaMap = {
+ "01": "Right Front Corner",
+ "02": "Right Front Side",
+ "03": "Right Side",
+ "04": "Right Rear Side",
+ "05": "Right Rear Corner",
+ "06": "Rear",
+ "07": "Left Rear Corner",
+ "08": "Left Rear Side",
+ "09": "Left Side",
+ 10: "Left Front Side",
+ 11: "Left Front Corner",
+ 12: "Front",
+ 13: "Rollover",
+ 14: "Uknown",
+ 15: "Total Loss",
+ 16: "Non-Collision",
+ 19: "All Over",
+ 25: "Hood",
+ 26: "Deck Lid",
+ 27: "Roof",
+ 28: "Undercarriage",
+ 34: "All Over"
+ };
+ return areaMap[loc] || null;
+};
+
+const generateLaborType = (type) => {
+ const laborTypeMap = {
+ laa: "Aluminum",
+ lab: "Body",
+ lad: "Diagnostic",
+ lae: "Electrical",
+ laf: "Frame",
+ lag: "Glass",
+ lam: "Mechanical",
+ lar: "Refinish",
+ las: "Structural",
+ lau: "Other - LAU",
+ la1: "Other - LA1",
+ la2: "Other - LA2",
+ la3: "Other - LA3",
+ la4: "Other - LA4",
+ null: "Other",
+ mapa: "Paint Materials",
+ mash: "Shop Materials",
+ rates_subtotal: "Labor Total",
+ "timetickets.labels.shift": "Shift",
+ "timetickets.labels.amshift": "Morning Shift",
+ "timetickets.labels.ambreak": "Morning Break",
+ "timetickets.labels.pmshift": "Afternoon Shift",
+ "timetickets.labels.pmbreak": "Afternoon Break",
+ "timetickets.labels.lunch": "Lunch"
+ };
+
+ return laborTypeMap[type?.toLowerCase()] || null;
+};
+
+const generatePartType = (type) => {
+ const partTypeMap = {
+ paa: "Aftermarket",
+ pae: "Existing",
+ pag: "Glass",
+ pal: "LKQ",
+ pan: "OEM",
+ pao: "Other",
+ pas: "Sublet",
+ pasl: "Sublet",
+ ccc: "CC Cleaning",
+ ccd: "CC Damage Waiver",
+ ccdr: "CC Daily Rate",
+ ccf: "CC Refuel",
+ ccm: "CC Mileage",
+ prt_dsmk_total: "Line Item Adjustment"
+ };
+
+ return partTypeMap[type?.toLowerCase()] || null;
+};
+
+const generateOpCodeDescription = (type) => {
+ const opCodeMap = {
+ OP0: "REMOVE / REPLACE PARTIAL",
+ OP1: "REFINISH / REPAIR",
+ OP10: "REPAIR , PARTIAL",
+ OP100: "REPLACE PRE-PRICED",
+ OP101: "REMOVE/REPLACE RECYCLED PART",
+ OP103: "REMOVE / REPLACE PARTIAL",
+ OP104: "REMOVE / REPLACE PARTIAL LABOUR",
+ OP105: "!!ADJUST MANUALLY!!",
+ OP106: "REPAIR , PARTIAL",
+ OP107: "CHIPGUARD",
+ OP108: "MULTI TONE",
+ OP109: "REPLACE PRE-PRICED",
+ OP11: "REMOVE / REPLACE",
+ OP110: "REFINISH / REPAIR",
+ OP111: "REMOVE / REPLACE",
+ OP112: "REMOVE / REPLACE",
+ OP113: "REPLACE PRE-PRICED",
+ OP114: "REPLACE PRE-PRICED",
+ OP12: "REMOVE / REPLACE PARTIAL",
+ OP120: "REPAIR , PARTIAL",
+ OP13: "ADDITIONAL COSTS",
+ OP14: "ADDITIONAL OPERATIONS",
+ OP15: "BLEND",
+ OP16: "SUBLET",
+ OP17: "POLICY LIMIT ADJUSTMENT",
+ OP18: "APPEAR ALLOWANCE",
+ OP2: "REMOVE / INSTALL",
+ OP24: "CHIPGUARD",
+ OP25: "TWO TONE",
+ OP26: "PAINTLESS DENT REPAIR",
+ OP260: "SUBLET",
+ OP3: "ADDITIONAL LABOR",
+ OP4: "ALIGNMENT",
+ OP5: "OVERHAUL",
+ OP6: "REFINISH",
+ OP7: "INSPECT",
+ OP8: "CHECK / ADJUST",
+ OP9: "REPAIR"
+ };
+
+ return opCodeMap[type?.toUpperCase()] || null;
+};
+
+const errorCode = ({ count, filename, results }) => {
+ if (count === 0) return 1;
+ if (!filename) return 3;
+ const sftpErrorCode = results?.sftpError?.code;
+ if (sftpErrorCode && ["ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "ECONNRESET"].includes(sftpErrorCode)) {
+ return 4;
+ }
+ if (sftpErrorCode) return 7;
+ return 0;
+};
+
+module.exports = {
+ default: carfaxExportRps,
+ ftpSetup
+};
diff --git a/server/data/carfax.js b/server/data/carfax.js
index 34a145dda..f2ff0bac2 100644
--- a/server/data/carfax.js
+++ b/server/data/carfax.js
@@ -37,12 +37,12 @@ const S3_BUCKET_NAME = InstanceManager({
const region = InstanceManager.InstanceRegion;
const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME);
-const uploadToS3 = (jsonObj) => {
+const uploadToS3 = (jsonObj, bucketName = S3_BUCKET_NAME) => {
const webPath = isLocal
- ? `https://${S3_BUCKET_NAME}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}`
- : `https://${S3_BUCKET_NAME}.s3.${region}.amazonaws.com/${jsonObj.filename}`;
+ ? `https://${bucketName}.s3.localhost.localstack.cloud:4566/${jsonObj.filename}`
+ : `https://${bucketName}.s3.${region}.amazonaws.com/${jsonObj.filename}`;
- uploadFileToS3({ bucketName: S3_BUCKET_NAME, key: jsonObj.filename, content: jsonObj.json })
+ uploadFileToS3({ bucketName: bucketName, key: jsonObj.filename, content: jsonObj.json })
.then(() => {
logger.log("CARFAX-s3-upload", "DEBUG", "api", jsonObj.bodyshopid, {
imexshopid: jsonObj.imexshopid,
@@ -61,7 +61,7 @@ const uploadToS3 = (jsonObj) => {
});
};
-exports.default = async (req, res) => {
+const carfaxExport = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
return res.sendStatus(403);
@@ -80,7 +80,7 @@ exports.default = async (req, res) => {
try {
logger.log("CARFAX-start", "DEBUG", "api", null, null);
- const allXMLResults = [];
+ const allJSONResults = [];
const allErrors = [];
const { bodyshops } = await client.request(queries.GET_CARFAX_SHOPS); //Query for the List of Bodyshop Clients.
@@ -96,12 +96,12 @@ exports.default = async (req, res) => {
return;
}
- await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors);
+ await processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors);
await sendServerEmail({
subject: `Project Mexico Report ${moment().format("MM-DD-YY")}`,
- text: `Errors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
- allXMLResults.map((x) => ({
+ text: `Total Count: ${allJSONResults.reduce((a, v) => a + v.count, 0)}\nErrors:\n${JSON.stringify(allErrors, null, 2)}\n\nUploaded:\n${JSON.stringify(
+ allJSONResults.map((x) => ({
imexshopid: x.imexshopid,
filename: x.filename,
count: x.count,
@@ -109,7 +109,8 @@ exports.default = async (req, res) => {
})),
null,
2
- )}`
+ )}`,
+ to: ["bradley.rhoades@convenient-brands.com"]
});
logger.log("CARFAX-end", "DEBUG", "api", null, null);
@@ -118,7 +119,7 @@ exports.default = async (req, res) => {
}
};
-async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allXMLResults, allErrors) {
+async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDateFilter, allJSONResults, allErrors) {
for (const bodyshop of shopsToProcess) {
const shopid = bodyshop.imexshopid?.toLowerCase() || bodyshop.shopname.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
const erroredJobs = [];
@@ -195,7 +196,7 @@ async function processShopData(shopsToProcess, start, end, skipUpload, ignoreDat
});
}
- allXMLResults.push({
+ allJSONResults.push({
bodyshopid: bodyshop.id,
imexshopid: shopid,
count: jsonObj.count,
@@ -447,3 +448,9 @@ const errorCode = ({ count, filename, results }) => {
if (sftpErrorCode) return 7;
return 0;
};
+
+module.exports = {
+ default: carfaxExport,
+ ftpSetup,
+ uploadToS3
+};
diff --git a/server/data/data.js b/server/data/data.js
index e9b80de5a..0aa7f6e34 100644
--- a/server/data/data.js
+++ b/server/data/data.js
@@ -7,4 +7,5 @@ exports.usageReport = require("./usageReport").default;
exports.podium = require("./podium").default;
exports.emsUpload = require("./emsUpload").default;
exports.carfax = require("./carfax").default;
+exports.carfaxRps = require("./carfax-rps").default;
exports.vehicletype = require("./vehicletype/vehicletype").default;
\ No newline at end of file
diff --git a/server/email/sendemail.js b/server/email/sendemail.js
index c9a72c17f..864031462 100644
--- a/server/email/sendemail.js
+++ b/server/email/sendemail.js
@@ -44,8 +44,14 @@ const logEmail = async (req, email) => {
}
};
-const sendServerEmail = async ({ subject, text }) => {
+const sendServerEmail = async ({ subject, text, to = [] }) => {
if (process.env.NODE_ENV === undefined) return;
+
+ let sentTo = ["support@imexsystems.ca"];
+ if (to?.length) {
+ sentTo = [...sentTo, ...to];
+ }
+
try {
mailer.sendMail(
{
@@ -53,7 +59,7 @@ const sendServerEmail = async ({ subject, text }) => {
imex: `ImEX Online API - ${process.env.NODE_ENV} `,
rome: `Rome Online API - ${process.env.NODE_ENV} `
}),
- to: ["support@thinkimex.com"],
+ to: sentTo,
subject: subject,
text: text,
ses: {
@@ -68,7 +74,7 @@ const sendServerEmail = async ({ subject, text }) => {
},
// eslint-disable-next-line no-unused-vars
(err, info) => {
- logger.log("server-email-failure", err ? "error" : "debug", null, null, {
+ logger.log("server-email-send", err ? "error" : "debug", null, null, {
message: err?.message,
stack: err?.stack
});
@@ -103,7 +109,7 @@ const sendMexicoBillingEmail = async ({ subject, text }) => {
},
// eslint-disable-next-line no-unused-vars
(err, info) => {
- logger.log("server-email-failure", err ? "error" : "debug", null, null, {
+ logger.log("server-email-send", err ? "error" : "debug", null, null, {
message: err?.message,
stack: err?.stack
});
@@ -258,7 +264,10 @@ const sendTaskEmail = async ({ to, subject, type = "text", html, text, attachmen
// eslint-disable-next-line no-unused-vars
(err, info) => {
// (message, type, user, record, meta
- logger.log("server-email", err ? "error" : "debug", null, null, { message: err?.message, stack: err?.stack });
+ logger.log("server-email-send", err ? "error" : "debug", null, null, {
+ message: err?.message,
+ stack: err?.stack
+ });
}
);
} catch (error) {
diff --git a/server/graphql-client/graphql-client.js b/server/graphql-client/graphql-client.js
index 79d86315b..2c42bf361 100644
--- a/server/graphql-client/graphql-client.js
+++ b/server/graphql-client/graphql-client.js
@@ -1,3 +1,5 @@
+const logger = require("../utils/logger");
+
const GraphQLClient = require("graphql-request").GraphQLClient;
//New bug introduced with Graphql Request.
@@ -11,9 +13,24 @@ const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {
}
});
+const rpsClient =
+ process.env.RPS_GRAPHQL_ENDPOINT && process.env.RPS_HASURA_ADMIN_SECRET ?
+ new GraphQLClient(process.env.RPS_GRAPHQL_ENDPOINT, {
+ headers: {
+ "x-hasura-admin-secret": process.env.RPS_HASURA_ADMIN_SECRET
+ }
+ }) : null;
+
+if (!rpsClient) {
+ //System log to disable RPS functions
+ logger.log(`RPS secrets are not set. Client is not configured.`, "WARN", "redis", "api", {
+ });
+}
+
const unauthorizedClient = new GraphQLClient(process.env.GRAPHQL_ENDPOINT);
module.exports = {
client,
+ rpsClient,
unauthorizedClient
};
diff --git a/server/graphql-client/queries.js b/server/graphql-client/queries.js
index 9ff853e0f..397e90de8 100644
--- a/server/graphql-client/queries.js
+++ b/server/graphql-client/queries.js
@@ -420,6 +420,8 @@ query QUERY_JOBS_FOR_PBS_EXPORT($id: uuid!) {
v_make_desc
v_color
ca_customer_gst
+ scheduled_completion
+ actual_completion
vehicle {
v_trimcode
v_makecode
@@ -920,6 +922,41 @@ exports.CARFAX_QUERY = `query CARFAX_EXPORT($start: timestamptz, $bodyshopid: uu
}
}`;
+exports.CARFAX_RPS_QUERY = `query CARFAX_RPS_EXPORT($starttz: timestamptz, $endtz: timestamptz,$start: date, $end: date, $bodyshopid: uuid!) {
+ bodyshops_by_pk(id: $bodyshopid) {
+ id
+ shopname
+ }
+ jobs(where: {_and: [{_or: [{close_date: {_gt: $start, _lte: $end}}, {created_at: {_gt: $starttz, _lte: $endtz}, close_date: {_is_null: true}}]}, {_not: {_and: [{close_date: {_is_null: true}}, {created_at: {_is_null: true}}]}}, {bodyshopid: {_eq: $bodyshopid}}, {v_vin: {_is_null: false}}]}) {
+ close_date
+ created_at
+ id
+ ins_co_nm
+ impact_1
+ impact_2
+ joblines {
+ act_price
+ alt_partno
+ line_desc
+ mod_lb_hrs
+ mod_lbr_ty
+ oem_partno
+ lbr_op
+ part_type
+ part_qty
+ }
+ loss_date
+ loss_desc
+ theft_ind
+ tlos_ind
+ totals
+ v_makedesc
+ v_model
+ v_model_yr
+ v_vin
+ }
+}`;
+
exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
bodyshops_by_pk(id: $bodyshopid){
id
@@ -1870,6 +1907,13 @@ exports.GET_CARFAX_SHOPS = `query GET_CARFAX_SHOPS {
}
}`;
+exports.GET_CARFAX_RPS_SHOPS = `query GET_CARFAX_RPS_SHOPS {
+ bodyshops(where: {carfax_exclude: {_neq: "true"}}){
+ id
+ shopname
+ }
+}`;
+
exports.GET_CLAIMSCORP_SHOPS = `query GET_CLAIMSCORP_SHOPS {
bodyshops(where: {claimscorpid: {_is_null: false}, _or: {claimscorpid: {_neq: ""}}}){
id
@@ -2164,18 +2208,16 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $
exports.INSERT_NEW_TRANSITION = (
includeOldTransition
-) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${
- includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
-}) {
+) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
+ }) {
insert_transitions_one(object: $newTransition) {
id
}
- ${
- includeOldTransition
- ? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
+ ${includeOldTransition
+ ? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
affected_rows
}`
- : ""
+ : ""
}
}`;
diff --git a/server/parts-scan/parts-scan.js b/server/parts-scan/parts-scan.js
index 6822bded6..79eda54d6 100644
--- a/server/parts-scan/parts-scan.js
+++ b/server/parts-scan/parts-scan.js
@@ -82,22 +82,35 @@ exports.partsScan = async function (req, res) {
criticalIds.add(jobline.id);
}
if (update_field && update_value) {
- const result = await client.setHeaders({ Authorization: BearerToken }).request(queries.UPDATE_JOB_LINE, {
- lineId: jobline.id,
- line: { [update_field]: update_value, manual_line: true }
- });
-
- const auditResult = await client
- .setHeaders({ Authorization: BearerToken })
- .request(queries.INSERT_AUDIT_TRAIL, {
- auditObj: {
- bodyshopid: data.jobs_by_pk.bodyshop.id,
- jobid,
- operation: `Jobline (#${jobline.line_no} ${jobline.line_desc}/${jobline.id}) ${update_field} updated from ${jobline[update_field]} to ${update_value}. Lined marked as manual line.`,
- type: "partscanupdate",
- useremail: req.user.email
+ let actualUpdateField = update_field;
+ if (update_field === "part_number") {
+ // Determine which part number field to update based on the match
+ if (!jobline.oem_partno) {
+ actualUpdateField = "oem_partno";
+ } else {
+ if (regex) {
+ actualUpdateField = regex.test(jobline.oem_partno || "") ? "oem_partno" : "alt_partno";
+ } else {
+ actualUpdateField = jobline.oem_partno === value ? "oem_partno" : "alt_partno";
}
+ }
+ }
+ if (actualUpdateField) {
+ await client.setHeaders({ Authorization: BearerToken }).request(queries.UPDATE_JOB_LINE, {
+ lineId: jobline.id,
+ line: { [actualUpdateField]: update_value, manual_line: true }
});
+ }
+
+ await client.setHeaders({ Authorization: BearerToken }).request(queries.INSERT_AUDIT_TRAIL, {
+ auditObj: {
+ bodyshopid: data.jobs_by_pk.bodyshop.id,
+ jobid,
+ operation: `Jobline (#${jobline.line_no} ${jobline.line_desc}/${jobline.id}) ${update_field} updated from ${jobline[update_field]} to ${update_value}. Lined marked as manual line.`,
+ type: "partscanupdate",
+ useremail: req.user.email
+ }
+ });
}
//break; // No need to evaluate further rules for this jobline
diff --git a/server/routes/dataRoutes.js b/server/routes/dataRoutes.js
index 8e7bc04fd..c72a2a502 100644
--- a/server/routes/dataRoutes.js
+++ b/server/routes/dataRoutes.js
@@ -1,6 +1,6 @@
const express = require("express");
const router = express.Router();
-const { autohouse, claimscorp, chatter, kaizen, usageReport, podium, carfax } = require("../data/data");
+const { autohouse, claimscorp, chatter, kaizen, usageReport, podium, carfax, carfaxRps } = require("../data/data");
router.post("/ah", autohouse);
router.post("/cc", claimscorp);
@@ -9,5 +9,6 @@ router.post("/kaizen", kaizen);
router.post("/usagereport", usageReport);
router.post("/podium", podium);
router.post("/carfax", carfax);
+router.post("/carfaxrps", carfaxRps);
module.exports = router;