Compare commits
91 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac6856b136 | ||
|
|
ddd3b3d056 | ||
|
|
2660466db1 | ||
|
|
fe67efe47c | ||
|
|
69a35772e5 | ||
|
|
38932f4bf9 | ||
|
|
3fcb36a28e | ||
|
|
fe78f5c7ff | ||
|
|
683846c3b0 | ||
|
|
2cc0b247b6 | ||
|
|
31579354d4 | ||
|
|
0e7531dc54 | ||
|
|
268b57c38a | ||
|
|
2b8b8b8073 | ||
|
|
808eeb91e9 | ||
|
|
23dd8fc9de | ||
|
|
f499859078 | ||
|
|
84d9e3251a | ||
|
|
bd7db4dd02 | ||
|
|
e9804b736b | ||
|
|
a14874f116 | ||
|
|
ac9fac458c | ||
|
|
8f9db15852 | ||
|
|
61b3d3c18c | ||
|
|
e6f08d3b1c | ||
|
|
d5cf0f8371 | ||
|
|
52e230fc54 | ||
|
|
4011237c22 | ||
|
|
c24bfbf655 | ||
|
|
08fe8c3c70 | ||
|
|
771a239773 | ||
|
|
82195a0584 | ||
|
|
838c24b3f1 | ||
|
|
13cb68b0af | ||
|
|
7c84b08707 | ||
|
|
3165957e95 | ||
|
|
d9f59fcad4 | ||
|
|
edaeb5d77a | ||
|
|
5365d95d6f | ||
|
|
5cfefd5afd | ||
|
|
bbccdb0650 | ||
|
|
f3535c01af | ||
|
|
7f8c82b300 | ||
|
|
609ac2bd33 | ||
|
|
0883274320 | ||
|
|
fa33b88632 | ||
|
|
bec32c1d70 | ||
|
|
eb18130e51 | ||
|
|
0fbf63dec8 | ||
|
|
f817902d5c | ||
|
|
e5c0ace6cb | ||
|
|
814447373a | ||
|
|
d766a468c3 | ||
|
|
0ede2d0649 | ||
|
|
54089c2ab3 | ||
|
|
73e3d71cf1 | ||
|
|
f071a5cc9e | ||
|
|
67002b8443 | ||
|
|
3f83c6afa7 | ||
|
|
b7f57e91aa | ||
|
|
b8465c0cc7 | ||
|
|
6bae0b8406 | ||
|
|
553c154e46 | ||
|
|
35cbb921d2 | ||
|
|
6b926401d0 | ||
|
|
5b400dce4f | ||
|
|
914946c264 | ||
|
|
2939b5795b | ||
|
|
531f968ce6 | ||
|
|
24cc3fa6a4 | ||
|
|
a6f17e7db5 | ||
|
|
29c904feea | ||
|
|
0743048e75 | ||
|
|
e2f1758378 | ||
|
|
b6dc7a4d92 | ||
|
|
130745d7e7 | ||
|
|
a722ab9758 | ||
|
|
41c9c0be49 | ||
|
|
52f62126e1 | ||
|
|
eb04a8b6c5 | ||
|
|
141b05f558 | ||
|
|
e9ea36fdad | ||
|
|
e24237e010 | ||
|
|
f00f5f2e4a | ||
|
|
f38b550e75 | ||
|
|
fabe1508ac | ||
|
|
5f3c880d0b | ||
|
|
86f0c02c82 | ||
|
|
2004eb840f | ||
|
|
2f0190e190 | ||
|
|
8c9ef375be |
@@ -14,3 +14,6 @@ VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
||||
@@ -16,3 +16,6 @@ VITE_APP_COUNTRY=USA
|
||||
VITE_APP_INSTANCE=ROME
|
||||
TEST_USERNAME="test@imex.dev"
|
||||
TEST_PASSWORD="test123"
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
||||
@@ -13,3 +13,6 @@ VITE_APP_AXIOS_BASE_API_URL=https://api.imex.online/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports.imex.online
|
||||
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
||||
@@ -13,3 +13,6 @@ VITE_APP_AXIOS_BASE_API_URL=https://api.romeonline.io/
|
||||
VITE_APP_REPORTS_SERVER_URL=https://reports.romeonline.io
|
||||
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
|
||||
VITE_APP_INSTANCE=ROME
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
||||
@@ -13,3 +13,6 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
|
||||
VITE_APP_IS_TEST=true
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=IMEX
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
||||
@@ -13,3 +13,6 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
|
||||
VITE_APP_IS_TEST=true
|
||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||
VITE_APP_INSTANCE=ROME
|
||||
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
|
||||
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com
|
||||
VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com
|
||||
3146
client/package-lock.json
generated
3146
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,46 +8,48 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.23.5",
|
||||
"@ant-design/pro-layout": "^7.22.6",
|
||||
"@apollo/client": "^3.13.9",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@emotion/is-prop-valid": "^1.4.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.17",
|
||||
"@firebase/app": "^0.14.1",
|
||||
"@firebase/app": "^0.14.2",
|
||||
"@firebase/auth": "^1.10.8",
|
||||
"@firebase/firestore": "^4.8.0",
|
||||
"@firebase/firestore": "^4.9.1",
|
||||
"@firebase/messaging": "^0.12.22",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/cli": "^2.52.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@sentry/cli": "^2.53.0",
|
||||
"@sentry/react": "^9.43.0",
|
||||
"@sentry/vite-plugin": "^4.1.1",
|
||||
"@sentry/vite-plugin": "^4.3.0",
|
||||
"@splitsoftware/splitio-react": "^2.3.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"antd": "^5.27.1",
|
||||
"antd": "^5.27.3",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.4.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dayjs-business-days2": "^1.3.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"graphql": "^16.11.0",
|
||||
"i18next": "^25.4.0",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.13",
|
||||
"libphonenumber-js": "^1.12.15",
|
||||
"logrocket": "^9.0.2",
|
||||
"markerjs2": "^2.32.6",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"phone": "^3.1.67",
|
||||
"posthog-js": "^1.261.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.2.2",
|
||||
"raf-schd": "^4.0.3",
|
||||
@@ -55,11 +57,12 @@
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^8.0.1",
|
||||
"lightningcss": "^1.30.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-i18next": "^15.7.1",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -78,7 +81,7 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.90.0",
|
||||
"sass": "^1.92.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.19",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
@@ -150,7 +153,6 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"lightningcss": "^1.30.1",
|
||||
"memfs": "^4.36.3",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.55.0",
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { ApolloProvider } from "@apollo/client";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||
import { ConfigProvider } from "antd";
|
||||
import enLocale from "antd/es/locale/en_US";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect, useSelector } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||
import { setDarkMode } from "../redux/application/application.actions";
|
||||
import { selectDarkMode } from "../redux/application/application.selectors";
|
||||
import { selectCurrentUser } from "../redux/user/user.selectors.js";
|
||||
import client from "../utils/GraphQLClient";
|
||||
import App from "./App";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import getTheme from "./themeProvider";
|
||||
import { CookiesProvider } from "react-cookie";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectCurrentUser } from "../redux/user/user.selectors.js";
|
||||
import { selectDarkMode } from "../redux/application/application.selectors";
|
||||
import { setDarkMode } from "../redux/application/application.actions";
|
||||
|
||||
// Base Split configuration
|
||||
const config = {
|
||||
|
||||
@@ -233,9 +233,7 @@ export function App({
|
||||
path="/parts/*"
|
||||
element={
|
||||
<ErrorBoundary>
|
||||
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</SocketProvider>
|
||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -109,6 +109,13 @@ export function BillsListTableComponent({
|
||||
key: "vendorname",
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
filters: bills
|
||||
? [...new Set(bills.map((bill) => bill.vendor.name))].map((name) => ({
|
||||
text: name,
|
||||
value: name
|
||||
}))
|
||||
: [],
|
||||
onFilter: (value, record) => record.vendor.name === value,
|
||||
render: (text, record) => <span>{record.vendor.name}</span>
|
||||
},
|
||||
{
|
||||
|
||||
@@ -80,7 +80,7 @@ export function DashboardGridComponent({ currentUser, bodyshop }) {
|
||||
};
|
||||
|
||||
const handleAddComponent = (e) => {
|
||||
logImEXEvent("dashboard_add_component", { name: e });
|
||||
logImEXEvent("dashboard_add_component", { name: e.key });
|
||||
setState({
|
||||
...state,
|
||||
items: [
|
||||
|
||||
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
export function GlobalFooter({ isPartsEntry }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
if (isPartsEntry) {
|
||||
return (
|
||||
<Footer>
|
||||
@@ -35,7 +35,6 @@ export function GlobalFooter({ isPartsEntry }) {
|
||||
rome: t("titles.romeonline")
|
||||
})} - ${import.meta.env.VITE_APP_GIT_SHA_DATE}`}
|
||||
</div>
|
||||
<WssStatusDisplayComponent />
|
||||
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
|
||||
Disclaimer & Notices
|
||||
</Link>
|
||||
|
||||
@@ -12,13 +12,15 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician
|
||||
technician: selectTechnician,
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
|
||||
export function JobLinesPartPriceChange({ job, line, refetch, technician, isPartsEntry }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
|
||||
const notification = useNotification();
|
||||
@@ -64,6 +66,7 @@ export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
|
||||
|
||||
const popcontent =
|
||||
!technician &&
|
||||
!isPartsEntry &&
|
||||
InstanceRenderManager({
|
||||
imex: null,
|
||||
rome: (
|
||||
|
||||
@@ -481,48 +481,50 @@ export function JobLinesComponent({
|
||||
{Enhanced_Payroll.treatment === "on" && (
|
||||
<JobLineBulkAssignComponent selectedLines={selectedLines} setSelectedLines={setSelectedLines} job={job} />
|
||||
)}
|
||||
<Button
|
||||
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
disableInvNumber: true,
|
||||
job: { id: job.id },
|
||||
bill: {
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
isinhouse: true,
|
||||
date: dayjs(),
|
||||
total: 0,
|
||||
billlines: selectedLines.map((p) => {
|
||||
return {
|
||||
joblineid: p.id,
|
||||
actual_price: p.act_price,
|
||||
actual_cost: 0, //p.act_price,
|
||||
line_desc: p.line_desc,
|
||||
line_remarks: p.line_remarks,
|
||||
part_type: p.part_type,
|
||||
quantity: p.quantity || 1,
|
||||
applicable_taxes: {
|
||||
local: false,
|
||||
state: false,
|
||||
federal: false
|
||||
}
|
||||
};
|
||||
})
|
||||
{!isPartsEntry && (
|
||||
<Button
|
||||
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
||||
onClick={() => {
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
disableInvNumber: true,
|
||||
job: { id: job.id },
|
||||
bill: {
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
isinhouse: true,
|
||||
date: dayjs(),
|
||||
total: 0,
|
||||
billlines: selectedLines.map((p) => {
|
||||
return {
|
||||
joblineid: p.id,
|
||||
actual_price: p.act_price,
|
||||
actual_cost: 0, //p.act_price,
|
||||
line_desc: p.line_desc,
|
||||
line_remarks: p.line_remarks,
|
||||
part_type: p.part_type,
|
||||
quantity: p.quantity || 1,
|
||||
applicable_taxes: {
|
||||
local: false,
|
||||
state: false,
|
||||
federal: false
|
||||
}
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
//Clear out the selected lines. IO-785
|
||||
setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
<HomeOutlined />
|
||||
{t("parts.actions.orderinhouse")}
|
||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||
</Button>
|
||||
//Clear out the selected lines. IO-785
|
||||
setSelectedLines([]);
|
||||
}}
|
||||
>
|
||||
<HomeOutlined />
|
||||
{t("parts.actions.orderinhouse")}
|
||||
{selectedLines.length > 0 && ` (${selectedLines.length})`}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
id="job-lines-order-parts-button"
|
||||
disabled={(job && !job.converted) || (selectedLines.length > 0 ? false : true) || jobRO || technician}
|
||||
@@ -578,7 +580,8 @@ export function JobLinesComponent({
|
||||
{t("joblines.actions.new")}
|
||||
</Button>
|
||||
)}
|
||||
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
||||
{!isPartsEntry &&
|
||||
InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
||||
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
|
||||
@@ -10,9 +10,12 @@ const mapStateToProps = createStructuredSelector({
|
||||
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, style }) {
|
||||
export function JobPartsQueueCount({ bodyshop, parts, defaultColLayout = DEFAULT_COL_LAYOUT }) {
|
||||
const partsStatus = useMemo(() => {
|
||||
if (!parts) return null;
|
||||
return parts.reduce(
|
||||
@@ -35,35 +38,34 @@ export function JobPartsQueueCount({ bodyshop, parts, style }) {
|
||||
}, [bodyshop, parts]);
|
||||
|
||||
if (!parts) return null;
|
||||
|
||||
return (
|
||||
<Row style={style}>
|
||||
<Col span={4}>
|
||||
<Row>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title="Total">
|
||||
<Tag>{partsStatus.total}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title="No Status">
|
||||
<Tag color="gold">{partsStatus["null"]}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_ordered}>
|
||||
<Tag color="blue">{partsStatus[bodyshop.md_order_statuses.default_ordered]}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_received}>
|
||||
<Tag color="green">{partsStatus[bodyshop.md_order_statuses.default_received]}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_returned}>
|
||||
<Tag color="orange">{partsStatus[bodyshop.md_order_statuses.default_returned]}</Tag>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Col {...defaultColLayout}>
|
||||
<Tooltip title={bodyshop.md_order_statuses.default_bo}>
|
||||
<Tag color="red">{partsStatus[bodyshop.md_order_statuses.default_bo]}</Tag>
|
||||
</Tooltip>
|
||||
|
||||
@@ -20,35 +20,27 @@ export function JobTotalsCashDiscount({ bodyshop, amountDinero }) {
|
||||
const notification = useNotification();
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (amountDinero && bodyshop) {
|
||||
setLoading(true);
|
||||
let response;
|
||||
try {
|
||||
response = await axios.post("/intellipay/checkfee", {
|
||||
bodyshop: { id: bodyshop.id, imexshopid: bodyshop.imexshopid, state: bodyshop.state },
|
||||
amount: Dinero(amountDinero).toFormat("0.00")
|
||||
});
|
||||
if (!amountDinero || !bodyshop) return;
|
||||
|
||||
if (response?.data?.error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message:
|
||||
response.data?.error ||
|
||||
"Error encountered when contacting IntelliPay service to determine cash discounted price."
|
||||
});
|
||||
} else {
|
||||
setFee(response.data?.fee || 0);
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message:
|
||||
error.response?.data?.error ||
|
||||
"Error encountered when contacting IntelliPay service to determine cash discounted price."
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
const errorMessage = "Error encountered when contacting IntelliPay service to determine cash discounted price.";
|
||||
|
||||
try {
|
||||
const { id, imexshopid, state } = bodyshop;
|
||||
const { data } = await axios.post("/intellipay/checkfee", {
|
||||
bodyshop: { id, imexshopid, state },
|
||||
amount: Dinero(amountDinero).toUnit()
|
||||
});
|
||||
|
||||
if (data?.error) {
|
||||
notification.open({ type: "error", message: data.error || errorMessage });
|
||||
} else {
|
||||
setFee(data?.fee ?? 0);
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({ type: "error", message: error.response?.data?.error || errorMessage });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [amountDinero, bodyshop, notification]);
|
||||
|
||||
|
||||
@@ -8,19 +8,20 @@ import { createStructuredSelector } from "reselect";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectIsPartsEntry, selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly
|
||||
jobRO: selectJobReadOnly,
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
|
||||
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail, isPartsEntry }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [availableStatuses, setAvailableStatuses] = useState([]);
|
||||
@@ -45,25 +46,43 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||
});
|
||||
};
|
||||
|
||||
// Updates available statuses based on job and bodyshop context
|
||||
useEffect(() => {
|
||||
//Figure out what scenario were in, populate accodingly
|
||||
if (job && bodyshop) {
|
||||
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) {
|
||||
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses);
|
||||
} else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) {
|
||||
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses);
|
||||
} else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) {
|
||||
setAvailableStatuses(
|
||||
bodyshop.md_ro_statuses.post_production_statuses.filter(
|
||||
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported
|
||||
)
|
||||
);
|
||||
} else {
|
||||
console.log("Status didn't match any restrictions. Allowing all status changes.");
|
||||
setAvailableStatuses(bodyshop.md_ro_statuses.statuses);
|
||||
}
|
||||
if (!job || !bodyshop) return;
|
||||
|
||||
const { md_ro_statuses } = bodyshop;
|
||||
const {
|
||||
parts_statuses,
|
||||
pre_production_statuses,
|
||||
production_statuses,
|
||||
post_production_statuses,
|
||||
statuses,
|
||||
default_invoiced,
|
||||
default_exported
|
||||
} = md_ro_statuses;
|
||||
|
||||
if (isPartsEntry) {
|
||||
// Set parts-specific statuses for parts entry scenario
|
||||
setAvailableStatuses(parts_statuses);
|
||||
return;
|
||||
}
|
||||
}, [job, setAvailableStatuses, bodyshop]);
|
||||
|
||||
// Handle non-parts entry scenarios based on job status
|
||||
if (pre_production_statuses.includes(job.status)) {
|
||||
setAvailableStatuses(pre_production_statuses);
|
||||
} else if (production_statuses.includes(job.status)) {
|
||||
setAvailableStatuses(production_statuses);
|
||||
} else if (post_production_statuses.includes(job.status)) {
|
||||
// Filter out invoiced and exported statuses for post-production
|
||||
setAvailableStatuses(
|
||||
post_production_statuses.filter((status) => status !== default_invoiced && status !== default_exported)
|
||||
);
|
||||
} else {
|
||||
// Default to all statuses if no specific restrictions apply
|
||||
console.log("Status didn't match any restrictions. Allowing all status changes.");
|
||||
setAvailableStatuses(statuses);
|
||||
}
|
||||
}, [job, bodyshop, isPartsEntry, setAvailableStatuses]);
|
||||
|
||||
const statusMenu = {
|
||||
items: [
|
||||
|
||||
@@ -139,17 +139,18 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<DataLabel label={t("jobs.fields.comment")} valueStyle={{ overflow: "hidden", textOverflow: "ellipsis" }}>
|
||||
<ProductionListColumnComment record={job} />
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>
|
||||
{!isPartsEntry && <DataLabel label={t("jobs.fields.ins_co_nm_short")}>{job.ins_co_nm}</DataLabel>}
|
||||
<DataLabel label={t("jobs.fields.clm_no")}>{job.clm_no}</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.ponumber")} hideIfNull>
|
||||
{job.po_number}
|
||||
</DataLabel>
|
||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
|
||||
{!isPartsEntry && (
|
||||
<DataLabel label={t("jobs.fields.repairtotal")}>
|
||||
<CurrencyFormatter>{job.clm_total}</CurrencyFormatter>
|
||||
<span style={{ margin: "0rem .5rem" }}>/</span>
|
||||
<CurrencyFormatter>{job.owner_owing}</CurrencyFormatter>
|
||||
</DataLabel>
|
||||
)}
|
||||
{!isPartsEntry && (
|
||||
<>
|
||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||
@@ -176,7 +177,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<Checkbox
|
||||
checked={!!job.estimate_sent_approval}
|
||||
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isPartsEntry}
|
||||
>
|
||||
{job.estimate_sent_approval && (
|
||||
<span style={{ color: "#888" }}>
|
||||
@@ -191,7 +192,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<Checkbox
|
||||
checked={!!job.estimate_approved}
|
||||
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isPartsEntry}
|
||||
>
|
||||
{job.estimate_approved && (
|
||||
<span style={{ color: "#888" }}>
|
||||
@@ -236,7 +237,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
disabled ? (
|
||||
disabled || isPartsEntry ? (
|
||||
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
|
||||
) : (
|
||||
<Link to={`/manage/owners/${job.owner.id}`}>
|
||||
@@ -247,14 +248,14 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
>
|
||||
<div>
|
||||
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||
{disabled ? (
|
||||
{disabled || isPartsEntry ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||
{disabled ? (
|
||||
{disabled || isPartsEntry ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||
@@ -266,7 +267,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||
{disabled ? (
|
||||
{disabled || isPartsEntry ? (
|
||||
<>{job.ownr_ea || ""}</>
|
||||
) : job.ownr_ea ? (
|
||||
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
||||
@@ -316,7 +317,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
||||
</DataLabel>
|
||||
{job.vehicle && job.vehicle.notes && (
|
||||
{job.vehicle?.notes && (
|
||||
<DataLabel
|
||||
label={t("vehicles.fields.notes")}
|
||||
valueStyle={{ whiteSpace: "pre-wrap" }}
|
||||
@@ -326,7 +327,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
{job.vehicle.notes}
|
||||
</DataLabel>
|
||||
)}
|
||||
{job.vehicle && job.vehicle.v_paint_codes && (
|
||||
{job.vehicle?.v_paint_codes && (
|
||||
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
|
||||
<span style={{ whiteSpace: "pre" }}>
|
||||
{Object.keys(job.vehicle.v_paint_codes)
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { Space, Tag } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import getPartsBasePath from "../../utils/getPartsBasePath.js";
|
||||
|
||||
export default function JobsRelatedRos({ job, disabled }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
|
||||
function JobsRelatedRos({ job, disabled, isPartsEntry }) {
|
||||
if (!(job?.vehicle && job.vehicle.jobs)) return null;
|
||||
const basePath = getPartsBasePath(isPartsEntry);
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
{job.vehicle.jobs
|
||||
@@ -12,7 +22,7 @@ export default function JobsRelatedRos({ job, disabled }) {
|
||||
{disabled ? (
|
||||
<>{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${j.status ? ` | ${j.status}` : ""}`}</>
|
||||
) : (
|
||||
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
|
||||
<Link to={`${basePath}/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
|
||||
j.clm_no ? ` | ${j.clm_no}` : ""
|
||||
}${j.status ? ` | ${j.status}` : ""}`}</Link>
|
||||
)}
|
||||
@@ -21,3 +31,4 @@ export default function JobsRelatedRos({ job, disabled }) {
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps)(JobsRelatedRos);
|
||||
|
||||
@@ -227,15 +227,21 @@ export function PartsOrderListTableComponent({
|
||||
sorter: (a, b) => a.order_date - b.order_date,
|
||||
sortOrder: state.sortedInfo.columnKey === "order_date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.order_date}</DateFormatter>
|
||||
},
|
||||
{
|
||||
}
|
||||
];
|
||||
|
||||
if (!isPartsEntry) {
|
||||
columns.push({
|
||||
title: t("parts_orders.fields.return"),
|
||||
dataIndex: "return",
|
||||
key: "return",
|
||||
sorter: (a, b) => a.return - b.return,
|
||||
sortOrder: state.sortedInfo.columnKey === "return" && state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.return} />
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
title: t("parts_orders.fields.deliver_by"),
|
||||
dataIndex: "deliver_by",
|
||||
@@ -256,7 +262,7 @@ export function PartsOrderListTableComponent({
|
||||
render: (text, record) => recordActions(record, true),
|
||||
id: "parts-order-list-table-actions"
|
||||
}
|
||||
];
|
||||
);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
|
||||
@@ -11,16 +11,27 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PartsOrderModalComponent);
|
||||
|
||||
export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState, isReturn, preferredMake, job, form }) {
|
||||
export function PartsOrderModalComponent({
|
||||
bodyshop,
|
||||
vendorList,
|
||||
sendTypeState,
|
||||
isReturn,
|
||||
preferredMake,
|
||||
job,
|
||||
form,
|
||||
isPartsEntry
|
||||
}) {
|
||||
const [sendType, setSendType] = sendTypeState;
|
||||
|
||||
const {
|
||||
@@ -83,7 +94,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
{!isReturn && (
|
||||
{!isReturn && !isPartsEntry && (
|
||||
<Form.Item
|
||||
name="removefrompartsqueue"
|
||||
label={t("parts_orders.labels.removefrompartsqueue")}
|
||||
@@ -92,7 +103,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
)}
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
{OEConnection.treatment === "on" && !isReturn && !isPartsEntry && (
|
||||
<Form.Item name="is_quote" label={t("parts_orders.labels.is_quote")} valuePropName="checked">
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
@@ -249,7 +260,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
<Radio disabled={is_quote} value={"p"}>
|
||||
{t("parts_orders.labels.print")}
|
||||
</Radio>
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
{OEConnection.treatment === "on" && !isReturn && !isPartsEntry && (
|
||||
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
|
||||
)}
|
||||
</Radio.Group>
|
||||
|
||||
@@ -233,7 +233,7 @@ export function PartsQueueListComponent({ bodyshop }) {
|
||||
title: t("jobs.fields.partsstatus"),
|
||||
dataIndex: "partsstatus",
|
||||
key: "partsstatus",
|
||||
render: (text, record) => <JobPartsQueueCount style={{ minWidth: "10rem" }} parts={record.joblines_status} />
|
||||
render: (text, record) => <JobPartsQueueCount parts={record.joblines_status} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.comment"),
|
||||
|
||||
@@ -37,7 +37,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
|
||||
.filter(
|
||||
(temp) =>
|
||||
(!temp.regions ||
|
||||
(temp.regions && temp.regions[bodyshop.region_config]) ||
|
||||
temp.regions?.[bodyshop.region_config] ||
|
||||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)) &&
|
||||
(!temp.dms || temp.dms === false)
|
||||
)
|
||||
@@ -46,7 +46,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
|
||||
.filter(
|
||||
(temp) =>
|
||||
!temp.regions ||
|
||||
(temp.regions && temp.regions[bodyshop.region_config]) ||
|
||||
temp.regions?.[bodyshop.region_config] ||
|
||||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
|
||||
);
|
||||
|
||||
@@ -82,7 +82,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
|
||||
variables: { id: jobId }
|
||||
},
|
||||
{
|
||||
to: job && job.ownr_ea,
|
||||
to: job?.ownr_ea,
|
||||
subject: cards.find((c) => c.key === key)?.subject
|
||||
},
|
||||
"e",
|
||||
@@ -129,7 +129,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
|
||||
const columns = `repeat(${actions.length}, 1fr)`;
|
||||
|
||||
return (
|
||||
<Col key={item.key} xs={24} sm={12}>
|
||||
<Col key={item.key} xs={24} sm={24} md={24} lg={24} xl={24}>
|
||||
<Card hoverable style={{ minHeight: 100 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<div style={{ flex: "1 1 70%", minWidth: 0 }}>
|
||||
|
||||
@@ -40,27 +40,26 @@ export function ScheduleCalendarWrapperComponent({
|
||||
const currentView = search.view || defaultView || "week";
|
||||
|
||||
const handleEventPropStyles = (event) => {
|
||||
const hasColor = Boolean(event?.color?.hex || event?.color);
|
||||
const { color, block, arrived } = event ?? {};
|
||||
const hasColor = Boolean(color?.hex || color);
|
||||
const useBg = currentView !== "agenda";
|
||||
|
||||
// Prioritize explicit blocked-day background to ensure red in all themes
|
||||
let bg;
|
||||
if (useBg) {
|
||||
if (event?.block) {
|
||||
bg = "var(--event-block-bg)";
|
||||
} else if (hasColor) {
|
||||
bg = event?.color?.hex ?? event?.color;
|
||||
} else {
|
||||
bg = "var(--event-bg-fallback)";
|
||||
}
|
||||
bg = block
|
||||
? "var(--event-block-bg)"
|
||||
: arrived
|
||||
? "var(--event-arrived-bg)"
|
||||
: (color?.hex ?? color ?? "var(--event-bg-fallback)");
|
||||
}
|
||||
|
||||
const usedFallback = !hasColor && !event?.block; // only mark as fallback when not blocked
|
||||
const usedFallback = !hasColor && !block && !arrived; // only mark as fallback when not blocked or arrived
|
||||
|
||||
const classes = [
|
||||
"imex-event",
|
||||
event.arrived && "imex-event-arrived",
|
||||
event.block && "imex-event-block",
|
||||
arrived && "imex-event-arrived",
|
||||
block && "imex-event-block",
|
||||
usedFallback && "imex-event-fallback"
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -23,13 +23,24 @@ export default function ShopInfoContainer() {
|
||||
});
|
||||
const notification = useNotification();
|
||||
|
||||
const combinedFeatureConfig = {
|
||||
...FEATURE_CONFIGS.general,
|
||||
...FEATURE_CONFIGS.responsibilitycenters
|
||||
};
|
||||
const combineFeatureConfigs = (...configs) =>
|
||||
(configs || [])
|
||||
.filter(Boolean)
|
||||
.flatMap((cfg) => Object.entries(cfg))
|
||||
.reduce((acc, [featureName, fieldPaths]) => {
|
||||
if (!Array.isArray(fieldPaths)) return acc;
|
||||
acc[featureName] = [...(acc[featureName] ?? []), ...fieldPaths];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const combinedFeatureConfig = combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters);
|
||||
|
||||
// Use form data preservation for all shop-info features
|
||||
const { createSubmissionHandler } = useFormDataPreservation(form, data?.bodyshops[0], combinedFeatureConfig);
|
||||
const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation(
|
||||
form,
|
||||
data?.bodyshops[0],
|
||||
combinedFeatureConfig
|
||||
);
|
||||
|
||||
const handleFinish = createSubmissionHandler((values) => {
|
||||
setSaveLoading(true);
|
||||
@@ -51,8 +62,11 @@ export default function ShopInfoContainer() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) form.resetFields();
|
||||
}, [form, data]);
|
||||
if (!data) return;
|
||||
form.resetFields();
|
||||
// After reset, re-apply hidden field preservation so values aren't wiped
|
||||
preserveHiddenFormData();
|
||||
}, [data, form, preserveHiddenFormData]);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
@@ -1183,7 +1183,17 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||
<Form.Item
|
||||
label={t("general.labels.label")}
|
||||
key={`${index}label`}
|
||||
name={[field.name, "label"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -1294,7 +1304,17 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||
<Form.Item
|
||||
label={t("general.labels.label")}
|
||||
key={`${index}label`}
|
||||
name={[field.name, "label"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -1483,15 +1503,31 @@ export function ShopInfoGeneral({ form, bodyshop }) {
|
||||
{fields.map((field, index) => (
|
||||
<Form.Item key={field.key}>
|
||||
<LayoutFormRow noDivider>
|
||||
<Form.Item label={t("general.labels.label")} key={`${index}label`} name={[field.name, "label"]}>
|
||||
<Form.Item
|
||||
label={t("general.labels.label")}
|
||||
key={`${index}label`}
|
||||
name={[field.name, "label"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bodyshop.labels.md_to_emails_emails")}
|
||||
key={`${index}emails`}
|
||||
name={[field.name, "emails"]}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Select mode="tags" tokenSeparators={[",", ";"]} />
|
||||
<FormItemEmail email={form.getFieldValue([field.name, "emails"])} />
|
||||
</Form.Item>
|
||||
|
||||
<Space>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
/**
|
||||
@@ -8,73 +8,57 @@ import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component
|
||||
* @param {Object} featureConfig - Configuration object defining which features and their associated fields to preserve
|
||||
*/
|
||||
export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
|
||||
const getNestedValue = (obj, path) => {
|
||||
return path.reduce((current, key) => current?.[key], obj);
|
||||
};
|
||||
|
||||
// Safe nested getters/setters using path arrays
|
||||
const getNestedValue = (obj, path) => path?.reduce((acc, key) => acc?.[key], obj);
|
||||
const setNestedValue = (obj, path, value) => {
|
||||
const lastKey = path[path.length - 1];
|
||||
const parentPath = path.slice(0, -1);
|
||||
|
||||
const parent = parentPath.reduce((current, key) => {
|
||||
if (!current[key]) current[key] = {};
|
||||
return current[key];
|
||||
const parent = path.slice(0, -1).reduce((curr, key) => {
|
||||
if (!curr[key] || typeof curr[key] !== "object") curr[key] = {};
|
||||
return curr[key];
|
||||
}, obj);
|
||||
|
||||
parent[lastKey] = value;
|
||||
};
|
||||
|
||||
const preserveHiddenFormData = useCallback(() => {
|
||||
const preservationData = {};
|
||||
let hasDataToPreserve = false;
|
||||
|
||||
// Paths for features that are NOT accessible
|
||||
const disabledPaths = useMemo(() => {
|
||||
const result = [];
|
||||
if (!featureConfig) return result;
|
||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||
if (hasAccess || !Array.isArray(fieldPaths)) return;
|
||||
fieldPaths.forEach((p) => Array.isArray(p) && p.length && result.push(p));
|
||||
});
|
||||
return result;
|
||||
}, [featureConfig, bodyshop]);
|
||||
|
||||
if (!hasAccess) {
|
||||
fieldPaths.forEach((fieldPath) => {
|
||||
const currentValues = form.getFieldsValue();
|
||||
let value = getNestedValue(currentValues, fieldPath);
|
||||
const preserveHiddenFormData = useCallback(() => {
|
||||
const currentValues = form.getFieldsValue();
|
||||
const preservationData = {};
|
||||
let hasAny = false;
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
value = getNestedValue(bodyshop, fieldPath);
|
||||
}
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
setNestedValue(preservationData, fieldPath, value);
|
||||
hasDataToPreserve = true;
|
||||
}
|
||||
});
|
||||
disabledPaths.forEach((path) => {
|
||||
let value = getNestedValue(currentValues, path);
|
||||
if (value == null) value = getNestedValue(bodyshop, path);
|
||||
if (value != null) {
|
||||
setNestedValue(preservationData, path, value);
|
||||
hasAny = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasDataToPreserve) {
|
||||
form.setFieldsValue(preservationData);
|
||||
}
|
||||
}, [form, featureConfig, bodyshop]);
|
||||
if (hasAny) form.setFieldsValue(preservationData);
|
||||
}, [form, bodyshop, disabledPaths]);
|
||||
|
||||
const getCompleteFormValues = () => {
|
||||
const currentFormValues = form.getFieldsValue();
|
||||
const completeValues = { ...currentFormValues };
|
||||
const currentValues = form.getFieldsValue();
|
||||
const complete = { ...currentValues };
|
||||
|
||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||
|
||||
if (!hasAccess) {
|
||||
fieldPaths.forEach((fieldPath) => {
|
||||
let value = getNestedValue(currentFormValues, fieldPath);
|
||||
if (value === undefined || value === null) {
|
||||
value = getNestedValue(bodyshop, fieldPath);
|
||||
}
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
setNestedValue(completeValues, fieldPath, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
disabledPaths.forEach((path) => {
|
||||
let value = getNestedValue(currentValues, path);
|
||||
if (value == null) value = getNestedValue(bodyshop, path);
|
||||
if (value != null) setNestedValue(complete, path, value);
|
||||
});
|
||||
|
||||
return completeValues;
|
||||
return complete;
|
||||
};
|
||||
|
||||
const createSubmissionHandler = (originalHandler) => {
|
||||
@@ -103,8 +87,8 @@ export const FEATURE_CONFIGS = {
|
||||
["md_responsibility_centers", "profits"],
|
||||
["md_responsibility_centers", "defaults"],
|
||||
["md_responsibility_centers", "dms_defaults"],
|
||||
["md_responsibility_centers", "taxes", "itemexemptcode"],
|
||||
["md_responsibility_centers", "taxes", "invoiceexemptcode"],
|
||||
["md_responsibility_centers", "taxes"],
|
||||
["md_responsibility_centers", "cieca_pfl"],
|
||||
["md_responsibility_centers", "ar"],
|
||||
["md_responsibility_centers", "refund"],
|
||||
["md_responsibility_centers", "sales_tax_codes"],
|
||||
|
||||
@@ -9,7 +9,6 @@ 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";
|
||||
@@ -96,13 +95,15 @@ export function SimplifiedPartsJobsListComponent({
|
||||
key: "status",
|
||||
|
||||
ellipsis: true,
|
||||
sorter: search?.search ? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses) : true,
|
||||
sorter: search?.search
|
||||
? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.parts_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) => {
|
||||
filters: bodyshop.md_ro_statuses.parts_statuses.map((s) => {
|
||||
return { text: s, value: [s] };
|
||||
}),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
@@ -142,31 +143,11 @@ export function SimplifiedPartsJobsListComponent({
|
||||
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 ? (
|
||||
<CurrencyFormatter>{record.clm_total}</CurrencyFormatter>
|
||||
) : (
|
||||
t("general.labels.unknown")
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.partsstatus"),
|
||||
dataIndex: "partsstatus",
|
||||
key: "partsstatus",
|
||||
render: (text, record) => <JobPartsQueueCount style={{ minWidth: "10rem" }} parts={record.joblines_status} />
|
||||
render: (text, record) => <JobPartsQueueCount parts={record.joblines_status} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.comment"),
|
||||
|
||||
@@ -14,16 +14,18 @@ import VendorsPhonebookAdd from "../vendors-phonebook-add/vendors-phonebook-add.
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VendorsFormComponent);
|
||||
|
||||
export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete, selectedvendor }) {
|
||||
export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete, selectedvendor, isPartsEntry }) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
|
||||
@@ -57,8 +59,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
|
||||
>
|
||||
{t("general.actions.delete")}
|
||||
</Button>
|
||||
|
||||
<VendorsPhonebookAdd form={form} disabled={form.isFieldsTouched()} />
|
||||
{!isPartsEntry && <VendorsPhonebookAdd form={form} disabled={form.isFieldsTouched()} />}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
@@ -148,12 +149,18 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("vendors.fields.discount")} name="discount">
|
||||
<InputNumber min={0} max={1} precision={2} step={0.01} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("vendors.fields.due_date")} name="due_date">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
{!isPartsEntry && (
|
||||
<>
|
||||
<Form.Item label={t("vendors.fields.discount")} name="discount">
|
||||
<InputNumber min={0} max={1} precision={2} step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("vendors.fields.due_date")} name="due_date">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{
|
||||
// <Form.Item
|
||||
// label={t("vendors.fields.cost_center")}
|
||||
@@ -173,7 +180,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
|
||||
|
||||
<Form.Item
|
||||
name="tags"
|
||||
label={t("vendor.fields.tags")}
|
||||
label={t("vendors.fields.tags")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
|
||||
@@ -4,6 +4,8 @@ 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 posthog from 'posthog-js'
|
||||
|
||||
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
|
||||
initializeApp(config);
|
||||
@@ -71,25 +73,33 @@ onMessage(messaging, (payload) => {
|
||||
});
|
||||
|
||||
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
||||
const state = stateProp || store.getState();
|
||||
const eventParams = {
|
||||
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
|
||||
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
|
||||
...additionalParams
|
||||
};
|
||||
// axios.post("/ioevent", {
|
||||
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
|
||||
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
|
||||
// operationName: eventName,
|
||||
// variables: additionalParams,
|
||||
// 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
|
||||
// );
|
||||
logEvent(analytics, eventName, eventParams);
|
||||
try {
|
||||
|
||||
const state = stateProp || store.getState();
|
||||
const eventParams = {
|
||||
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
|
||||
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
|
||||
...additionalParams
|
||||
};
|
||||
// axios.post("/ioevent", {
|
||||
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
|
||||
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
|
||||
// operationName: eventName,
|
||||
// variables: additionalParams,
|
||||
// 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
|
||||
// );
|
||||
logEvent(analytics, eventName, eventParams);
|
||||
amplitude.track(eventName, eventParams);
|
||||
posthog.capture(eventName, eventParams);
|
||||
|
||||
} finally {
|
||||
//If it fails, just keep going.
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,9 @@ import { persistor, store } from "./redux/store";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import "./translations/i18n";
|
||||
import "./utils/CleanAxios";
|
||||
import * as amplitude from "@amplitude/analytics-browser";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import posthog from "posthog-js";
|
||||
|
||||
window.global ||= window;
|
||||
|
||||
@@ -23,10 +26,33 @@ registerSW({ immediate: true });
|
||||
// Dinero.globalLocale = "en-CA";
|
||||
Dinero.globalRoundingMode = "HALF_EVEN";
|
||||
|
||||
amplitude.init("6228a598e57cd66875cfd41604f1f891", {
|
||||
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,
|
||||
capture_exceptions: true,
|
||||
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST
|
||||
});
|
||||
|
||||
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
|
||||
|
||||
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
let styles =
|
||||
"font-weight: bold; font-size: 50px;color: red; 6px 6px 0 rgb(226,91,14) , 9px 9px 0 rgb(245,221,8) , 12px 12px 0 rgb(5,148,68) ";
|
||||
@@ -37,7 +63,9 @@ function App() {
|
||||
return (
|
||||
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
|
||||
<Provider store={store}>
|
||||
<RouterProvider router={router} />
|
||||
<PostHogProvider client={posthog}>
|
||||
<RouterProvider router={router} />
|
||||
</PostHogProvider>
|
||||
</Provider>
|
||||
</PersistGate>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { Button, Card, Checkbox, Input, Space, Table, Typography } from "antd";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import axios from "axios";
|
||||
import queryString from "query-string";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -16,6 +17,7 @@ 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";
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" }))
|
||||
@@ -33,25 +35,22 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
|
||||
});
|
||||
const Templates = TemplateList("bill");
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: vendorsData } = useQuery(QUERY_ALL_VENDORS);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("bills.fields.vendorname"),
|
||||
dataIndex: "vendorname",
|
||||
key: "vendorname",
|
||||
// sortObject: (direction) => {
|
||||
// return {
|
||||
// vendor: {
|
||||
// name: direction
|
||||
// ? direction === "descend"
|
||||
// ? "desc"
|
||||
// : "asc"
|
||||
// : "desc",
|
||||
// },
|
||||
// };
|
||||
// },
|
||||
// sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
|
||||
sortObject: (order) => ({
|
||||
vendor: { name: order === "descend" ? "desc" : "asc" }
|
||||
}),
|
||||
filters: (vendorsData?.vendors || []).map((v) => ({ text: v.name, value: v.id })),
|
||||
filteredValue: state.filteredInfo.vendorname || null,
|
||||
onFilter: (value, record) => record.vendorid === value,
|
||||
sortOrder: state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
|
||||
render: (text, record) => <span>{record.vendor.name}</span>
|
||||
},
|
||||
{
|
||||
@@ -65,20 +64,11 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
|
||||
title: t("jobs.fields.ro_number"),
|
||||
dataIndex: "ro_number",
|
||||
key: "ro_number",
|
||||
// sortObject: (direction) => {
|
||||
// return {
|
||||
// job: {
|
||||
// ro_number: direction
|
||||
// ? direction === "descend"
|
||||
// ? "desc"
|
||||
// : "asc"
|
||||
// : "desc",
|
||||
// },
|
||||
// };
|
||||
// },
|
||||
// sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
// sortOrder:
|
||||
// state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
sorter: (a, b) => alphaSort(a.job.ro_number, b.job.ro_number),
|
||||
sortObject: (order) => ({
|
||||
job: { ro_number: order === "descend" ? "desc" : "asc" }
|
||||
}),
|
||||
sortOrder: state.sortedInfo.columnKey === "ro_number" && state.sortedInfo.order,
|
||||
render: (text, record) => record.job && <Link to={`/manage/jobs/${record.job.id}`}>{record.job.ro_number}</Link>
|
||||
},
|
||||
{
|
||||
@@ -175,7 +165,8 @@ export function BillsListPage({ loading, data, refetch, total, setBillEnterConte
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
// Persist filters (including vendorname) and sorting
|
||||
setState({ ...state, filteredInfo: { ...state.filteredInfo, ...filters }, sortedInfo: sorter });
|
||||
search.page = pagination.current;
|
||||
if (sorter && sorter.column && sorter.column.sortObject) {
|
||||
search.searchObj = JSON.stringify(sorter.column.sortObject(sorter.order));
|
||||
|
||||
@@ -11,7 +11,6 @@ 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";
|
||||
@@ -133,9 +132,8 @@ export function SimplifiedPartsJobDetailComponent({ setPrintCenterContext, jobRO
|
||||
<JobLineUpsertModalContainer />
|
||||
|
||||
<PageHeader title={<Space>{job.ro_number || t("general.labels.na")}</Space>} extra={menuExtra} />
|
||||
<JobsDetailHeader job={job} disabled={true} />
|
||||
<JobsDetailHeader job={job} />
|
||||
<Divider type="horizontal" />
|
||||
<JobProfileDataWarning job={job} />
|
||||
<FormFieldsChanged form={form} />
|
||||
<Tabs
|
||||
defaultActiveKey={search.tab}
|
||||
|
||||
@@ -49,6 +49,8 @@ import {
|
||||
validatePasswordResetSuccess
|
||||
} from "./user.actions";
|
||||
import UserActionTypes from "./user.types";
|
||||
import * as amplitude from '@amplitude/analytics-browser';
|
||||
import posthog from 'posthog-js';
|
||||
|
||||
const fpPromise = FingerprintJS.load();
|
||||
|
||||
@@ -83,8 +85,6 @@ export function* onCheckUserSession() {
|
||||
|
||||
export function* isUserAuthenticated() {
|
||||
try {
|
||||
logImEXEvent("redux_auth_check");
|
||||
|
||||
const user = yield getCurrentUser();
|
||||
if (!user) {
|
||||
yield put(unauthorizedUser());
|
||||
@@ -92,6 +92,8 @@ export function* isUserAuthenticated() {
|
||||
}
|
||||
|
||||
LogRocket.identify(user.email);
|
||||
amplitude.setUserId(user.email);
|
||||
posthog.identify(user.email);
|
||||
|
||||
const eulaQuery = yield client.query({
|
||||
query: QUERY_EULA,
|
||||
@@ -137,6 +139,7 @@ export function* signOutStart() {
|
||||
imexshopid: state.user.bodyshop.imexshopid,
|
||||
type: "messaging"
|
||||
});
|
||||
amplitude.reset();
|
||||
} catch {
|
||||
console.log("No FCM token. Skipping unsubscribe.");
|
||||
}
|
||||
@@ -266,11 +269,11 @@ export function* signInSuccessSaga({ payload }) {
|
||||
instanceSeg,
|
||||
...(isParts
|
||||
? [
|
||||
InstanceRenderManager({
|
||||
imex: "ImexPartsManagement",
|
||||
rome: "RomePartsManagement"
|
||||
})
|
||||
]
|
||||
InstanceRenderManager({
|
||||
imex: "ImexPartsManagement",
|
||||
rome: "RomePartsManagement"
|
||||
})
|
||||
]
|
||||
: [])
|
||||
];
|
||||
window.$crisp.push(["set", "session:segments", [segs]]);
|
||||
@@ -295,7 +298,7 @@ export function* signInSuccessSaga({ payload }) {
|
||||
|
||||
setUserId(analytics, payload.email);
|
||||
setUserProperties(analytics, payload);
|
||||
yield logImEXEvent("redux_sign_in_success");
|
||||
yield;
|
||||
}
|
||||
|
||||
export function* onSendPasswordResetStart() {
|
||||
@@ -362,6 +365,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
}
|
||||
|
||||
try {
|
||||
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]]);
|
||||
|
||||
@@ -1249,7 +1249,8 @@
|
||||
"sizelimit": "The selected items exceed the size limit.",
|
||||
"sub_status": {
|
||||
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.",
|
||||
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
|
||||
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate.",
|
||||
"undefined": "The subscription for this shop is removed. Please contact Sales to reactivate."
|
||||
},
|
||||
"submit-for-testing": "Error submitting Job for testing."
|
||||
},
|
||||
|
||||
@@ -1249,7 +1249,8 @@
|
||||
"sizelimit": "",
|
||||
"sub_status": {
|
||||
"expired": "",
|
||||
"trial-expired": ""
|
||||
"trial-expired": "",
|
||||
"undefined": ""
|
||||
},
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
|
||||
@@ -1249,7 +1249,8 @@
|
||||
"sizelimit": "",
|
||||
"sub_status": {
|
||||
"expired": "",
|
||||
"trial-expired": ""
|
||||
"trial-expired": "",
|
||||
"undefined": ""
|
||||
},
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
|
||||
@@ -181,7 +181,10 @@ const cache = new InMemoryCache({
|
||||
const client = new ApolloClient({
|
||||
link: ApolloLink.from(middlewares),
|
||||
cache,
|
||||
connectToDevTools: import.meta.env.DEV,
|
||||
devtools: {
|
||||
name: "Imex Client",
|
||||
enabled: import.meta.env.DEV
|
||||
},
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: "network-only",
|
||||
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
- redis-node-1-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
- redis-node-2-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
- redis-node-3-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
ports:
|
||||
- "4566:4566"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:4566/_localstack/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -118,6 +118,7 @@ services:
|
||||
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-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
|
||||
"
|
||||
# Node App: The Main IMEX API
|
||||
node-app:
|
||||
|
||||
@@ -959,6 +959,7 @@
|
||||
- enforce_referral
|
||||
- entegral_configuration
|
||||
- entegral_id
|
||||
- external_shop_id
|
||||
- features
|
||||
- federal_tax_id
|
||||
- id
|
||||
@@ -1012,6 +1013,8 @@
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- region_config
|
||||
- rr_configuration
|
||||
- rr_dealerid
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
@@ -1035,7 +1038,6 @@
|
||||
- use_fippa
|
||||
- use_paint_scale_data
|
||||
- uselocalmediaserver
|
||||
- external_shop_id
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
@@ -1068,6 +1070,7 @@
|
||||
- enforce_conversion_category
|
||||
- enforce_conversion_csr
|
||||
- enforce_referral
|
||||
- external_shop_id
|
||||
- federal_tax_id
|
||||
- id
|
||||
- inhousevendorid
|
||||
@@ -1113,6 +1116,7 @@
|
||||
- phone
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- rr_configuration
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
@@ -1131,7 +1135,6 @@
|
||||
- use_fippa
|
||||
- use_paint_scale_data
|
||||
- uselocalmediaserver
|
||||
- external_shop_id
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "rr_configuration" jsonb
|
||||
-- null default jsonb_build_object();
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "rr_configuration" jsonb
|
||||
null default jsonb_build_object();
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "rr_dealierid" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "rr_dealierid" text
|
||||
null;
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."bodyshops" rename column "rr_dealerid" to "rr_dealierid";
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."bodyshops" rename column "rr_dealierid" to "rr_dealerid";
|
||||
3235
package-lock.json
generated
3235
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -18,14 +18,14 @@
|
||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.873.0",
|
||||
"@aws-sdk/client-elasticache": "^3.872.0",
|
||||
"@aws-sdk/client-s3": "^3.872.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.872.0",
|
||||
"@aws-sdk/client-ses": "^3.872.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.873.0",
|
||||
"@aws-sdk/lib-storage": "^3.872.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.872.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.882.0",
|
||||
"@aws-sdk/client-elasticache": "^3.882.0",
|
||||
"@aws-sdk/client-s3": "^3.882.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.882.0",
|
||||
"@aws-sdk/client-ses": "^3.882.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.882.0",
|
||||
"@aws-sdk/lib-storage": "^3.882.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.882.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
@@ -33,18 +33,18 @@
|
||||
"aws4": "^1.13.2",
|
||||
"axios": "^1.11.0",
|
||||
"better-queue": "^3.8.12",
|
||||
"bullmq": "^5.58.0",
|
||||
"bullmq": "^5.58.5",
|
||||
"chart.js": "^4.5.0",
|
||||
"cloudinary": "^2.7.0",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"crisp-status-reporter": "^1.2.2",
|
||||
"dd-trace": "^5.63.3",
|
||||
"dd-trace": "^5.65.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^4.21.1",
|
||||
"firebase-admin": "^13.4.0",
|
||||
"firebase-admin": "^13.5.0",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-request": "^6.1.0",
|
||||
"intuit-oauth": "^4.2.0",
|
||||
@@ -62,12 +62,12 @@
|
||||
"query-string": "7.1.3",
|
||||
"recursive-diff": "^1.0.9",
|
||||
"rimraf": "^6.0.1",
|
||||
"skia-canvas": "^3.0.3",
|
||||
"skia-canvas": "^3.0.6",
|
||||
"soap": "^1.3.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-adapter": "^2.5.5",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
"twilio": "^5.8.0",
|
||||
"twilio": "^5.9.0",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-cloudwatch": "^6.3.0",
|
||||
@@ -76,8 +76,8 @@
|
||||
"yazl": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"eslint": "^9.33.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"mock-require": "^3.0.3",
|
||||
|
||||
@@ -6,4 +6,5 @@ exports.kaizen = require("./kaizen").default;
|
||||
exports.usageReport = require("./usageReport").default;
|
||||
exports.podium = require("./podium").default;
|
||||
exports.emsUpload = require("./emsUpload").default;
|
||||
exports.carfax = require("./carfax").default;
|
||||
exports.carfax = require("./carfax").default;
|
||||
exports.vehicletype = require("./vehicletype/vehicletype").default;
|
||||
126
server/data/vehicletype/cargovans.json
Normal file
126
server/data/vehicletype/cargovans.json
Normal file
@@ -0,0 +1,126 @@
|
||||
[
|
||||
"PROMASTER 1500",
|
||||
"PROMASTER 2500",
|
||||
"PROMASTER CITY",
|
||||
"NV 1500",
|
||||
"NV 200",
|
||||
"NV 2500",
|
||||
"NV 3500",
|
||||
"NV1500",
|
||||
"NV200",
|
||||
"NV2500",
|
||||
"NV3500",
|
||||
"SPRINTER",
|
||||
"E150 ECONOLINE CARGO VAN",
|
||||
"E150 ECONOLINE XL",
|
||||
"E250 ECONOLINE CARGO",
|
||||
"E250 ECONOLINE CARGO (AMALGAM)",
|
||||
"E250 ECONOLINE CARGO (INSPECT)",
|
||||
"E250 ECONOLINE CARGO VAN EXT",
|
||||
"E250 ECONOLINE SUPER CARGO VAN",
|
||||
"E350 CUTAWAY VAN",
|
||||
"E350 ECONO SD CARGO VAN EXT",
|
||||
"E350 ECONOLINE CARGO VAN",
|
||||
"E350 ECONOLINE CUTAWAY",
|
||||
"E350 ECONOLINE SD CARGO VAN",
|
||||
"E350 ECONOLINE SD XL",
|
||||
"E350 ECONOLINE SD XL EXT",
|
||||
"E350 ECONOLINE SD XLT",
|
||||
"E350 ECONOLINE SD XLT EXT",
|
||||
"E350 SD CUTAWAY",
|
||||
"E450",
|
||||
"E450 ECONOLINE",
|
||||
"E450 ECONOLINE SD",
|
||||
"E450 ECONOLINE SD CUTAWAY",
|
||||
"TRANSIT 150 WB 130 CARGO VAN",
|
||||
"TRANSIT 150 WB 130 XLT",
|
||||
"TRANSIT 150 WB 148 CARGO VAN",
|
||||
"TRANSIT 250 WB 130 CARGO VAN",
|
||||
"TRANSIT 250 WB 148 CARGO VAN",
|
||||
"TRANSIT 250 WB 148 EL CARGO",
|
||||
"TRANSIT 350 WB 148 CARGO VAN",
|
||||
"TRANSIT 350 WB 148 EL CARGO",
|
||||
"TRANSIT CONNECT XL CARGO VAN",
|
||||
"TRANSIT CONNECT XLT CARGO VAN",
|
||||
"250 TRANSIT",
|
||||
"CITY EXPRESS LS CARGO VAN",
|
||||
"CITY EXPRESS LT CARGO VAN",
|
||||
"EXPRESS 1500",
|
||||
"EXPRESS 1500 CARGO VAN",
|
||||
"EXPRESS 1500 LS",
|
||||
"EXPRESS 1500 LT",
|
||||
"EXPRESS 2500 CARGO VAN",
|
||||
"EXPRESS 2500 CARGO VAN EXT",
|
||||
"EXPRESS 2500 LS",
|
||||
"EXPRESS 2500 LT",
|
||||
"EXPRESS 3500",
|
||||
"EXPRESS 3500 CARGO VAN",
|
||||
"EXPRESS 3500 CARGO VAN EXT",
|
||||
"EXPRESS 3500 EXT",
|
||||
"EXPRESS 3500 LS",
|
||||
"EXPRESS 3500 LS EXT",
|
||||
"EXPRESS 3500 LT",
|
||||
"EXPRESS 3500 LT EXT",
|
||||
"G3500 EXPRESS CUTAWAY",
|
||||
"SAVANA 1500 CARGO VAN",
|
||||
"SAVANA 1500 SL",
|
||||
"SAVANA 1500 SLE",
|
||||
"SAVANA 2500",
|
||||
"2500 SAVANA",
|
||||
"SAVANA 2500 CARGO VAN",
|
||||
"SAVANA 2500 CARGO VAN EXT",
|
||||
"SAVANA 2500 LT",
|
||||
"SAVANA 2500 SLE",
|
||||
"SAVANA 3500",
|
||||
"SAVANA 3500 CARGO VAN",
|
||||
"SAVANA 3500 CARGO VAN EXT",
|
||||
"SAVANA 3500 EXT",
|
||||
"SAVANA 3500 LT EXT",
|
||||
"SAVANA 3500 SLE EXT",
|
||||
"SAVANA G3500 CUTAWAY",
|
||||
"SAVANA G4500 CUTAWAY",
|
||||
"EXPRESS 1500 LS CARGO VAN",
|
||||
"G20 SPORTVAN",
|
||||
"NV 3500 S V8 CARGO VAN",
|
||||
"E-150",
|
||||
"E-250",
|
||||
"E-350",
|
||||
"E-450",
|
||||
"E150",
|
||||
"E250",
|
||||
"E350",
|
||||
"TRANSIT",
|
||||
"CITY",
|
||||
"CITY EXPRESS",
|
||||
"EXPRESS",
|
||||
"EXPRESS 2500",
|
||||
"G3500",
|
||||
"SAVANA",
|
||||
"SAVANA 1500",
|
||||
"CHEVY EXPRESS G2500",
|
||||
"CLUBWAGON E350",
|
||||
"TRANSIT CONNECT",
|
||||
"SPRINTER 2500",
|
||||
"TRANSIT 150",
|
||||
"ECONOLINE E250",
|
||||
"TRANSIT 250",
|
||||
"ECONOLINE E350",
|
||||
"NV3500 HD",
|
||||
"TRANSIT 350HD",
|
||||
"ECONOLINE E150",
|
||||
"E250 ECONOLINE",
|
||||
"C/V",
|
||||
"E350 CHSCAB",
|
||||
"G1500 CHEVY EXPRESS",
|
||||
"2500 SPRINTER",
|
||||
"E150 ECONOLINE",
|
||||
"350 TRANSIT",
|
||||
"E450 CUTAWAY",
|
||||
"PROMASTER 3500",
|
||||
"CHEVY EXPRESS G3500",
|
||||
"SAVANA G3500",
|
||||
"1500 PROMASTER",
|
||||
"2500 EXPRESS",
|
||||
"3500 EXPRESS",
|
||||
"3500 SPRINTER"
|
||||
]
|
||||
33
server/data/vehicletype/passengervans.json
Normal file
33
server/data/vehicletype/passengervans.json
Normal file
@@ -0,0 +1,33 @@
|
||||
[
|
||||
"GRAND CARAVAN",
|
||||
"GRANDCARAVAN",
|
||||
"GRAND CARAVAN CREW",
|
||||
"GRAND CARAVAN CV",
|
||||
"GRAND CARAVAN CVP",
|
||||
"GRAND CARAVAN SE",
|
||||
"GRAND CARAVAN SXT",
|
||||
"CARAVAN CV",
|
||||
"SIENNA CE V6",
|
||||
"SIENNA LE V6",
|
||||
"SIENNA XLE V6",
|
||||
"SIENNA",
|
||||
"ODYSSEY",
|
||||
"SEDONA",
|
||||
"PACIFICA (NEW)",
|
||||
"QUEST",
|
||||
"CARAVAN",
|
||||
"MONTANA SV6",
|
||||
"FREESTAR",
|
||||
"UPLANDER",
|
||||
"MONTANA",
|
||||
"VOYAGER",
|
||||
"ENTOURAGE",
|
||||
"PACIFICA",
|
||||
"CARNIVAL",
|
||||
"VENTURE",
|
||||
"SAFARI",
|
||||
"VANAGON",
|
||||
"WINDSTAR",
|
||||
"TOWN&COUNTRY",
|
||||
"ROUTAN"
|
||||
]
|
||||
485
server/data/vehicletype/suvs.json
Normal file
485
server/data/vehicletype/suvs.json
Normal file
@@ -0,0 +1,485 @@
|
||||
[
|
||||
"EDGE SEL",
|
||||
"ESCAPE",
|
||||
"ESCAPE SE",
|
||||
"ESCAPE SEL",
|
||||
"ESCAPE XLT V6",
|
||||
"EXPEDITION",
|
||||
"EXPEDITION LIMITED",
|
||||
"EXPEDITION MAX",
|
||||
"EXPEDITION MAX LIMITED",
|
||||
"EXPLORER",
|
||||
"EXCURSION",
|
||||
"EXPLORER LIMITED",
|
||||
"EXPLORER PLATINUM ECOBOOST",
|
||||
"EXPLORER XLT",
|
||||
"FLEX",
|
||||
"FLEX SE",
|
||||
"ECOSPORT",
|
||||
"ESCAPE HYBRID",
|
||||
"MUSTANG MACH-E",
|
||||
"BRONCO",
|
||||
"BRONCO SPORT",
|
||||
"TRAILBLAZER",
|
||||
"BLAZER LT",
|
||||
"CHEROKEE",
|
||||
"CHEROKEE CLASSIC",
|
||||
"CHEROKEE COUNTRY",
|
||||
"CHEROKEE LIMITED",
|
||||
"CHEROKEE NORTH",
|
||||
"CHEROKEE OVERLAND",
|
||||
"CHEROKEE SPORT",
|
||||
"CHEROKEE TRAILHAWK",
|
||||
"CJ",
|
||||
"CJ7",
|
||||
"CJ7 RENEGADE",
|
||||
"COMMANDER",
|
||||
"COMMANDER LIMITED",
|
||||
"COMMANDER SPORT",
|
||||
"COMPASS",
|
||||
"COMPASS HIGH ALTITUDE",
|
||||
"COMPASS LATITUDE",
|
||||
"COMPASS LIMITED",
|
||||
"COMPASS NORTH",
|
||||
"COMPASS SPORT",
|
||||
"COMPASS TRAILHAWK",
|
||||
"GLADIATOR OVERLAND",
|
||||
"GLADIATOR RUBICON",
|
||||
"GRAND CHEROKEE LAREDO",
|
||||
"GRAND CHEROKEE LIMITED",
|
||||
"GRAND CHEROKEE OVERLAND",
|
||||
"GRAND CHEROKEE SE",
|
||||
"GRAND CHEROKEE SRT",
|
||||
"GRAND CHEROKEE SRT8",
|
||||
"GRAND CHEROKEE SUMMIT",
|
||||
"GRAND CHEROKEE TRACKHAWK",
|
||||
"GRAND CHEROKEE TRAILHAWK",
|
||||
"GRAND CHEROKEE",
|
||||
"GRANDCHEROKEE",
|
||||
"LIBERTY LIMITED",
|
||||
"LIBERTY RENEGADE",
|
||||
"LIBERTY SPORT",
|
||||
"LIBERTY",
|
||||
"PATRIOT",
|
||||
"PATRIOT HIGH ALTITUDE",
|
||||
"PATRIOT LATITUDE",
|
||||
"PATRIOT LIMITED",
|
||||
"PATRIOT NORTH",
|
||||
"PATRIOT SPORT",
|
||||
"RENEGADE LIMITED",
|
||||
"RENEGADE NORTH",
|
||||
"RENEGADE SPORT",
|
||||
"RENEGADE TRAILHAWK",
|
||||
"TJ",
|
||||
"TJ RUBICON",
|
||||
"TJ SAHARA",
|
||||
"TJ SPORT",
|
||||
"TJ UNLIMITED",
|
||||
"WRANGLER",
|
||||
"WRANGLER RUBICON",
|
||||
"WRANGLER SAHARA",
|
||||
"WRANGLER SPORT",
|
||||
"WRANGLER UNLIMITED",
|
||||
"WRANGLER UNLIMITED 70TH ANNIV",
|
||||
"WRANGLER UNLIMITED RUBICON",
|
||||
"WRANGLER UNLIMITED SAHARA",
|
||||
"WRANGLER UNLIMITED SPORT",
|
||||
"WRANGLER UNLIMITED X",
|
||||
"WRANGLER X",
|
||||
"YJ WRANGLER",
|
||||
"AVIATOR",
|
||||
"AVIATOR RESERVE",
|
||||
"MKC",
|
||||
"MKC RESERVE",
|
||||
"MKC SELECT",
|
||||
"MKT",
|
||||
"MKT ECOBOOST",
|
||||
"MKX",
|
||||
"MKX RESERVE",
|
||||
"NAUTILUS RESERVE",
|
||||
"NAUTILUS RESERVE V6",
|
||||
"NAVIGATOR",
|
||||
"NAVIGATOR L",
|
||||
"NAVIGATOR L RESERVE",
|
||||
"NAVIGATOR L SELECT",
|
||||
"NAVIGATOR RESERVE",
|
||||
"PILOT",
|
||||
"PILOT BLACK EDITION",
|
||||
"PILOT ELITE",
|
||||
"PILOT EX",
|
||||
"PILOT EX-L",
|
||||
"PILOT GRANITE",
|
||||
"PILOT LX",
|
||||
"PILOT SE",
|
||||
"PILOT SE-L",
|
||||
"PILOT TOURING",
|
||||
"DURANGO R/T",
|
||||
"DURANGO SLT PLUS",
|
||||
"DURANGO SRT",
|
||||
"DURANGO",
|
||||
"JOURNEY",
|
||||
"JOURNEY CROSSROAD",
|
||||
"JOURNEY CVP",
|
||||
"JOURNEY LIMITED",
|
||||
"JOURNEY R/T",
|
||||
"JOURNEY SXT",
|
||||
"NITRO SE",
|
||||
"NITRO",
|
||||
"K1500 SUBURBAN",
|
||||
"SUBURBAN 1500 LT",
|
||||
"SUBURBAN 1500 LTZ",
|
||||
"SUBURBAN 1500 PREMIER",
|
||||
"SUBURBAN 2500 LS",
|
||||
"TAHOE LT",
|
||||
"TRAVERSE LS",
|
||||
"TRAVERSE LT",
|
||||
"TRAVERSE PREMIER",
|
||||
"TRAX LT",
|
||||
"TRAX PREMIER",
|
||||
"UPLANDER LT EXT",
|
||||
"SUBURBAN",
|
||||
"TAHOE",
|
||||
"TRAVERSE",
|
||||
"TRAX",
|
||||
"UPLANDER",
|
||||
"YUKON",
|
||||
"YUKON DENALI",
|
||||
"YUKON XL",
|
||||
"YUKON XL DENALI",
|
||||
"EQUINOX LS",
|
||||
"EQUINOX LT",
|
||||
"EQUINOX PREMIER",
|
||||
"EQUINOX",
|
||||
"RAV4 LE",
|
||||
"RAV4 XLE",
|
||||
"HIGHLANDER SPORT V6",
|
||||
"4RUNNER SR5 V6",
|
||||
"RAV4",
|
||||
"RAV4 HYBRID",
|
||||
"RAV4 XLE HYBRID",
|
||||
"HIGHLANDER",
|
||||
"4RUNNER",
|
||||
"SEQUOIA",
|
||||
"PATHFINDER SE",
|
||||
"PATHFINDER SL",
|
||||
"PATHFINDER",
|
||||
"MURANO PLATINUM",
|
||||
"MURANO SV",
|
||||
"MURANO",
|
||||
"TUCSON",
|
||||
"TERRAIN",
|
||||
"SORENTO",
|
||||
"EDGE",
|
||||
"KICKS",
|
||||
"QASHQAI",
|
||||
"SANTA FE",
|
||||
"ARMADA",
|
||||
"TELLURIDE",
|
||||
"PALISADE",
|
||||
"SELTOS",
|
||||
"TORRENT",
|
||||
"C-HR",
|
||||
"SPORTAGE",
|
||||
"VENZA",
|
||||
"ACADIA",
|
||||
"CR-V",
|
||||
"HR-V",
|
||||
"CX-5",
|
||||
"CX-50",
|
||||
"CX-7",
|
||||
"CX-9",
|
||||
"CX-3",
|
||||
"Q3",
|
||||
"Q5",
|
||||
"Q7",
|
||||
"Q8",
|
||||
"JUKE SV",
|
||||
"JUKE",
|
||||
"ROGUE",
|
||||
"ROGUE SV",
|
||||
"XTERRA",
|
||||
"COROLLA CROSS",
|
||||
"ACADIA DENALI",
|
||||
"TAURUS X",
|
||||
"MACAN",
|
||||
"FJ CRUISER",
|
||||
"BRONCO SPORT BADLANDS",
|
||||
"ESCALADE",
|
||||
"RX 350",
|
||||
"KONA",
|
||||
"MDX",
|
||||
"RDX",
|
||||
"COOPER COUNTRYMAN",
|
||||
"V70",
|
||||
"OUTLANDER",
|
||||
"RIO5",
|
||||
"GLC300 COUPE",
|
||||
"ENCORE",
|
||||
"SRX",
|
||||
"SANTA FE SPORT",
|
||||
"NX 300",
|
||||
"WRANGLER UNLIMITE",
|
||||
"WRANGLER JK UNLIM",
|
||||
"RANGEROVER EVOQUE",
|
||||
"CROSSTREK",
|
||||
"FORESTER",
|
||||
"TIGUAN",
|
||||
"XV CROSSTREK",
|
||||
"ENDEAVOR",
|
||||
"RX 330",
|
||||
"ATLAS",
|
||||
"XC90",
|
||||
"TOUAREG",
|
||||
"STELVIO",
|
||||
"RANGE ROVER SPORT",
|
||||
"GLE350D",
|
||||
"EX35",
|
||||
"RVR",
|
||||
"MONTERO",
|
||||
"X-TRAIL",
|
||||
"GRAND VITARA",
|
||||
"TRIBUTE",
|
||||
"X3",
|
||||
"XC60",
|
||||
"GLK250 BLUETEC",
|
||||
"ENVOY",
|
||||
"ML350 BLUETEC",
|
||||
"ENVISION",
|
||||
"FX35",
|
||||
"X1",
|
||||
"VENUE",
|
||||
"TAOS",
|
||||
"KONA ELECTRIC",
|
||||
"OUTLANDER PHEV",
|
||||
"PASSPORT",
|
||||
"H3",
|
||||
"EXPLORERSPORTTRAC",
|
||||
"F-PACE",
|
||||
"ML320 BLUETEC",
|
||||
"REGAL SPORTBACK",
|
||||
"DISCOVERY SPORT",
|
||||
"RENDEZVOUS",
|
||||
"XC70",
|
||||
"COMPASS (NEW)",
|
||||
"CUBE",
|
||||
"V60 CROSS COUNTRY",
|
||||
"QX70",
|
||||
"X6",
|
||||
"ELEMENT",
|
||||
"RX 400H",
|
||||
"VUE",
|
||||
"RANGE ROVER VELAR",
|
||||
"E-PACE",
|
||||
"RAV4 PRIME",
|
||||
"LX 570",
|
||||
"GX 470",
|
||||
"EX37",
|
||||
"GLE43",
|
||||
"NAUTILUS",
|
||||
"XT6",
|
||||
"RX 450H",
|
||||
"ESCALADE ESV",
|
||||
"OUTLOOK",
|
||||
"CAYENNE",
|
||||
"XC90 PLUG-IN",
|
||||
"MODEL X",
|
||||
"MODEL Y",
|
||||
"GLC300",
|
||||
"SANTA FE HYBRID",
|
||||
"G63",
|
||||
"XV CROSSTREK HYBR",
|
||||
"JX35",
|
||||
"JIMMY",
|
||||
"TUCSON HYBRID",
|
||||
"XC40 ELECTRIC",
|
||||
"RX 300",
|
||||
"ML320",
|
||||
"WRANGLER JK UNLIMITED",
|
||||
"POLICE INTERCEPTOR UTILITY",
|
||||
"WRANGLER JK",
|
||||
"TRIBECA",
|
||||
"E-TRON SPORTBACK",
|
||||
"500X",
|
||||
"RX 350H",
|
||||
"GL350 BLUETEC",
|
||||
"WRANGLER UNLIMITED 4XE",
|
||||
"GV80",
|
||||
"GL550",
|
||||
"Q5 E",
|
||||
"H2 SUV",
|
||||
"Q5 HYBRID",
|
||||
"IONIQ 5",
|
||||
"SQ5 SPORTBACK",
|
||||
"LEVANTE",
|
||||
"TONALE",
|
||||
"GLE43 COUPE",
|
||||
"GRAND CHEROKEE WK",
|
||||
"DEFENDER",
|
||||
"NX 450H+",
|
||||
"ML400",
|
||||
"LX 600",
|
||||
"RX 450HL",
|
||||
"SORENTO HYBRID",
|
||||
"NX 350",
|
||||
"TRACKER",
|
||||
"GLE450",
|
||||
"Q5 SPORTBACK",
|
||||
"CR-V HYBRID",
|
||||
"LX 470",
|
||||
"EQS580 SUV",
|
||||
"H2",
|
||||
"EV9",
|
||||
"SORENTO PLUG-IN",
|
||||
"LYRIQ",
|
||||
"GLE550",
|
||||
"RX 500H",
|
||||
"X1 SAV",
|
||||
"E-TRON S SPORTBACK",
|
||||
"ML500",
|
||||
"GRAND HIGHLANDER HYBRID",
|
||||
"RS Q8",
|
||||
"GLS550",
|
||||
"GLS580",
|
||||
"IX",
|
||||
"CAYENNE COUPE",
|
||||
"SOLTERRA",
|
||||
"PATHFINDER HYBRID",
|
||||
"Q8 E-TRON",
|
||||
"TX 350",
|
||||
"TX 500H",
|
||||
"EQUINOX EV",
|
||||
"NAUTILUS HYBRID",
|
||||
"TRAVERSE LIMITED",
|
||||
"CX-70",
|
||||
"SANTA FE XL",
|
||||
"RENEGADE",
|
||||
"QX50",
|
||||
"ECLIPSE CROSS",
|
||||
"QX80",
|
||||
"X5",
|
||||
"X3",
|
||||
"X1",
|
||||
"X4",
|
||||
"ENCLAVE",
|
||||
"ENCORE GX",
|
||||
"CAYENNE HYBRID",
|
||||
"SOUL",
|
||||
"GX 460",
|
||||
"UX 250H",
|
||||
"XT5",
|
||||
"GLE53",
|
||||
"XT4",
|
||||
"SQ7",
|
||||
"NX 350H",
|
||||
"GLK350",
|
||||
"GLE350",
|
||||
"NX 300H",
|
||||
"NX 200T",
|
||||
"RANGE ROVER EVOQUE",
|
||||
"GLS450",
|
||||
"TERRAIN DENALI",
|
||||
"GRAND CHEROKEE L",
|
||||
"GLE400",
|
||||
"TUCSON PLUG-IN",
|
||||
"BLAZER",
|
||||
"ASCENT",
|
||||
"HIGHLANDER HYBRID",
|
||||
"ATLAS CROSS SPORT",
|
||||
"XC40",
|
||||
"VENZA HYBRID",
|
||||
"GLA45",
|
||||
"GLB250",
|
||||
"GRAND HIGHLANDER",
|
||||
"GV70",
|
||||
"NIRO",
|
||||
"NIRO EV",
|
||||
"GLA250",
|
||||
"ESCAPE PLUG-IN",
|
||||
"WAGONEER",
|
||||
"CX-30",
|
||||
"QX60",
|
||||
"GRAND CHEROKEE 4XE",
|
||||
"SPORTAGE HYBRID",
|
||||
"EV6",
|
||||
"TONALE PLUG-IN",
|
||||
"GLC43 COUPE",
|
||||
"X2",
|
||||
"RX 350L",
|
||||
"HORNET",
|
||||
"ENVISTA",
|
||||
"LEVANTE S",
|
||||
"SPORTAGE PLUG-IN",
|
||||
"ORLANDO",
|
||||
"X5 M",
|
||||
"EXPLORER HYBRID",
|
||||
"FREESTYLE",
|
||||
"CORSAIR",
|
||||
"K1500 YUKON XL",
|
||||
"RANGE ROVER",
|
||||
"SUV W/O LABOR",
|
||||
"ID.4",
|
||||
"CX-90",
|
||||
"X7",
|
||||
"CORSAIR PLUG-IN",
|
||||
"ESCALADE EXT",
|
||||
"QX55",
|
||||
"DISCOVERY",
|
||||
"BOLT EUV",
|
||||
"C40 ELECTRIC",
|
||||
"LR4",
|
||||
"GRAND WAGONEER",
|
||||
"XC60 PLUG-IN",
|
||||
"LR2",
|
||||
"EQE350 SUV",
|
||||
"COROLLA CROSS HYBRID",
|
||||
"SOUL EV",
|
||||
"GRECALE",
|
||||
"SUV W/O LABOR",
|
||||
"QX30",
|
||||
"SQ5",
|
||||
"NIRO PLUG-IN",
|
||||
"BORREGO",
|
||||
"CX-90 PLUG-IN",
|
||||
"XL-7",
|
||||
"SUV W/O LABOR",
|
||||
"SUV W/O LABOR",
|
||||
"I-PACE",
|
||||
"HORNET PLUG-IN",
|
||||
"UX 300H",
|
||||
"ML320 CDI",
|
||||
"VERACRUZ",
|
||||
"SQ8",
|
||||
"GLE53 COUPE",
|
||||
"ZDX",
|
||||
"9-7X",
|
||||
"ARIYA",
|
||||
"ASPEN",
|
||||
"AVIATOR PLUG-IN",
|
||||
"B9 TRIBECA",
|
||||
"BRAVADA",
|
||||
"ENVOY XL",
|
||||
"EQB350",
|
||||
"EQB350 SUV",
|
||||
"ESCALADE-V",
|
||||
"E-TRON",
|
||||
"FX37",
|
||||
"GL320 CDI",
|
||||
"GLADIATOR",
|
||||
"GLC43",
|
||||
"GLE450 COUPE",
|
||||
"GLE63",
|
||||
"GV60",
|
||||
"MKT TOWN CAR",
|
||||
"ML350",
|
||||
"ML550",
|
||||
"ML63",
|
||||
"NX 250",
|
||||
"Q4 E-TRON",
|
||||
"Q8 E-TRON SPORTBACK",
|
||||
"QX4",
|
||||
"QX56",
|
||||
"SANTA FE PLUG-IN",
|
||||
"UX 200",
|
||||
"WAGONEER L",
|
||||
"XB"
|
||||
]
|
||||
567
server/data/vehicletype/trucks.json
Normal file
567
server/data/vehicletype/trucks.json
Normal file
@@ -0,0 +1,567 @@
|
||||
[
|
||||
"MARK LT",
|
||||
|
||||
"F-150",
|
||||
"F-250",
|
||||
"F-350",
|
||||
"F-450",
|
||||
"F-550",
|
||||
"F-650",
|
||||
"F100 PICKUP",
|
||||
"F150 FX2 SUPERCAB",
|
||||
"F150 FX4 PICKUP",
|
||||
"F150 FX4 SUPERCAB",
|
||||
"F150 FX4 SUPERCREW",
|
||||
"F150 HARLEY DAVIDSON SUPERCAB",
|
||||
"F150 HARLEY DAVIDSON SUPERCREW",
|
||||
"F150 KING RANCH SUPERCREW",
|
||||
"F150 LARIAT FX4 SUPERCREW",
|
||||
"F150 LARIAT HARLEY DAVIDSON SC",
|
||||
"F150 LARIAT KING RANCH SUPCREW",
|
||||
"F150 LARIAT LIMITED SUPERCREW",
|
||||
"F150 LARIAT PICKUP",
|
||||
"F150 LARIAT SUPERCAB",
|
||||
"F150 LARIAT SUPERCAB (AMALGAM)",
|
||||
"F150 LARIAT SUPERCREW",
|
||||
"F150 LARIAT SUPERCREW (AMALGA)",
|
||||
"F150 LIMITED SUPERCREW",
|
||||
"F150 PICKUP",
|
||||
"F150 PLATINUM SUPERCREW",
|
||||
"F150 RAPTOR SUPERCAB",
|
||||
"F150 RAPTOR SUPERCREW",
|
||||
"F150 STX PICKUP",
|
||||
"F150 STX SUPERCAB",
|
||||
"F150 SUPERCAB",
|
||||
"F150 SUPERCREW",
|
||||
"F150 SUPERCREW (AMALGAMATED)",
|
||||
"F150 SVT RAPTOR SUPERCAB",
|
||||
"F150 XL PICKUP",
|
||||
"F150 XL SUPERCAB",
|
||||
"F150 XL SUPERCREW",
|
||||
"F150 XLT LARIAT SUPERCAB",
|
||||
"F150 XLT PICKUP",
|
||||
"F150 XLT SUPERCAB",
|
||||
"F150 XLT SUPERCREW",
|
||||
"F150 XLT SUPERCREW (AMALGAMAT)",
|
||||
"F150 XTR SUPERCAB",
|
||||
"F250 PICKUP",
|
||||
"F250 SD CREW CAB",
|
||||
"F250 SD FX4 CREW CAB",
|
||||
"F250 SD FX4 SUPERCAB",
|
||||
"F250 SD KING RANCH CREW CAB",
|
||||
"F250 SD LARIAT CREW CAB",
|
||||
"F250 SD LARIAT CREW CAB (AMAL)",
|
||||
"F250 SD LARIAT PICKUP",
|
||||
"F250 SD LARIAT SUPERCAB",
|
||||
"F250 SD LIMITED CREW CAB",
|
||||
"F250 SD PLATINUM CREW CAB",
|
||||
"F250 SD SUPERCAB",
|
||||
"F250 SD XL CREW CAB",
|
||||
"F250 SD XL PICKUP",
|
||||
"F250 SD XL SUPERCAB",
|
||||
"F250 SD XLT CREW CAB",
|
||||
"F250 SD XLT PICKUP",
|
||||
"F250 SD XLT SUPERCAB",
|
||||
"F250 SUPERCAB",
|
||||
"F250 XL CREW CAB",
|
||||
"F350 CREW CAB",
|
||||
"F350 PICKUP",
|
||||
"F350 PICKUP 2WD",
|
||||
"F350 SD CABELAS CREW CAB",
|
||||
"F350 SD CREW CAB",
|
||||
"F350 SD FX4 CREW CAB",
|
||||
"F350 SD FX4 SUPERCAB",
|
||||
"F350 SD HARLEY DAVIDSON",
|
||||
"F350 SD KING RANCH CREW CAB",
|
||||
"F350 SD LARIAT CREW CAB",
|
||||
"F350 SD LARIAT CREW CAB (AMAL)",
|
||||
"F350 SD LARIAT KING RANCH",
|
||||
"F350 SD LARIAT SUPERCAB",
|
||||
"F350 SD LIMITED CREW CAB",
|
||||
"F350 SD PICKUP",
|
||||
"F350 SD PLATINUM CREW CAB",
|
||||
"F350 SD SUPERCAB",
|
||||
"F350 SD XL CREW CAB",
|
||||
"F350 SD XL PICKUP",
|
||||
"F350 SD XL SUPERCAB",
|
||||
"F350 SD XLT CREW CAB",
|
||||
"F350 SD XLT SUPERCAB",
|
||||
"F350 SUPER DUTY",
|
||||
"F350 SUPER DUTY XL",
|
||||
"F350 XL PICKUP",
|
||||
"F450",
|
||||
"F450 Pickup",
|
||||
"F450 SD KING RANCH CREW CAB",
|
||||
"F450 SD LARIAT CREW CAB",
|
||||
"F450 SD PICKUP",
|
||||
"F450 SD PLATINUM CREW CAB",
|
||||
"F450 SD XL",
|
||||
"F450 SD XL CREW CAB",
|
||||
"F450 SD XL PICKUP",
|
||||
"F450 SD XLT CREW CAB",
|
||||
"F450 SUPER DUTY XLT",
|
||||
"F550",
|
||||
"F550 SD",
|
||||
"F550 SD XL",
|
||||
"F550 SD XL PICKUP",
|
||||
"F550 SD XLT CREW CAB",
|
||||
"F550 SD XLT SUPERCAB",
|
||||
"F550 SUPER DUTY",
|
||||
"F550 SUPER DUTY XL",
|
||||
"F550 SUPER DUTY XLT",
|
||||
"F550 SUPER DUTY XLT CREW CAB",
|
||||
"F550 XL",
|
||||
"F650 SD XLT SUPERCAB",
|
||||
"F68",
|
||||
"F750 XL",
|
||||
|
||||
"RANGER",
|
||||
"RANGER EDGE SUPERCAB",
|
||||
"RANGER FX4 SUPERCAB",
|
||||
"RANGER LARIAT SUPERCREW",
|
||||
"RANGER SPORT SUPERCAB",
|
||||
"RANGER STX SUPERCAB",
|
||||
"RANGER SUPERCAB",
|
||||
"RANGER XL",
|
||||
"RANGER XL SUPERCAB",
|
||||
"RANGER XLT",
|
||||
"RANGER XLT SUPERCAB",
|
||||
"RANGER XLT SUPERCREW",
|
||||
|
||||
"FRONTIER LE CREW CAB V6",
|
||||
"FRONTIER NISMO CREW CAB V6",
|
||||
"FRONTIER NISMO KING CAB V6",
|
||||
"FRONTIER PRO-4X CREW CAB V6",
|
||||
"FRONTIER PRO-4X KING CAB V6",
|
||||
"FRONTIER S KING CAB",
|
||||
"FRONTIER SC CREW CAB V6",
|
||||
"FRONTIER SC V6",
|
||||
"FRONTIER SE CREW CAB V6",
|
||||
"FRONTIER SE KING CAB V6",
|
||||
"FRONTIER SL CREW CAB V6",
|
||||
"FRONTIER SV CREW CAB V6",
|
||||
"FRONTIER SV KING CAB V6",
|
||||
"FRONTIER XE KING CAB",
|
||||
"FRONTIER XE KING CAB V6",
|
||||
"KING CAB",
|
||||
|
||||
"TITAN 5.6 LE CREW CAB",
|
||||
"TITAN 5.6 LE KING CAB",
|
||||
"TITAN 5.6 MIDNIGHT CREW CAB",
|
||||
"TITAN 5.6 PLATINUM RESERVE CC",
|
||||
"TITAN 5.6 PRO-4X CREW CAB",
|
||||
"TITAN 5.6 PRO-4X KING CAB",
|
||||
"TITAN 5.6 S CREW CAB",
|
||||
"TITAN 5.6 SE CREW CAB",
|
||||
"TITAN 5.6 SE KING CAB",
|
||||
"TITAN 5.6 SL CREW CAB",
|
||||
"TITAN 5.6 SV CREW CAB",
|
||||
"TITAN 5.6 SV KING CAB",
|
||||
"TITAN 5.6 XE CREW CAB",
|
||||
"TITAN 5.6 XE KING CAB",
|
||||
"TITAN XD PLATINUM CREW CAB",
|
||||
"TITAN XD PRO-4X CREW CAB",
|
||||
"TITAN XD S CREW CAB",
|
||||
"TITAN XD SL CREW CAB",
|
||||
"TITAN XD SV CREW CAB",
|
||||
|
||||
"PICKUP SR5",
|
||||
|
||||
"TACOMA",
|
||||
"TACOMA ACCESS CAB",
|
||||
"TACOMA DOUBLE CAB V6",
|
||||
"TACOMA LIMITED DOUBLE CAB V6",
|
||||
"TACOMA PRERUNNER DOUBLE CAB V6",
|
||||
"TACOMA PRERUNNER V6 ACCESS CAB",
|
||||
"TACOMA PRERUNNER XTRACAB",
|
||||
"TACOMA PRERUNNER XTRACAB V6",
|
||||
"TACOMA SR5 DOUBLE CAB V6",
|
||||
"TACOMA SR5 V6 ACCESS CAB",
|
||||
"TACOMA SR5 V6 XTRACAB",
|
||||
"TACOMA V6 ACCESS CAB",
|
||||
"TACOMA XTRACAB",
|
||||
"TACOMA XTRACAB V6",
|
||||
"TUNDRA ACCESS CAB V8",
|
||||
"TUNDRA DOUBLE CAB V8",
|
||||
"TUNDRA LIMITED ACCESS CAB V8",
|
||||
"TUNDRA LIMITED SR5 DBLCAB V8",
|
||||
"TUNDRA LIMITED V8",
|
||||
"TUNDRA LIMITED V8 CREWMAX",
|
||||
"TUNDRA LIMITED V8 DOUBLE CAB",
|
||||
"TUNDRA PLATINUM V8 CREWMAX",
|
||||
"TUNDRA SR DOUBLE CAB V8",
|
||||
"TUNDRA SR V8",
|
||||
"TUNDRA SR5 DOUBLE CAB V8",
|
||||
"TUNDRA SR5 TRD DOUBLE CAB V8",
|
||||
"TUNDRA SR5 V8 CREWMAX",
|
||||
"TUNDRA V8",
|
||||
"TUNDRA V8 CREWMAX",
|
||||
"XTRACAB LONG BOX",
|
||||
|
||||
"AVALANCHE 1500",
|
||||
"AVALANCHE 1500 LS",
|
||||
"AVALANCHE 1500 LS Z71",
|
||||
"AVALANCHE 1500 LT",
|
||||
"AVALANCHE 1500 LT Z71",
|
||||
"AVALANCHE 1500 LTZ",
|
||||
"C/R 10/1500 4+CAB",
|
||||
"C/R 10/1500 PICKUP",
|
||||
"C/R 20/2500 4+CAB",
|
||||
"C/R 20/2500 PICKUP",
|
||||
"C3500",
|
||||
|
||||
"COLORADO",
|
||||
"COLORADO EXT CAB",
|
||||
"COLORADO LS",
|
||||
"COLORADO LS CREW CAB",
|
||||
"COLORADO LS EXT CAB",
|
||||
"COLORADO LT",
|
||||
"COLORADO LT CREW CAB",
|
||||
"COLORADO LT EXT CAB",
|
||||
"COLORADO WT CREW CAB",
|
||||
"COLORADO WT EXT CAB",
|
||||
"COLORADO Z71 CREW CAB",
|
||||
"COLORADO Z71 EXT CAB",
|
||||
"COLORADO ZR2 CREW CAB",
|
||||
"COLORADO ZR2 EXT CAB",
|
||||
|
||||
"HHR LS PANEL",
|
||||
"K/V 10/1500 4+CAB",
|
||||
"K/V 10/1500 PICKUP",
|
||||
"K/V 20/2500 4+CAB",
|
||||
"K/V 20/2500 PICKUP",
|
||||
"K/V 30/3500 4+CAB",
|
||||
"Pickup K3500",
|
||||
"Pickup Silverado C2500 HD",
|
||||
"S10 4+CAB",
|
||||
"S10 LS 4+CAB",
|
||||
"SILVERADO 1500",
|
||||
"SILVERADO 1500 CHEYENNE CREW",
|
||||
"SILVERADO 1500 CREW CAB",
|
||||
"SILVERADO 1500 CREW CAB (AMAL)",
|
||||
"SILVERADO 1500 CUST TRAIL DC",
|
||||
"SILVERADO 1500 CUSTOM CREW CAB",
|
||||
"SILVERADO 1500 CUSTOM DC",
|
||||
"SILVERADO 1500 CUSTOM TRAIL CC",
|
||||
"SILVERADO 1500 DOUBLE (AMALGA)",
|
||||
"SILVERADO 1500 EXT CAB",
|
||||
"SILVERADO 1500 HD LS CREW CAB",
|
||||
"SILVERADO 1500 HD LT CREW CAB",
|
||||
"SILVERADO 1500 HIGH COUNTRY CC",
|
||||
"SILVERADO 1500 HYBRID CREW CAB",
|
||||
"SILVERADO 1500 LS",
|
||||
"SILVERADO 1500 LS CREW CAB",
|
||||
"SILVERADO 1500 LS DOUBLE CAB",
|
||||
"SILVERADO 1500 LS EXT CAB",
|
||||
"SILVERADO 1500 LT",
|
||||
"SILVERADO 1500 LT CC (AMALGAM)",
|
||||
"SILVERADO 1500 LT CREW CAB",
|
||||
"SILVERADO 1500 LT DOUBLE CAB",
|
||||
"SILVERADO 1500 LT EXT CAB",
|
||||
"SILVERADO 1500 LT TRAIL CC",
|
||||
"SILVERADO 1500 LT TRAIL DC",
|
||||
"SILVERADO 1500 LTZ CREW CAB",
|
||||
"SILVERADO 1500 LTZ DOUBLE CAB",
|
||||
"SILVERADO 1500 LTZ EXT CAB",
|
||||
"SILVERADO 1500 RST CREW CAB",
|
||||
"SILVERADO 1500 RST DOUBLE CAB",
|
||||
"SILVERADO 1500 SS EXT CAB",
|
||||
"SILVERADO 1500 WT",
|
||||
"SILVERADO 1500 WT CREW CAB",
|
||||
"SILVERADO 1500 WT DOUBLE CAB",
|
||||
"SILVERADO 1500 WT EXT CAB",
|
||||
"SILVERADO 2500 EXT CAB",
|
||||
"SILVERADO 2500 HD",
|
||||
"SILVERADO 2500 HD CREW CAB",
|
||||
"SILVERADO 2500 HD EXT CAB",
|
||||
"SILVERADO 2500 HD HC CREW CAB",
|
||||
"SILVERADO 2500 HD LS CREW CAB",
|
||||
"SILVERADO 2500 HD LS EXT CAB",
|
||||
"SILVERADO 2500 HD LT",
|
||||
"SILVERADO 2500 HD LT CREW CAB",
|
||||
"SILVERADO 2500 HD LT DBL CAB",
|
||||
"SILVERADO 2500 HD LT EXT CAB",
|
||||
"SILVERADO 2500 HD LTZ CREW CAB",
|
||||
"SILVERADO 2500 HD LTZ DBL CAB",
|
||||
"SILVERADO 2500 HD LTZ EXT CAB",
|
||||
"SILVERADO 2500 HD WT",
|
||||
"SILVERADO 2500 HD WT CREW CAB",
|
||||
"SILVERADO 2500 HD WT DBL CAB",
|
||||
"SILVERADO 2500 HD WT EXT CAB",
|
||||
"SILVERADO 3500",
|
||||
"SILVERADO 3500 CREW CAB",
|
||||
"SILVERADO 3500 CREW CAB (AMAL)",
|
||||
"SILVERADO 3500 EXT CAB",
|
||||
"SILVERADO 3500 HC CREW CAB",
|
||||
"SILVERADO 3500 HD (AMALGAMATE)",
|
||||
"SILVERADO 3500 LS",
|
||||
"SILVERADO 3500 LS CREW CAB",
|
||||
"SILVERADO 3500 LS EXT CAB",
|
||||
"SILVERADO 3500 LT CREW CAB",
|
||||
"SILVERADO 3500 LT DOUBLE CAB",
|
||||
"SILVERADO 3500 LT EXT CAB",
|
||||
"SILVERADO 3500 LTZ CREW CAB",
|
||||
"SILVERADO 3500 LTZ EXT CAB",
|
||||
"SILVERADO 3500 WT CREW CAB",
|
||||
"Silverado 3500HD",
|
||||
|
||||
"B250 SPORTSMAN",
|
||||
|
||||
"DAKOTA CLUB CAB",
|
||||
"DAKOTA LARAMIE V8 CLUB CAB",
|
||||
"DAKOTA LARAMIE V8 QUAD CAB",
|
||||
"DAKOTA QUAD CAB",
|
||||
"DAKOTA SLT CREW CAB",
|
||||
"DAKOTA SLT EXT CAB",
|
||||
"DAKOTA SLT PLUS QUAD CAB",
|
||||
"DAKOTA SLT PLUS V8 CLUB CAB",
|
||||
"DAKOTA SLT PLUS V8 QUAD CAB",
|
||||
"DAKOTA SLT QUAD CAB",
|
||||
"DAKOTA SLT V8 CLUB CAB",
|
||||
"DAKOTA SLT V8 CREW CAB",
|
||||
"DAKOTA SLT V8 EXT CAB",
|
||||
"DAKOTA SLT V8 QUAD CAB",
|
||||
"DAKOTA SPORT V8",
|
||||
"DAKOTA SPORT V8 CLUB CAB",
|
||||
"DAKOTA SPORT V8 QUAD CAB",
|
||||
"DAKOTA ST CLUB CAB",
|
||||
"DAKOTA ST QUAD CAB",
|
||||
"DAKOTA ST V8 QUAD CAB",
|
||||
"DAKOTA SXT CREW CAB",
|
||||
"DAKOTA SXT EXT CAB",
|
||||
"DAKOTA SXT V8 CREW CAB",
|
||||
"DAKOTA SXT V8 EXT CAB",
|
||||
"DAKOTA V8 CLUB CAB",
|
||||
"DAKOTA V8 QUAD CAB",
|
||||
|
||||
"RAM 1500",
|
||||
"RAM 1500 BIG HORN CREW CAB",
|
||||
"RAM 1500 BIG HORN QUAD CAB",
|
||||
"RAM 1500 CLUB CAB",
|
||||
"RAM 1500 CREW CAB (AMALGAMATE)",
|
||||
"RAM 1500 EXPRESS",
|
||||
"RAM 1500 LARAMIE CREW (AMALGA)",
|
||||
"RAM 1500 LARAMIE CREW CAB",
|
||||
"RAM 1500 LARAMIE LONGHORN CREW",
|
||||
"RAM 1500 LARAMIE MEGA CAB",
|
||||
"RAM 1500 LARAMIE QUAD CAB",
|
||||
"RAM 1500 LARAMIE SLT QUAD CAB",
|
||||
"RAM 1500 LIMITED CREW CAB",
|
||||
"RAM 1500 LONGHORN CREW CAB",
|
||||
"RAM 1500 OUTDOORSMAN CREW CAB",
|
||||
"RAM 1500 OUTDOORSMAN QC (AMAL)",
|
||||
"RAM 1500 OUTDOORSMAN QUAD CAB",
|
||||
"RAM 1500 QUAD CAB",
|
||||
"RAM 1500 R/T",
|
||||
"RAM 1500 REBEL CREW CAB",
|
||||
"RAM 1500 REBEL QUAD CAB",
|
||||
"RAM 1500 SLT",
|
||||
"RAM 1500 SLT CREW (AMALGAMATE)",
|
||||
"RAM 1500 SLT CREW CAB",
|
||||
"RAM 1500 SLT MEGA CAB",
|
||||
"RAM 1500 SLT QUAD (AMALGAMATE)",
|
||||
"RAM 1500 SLT QUAD CAB",
|
||||
"RAM 1500 SPORT",
|
||||
"RAM 1500 SPORT CLUB CAB",
|
||||
"RAM 1500 SPORT CREW CAB",
|
||||
"RAM 1500 SPORT CREW CAB (AMAL)",
|
||||
"RAM 1500 SPORT QUAD CAB",
|
||||
"RAM 1500 ST",
|
||||
"RAM 1500 ST CREW CAB",
|
||||
"RAM 1500 ST QUAD CAB",
|
||||
"RAM 1500 SXT CREW CAB",
|
||||
"RAM 1500 SXT QUAD CAB",
|
||||
"RAM 1500 TRADESMAN CREW CAB",
|
||||
"RAM 1500 TRADESMAN QUAD CAB",
|
||||
"RAM 1500 TRX QUAD CAB",
|
||||
"RAM 2500",
|
||||
"RAM 2500 BIG HORN CREW CAB",
|
||||
"RAM 2500 BIG HORN MEGA CAB",
|
||||
"RAM 2500 CLUB CAB",
|
||||
"RAM 2500 LARAMIE CREW CAB",
|
||||
"RAM 2500 LARAMIE LONGHORN CREW",
|
||||
"RAM 2500 LARAMIE LONGHORN MEGA",
|
||||
"RAM 2500 LARAMIE MEGA CAB",
|
||||
"RAM 2500 LARAMIE QUAD CAB",
|
||||
"RAM 2500 LARAMIE SLT",
|
||||
"RAM 2500 LARAMIE SLT QUAD CAB",
|
||||
"RAM 2500 LIMITED CREW CAB",
|
||||
"RAM 2500 OUTDOORSMAN CREW CAB",
|
||||
"RAM 2500 POWER WAGON CREW CAB",
|
||||
"RAM 2500 QUAD CAB",
|
||||
"RAM 2500 SLT",
|
||||
"RAM 2500 SLT CREW CAB",
|
||||
"RAM 2500 SLT MEGA CAB",
|
||||
"RAM 2500 SLT QUAD CAB",
|
||||
"RAM 2500 SLT QUAD CAB (AMALGA)",
|
||||
"RAM 2500 SPORT QUAD CAB",
|
||||
"RAM 2500 ST",
|
||||
"RAM 2500 ST CREW CAB",
|
||||
"RAM 2500 ST QUAD CAB",
|
||||
"RAM 2500 SXT QUAD CAB",
|
||||
"RAM 2500 TRADESMAN",
|
||||
"RAM 2500 TRADESMAN CREW CAB",
|
||||
"RAM 2500 TRX CREW CAB",
|
||||
"RAM 2500 TRX QUAD CAB",
|
||||
"RAM 3500",
|
||||
"RAM 3500 4WD",
|
||||
"RAM 3500 BIG HORN CREW CAB",
|
||||
"RAM 3500 CREW CAB",
|
||||
"RAM 3500 CREW CAB (AMALGAMATE)",
|
||||
"RAM 3500 LARAMIE CREW CAB",
|
||||
"RAM 3500 LARAMIE LONGHORN CREW",
|
||||
"RAM 3500 LARAMIE LONGHORN MEGA",
|
||||
"RAM 3500 LARAMIE MEGA CAB",
|
||||
"RAM 3500 LARAMIE QUAD CAB",
|
||||
"RAM 3500 LARAMIE SLT",
|
||||
"RAM 3500 LARAMIE SLT QUAD CAB",
|
||||
"RAM 3500 LIMITED MEGA CAB",
|
||||
"RAM 3500 LONGHORN CREW CAB",
|
||||
"RAM 3500 QUAD CAB",
|
||||
"RAM 3500 SLT",
|
||||
"RAM 3500 SLT CREW CAB",
|
||||
"RAM 3500 SLT MEGA CAB",
|
||||
"RAM 3500 SLT QUAD CAB",
|
||||
"RAM 3500 SPORT QUAD CAB",
|
||||
"RAM 3500 ST",
|
||||
"RAM 3500 ST CREW CAB",
|
||||
"RAM 3500 ST QUAD CAB",
|
||||
"RAM 3500 TRX QUAD CAB",
|
||||
"RAM 4500",
|
||||
"RAM 4500 CREW CAB",
|
||||
"RAM 5500",
|
||||
"RAM 5500 CREW CAB",
|
||||
"W250 TURBO DIESEL",
|
||||
|
||||
"C Series 5500",
|
||||
"C/R 1500 4+CAB",
|
||||
"C/R 1500 PICKUP",
|
||||
"C/R 1500 SIERRA SL EXT CAB",
|
||||
"C/R 3500",
|
||||
"C/R 3500 PICKUP",
|
||||
"CANYON ALL TERRAIN CREW CAB",
|
||||
"CANYON CREW CAB",
|
||||
"CANYON DENALI CREW CAB",
|
||||
"CANYON EXT CAB",
|
||||
"CANYON SL",
|
||||
"CANYON SL EXT CAB",
|
||||
"CANYON SLE",
|
||||
"CANYON SLE CREW CAB",
|
||||
"CANYON SLE EXT CAB",
|
||||
"CANYON SLT CREW CAB",
|
||||
"CANYON SLT CREW CAB (AMALGAMA)",
|
||||
"K/V 1500 4+CAB",
|
||||
"K/V 1500 PICKUP",
|
||||
"K/V 2500 4+CAB",
|
||||
"K/V 2500 PICKUP",
|
||||
"K/V 3500 SIERRA SL CREW CAB",
|
||||
"K/V 3500 SIERRA SLE CREW CAB",
|
||||
"SIERRA 1500 AT4 CREW CAB",
|
||||
"SIERRA 1500 AT4 DOUBLE CAB",
|
||||
"SIERRA 1500 CREW CAB",
|
||||
"SIERRA 1500 CREW CAB (AMALGAM)",
|
||||
"SIERRA 1500 DENALI CREW CAB",
|
||||
"SIERRA 1500 DENALI EXT CAB",
|
||||
"SIERRA 1500 DOUBLE CAB",
|
||||
"SIERRA 1500 ELEVATION CREW CAB",
|
||||
"SIERRA 1500 ELEVATION DC",
|
||||
"SIERRA 1500 EXT CAB",
|
||||
"SIERRA 1500 HD CREW CAB",
|
||||
"SIERRA 1500 HD SLE CREW CAB",
|
||||
"SIERRA 1500 HD SLT CREW CAB",
|
||||
"SIERRA 1500 NEVADA EDITION",
|
||||
"SIERRA 1500 PICKUP",
|
||||
"SIERRA 1500 SL CREW CAB",
|
||||
"SIERRA 1500 SL EXT CAB",
|
||||
"SIERRA 1500 SL PICKUP",
|
||||
"SIERRA 1500 SLE CREW CAB",
|
||||
"SIERRA 1500 SLE DC (AMALGAMAT)",
|
||||
"SIERRA 1500 SLE DOUBLE CAB",
|
||||
"SIERRA 1500 SLE EXT CAB",
|
||||
"SIERRA 1500 SLE EXT CAB (AMAL)",
|
||||
"SIERRA 1500 SLE PICKUP",
|
||||
"SIERRA 1500 SLT CREW (AMALGAM)",
|
||||
"SIERRA 1500 SLT CREW CAB",
|
||||
"SIERRA 1500 SLT DOUBLE CAB",
|
||||
"SIERRA 1500 SLT EXT CAB",
|
||||
"SIERRA 1500 WT CREW CAB",
|
||||
"SIERRA 1500 WT EXT CAB",
|
||||
"SIERRA 1500 WT PICKUP",
|
||||
"SIERRA 2500 EXT CAB",
|
||||
"SIERRA 2500 HD AT4 CREW CAB",
|
||||
"SIERRA 2500 HD CREW CAB",
|
||||
"SIERRA 2500 HD DENALI CREW CAB",
|
||||
"SIERRA 2500 HD DOUBLE CAB",
|
||||
"SIERRA 2500 HD EXT CAB",
|
||||
"SIERRA 2500 HD PICKUP",
|
||||
"SIERRA 2500 HD SL EXT CAB",
|
||||
"SIERRA 2500 HD SL PICKUP",
|
||||
"SIERRA 2500 HD SLE CREW CAB",
|
||||
"SIERRA 2500 HD SLE DOUBLE CAB",
|
||||
"SIERRA 2500 HD SLE EXT CAB",
|
||||
"SIERRA 2500 HD SLE PICKUP",
|
||||
"SIERRA 2500 HD SLT CREW CAB",
|
||||
"SIERRA 2500 HD SLT DOUBLE CAB",
|
||||
"SIERRA 2500 HD SLT EXT CAB",
|
||||
"SIERRA 2500 HD WT CREW CAB",
|
||||
"SIERRA 2500 HD WT DOUBLE CAB",
|
||||
"SIERRA 2500 HD WT EXT CAB",
|
||||
"SIERRA 2500 HD WT PICKUP",
|
||||
"SIERRA 2500 SLE EXT CAB",
|
||||
"SIERRA 3500 AT4 CREW CAB",
|
||||
"SIERRA 3500 CREW CAB",
|
||||
"SIERRA 3500 DENALI CREW CAB",
|
||||
"SIERRA 3500 EXT CAB",
|
||||
"SIERRA 3500 PICKUP",
|
||||
"SIERRA 3500 SL CREW CAB",
|
||||
"SIERRA 3500 SLE",
|
||||
"SIERRA 3500 SLE CREW CAB",
|
||||
"SIERRA 3500 SLE EXT CAB",
|
||||
"SIERRA 3500 SLT CREW CAB",
|
||||
"SIERRA 3500 WT CREW CAB",
|
||||
"SONOMA",
|
||||
"SONOMA CREW CAB",
|
||||
"SONOMA EXT CAB",
|
||||
|
||||
"1500",
|
||||
"1500 Classic",
|
||||
"Pickup 1500",
|
||||
"Pickup 3500",
|
||||
"ProMaster 1500",
|
||||
|
||||
"RIDGELINE",
|
||||
"RIDGELINE BLACK EDITION",
|
||||
"RIDGELINE DX",
|
||||
"RIDGELINE EX-L",
|
||||
"RIDGELINE LX",
|
||||
"RIDGELINE RT",
|
||||
"RIDGELINE RTL",
|
||||
"RIDGELINE RTS",
|
||||
"RIDGELINE RTX",
|
||||
"RIDGELINE SE",
|
||||
"RIDGELINE SPORT",
|
||||
"RIDGELINE TOURING",
|
||||
"RIDGELINE VP",
|
||||
|
||||
"TITAN",
|
||||
"TACOMA",
|
||||
"TUNDRA",
|
||||
"AVALANCE",
|
||||
"COLORADO",
|
||||
"SILVERADO",
|
||||
"SILVERADO 1500",
|
||||
"SILVERADO 2500",
|
||||
"SILVERADO 3500",
|
||||
"DAKOTA",
|
||||
"RAM 1500",
|
||||
"RAM 2500",
|
||||
"RAM 3500",
|
||||
"RAM 4500",
|
||||
"RAM 5500",
|
||||
"CANYON",
|
||||
"SIERRA 1500",
|
||||
"SIERRA 2500",
|
||||
"SIERRA 3500",
|
||||
"SONOMA",
|
||||
"1500"
|
||||
]
|
||||
39
server/data/vehicletype/vehicletype.js
Normal file
39
server/data/vehicletype/vehicletype.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const logger = require("../../utils/logger");
|
||||
const TrucksList = require("./trucks.json");
|
||||
const CargoVanList = require("./cargovans.json");
|
||||
const PassengerVanList = require("./passengervans.json");
|
||||
const SuvList = require("./suvs.json");
|
||||
|
||||
|
||||
const vehicletype = async (req, res) => {
|
||||
try {
|
||||
const { model } = req.body;
|
||||
if (!model || model.trim() === "") {
|
||||
res.status(400).json({ success: false, error: "Please provide a model" });
|
||||
} else {
|
||||
|
||||
const type = getVehicleType(model.trim())
|
||||
res.status(200).json({ success: true, ...type });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("vehicletype-error", "ERROR", req?.user?.email, null, {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.status(500).json({ error: error.message, stack: error.stack });
|
||||
}
|
||||
};
|
||||
|
||||
function getVehicleType(model) {
|
||||
const inTrucks = TrucksList.includes(model.toUpperCase());
|
||||
const inPV = PassengerVanList.includes(model.toUpperCase());
|
||||
const inSuv = SuvList.includes(model.toUpperCase());
|
||||
const inCv = CargoVanList.includes(model.toUpperCase());
|
||||
|
||||
if (inTrucks) return { type: "TK", match: true };
|
||||
else if (inPV) return { type: "PC", match: true };
|
||||
else if (inSuv) return { type: "SUV", match: true };
|
||||
else if (inCv) return { type: "VN", match: true };
|
||||
else return { type: "PC", match: false };
|
||||
}
|
||||
exports.default = vehicletype;
|
||||
@@ -1854,7 +1854,7 @@ exports.GET_CHATTER_SHOPS = `query GET_CHATTER_SHOPS {
|
||||
}`;
|
||||
|
||||
exports.GET_CARFAX_SHOPS = `query GET_CARFAX_SHOPS {
|
||||
bodyshops{
|
||||
bodyshops(where: {external_shop_id: {_is_null: true}}){
|
||||
id
|
||||
shopname
|
||||
imexshopid
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
"headerMargin": "135"
|
||||
},
|
||||
"md_ro_statuses": {
|
||||
"parts_statuses": [
|
||||
"Open",
|
||||
"In Progress",
|
||||
"Completed"
|
||||
],
|
||||
"statuses": [
|
||||
"Open",
|
||||
"Scheduled",
|
||||
@@ -54,6 +59,10 @@
|
||||
"Void"
|
||||
],
|
||||
"default_void": "Void",
|
||||
"parts_active_statuses": [
|
||||
"Open",
|
||||
"In Progress"
|
||||
],
|
||||
"active_statuses": [
|
||||
"Open",
|
||||
"Scheduled",
|
||||
@@ -766,6 +775,7 @@
|
||||
"csi:page": 11,
|
||||
"jobs:void": 80,
|
||||
"shop:rbac": 99,
|
||||
"shop:responsibilitycenter": 99,
|
||||
"bills:list": 11,
|
||||
"bills:view": 11,
|
||||
"csi:export": 11,
|
||||
|
||||
@@ -23,10 +23,16 @@ const KNOWN_PART_RATE_TYPES = [
|
||||
* @returns {object} The parts tax rates object.
|
||||
*/
|
||||
|
||||
//TODO: Major validation would be required on this - EMS files are inconsistent with things like 5% being passed as 5.0 or .05.
|
||||
const extractPartsTaxRates = (profile = {}) => {
|
||||
const rateInfos = Array.isArray(profile.RateInfo) ? profile.RateInfo : [profile.RateInfo || {}];
|
||||
const partsTaxRates = {};
|
||||
|
||||
/**
|
||||
* In this context, r.RateType._ accesses the property named _ on the RateType object.
|
||||
* This pattern is common when handling data parsed from XML, where element values are stored under the _ key. So,
|
||||
* _ aligns to the actual value/content of the RateType field when RateType is an object (not a string).
|
||||
*/
|
||||
for (const r of rateInfos) {
|
||||
const rateTypeRaw =
|
||||
typeof r?.RateType === "string"
|
||||
|
||||
197
server/integrations/partsManagement/endpoints/lib/opCodes.json
Normal file
197
server/integrations/partsManagement/endpoints/lib/opCodes.json
Normal file
@@ -0,0 +1,197 @@
|
||||
{
|
||||
"OP0": {
|
||||
"desc": "REMOVE / REPLACE PARTIAL",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAA"
|
||||
},
|
||||
"OP1": {
|
||||
"desc": "REFINISH / REPAIR",
|
||||
"opcode": "OP1",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP2": {
|
||||
"desc": "REMOVE / INSTALL",
|
||||
"opcode": "OP2",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP3": {
|
||||
"desc": "ADDITIONAL LABOR",
|
||||
"opcode": "OP9",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP4": {
|
||||
"desc": "ALIGNMENT",
|
||||
"opcode": "OP4",
|
||||
"partcode": "PAS"
|
||||
},
|
||||
"OP5": {
|
||||
"desc": "OVERHAUL",
|
||||
"opcode": "OP5",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP6": {
|
||||
"desc": "REFINISH",
|
||||
"opcode": "OP6",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP7": {
|
||||
"desc": "INSPECT",
|
||||
"opcode": "OP7",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP8": {
|
||||
"desc": "CHECK / ADJUST",
|
||||
"opcode": "OP8",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP9": {
|
||||
"desc": "REPAIR",
|
||||
"opcode": "OP9",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP10": {
|
||||
"desc": "REPAIR , PARTIAL",
|
||||
"opcode": "OP9",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP11": {
|
||||
"desc": "REMOVE / REPLACE",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAN"
|
||||
},
|
||||
"OP12": {
|
||||
"desc": "REMOVE / REPLACE PARTIAL",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAN"
|
||||
},
|
||||
"OP13": {
|
||||
"desc": "ADDITIONAL COSTS",
|
||||
"opcode": "OP13",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP14": {
|
||||
"desc": "ADDITIONAL OPERATIONS",
|
||||
"opcode": "OP14",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP15": {
|
||||
"desc": "BLEND",
|
||||
"opcode": "OP15",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP16": {
|
||||
"desc": "SUBLET",
|
||||
"opcode": "OP16",
|
||||
"partcode": "PAS"
|
||||
},
|
||||
"OP17": {
|
||||
"desc": "POLICY LIMIT ADJUSTMENT",
|
||||
"opcode": "OP9",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP18": {
|
||||
"desc": "APPEAR ALLOWANCE",
|
||||
"opcode": "OP7",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP20": {
|
||||
"desc": "REMOVE AND REINSTALL",
|
||||
"opcode": "OP20",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP24": {
|
||||
"desc": "CHIPGUARD",
|
||||
"opcode": "OP6",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP25": {
|
||||
"desc": "TWO TONE",
|
||||
"opcode": "OP6",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP26": {
|
||||
"desc": "PAINTLESS DENT REPAIR",
|
||||
"opcode": "OP16",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP100": {
|
||||
"desc": "REPLACE PRE-PRICED",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAA"
|
||||
},
|
||||
"OP101": {
|
||||
"desc": "REMOVE/REPLACE RECYCLED PART",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAL"
|
||||
},
|
||||
"OP103": {
|
||||
"desc": "REMOVE / REPLACE PARTIAL",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAA"
|
||||
},
|
||||
"OP104": {
|
||||
"desc": "REMOVE / REPLACE PARTIAL LABOUR",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAA"
|
||||
},
|
||||
"OP105": {
|
||||
"desc": "!!ADJUST MANUALLY!!",
|
||||
"opcode": "OP99",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP106": {
|
||||
"desc": "REPAIR , PARTIAL",
|
||||
"opcode": "OP9",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP107": {
|
||||
"desc": "CHIPGUARD",
|
||||
"opcode": "OP6",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP108": {
|
||||
"desc": "MULTI TONE",
|
||||
"opcode": "OP6",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP109": {
|
||||
"desc": "REPLACE PRE-PRICED",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAA"
|
||||
},
|
||||
"OP110": {
|
||||
"desc": "REFINISH / REPAIR",
|
||||
"opcode": "OP1",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP111": {
|
||||
"desc": "REMOVE / REPLACE",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAN"
|
||||
},
|
||||
"OP112": {
|
||||
"desc": "REMOVE / REPLACE",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAA"
|
||||
},
|
||||
"OP113": {
|
||||
"desc": "REPLACE PRE-PRICED",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAA"
|
||||
},
|
||||
"OP114": {
|
||||
"desc": "REPLACE PRE-PRICED",
|
||||
"opcode": "OP11",
|
||||
"partcode": "PAA"
|
||||
},
|
||||
"OP120": {
|
||||
"desc": "REPAIR , PARTIAL",
|
||||
"opcode": "OP9",
|
||||
"partcode": "PAE"
|
||||
},
|
||||
"OP260": {
|
||||
"desc": "SUBLET",
|
||||
"opcode": "OP16",
|
||||
"partcode": "PAE"
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
const admin = require("firebase-admin");
|
||||
const client = require("../../../graphql-client/graphql-client").client;
|
||||
|
||||
const {
|
||||
DELETE_SHOP,
|
||||
DELETE_VENDORS_BY_SHOP,
|
||||
@@ -18,143 +17,154 @@ const {
|
||||
|
||||
/**
|
||||
* Deletes a Firebase user by UID.
|
||||
* @param uid
|
||||
* @param {string} uid - The Firebase user ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteFirebaseUser = async (uid) => {
|
||||
if (!uid) throw new Error("User UID is required");
|
||||
return admin.auth().deleteUser(uid);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes all vendors associated with a shop.
|
||||
* @param shopId
|
||||
* @param {string} shopId - The shop ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteVendorsByShop = async (shopId) => {
|
||||
if (!shopId) throw new Error("Shop ID is required");
|
||||
await client.request(DELETE_VENDORS_BY_SHOP, { shopId });
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a bodyshop from the database.
|
||||
* @param shopId
|
||||
* @param {string} shopId - The shop ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteBodyshop = async (shopId) => {
|
||||
if (!shopId) throw new Error("Shop ID is required");
|
||||
await client.request(DELETE_SHOP, { id: shopId });
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch job ids for a given shop
|
||||
* @param shopId
|
||||
* @param {string} shopId - The shop ID
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
const getJobIdsForShop = async (shopId) => {
|
||||
if (!shopId) throw new Error("Shop ID is required");
|
||||
const resp = await client.request(GET_JOBS_BY_SHOP, { shopId });
|
||||
return resp.jobs.map((j) => j.id);
|
||||
return resp.jobs?.map((j) => j.id) || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete joblines for the given job ids
|
||||
* @param jobIds {string[]}
|
||||
* @param {string[]} jobIds - Array of job IDs
|
||||
* @returns {Promise<number>} affected rows
|
||||
*/
|
||||
const deleteJoblinesForJobs = async (jobIds) => {
|
||||
if (!jobIds.length) return 0;
|
||||
if (!jobIds?.length) return 0;
|
||||
const resp = await client.request(DELETE_JOBLINES_BY_JOB_IDS, { jobIds });
|
||||
return resp.delete_joblines.affected_rows;
|
||||
return resp.delete_joblines?.affected_rows || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete jobs for the given job ids
|
||||
* @param jobIds {string[]}
|
||||
* @param {string[]} jobIds - Array of job IDs
|
||||
* @returns {Promise<number>} affected rows
|
||||
*/
|
||||
const deleteJobsByIds = async (jobIds) => {
|
||||
if (!jobIds.length) return 0;
|
||||
if (!jobIds?.length) return 0;
|
||||
const resp = await client.request(DELETE_JOBS_BY_IDS, { jobIds });
|
||||
return resp.delete_jobs.affected_rows;
|
||||
return resp.delete_jobs?.affected_rows || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles deprovisioning a shop for parts management.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param {Object} req - Express request object
|
||||
* @param {Object} res - Express response object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const partsManagementDeprovisioning = async (req, res) => {
|
||||
const { logger } = req;
|
||||
const p = req.body;
|
||||
const { shopId } = req.body;
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (process.env.NODE_ENV === "production" || process.env.HOSTNAME?.endsWith("compute.internal")) {
|
||||
return res.status(403).json({ error: "Deprovisioning not allowed in production environment." });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!p.shopId) {
|
||||
if (!shopId) {
|
||||
throw { status: 400, message: "shopId is required." };
|
||||
}
|
||||
|
||||
// Fetch bodyshop and check external_shop_id
|
||||
const shopResp = await client.request(GET_BODYSHOP, { id: p.shopId });
|
||||
const shopResp = await client.request(GET_BODYSHOP, { id: shopId });
|
||||
const shop = shopResp.bodyshops_by_pk;
|
||||
if (!shop) {
|
||||
throw { status: 404, message: `Bodyshop with id ${p.shopId} not found.` };
|
||||
throw { status: 404, message: `Bodyshop with id ${shopId} not found.` };
|
||||
}
|
||||
if (!shop.external_shop_id) {
|
||||
throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." };
|
||||
}
|
||||
|
||||
logger.log("admin-delete-shop", "debug", null, null, {
|
||||
shopId: p.shopId,
|
||||
shopId,
|
||||
shopname: shop.shopname,
|
||||
ioadmin: true
|
||||
});
|
||||
|
||||
// Get vendors
|
||||
const vendorsResp = await client.request(GET_VENDORS, { shopId: p.shopId });
|
||||
const deletedVendors = vendorsResp.vendors.map((v) => v.name);
|
||||
const vendorsResp = await client.request(GET_VENDORS, { shopId });
|
||||
const deletedVendors = vendorsResp.vendors?.map((v) => v.name) || [];
|
||||
|
||||
// Get associated users
|
||||
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: p.shopId });
|
||||
const associatedUsers = assocResp.associations.map((assoc) => ({
|
||||
authId: assoc.user.authid,
|
||||
email: assoc.user.email
|
||||
}));
|
||||
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId });
|
||||
const associatedUsers =
|
||||
assocResp.associations?.map((assoc) => ({
|
||||
authId: assoc.user?.authid,
|
||||
email: assoc.user?.email
|
||||
})) || [];
|
||||
|
||||
// Delete associations for the shop
|
||||
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: p.shopId });
|
||||
const associationsDeleted = assocDeleteResp.delete_associations.affected_rows;
|
||||
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId });
|
||||
const associationsDeleted = assocDeleteResp.delete_associations?.affected_rows || 0;
|
||||
|
||||
// For each user, check if they have remaining associations; if not, delete user and Firebase account
|
||||
// Delete users with no remaining associations
|
||||
const deletedUsers = [];
|
||||
for (const user of associatedUsers) {
|
||||
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
|
||||
const assocCount = countResp.associations_aggregate.aggregate.count;
|
||||
if (assocCount === 0) {
|
||||
await client.request(DELETE_USER, { email: user.email });
|
||||
await deleteFirebaseUser(user.authId);
|
||||
deletedUsers.push(user.email);
|
||||
if (!user.email || !user.authId) continue;
|
||||
try {
|
||||
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
|
||||
const assocCount = countResp.associations_aggregate?.aggregate?.count || 0;
|
||||
if (assocCount === 0) {
|
||||
await client.request(DELETE_USER, { email: user.email });
|
||||
await deleteFirebaseUser(user.authId);
|
||||
deletedUsers.push(user.email);
|
||||
}
|
||||
} catch (userError) {
|
||||
logger.log("admin-delete-user-error", "warn", null, null, {
|
||||
email: user.email,
|
||||
error: userError.message || userError
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get all job ids for this shop, then delete joblines and jobs (joblines first)
|
||||
const jobIds = await getJobIdsForShop(p.shopId);
|
||||
// Delete jobs and joblines
|
||||
const jobIds = await getJobIdsForShop(shopId);
|
||||
const joblinesDeleted = await deleteJoblinesForJobs(jobIds);
|
||||
const jobsDeleted = await deleteJobsByIds(jobIds);
|
||||
|
||||
// Delete any audit trail entries tied to this bodyshop to avoid FK violations
|
||||
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: p.shopId });
|
||||
const auditDeleted = auditResp.delete_audit_trail.affected_rows;
|
||||
// Delete audit trail
|
||||
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId });
|
||||
const auditDeleted = auditResp.delete_audit_trail?.affected_rows || 0;
|
||||
|
||||
// Delete vendors
|
||||
await deleteVendorsByShop(p.shopId);
|
||||
|
||||
// Delete shop
|
||||
await deleteBodyshop(p.shopId);
|
||||
// Delete vendors and shop
|
||||
await deleteVendorsByShop(shopId);
|
||||
await deleteBodyshop(shopId);
|
||||
|
||||
// Summary log
|
||||
logger.log("admin-delete-shop-summary", "info", null, null, {
|
||||
shopId: p.shopId,
|
||||
shopId,
|
||||
shopname: shop.shopname,
|
||||
associationsDeleted,
|
||||
deletedUsers,
|
||||
@@ -165,11 +175,11 @@ const partsManagementDeprovisioning = async (req, res) => {
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
message: `Bodyshop ${p.shopId} and associated resources deleted successfully.`,
|
||||
deletedShop: { id: p.shopId, name: shop.shopname },
|
||||
message: `Bodyshop ${shopId} and associated resources deleted successfully.`,
|
||||
deletedShop: { id: shopId, name: shop.shopname },
|
||||
deletedAssociationsCount: associationsDeleted,
|
||||
deletedUsers: deletedUsers,
|
||||
deletedVendors: deletedVendors,
|
||||
deletedUsers,
|
||||
deletedVendors,
|
||||
deletedJoblinesCount: joblinesDeleted,
|
||||
deletedJobsCount: jobsDeleted,
|
||||
deletedAuditTrailCount: auditDeleted
|
||||
@@ -177,9 +187,8 @@ const partsManagementDeprovisioning = async (req, res) => {
|
||||
} catch (err) {
|
||||
logger.log("admin-delete-shop-error", "error", null, null, {
|
||||
message: err.message,
|
||||
detail: err.detail || err
|
||||
detail: err.detail || err.stack || err
|
||||
});
|
||||
|
||||
return res.status(err.status || 500).json({ error: err.message || "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,7 +123,7 @@ const insertUserAssociation = async (uid, email, shopId) => {
|
||||
authid: uid,
|
||||
validemail: true,
|
||||
associations: {
|
||||
data: [{ shopid: shopId, authlevel: 80, active: true }]
|
||||
data: [{ shopid: shopId, authlevel: 99, active: true }]
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -139,11 +139,11 @@ const insertUserAssociation = async (uid, email, shopId) => {
|
||||
*/
|
||||
const partsManagementProvisioning = async (req, res) => {
|
||||
const { logger } = req;
|
||||
const p = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
|
||||
const body = { ...req.body, userEmail: req.body.userEmail?.toLowerCase() };
|
||||
|
||||
try {
|
||||
await ensureEmailNotRegistered(p.userEmail);
|
||||
requireFields(p, [
|
||||
await ensureEmailNotRegistered(body.userEmail);
|
||||
requireFields(body, [
|
||||
"external_shop_id",
|
||||
"shopname",
|
||||
"address1",
|
||||
@@ -155,33 +155,33 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
"phone",
|
||||
"userEmail"
|
||||
]);
|
||||
await ensureExternalIdUnique(p.external_shop_id);
|
||||
await ensureExternalIdUnique(body.external_shop_id);
|
||||
|
||||
logger.log("admin-create-shop-user", "debug", p.userEmail, null, {
|
||||
logger.log("admin-create-shop-user", "debug", body.userEmail, null, {
|
||||
request: req.body,
|
||||
ioadmin: true
|
||||
});
|
||||
|
||||
const shopInput = {
|
||||
shopname: p.shopname,
|
||||
address1: p.address1,
|
||||
address2: p.address2 || null,
|
||||
city: p.city,
|
||||
state: p.state,
|
||||
zip_post: p.zip_post,
|
||||
country: p.country,
|
||||
email: p.email,
|
||||
external_shop_id: p.external_shop_id,
|
||||
timezone: p.timezone || DefaultNewShop.timezone,
|
||||
phone: p.phone,
|
||||
shopname: body.shopname,
|
||||
address1: body.address1,
|
||||
address2: body.address2 || null,
|
||||
city: body.city,
|
||||
state: body.state,
|
||||
zip_post: body.zip_post,
|
||||
country: body.country,
|
||||
email: body.email,
|
||||
external_shop_id: body.external_shop_id,
|
||||
timezone: body.timezone || DefaultNewShop.timezone,
|
||||
phone: body.phone,
|
||||
logo_img_path: {
|
||||
src: p.logoUrl,
|
||||
src: body.logoUrl,
|
||||
width: "",
|
||||
height: "",
|
||||
headerMargin: DefaultNewShop.logo_img_path.headerMargin
|
||||
},
|
||||
features: {
|
||||
allAccess: true, // TODO: should be false?
|
||||
allAccess: false,
|
||||
partsManagementOnly: true
|
||||
},
|
||||
md_ro_statuses: DefaultNewShop.md_ro_statuses,
|
||||
@@ -191,16 +191,16 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
md_messaging_presets: DefaultNewShop.md_messaging_presets,
|
||||
md_rbac: DefaultNewShop.md_rbac,
|
||||
md_classes: DefaultNewShop.md_classes,
|
||||
md_ins_cos: DefaultNewShop.md_ins_cos, // TODO need?
|
||||
md_categories: DefaultNewShop.md_categories, // TODO need?
|
||||
md_labor_rates: DefaultNewShop.md_labor_rates, // TODO need?
|
||||
md_payment_types: DefaultNewShop.md_payment_types, // TODO need?
|
||||
md_hour_split: DefaultNewShop.md_hour_split, // TODO need?
|
||||
md_ccc_rates: DefaultNewShop.md_ccc_rates, // TODO need?
|
||||
appt_alt_transport: DefaultNewShop.appt_alt_transport, // TODO need?
|
||||
md_jobline_presets: DefaultNewShop.md_jobline_presets, // TODO need?
|
||||
md_ins_cos: DefaultNewShop.md_ins_cos,
|
||||
md_categories: DefaultNewShop.md_categories,
|
||||
md_labor_rates: DefaultNewShop.md_labor_rates,
|
||||
md_payment_types: DefaultNewShop.md_payment_types,
|
||||
md_hour_split: DefaultNewShop.md_hour_split,
|
||||
md_ccc_rates: DefaultNewShop.md_ccc_rates,
|
||||
appt_alt_transport: DefaultNewShop.appt_alt_transport,
|
||||
md_jobline_presets: DefaultNewShop.md_jobline_presets,
|
||||
vendors: {
|
||||
data: p.vendors.map((v) => ({
|
||||
data: body.vendors.map((v) => ({
|
||||
name: v.name,
|
||||
street1: v.street1 || null,
|
||||
street2: v.street2 || null,
|
||||
@@ -221,14 +221,14 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
};
|
||||
|
||||
const newShopId = await insertBodyshop(shopInput);
|
||||
const userRecord = await createFirebaseUser(p.userEmail, p.userPassword);
|
||||
const userRecord = await createFirebaseUser(body.userEmail, body.userPassword);
|
||||
let resetLink = null;
|
||||
if (!p.userPassword) resetLink = await generateResetLink(p.userEmail);
|
||||
if (!body.userPassword) resetLink = await generateResetLink(body.userEmail);
|
||||
|
||||
const createdUser = await insertUserAssociation(userRecord.uid, p.userEmail, newShopId);
|
||||
const createdUser = await insertUserAssociation(userRecord.uid, body.userEmail, newShopId);
|
||||
|
||||
return res.status(200).json({
|
||||
shop: { id: newShopId, shopname: p.shopname },
|
||||
shop: { id: newShopId, shopname: body.shopname },
|
||||
user: {
|
||||
id: createdUser.id,
|
||||
email: createdUser.email,
|
||||
@@ -236,7 +236,7 @@ const partsManagementProvisioning = async (req, res) => {
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.log("admin-create-shop-user-error", "error", p.userEmail, null, {
|
||||
logger.log("admin-create-shop-user-error", "error", body.userEmail, null, {
|
||||
message: err.message,
|
||||
detail: err.detail || err
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// no-dd-sa:javascript-code-style/assignment-name
|
||||
// CamelCase is used for GraphQL and database fields.
|
||||
|
||||
const client = require("../../../graphql-client/graphql-client").client;
|
||||
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
||||
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
||||
const opCodes = require("./lib/opCodes.json");
|
||||
// New imports for S3 XML archival
|
||||
const { uploadFileToS3 } = require("../../../utils/s3");
|
||||
const InstanceMgr = require("../../../utils/instanceMgr").default;
|
||||
|
||||
// GraphQL Queries and Mutations
|
||||
const {
|
||||
@@ -12,9 +13,27 @@ const {
|
||||
INSERT_OWNER,
|
||||
INSERT_JOB_WITH_LINES
|
||||
} = require("../partsManagement.queries");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
|
||||
// Defaults
|
||||
const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN";
|
||||
const FALLBACK_DEFAULT_JOB_STATUS = "Open";
|
||||
|
||||
const ESTIMATE_XML_BUCKET =
|
||||
process.env?.NODE_ENV === "development"
|
||||
? "parts-estimates" // local/dev shared bucket name
|
||||
: InstanceMgr({
|
||||
imex: `imex-webest-xml`,
|
||||
rome: `rome-webest-xml`
|
||||
});
|
||||
|
||||
const buildEstimateXmlKey = (rq) => {
|
||||
const refClaimNum = rq.RefClaimNum;
|
||||
const shopId = rq.ShopID;
|
||||
|
||||
const ts = new Date().toISOString().replace(/:/g, "-");
|
||||
const safeClaim = (refClaimNum || "no-claim").toString().replace(/[^A-Za-z0-9_-]/g, "_");
|
||||
return `addRequest/${shopId}/${safeClaim}/${ts}-${uuidv4()}.xml`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the default order status for a bodyshop.
|
||||
@@ -22,13 +41,13 @@ const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN";
|
||||
* @param {object} logger - The logger instance.
|
||||
* @returns {Promise<string>} The default status or fallback.
|
||||
*/
|
||||
const getDefaultOrderStatus = async (shopId, logger) => {
|
||||
const getDefaultJobStatus = async (shopId, logger) => {
|
||||
try {
|
||||
const { bodyshop_by_pk } = await client.request(GET_BODYSHOP_STATUS, { id: shopId });
|
||||
return bodyshop_by_pk?.md_order_statuses?.default_open || FALLBACK_DEFAULT_ORDER_STATUS;
|
||||
return bodyshop_by_pk?.md_ro_statuses?.default_imported || FALLBACK_DEFAULT_JOB_STATUS;
|
||||
} catch (err) {
|
||||
logger.log("parts-bodyshop-fetch-failed", "warn", shopId, null, { error: err });
|
||||
return FALLBACK_DEFAULT_ORDER_STATUS;
|
||||
return FALLBACK_DEFAULT_JOB_STATUS;
|
||||
}
|
||||
};
|
||||
/**
|
||||
@@ -65,7 +84,9 @@ const extractJobData = (rq) => {
|
||||
const ci = rq.ClaimInfo || {};
|
||||
|
||||
return {
|
||||
driveable: !!rq.VehicleInfo?.Condition?.DrivableInd,
|
||||
shopId: rq.ShopID || rq.shopId,
|
||||
// status: ci.ClaimStatus || null, Proper, setting it default for now
|
||||
refClaimNum: rq.RefClaimNum,
|
||||
ciecaid: rq.RqUID || null,
|
||||
// Pull Cieca_ttl from ClaimInfo per schema/sample
|
||||
@@ -81,7 +102,6 @@ const extractJobData = (rq) => {
|
||||
scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null,
|
||||
scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null,
|
||||
clm_no: ci.ClaimNum || null,
|
||||
status: ci.ClaimStatus || null,
|
||||
policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null,
|
||||
ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0)
|
||||
};
|
||||
@@ -100,17 +120,18 @@ const extractOwnerData = (rq, shopId) => {
|
||||
const personName = personInfo.PersonName || {};
|
||||
const address = personInfo.Communications?.Address || {};
|
||||
|
||||
let ownr_ph1, ownr_ph2, ownr_ea, ownr_alt_ph;
|
||||
let ownr_ph1, ownr_ph2, ownr_ea;
|
||||
|
||||
const comms = Array.isArray(ownerOrClaimant.ContactInfo?.Communications)
|
||||
? ownerOrClaimant.ContactInfo.Communications
|
||||
: [ownerOrClaimant.ContactInfo?.Communications || {}];
|
||||
|
||||
for (const c of comms) {
|
||||
// -- Document
|
||||
if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone;
|
||||
if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone;
|
||||
if (c.CommQualifier === "EM") ownr_ea = c.CommEmail;
|
||||
if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone;
|
||||
// if (c.CommQualifier === "AL") ownr_alt_ph = c.CommPhone;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -126,8 +147,8 @@ const extractOwnerData = (rq, shopId) => {
|
||||
ownr_ctry: address.Country || null,
|
||||
ownr_ph1,
|
||||
ownr_ph2,
|
||||
ownr_ea,
|
||||
ownr_alt_ph
|
||||
ownr_ea
|
||||
// ownr_alt_ph
|
||||
// ownr_id_qualifier: ownerOrClaimant.IDInfo?.IDQualifierCode || null // New
|
||||
// ownr_id_num: ownerOrClaimant.IDInfo?.IDNum || null, // New
|
||||
// ownr_preferred_contact: ownerOrClaimant.PreferredContactMethod || null // New
|
||||
@@ -158,37 +179,40 @@ const extractEstimatorData = (rq) => {
|
||||
* @param {object} rq - The VehicleDamageEstimateAddRq object.
|
||||
* @returns {object} Adjuster data.
|
||||
*/
|
||||
const extractAdjusterData = (rq) => {
|
||||
const adjParty = rq.AdminInfo?.Adjuster?.Party || {};
|
||||
const adjComms = Array.isArray(adjParty.ContactInfo?.Communications)
|
||||
? adjParty.ContactInfo.Communications
|
||||
: [adjParty.ContactInfo?.Communications || {}];
|
||||
|
||||
return {
|
||||
agt_ct_fn: adjParty.PersonInfo?.PersonName?.FirstName || null,
|
||||
agt_ct_ln: adjParty.PersonInfo?.PersonName?.LastName || null,
|
||||
agt_ct_ph: adjComms.find((c) => c.CommQualifier === "CP")?.CommPhone || null,
|
||||
agt_ea: adjComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null
|
||||
};
|
||||
};
|
||||
// const extractAdjusterData = (rq) => {
|
||||
// const adjParty = rq.AdminInfo?.Adjuster?.Party || {};
|
||||
// const adjComms = Array.isArray(adjParty.ContactInfo?.Communications)
|
||||
// ? adjParty.ContactInfo.Communications
|
||||
// : [adjParty.ContactInfo?.Communications || {}];
|
||||
//
|
||||
// return {
|
||||
// //TODO (FUTURE): I dont think we display agt_ct_* fields in app. Have they typically been sending data here?
|
||||
// agt_ct_fn: adjParty.PersonInfo?.PersonName?.FirstName || null,
|
||||
// agt_ct_ln: adjParty.PersonInfo?.PersonName?.LastName || null,
|
||||
// agt_ct_ph: adjComms.find((c) => c.CommQualifier === "CP")?.CommPhone || null,
|
||||
// agt_ea: adjComms.find((c) => c.CommQualifier === "EM")?.CommEmail || null
|
||||
// };
|
||||
// };
|
||||
|
||||
/**
|
||||
* Extracts repair facility data from the XML request.
|
||||
* @param {object} rq - The VehicleDamageEstimateAddRq object.
|
||||
* @returns {object} Repair facility data.
|
||||
*/
|
||||
const extractRepairFacilityData = (rq) => {
|
||||
const rfParty = rq.AdminInfo?.RepairFacility?.Party || {};
|
||||
const rfComms = Array.isArray(rfParty.ContactInfo?.Communications)
|
||||
? rfParty.ContactInfo.Communications
|
||||
: [rfParty.ContactInfo?.Communications || {}];
|
||||
|
||||
return {
|
||||
servicing_dealer: rfParty.OrgInfo?.CompanyName || null,
|
||||
servicing_dealer_contact:
|
||||
rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null
|
||||
};
|
||||
};
|
||||
// const extractRepairFacilityData = (rq) => {
|
||||
// const rfParty = rq.AdminInfo?.RepairFacility?.Party || {};
|
||||
// const rfComms = Array.isArray(rfParty.ContactInfo?.Communications)
|
||||
// ? rfParty.ContactInfo.Communications
|
||||
// : [rfParty.ContactInfo?.Communications || {}];
|
||||
//
|
||||
// return {
|
||||
// servicing_dealer: rfParty.OrgInfo?.CompanyName || null,
|
||||
// // TODO (Future): The servicing dealer fields are a relic from synergy for a few folks
|
||||
// // TODO (Future): I suspect RF data could be ignored since they are the RF.
|
||||
// servicing_dealer_contact:
|
||||
// rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null
|
||||
// };
|
||||
// };
|
||||
|
||||
/**
|
||||
* Extracts loss information from the XML request.
|
||||
@@ -202,10 +226,12 @@ const extractLossInfo = (rq) => {
|
||||
loss_date: loss.LossDateTime || null,
|
||||
loss_type: custom.LossTypeCode || null,
|
||||
loss_desc: custom.LossTypeDesc || null
|
||||
// primary_poi: loss.PrimaryPOI?.POICode || null,
|
||||
// secondary_poi: loss.SecondaryPOI?.POICode || null,
|
||||
// area_of_impact: {
|
||||
// impact_1: loss.PrimaryPOI?.POICode || null,
|
||||
// imact_2 :loss.SecondaryPOI?.POICode || null,
|
||||
// },
|
||||
// tlosind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null,
|
||||
// damage_memo: loss.DamageMemo || null, //(maybe ins_memo)
|
||||
// total_loss_ind: rq.ClaimInfo?.LossInfo?.TotalLossInd || null // New
|
||||
};
|
||||
};
|
||||
|
||||
@@ -287,7 +313,8 @@ const extractVehicleData = (rq, shopId) => {
|
||||
v_color: exterior.Color?.ColorName || null,
|
||||
v_bstyle: desc.BodyStyle || null,
|
||||
v_engine: desc.EngineDesc || null,
|
||||
v_options: desc.SubModelDesc || null,
|
||||
// TODO (for future) Need to confirm with exact data, but this is typically a list of options. Not used AFAIK.
|
||||
// v_options: desc.SubModelDesc || null,
|
||||
v_type: desc.FuelType || null,
|
||||
v_cond: rq.VehicleInfo?.Condition?.DrivableInd,
|
||||
v_trimcode: desc.TrimCode || null,
|
||||
@@ -336,27 +363,34 @@ const extractJobLines = (rq) => {
|
||||
const lineOut = { ...base };
|
||||
|
||||
// Manual line flag coercion
|
||||
if (line.ManualLineInd !== undefined) {
|
||||
lineOut.manual_line =
|
||||
line.ManualLineInd === true ||
|
||||
line.ManualLineInd === 1 ||
|
||||
line.ManualLineInd === "1" ||
|
||||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y");
|
||||
} else {
|
||||
lineOut.manual_line = null;
|
||||
}
|
||||
// if (line.ManualLineInd !== undefined) {
|
||||
// lineOut.manual_line =
|
||||
// line.ManualLineInd === true ||
|
||||
// line.ManualLineInd === 1 ||
|
||||
// line.ManualLineInd === "1" ||
|
||||
// // TODO (FUTURE): manual line tracks manual in IO or not, this woudl presumably always be false
|
||||
// (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y");
|
||||
// } else {
|
||||
// lineOut.manual_line = null;
|
||||
// }
|
||||
|
||||
// Is set to false because anything coming from the DMS is considered not a manual line, it becomes
|
||||
// a manual line once it is edited in OUR system.
|
||||
lineOut.manual_line = false;
|
||||
|
||||
// Parts (preferred) or Sublet (fallback when no PartInfo)
|
||||
const hasPart = Object.keys(partInfo).length > 0;
|
||||
const hasSublet = Object.keys(subletInfo).length > 0;
|
||||
|
||||
if (hasPart) {
|
||||
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
|
||||
lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
|
||||
lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
|
||||
lineOut.db_price = isNaN(price) ? 0 : price;
|
||||
lineOut.act_price = isNaN(price) ? 0 : price;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum;
|
||||
lineOut.alt_partno = partInfo?.NonOEM?.NonOEMPartNum;
|
||||
|
||||
// THIS NEEDS TO BE CHANGED IN CHANGE REQUEST
|
||||
lineOut.act_price = parseFloat(partInfo?.PartPrice || 0);
|
||||
lineOut.db_price = parseFloat(partInfo?.OEMPartPrice || 0);
|
||||
|
||||
// Tax flag from PartInfo.TaxableInd when provided
|
||||
if (
|
||||
@@ -371,7 +405,12 @@ const extractJobLines = (rq) => {
|
||||
partInfo.TaxableInd === "1" ||
|
||||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
|
||||
}
|
||||
} else if (hasSublet) {
|
||||
}
|
||||
|
||||
//TODO (FUTURE): Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically
|
||||
// could. May require additional discussion.
|
||||
// EMS - > Misc Amount, calibration for example, painting, etc
|
||||
else if (hasSublet) {
|
||||
const amt = parseFloat(subletInfo.SubletAmount || 0);
|
||||
lineOut.part_type = "PAS"; // Sublet as parts-as-service
|
||||
lineOut.part_qty = 1;
|
||||
@@ -385,14 +424,22 @@ const extractJobLines = (rq) => {
|
||||
(!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) ||
|
||||
(!isNaN(hrs) && hrs !== 0) ||
|
||||
(!isNaN(amt) && amt !== 0);
|
||||
|
||||
if (hasLabor) {
|
||||
lineOut.mod_lbr_ty = laborInfo.LaborType || null;
|
||||
lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
|
||||
lineOut.lbr_op = laborInfo.LaborOperation || null;
|
||||
const opCodeKey =
|
||||
typeof laborInfo.LaborOperation === "string" ? laborInfo.LaborOperation.trim().toUpperCase() : null;
|
||||
lineOut.op_code_desc = opCodes?.[opCodeKey]?.desc || null;
|
||||
|
||||
lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
|
||||
}
|
||||
|
||||
//TODO (FUTURE): what's the BMS logic for this? Body and refinish operations can often happen to the same part,
|
||||
// but most systems output a second line for the refinish labor.
|
||||
//TODO (FUTURE): 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC.
|
||||
// Refinish labor (if present) recorded on the same line using secondary labor fields
|
||||
|
||||
const rHrs = parseFloat(refinishInfo.LaborHours || 0);
|
||||
const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
|
||||
const hasRefinish =
|
||||
@@ -402,9 +449,9 @@ const extractJobLines = (rq) => {
|
||||
!isNaN(rAmt) ||
|
||||
!!refinishInfo.LaborOperation);
|
||||
if (hasRefinish) {
|
||||
lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR";
|
||||
lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;
|
||||
lineOut.lbr_op_j = refinishInfo.LaborOperation || null;
|
||||
lineOut.lbr_typ_j = !!refinishInfo?.LaborAmtJudgmentInd;
|
||||
lineOut.lbr_hrs_j = !!refinishInfo?.LaborHoursJudgmentInd;
|
||||
lineOut.lbr_op_j = !!refinishInfo.LaborOperationJudgmentInd;
|
||||
// Aggregate refinish labor amount into the total labor amount for the line
|
||||
if (!isNaN(rAmt)) {
|
||||
lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
|
||||
@@ -420,26 +467,26 @@ const extractJobLines = (rq) => {
|
||||
};
|
||||
|
||||
// Helper to extract a GRAND TOTAL amount from RepairTotalsInfo
|
||||
const extractGrandTotal = (rq) => {
|
||||
const rti = rq.RepairTotalsInfo;
|
||||
const groups = Array.isArray(rti) ? rti : rti ? [rti] : [];
|
||||
for (const grp of groups) {
|
||||
const sums = Array.isArray(grp.SummaryTotalsInfo)
|
||||
? grp.SummaryTotalsInfo
|
||||
: grp.SummaryTotalsInfo
|
||||
? [grp.SummaryTotalsInfo]
|
||||
: [];
|
||||
for (const s of sums) {
|
||||
const type = (s.TotalType || "").toString().toUpperCase();
|
||||
const desc = (s.TotalTypeDesc || "").toString().toUpperCase();
|
||||
if (type.includes("GRAND") || type === "TOTAL" || desc.includes("GRAND")) {
|
||||
const amt = parseFloat(s.TotalAmt ?? "NaN");
|
||||
if (!isNaN(amt)) return amt;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// const extractGrandTotal = (rq) => {
|
||||
// const rti = rq.RepairTotalsInfo;
|
||||
// const groups = Array.isArray(rti) ? rti : rti ? [rti] : [];
|
||||
// for (const grp of groups) {
|
||||
// const sums = Array.isArray(grp.SummaryTotalsInfo)
|
||||
// ? grp.SummaryTotalsInfo
|
||||
// : grp.SummaryTotalsInfo
|
||||
// ? [grp.SummaryTotalsInfo]
|
||||
// : [];
|
||||
// for (const s of sums) {
|
||||
// const type = (s.TotalType || "").toString().toUpperCase();
|
||||
// const desc = (s.TotalTypeDesc || "").toString().toUpperCase();
|
||||
// if (type.includes("GRAND") || type === "TOTAL" || desc.includes("GRAND")) {
|
||||
// const amt = parseFloat(s.TotalAmt ?? "NaN");
|
||||
// if (!isNaN(amt)) return amt;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return null;
|
||||
// };
|
||||
|
||||
/**
|
||||
* Inserts an owner and returns the owner ID.
|
||||
@@ -458,24 +505,27 @@ const insertOwner = async (ownerInput, logger) => {
|
||||
};
|
||||
|
||||
// Fallback: compute a naive total from joblines (parts + sublet + labor amounts)
|
||||
const computeLinesTotal = (joblines = []) => {
|
||||
let parts = 0;
|
||||
let labor = 0;
|
||||
for (const jl of joblines) {
|
||||
if (jl?.part_type) {
|
||||
const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1;
|
||||
const price = Number.isFinite(jl.act_price) ? jl.act_price : 0;
|
||||
parts += price * (qty || 1);
|
||||
} else if (!jl.part_type && Number.isFinite(jl.act_price)) {
|
||||
parts += jl.act_price;
|
||||
}
|
||||
if (Number.isFinite(jl.lbr_amt)) {
|
||||
labor += jl.lbr_amt;
|
||||
}
|
||||
}
|
||||
const total = parts + labor;
|
||||
return Number.isFinite(total) && total > 0 ? total : 0;
|
||||
};
|
||||
// const computeLinesTotal = (joblines = []) => {
|
||||
// let parts = 0;
|
||||
// let labor = 0;
|
||||
// for (const jl of joblines) {
|
||||
// if (jl?.part_type) {
|
||||
// const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1;
|
||||
// const price = Number.isFinite(jl.act_price) ? jl.act_price : 0;
|
||||
// parts += price * (qty || 1);
|
||||
// } else if (!jl.part_type && Number.isFinite(jl.act_price)) {
|
||||
// parts += jl.act_price;
|
||||
// }
|
||||
// if (Number.isFinite(jl.lbr_amt)) {
|
||||
// labor += jl.lbr_amt;
|
||||
// }
|
||||
// }
|
||||
// const total = parts + labor;
|
||||
//
|
||||
// //TODO (FUTURE): clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system.
|
||||
// return Number.isFinite(total) && total > 0 ? total : 0;
|
||||
// //TODO (FUTURE): clm_total is the 100% full amount of the repair including deductible,
|
||||
// // betterment and taxes. Typically provided by the source system.
|
||||
|
||||
/**
|
||||
* Handles the VehicleDamageEstimateAddRq XML request from parts management.
|
||||
@@ -485,17 +535,10 @@ const computeLinesTotal = (joblines = []) => {
|
||||
*/
|
||||
const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
const { logger } = req;
|
||||
|
||||
const rawXml = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf8") : "";
|
||||
try {
|
||||
// Parse XML
|
||||
const payload = await parseXml(req.body, logger);
|
||||
const rq = normalizeXmlObject(payload.VehicleDamageEstimateAddRq);
|
||||
if (!rq) {
|
||||
logger.log("parts-missing-root", "error");
|
||||
return res.status(400).send("Missing <VehicleDamageEstimateAddRq>");
|
||||
}
|
||||
|
||||
// Extract job data
|
||||
const {
|
||||
shopId,
|
||||
refClaimNum,
|
||||
@@ -512,40 +555,23 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
scheduled_in,
|
||||
scheduled_completion,
|
||||
clm_no,
|
||||
status,
|
||||
policy_no,
|
||||
ded_amt
|
||||
ded_amt,
|
||||
driveable
|
||||
} = extractJobData(rq);
|
||||
|
||||
if (!shopId) {
|
||||
throw { status: 400, message: "Missing <ShopID> in XML" };
|
||||
}
|
||||
|
||||
// Get default status
|
||||
const defaultStatus = await getDefaultOrderStatus(shopId, logger);
|
||||
|
||||
// Extract additional data
|
||||
const defaultStatus = await getDefaultJobStatus(shopId, logger);
|
||||
const parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
|
||||
const ownerData = extractOwnerData(rq, shopId);
|
||||
const estimatorData = extractEstimatorData(rq);
|
||||
const adjusterData = extractAdjusterData(rq);
|
||||
const repairFacilityData = extractRepairFacilityData(rq);
|
||||
const vehicleData = extractVehicleData(rq, shopId);
|
||||
const lossInfo = extractLossInfo(rq);
|
||||
const joblinesData = extractJobLines(rq);
|
||||
const insuranceData = extractInsuranceData(rq);
|
||||
|
||||
// Derive clm_total: prefer RepairTotalsInfo SummaryTotals GRAND TOTAL; else sum from lines
|
||||
const grandTotal = extractGrandTotal(rq);
|
||||
const computedTotal = grandTotal ?? computeLinesTotal(joblinesData);
|
||||
|
||||
// Find or create relationships
|
||||
const ownerid = await insertOwner(ownerData, logger);
|
||||
const vehicleid = await findExistingVehicle(shopId, vehicleData.v_vin, logger);
|
||||
|
||||
// Build job input
|
||||
const jobInput = {
|
||||
shopid: shopId,
|
||||
driveable,
|
||||
converted: true,
|
||||
ownerid,
|
||||
ro_number: refClaimNum,
|
||||
@@ -556,8 +582,8 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
class: classType,
|
||||
parts_tax_rates,
|
||||
clm_no,
|
||||
status: status || defaultStatus,
|
||||
clm_total: computedTotal || null,
|
||||
status: defaultStatus,
|
||||
clm_total: 0,
|
||||
policy_no,
|
||||
ded_amt,
|
||||
comment,
|
||||
@@ -567,14 +593,10 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
asgn_date,
|
||||
scheduled_in,
|
||||
scheduled_completion,
|
||||
// Inline insurance/loss/contacts
|
||||
...insuranceData,
|
||||
...lossInfo,
|
||||
...ownerData,
|
||||
...estimatorData,
|
||||
...adjusterData,
|
||||
...repairFacilityData,
|
||||
// Inline vehicle data
|
||||
v_vin: vehicleData.v_vin,
|
||||
v_model_yr: vehicleData.v_model_yr,
|
||||
v_model_desc: vehicleData.v_model_desc,
|
||||
@@ -585,10 +607,23 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
...(vehicleid ? { vehicleid } : { vehicle: { data: vehicleData } }),
|
||||
joblines: { data: joblinesData }
|
||||
};
|
||||
|
||||
// Insert job
|
||||
const { insert_jobs_one: newJob } = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput });
|
||||
|
||||
// Upload AFTER job creation to include job id in filename
|
||||
(async () => {
|
||||
try {
|
||||
const key = buildEstimateXmlKey(rq);
|
||||
await uploadFileToS3({
|
||||
bucketName: ESTIMATE_XML_BUCKET,
|
||||
key,
|
||||
content: rawXml || "",
|
||||
contentType: "application/xml"
|
||||
});
|
||||
logger.log("parts-estimate-xml-uploaded", "info", shopId, newJob.id, { key, bytes: rawXml?.length || 0 });
|
||||
} catch (e) {
|
||||
logger.log("parts-estimate-xml-upload-failed", "warn", shopId, null, { error: e?.message });
|
||||
}
|
||||
})();
|
||||
return res.status(200).json({ success: true, jobId: newJob.id });
|
||||
} catch (err) {
|
||||
logger.log("parts-route-error", "error", null, null, { error: err });
|
||||
|
||||
@@ -4,24 +4,30 @@
|
||||
const client = require("../../../graphql-client/graphql-client").client;
|
||||
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
||||
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
||||
const opCodes = require("./lib/opCodes.json");
|
||||
const { uploadFileToS3 } = require("../../../utils/s3");
|
||||
const InstanceMgr = require("../../../utils/instanceMgr").default;
|
||||
|
||||
const {
|
||||
GET_JOB_BY_CLAIM,
|
||||
GET_JOB_BY_ID,
|
||||
UPDATE_JOB_BY_ID,
|
||||
SOFT_DELETE_JOBLINES_BY_IDS,
|
||||
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ,
|
||||
GET_JOBLINE_IDS_BY_JOBID_UNQSEQ,
|
||||
UPDATE_JOBLINE_BY_PK,
|
||||
INSERT_JOBLINES
|
||||
} = require("../partsManagement.queries");
|
||||
|
||||
/**
|
||||
* Finds a job by shop ID and claim number.
|
||||
* Finds a job by shop ID and job ID.
|
||||
* @param shopId
|
||||
* @param claimNum
|
||||
* @param jobId
|
||||
* @param logger
|
||||
* @returns {Promise<*|null>}
|
||||
*/
|
||||
const findJob = async (shopId, claimNum, logger) => {
|
||||
const findJob = async (shopId, jobId, logger) => {
|
||||
try {
|
||||
const { jobs } = await client.request(GET_JOB_BY_CLAIM, { shopid: shopId, clm_no: claimNum });
|
||||
const { jobs } = await client.request(GET_JOB_BY_ID, { shopid: shopId, jobid: jobId });
|
||||
return jobs?.[0] || null;
|
||||
} catch (err) {
|
||||
logger.log("parts-job-lookup-failed", "error", null, null, { error: err });
|
||||
@@ -31,37 +37,39 @@ const findJob = async (shopId, claimNum, logger) => {
|
||||
|
||||
/**
|
||||
* Extracts updated job data from the request payload.
|
||||
* Mirrors AddRq for parts_tax_rates + driveable when present.
|
||||
* @param rq
|
||||
* @returns {{comment: (number|((comment: Comment, helper: postcss.Helpers) => (Promise<void> | void))|string|null), clm_no: null, status: (*|null), policy_no: (*|null)}}
|
||||
*/
|
||||
const extractUpdatedJobData = (rq) => {
|
||||
const doc = rq.DocumentInfo || {};
|
||||
const claim = rq.ClaimInfo || {};
|
||||
|
||||
const policyNo = claim.PolicyInfo?.PolicyInfo?.PolicyNum || claim.PolicyInfo?.PolicyNum || null;
|
||||
|
||||
const out = {
|
||||
comment: doc.Comment || null,
|
||||
clm_no: claim.ClaimNum || null,
|
||||
status: claim.ClaimStatus || null,
|
||||
// TODO (future): status omitted intentionally to avoid overwriting with 'Auth Cust'
|
||||
policy_no: policyNo
|
||||
};
|
||||
|
||||
// If ProfileInfo provided in ChangeRq, update parts_tax_rates to stay in sync with AddRq behavior
|
||||
if (rq.ProfileInfo) {
|
||||
out.parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
|
||||
}
|
||||
|
||||
if (rq.VehicleInfo?.Condition?.DrivableInd !== undefined) {
|
||||
out.driveable = !!rq.VehicleInfo.Condition.DrivableInd;
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts updated job lines from the request payload without splitting parts and labor:
|
||||
* - Keep part and labor on the same jobline
|
||||
* - Aggregate RefinishLabor into secondary labor fields and add its amount to lbr_amt
|
||||
* - SUBLET-only lines become PAS part_type with act_price = SubletAmount
|
||||
* Build jobline payloads for updates/inserts (no split between parts & labor).
|
||||
* - Refinish labor aggregated into lbr_* secondary fields and lbr_amt.
|
||||
* - SUBLET-only -> PAS line with act_price = SubletAmount.
|
||||
* - Notes merged with current DB value by unq_seq.
|
||||
*/
|
||||
const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
|
||||
const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {}) => {
|
||||
const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}];
|
||||
|
||||
const coerceManual = (val) =>
|
||||
@@ -83,35 +91,39 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
|
||||
unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
|
||||
status: line.LineStatusCode || null,
|
||||
line_desc: line.LineDesc || null,
|
||||
notes: line.LineMemo || null,
|
||||
manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
|
||||
manual_line: false
|
||||
// manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
|
||||
};
|
||||
|
||||
const lineOut = { ...base };
|
||||
|
||||
// --- Notes merge ---
|
||||
const unqSeq = lineOut.unq_seq;
|
||||
const currentNotes = currentJobLineNotes?.[unqSeq] || null;
|
||||
const newNotes = line.LineMemo || null;
|
||||
if (newNotes && currentNotes) {
|
||||
if (currentNotes === newNotes || currentNotes.includes(newNotes)) lineOut.notes = currentNotes;
|
||||
else lineOut.notes = `${currentNotes} | ${newNotes}`;
|
||||
} else if (newNotes) lineOut.notes = newNotes;
|
||||
else if (currentNotes) lineOut.notes = currentNotes;
|
||||
else lineOut.notes = null;
|
||||
// --- end notes merge ---
|
||||
|
||||
const hasPart = Object.keys(partInfo).length > 0;
|
||||
const hasSublet = Object.keys(subletInfo).length > 0;
|
||||
|
||||
if (hasPart) {
|
||||
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
|
||||
lineOut.part_type = partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null;
|
||||
lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
|
||||
lineOut.db_price = isNaN(price) ? 0 : price;
|
||||
lineOut.act_price = isNaN(price) ? 0 : price;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum;
|
||||
lineOut.alt_partno = partInfo?.NonOEM?.NonOEMPartNum;
|
||||
lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
|
||||
|
||||
// Optional: taxability flag for parts
|
||||
if (
|
||||
partInfo.TaxableInd !== undefined &&
|
||||
(typeof partInfo.TaxableInd === "string" ||
|
||||
typeof partInfo.TaxableInd === "number" ||
|
||||
typeof partInfo.TaxableInd === "boolean")
|
||||
) {
|
||||
lineOut.tax_part =
|
||||
partInfo.TaxableInd === true ||
|
||||
partInfo.TaxableInd === 1 ||
|
||||
partInfo.TaxableInd === "1" ||
|
||||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
|
||||
lineOut.act_price = parseFloat(partInfo?.PartPrice || 0);
|
||||
lineOut.db_price = parseFloat(partInfo?.OEMPartPrice || 0);
|
||||
|
||||
if (partInfo.TaxableInd !== undefined) {
|
||||
const t = partInfo.TaxableInd;
|
||||
lineOut.tax_part = t === true || t === 1 || t === "1" || (typeof t === "string" && t.toUpperCase() === "Y");
|
||||
}
|
||||
} else if (hasSublet) {
|
||||
const amt = parseFloat(subletInfo.SubletAmount || 0);
|
||||
@@ -120,7 +132,7 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
|
||||
lineOut.act_price = isNaN(amt) ? 0 : amt;
|
||||
}
|
||||
|
||||
// Primary labor on same line
|
||||
// Primary labor
|
||||
const hrs = parseFloat(laborInfo.LaborHours || 0);
|
||||
const amt = parseFloat(laborInfo.LaborAmt || 0);
|
||||
const hasLabor =
|
||||
@@ -130,11 +142,15 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
|
||||
if (hasLabor) {
|
||||
lineOut.mod_lbr_ty = laborInfo.LaborType || null;
|
||||
lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
|
||||
lineOut.lbr_op = laborInfo.LaborOperation || null;
|
||||
|
||||
const opCodeKey =
|
||||
typeof laborInfo.LaborOperation === "string" ? laborInfo.LaborOperation.trim().toUpperCase() : null;
|
||||
lineOut.op_code_desc = opCodeKey && opCodes?.[opCodeKey]?.desc ? opCodes[opCodeKey].desc : null;
|
||||
|
||||
lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
|
||||
}
|
||||
|
||||
// Refinish labor on same line using secondary fields; aggregate amount into lbr_amt
|
||||
// Refinish (secondary fields, add amount)
|
||||
const rHrs = parseFloat(refinishInfo.LaborHours || 0);
|
||||
const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
|
||||
const hasRefinish =
|
||||
@@ -147,9 +163,7 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
|
||||
lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR";
|
||||
lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;
|
||||
lineOut.lbr_op_j = refinishInfo.LaborOperation || null;
|
||||
if (!isNaN(rAmt)) {
|
||||
lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
|
||||
}
|
||||
if (!isNaN(rAmt)) lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
|
||||
if (refinishInfo.PaintStagesNum !== undefined) lineOut.paint_stg = refinishInfo.PaintStagesNum;
|
||||
if (refinishInfo.PaintTonesNum !== undefined) lineOut.paint_tone = refinishInfo.PaintTonesNum;
|
||||
}
|
||||
@@ -161,68 +175,186 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts deletion IDs from the deletions object, also removing any derived labor/refinish lines
|
||||
* by including offsets (base + 400000, base + 500000).
|
||||
* Expand deletion IDs to include derived labor/refinish offsets.
|
||||
*/
|
||||
const extractDeletions = (deletions = {}) => {
|
||||
const items = Array.isArray(deletions.DamageLineInfo) ? deletions.DamageLineInfo : [deletions.DamageLineInfo || {}];
|
||||
const baseSeqs = items.map((line) => parseInt(line.UniqueSequenceNum, 10)).filter((id) => Number.isInteger(id));
|
||||
|
||||
const allSeqs = [];
|
||||
for (const u of baseSeqs) {
|
||||
allSeqs.push(u, u + 400000, u + 500000);
|
||||
}
|
||||
// De-dup
|
||||
for (const u of baseSeqs) allSeqs.push(u, u + 400000, u + 500000);
|
||||
return Array.from(new Set(allSeqs));
|
||||
};
|
||||
|
||||
// S3 bucket + key builder (mirrors AddRq but with changeRequest prefix)
|
||||
const ESTIMATE_XML_BUCKET =
|
||||
process.env?.NODE_ENV === "development"
|
||||
? "parts-estimates"
|
||||
: InstanceMgr({
|
||||
imex: `imex-webest-xml`,
|
||||
rome: `rome-webest-xml`
|
||||
});
|
||||
|
||||
const buildEstimateXmlKey = (rq) => {
|
||||
const shopId = rq.ShopID;
|
||||
const jobId = rq.JobID;
|
||||
const ts = new Date().toISOString().replace(/:/g, "-");
|
||||
return `changeRequest/${shopId}/${jobId}/${ts}.xml`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles VehicleDamageEstimateChgRq requests.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<*>}
|
||||
* Convert a full jobline object into a jobs_set_input for update_by_pk (omit immutable fields).
|
||||
*/
|
||||
const toJoblineSetInput = (jl) => {
|
||||
const {
|
||||
// immutable identity fields:
|
||||
// jobid,
|
||||
// unq_seq,
|
||||
// everything else:
|
||||
line_no,
|
||||
status,
|
||||
line_desc,
|
||||
manual_line,
|
||||
notes,
|
||||
part_qty,
|
||||
oem_partno,
|
||||
alt_partno,
|
||||
part_type,
|
||||
act_price,
|
||||
db_price,
|
||||
tax_part,
|
||||
mod_lbr_ty,
|
||||
mod_lb_hrs,
|
||||
op_code_desc,
|
||||
lbr_amt,
|
||||
lbr_typ_j,
|
||||
lbr_hrs_j,
|
||||
lbr_op_j,
|
||||
paint_stg,
|
||||
paint_tone
|
||||
} = jl;
|
||||
|
||||
return {
|
||||
line_no,
|
||||
status,
|
||||
line_desc,
|
||||
manual_line,
|
||||
notes,
|
||||
part_qty,
|
||||
oem_partno,
|
||||
alt_partno,
|
||||
part_type,
|
||||
act_price,
|
||||
db_price,
|
||||
tax_part,
|
||||
mod_lbr_ty,
|
||||
mod_lb_hrs,
|
||||
op_code_desc,
|
||||
lbr_amt,
|
||||
lbr_typ_j,
|
||||
lbr_hrs_j,
|
||||
lbr_op_j,
|
||||
paint_stg,
|
||||
paint_tone
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles VehicleDamageEstimateChgRq requests:
|
||||
* - Update core job fields
|
||||
* - For lines: update by PK if existing; otherwise bulk insert
|
||||
* - Soft-delete only explicit deletions (exclude any updated seqs)
|
||||
*/
|
||||
const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
|
||||
const { logger } = req;
|
||||
|
||||
const rawXml = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf8") : "";
|
||||
try {
|
||||
const payload = await parseXml(req.body, logger);
|
||||
const rq = normalizeXmlObject(payload.VehicleDamageEstimateChgRq);
|
||||
if (!rq) return res.status(400).send("Missing <VehicleDamageEstimateChgRq>");
|
||||
|
||||
const jobId = rq.JobID;
|
||||
const shopId = rq.ShopID;
|
||||
const claimNum = rq.ClaimInfo?.ClaimNum;
|
||||
|
||||
if (!shopId || !claimNum) return res.status(400).send("Missing ShopID or ClaimNum");
|
||||
// Fire-and-forget archival on valid request
|
||||
(async () => {
|
||||
try {
|
||||
const key = buildEstimateXmlKey(rq);
|
||||
await uploadFileToS3({
|
||||
bucketName: ESTIMATE_XML_BUCKET,
|
||||
key,
|
||||
content: rawXml || "",
|
||||
contentType: "application/xml"
|
||||
});
|
||||
logger.log("parts-estimate-xml-uploaded", "info", jobId, null, { key, bytes: rawXml?.length || 0 });
|
||||
} catch (e) {
|
||||
logger.log("parts-estimate-xml-upload-failed", "warn", jobId, null, { error: e?.message });
|
||||
}
|
||||
})();
|
||||
|
||||
const job = await findJob(shopId, claimNum, logger);
|
||||
const job = await findJob(shopId, jobId, logger);
|
||||
if (!job) return res.status(404).send("Job not found");
|
||||
|
||||
const updatedJobData = extractUpdatedJobData(rq);
|
||||
const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id);
|
||||
const deletedLineIds = extractDeletions(rq.Deletions);
|
||||
// --- Updated seqs from incoming changes ---
|
||||
const linesIn = Array.isArray(rq.AddsChgs?.DamageLineInfo)
|
||||
? rq.AddsChgs.DamageLineInfo
|
||||
: [rq.AddsChgs?.DamageLineInfo || {}];
|
||||
|
||||
await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
|
||||
|
||||
// Build a set of unq_seq that will be updated (replaced). We delete them first to avoid duplicates.
|
||||
const updatedSeqs = Array.from(
|
||||
new Set((updatedLines || []).map((l) => l?.unq_seq).filter((v) => Number.isInteger(v)))
|
||||
new Set((linesIn || []).map((l) => parseInt(l?.UniqueSequenceNum || 0, 10)).filter((v) => Number.isInteger(v)))
|
||||
);
|
||||
|
||||
if (deletedLineIds?.length || updatedSeqs?.length) {
|
||||
const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])]));
|
||||
if (allToDelete.length) {
|
||||
await client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete });
|
||||
// --- Fetch current notes for merge ---
|
||||
let currentJobLineNotes = {};
|
||||
if (updatedSeqs.length > 0) {
|
||||
const resp = await client.request(GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ, { jobid: job.id, unqSeqs: updatedSeqs });
|
||||
if (resp?.joblines) {
|
||||
for (const jl of resp.joblines) currentJobLineNotes[jl.unq_seq] = jl.notes;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedLines.length > 0) {
|
||||
// Insert fresh versions after deletion so we don’t depend on a unique constraint
|
||||
await client.request(INSERT_JOBLINES, {
|
||||
joblines: updatedLines
|
||||
});
|
||||
const updatedJobData = extractUpdatedJobData(rq);
|
||||
const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id, currentJobLineNotes);
|
||||
|
||||
// --- Look up existing rows (by natural key) to decide update vs insert ---
|
||||
let existingIdByUnqSeq = {};
|
||||
if (updatedSeqs.length > 0) {
|
||||
const existing = await client.request(GET_JOBLINE_IDS_BY_JOBID_UNQSEQ, { jobid: job.id, unqSeqs: updatedSeqs });
|
||||
if (existing?.joblines) {
|
||||
for (const row of existing.joblines) existingIdByUnqSeq[row.unq_seq] = row.id;
|
||||
}
|
||||
}
|
||||
|
||||
const toUpdate = [];
|
||||
const toInsert = [];
|
||||
for (const jl of updatedLines) {
|
||||
const id = existingIdByUnqSeq[jl.unq_seq];
|
||||
if (id) toUpdate.push({ id, _set: toJoblineSetInput(jl) });
|
||||
else toInsert.push(jl);
|
||||
}
|
||||
|
||||
// Build deletions list and exclude any seqs we are updating (avoid accidental removal)
|
||||
const deletedLineIdsAll = extractDeletions(rq.Deletions);
|
||||
const deletionSeqs = deletedLineIdsAll.filter((u) => !updatedSeqs.includes(u));
|
||||
|
||||
// Mutations:
|
||||
const updateJobPromise = client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
|
||||
|
||||
const softDeletePromise = deletionSeqs.length
|
||||
? client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: deletionSeqs })
|
||||
: Promise.resolve({});
|
||||
|
||||
// Update each existing row by primary key (parallelized)
|
||||
const perRowUpdatesPromise =
|
||||
toUpdate.length > 0
|
||||
? Promise.all(toUpdate.map(({ id, _set }) => client.request(UPDATE_JOBLINE_BY_PK, { id, jl: _set })))
|
||||
: Promise.resolve([]);
|
||||
|
||||
// Insert brand-new rows in bulk
|
||||
const insertPromise =
|
||||
toInsert.length > 0 ? client.request(INSERT_JOBLINES, { joblines: toInsert }) : Promise.resolve({});
|
||||
|
||||
await Promise.all([updateJobPromise, softDeletePromise, perRowUpdatesPromise, insertPromise]);
|
||||
|
||||
logger.log("parts-job-changed", "info", job.id, null);
|
||||
return res.status(200).json({ success: true, jobId: job.id });
|
||||
} catch (err) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
const GET_BODYSHOP_STATUS = `
|
||||
query GetBodyshopStatus($id: uuid!) {
|
||||
bodyshops_by_pk(id: $id) {
|
||||
md_order_statuses
|
||||
md_ro_statuses
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -44,60 +44,22 @@ const GET_JOB_BY_CLAIM = `
|
||||
}
|
||||
`;
|
||||
|
||||
const UPDATE_JOB_BY_ID = `
|
||||
mutation UpdateJobById($id: uuid!, $job: jobs_set_input!) {
|
||||
update_jobs_by_pk(pk_columns: { id: $id }, _set: $job) {
|
||||
const GET_JOB_BY_ID = `
|
||||
query GetJobByID($shopid: uuid!, $jobid: uuid!) {
|
||||
jobs(
|
||||
where: { shopid: { _eq: $shopid }, id: { _eq: $jobid } }
|
||||
order_by: { created_at: desc }
|
||||
limit: 1
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const UPSERT_JOBLINES = `
|
||||
mutation UpsertJoblines($joblines: [joblines_insert_input!]!) {
|
||||
insert_joblines(
|
||||
objects: $joblines
|
||||
on_conflict: {
|
||||
constraint: joblines_pkey
|
||||
update_columns: [
|
||||
jobid
|
||||
status
|
||||
line_desc
|
||||
part_type
|
||||
part_qty
|
||||
oem_partno
|
||||
db_price
|
||||
act_price
|
||||
mod_lbr_ty
|
||||
mod_lb_hrs
|
||||
lbr_op
|
||||
lbr_amt
|
||||
notes
|
||||
manual_line
|
||||
]
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DELETE_JOBLINES_BY_JOBID = `
|
||||
mutation DeleteJoblinesByJobId($jobid: uuid!) {
|
||||
delete_joblines(where: { jobid: { _eq: $jobid } }) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const DELETE_JOBLINES_BY_IDS = `
|
||||
mutation DeleteJoblinesByIds($jobid: uuid!, $unqSeqs: [Int!]!) {
|
||||
delete_joblines(
|
||||
where: {
|
||||
jobid: { _eq: $jobid },
|
||||
unq_seq: { _in: $unqSeqs }
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
const UPDATE_JOB_BY_ID = `
|
||||
mutation UpdateJobById($id: uuid!, $job: jobs_set_input!) {
|
||||
update_jobs_by_pk(pk_columns: { id: $id }, _set: $job) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -245,6 +207,97 @@ const DELETE_AUDIT_TRAIL_BY_SHOP = `
|
||||
}
|
||||
`;
|
||||
|
||||
const GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ = `
|
||||
query GetJoblinesNotesByJobIdUnqSeq($jobid: uuid!, $unqSeqs: [Int!]!) {
|
||||
joblines(where: { jobid: { _eq: $jobid }, unq_seq: { _in: $unqSeqs }, removed: { _neq: true } }) {
|
||||
unq_seq
|
||||
notes
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Clear task links to parts orders for all jobs in a shop to avoid FK violations when deleting parts orders
|
||||
const CLEAR_TASKS_PARTSORDER_LINKS_BY_JOBIDS = `
|
||||
mutation ClearTasksPartsOrderLinks($jobIds: [uuid!]!) {
|
||||
update_tasks(
|
||||
where: { parts_order: { jobid: { _in: $jobIds } } },
|
||||
_set: { partsorderid: null }
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Delete parts order lines where the parent order belongs to any of the provided job IDs
|
||||
const DELETE_PARTS_ORDER_LINES_BY_JOB_IDS = `
|
||||
mutation DeletePartsOrderLinesByJobIds($jobIds: [uuid!]!) {
|
||||
delete_parts_order_lines(where: { parts_order: { jobid: { _in: $jobIds } } }) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Delete parts orders for the given job IDs
|
||||
const DELETE_PARTS_ORDERS_BY_JOB_IDS = `
|
||||
mutation DeletePartsOrdersByJobIds($jobIds: [uuid!]!) {
|
||||
delete_parts_orders(where: { jobid: { _in: $jobIds } }) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const UPSERT_JOBLINES = `
|
||||
mutation UpsertJoblines($joblines: [joblines_insert_input!]!) {
|
||||
insert_joblines(
|
||||
objects: $joblines,
|
||||
on_conflict: {
|
||||
constraint: joblines_jobid_unq_seq_key,
|
||||
update_columns: [
|
||||
status,
|
||||
line_desc,
|
||||
notes,
|
||||
manual_line,
|
||||
part_qty,
|
||||
oem_partno,
|
||||
alt_partno,
|
||||
part_type,
|
||||
act_price,
|
||||
db_price,
|
||||
tax_part,
|
||||
mod_lbr_ty,
|
||||
mod_lb_hrs,
|
||||
op_code_desc,
|
||||
lbr_amt,
|
||||
lbr_typ_j,
|
||||
lbr_hrs_j,
|
||||
lbr_op_j,
|
||||
paint_stg,
|
||||
paint_tone
|
||||
]
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Get jobline IDs for the incoming unq_seq values (only non-removed)
|
||||
const GET_JOBLINE_IDS_BY_JOBID_UNQSEQ = `
|
||||
query GetJoblineIdsByJobIdUnqSeq($jobid: uuid!, $unqSeqs: [Int!]!) {
|
||||
joblines(where: { jobid: { _eq: $jobid }, unq_seq: { _in: $unqSeqs }, removed: { _neq: true } }) {
|
||||
id
|
||||
unq_seq
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Update a single jobline by primary key
|
||||
const UPDATE_JOBLINE_BY_PK = `
|
||||
mutation UpdateJoblineByPk($id: uuid!, $jl: joblines_set_input!) {
|
||||
update_joblines_by_pk(pk_columns: { id: $id }, _set: $jl) { id }
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
GET_BODYSHOP_STATUS,
|
||||
GET_VEHICLE_BY_SHOP_VIN,
|
||||
@@ -252,9 +305,6 @@ module.exports = {
|
||||
INSERT_JOB_WITH_LINES,
|
||||
GET_JOB_BY_CLAIM,
|
||||
UPDATE_JOB_BY_ID,
|
||||
UPSERT_JOBLINES,
|
||||
DELETE_JOBLINES_BY_JOBID,
|
||||
DELETE_JOBLINES_BY_IDS,
|
||||
SOFT_DELETE_JOBLINES_BY_IDS,
|
||||
INSERT_JOBLINES,
|
||||
CHECK_EXTERNAL_SHOP_ID,
|
||||
@@ -271,5 +321,13 @@ module.exports = {
|
||||
GET_JOBS_BY_SHOP,
|
||||
DELETE_JOBLINES_BY_JOB_IDS,
|
||||
DELETE_JOBS_BY_IDS,
|
||||
DELETE_AUDIT_TRAIL_BY_SHOP
|
||||
DELETE_AUDIT_TRAIL_BY_SHOP,
|
||||
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ,
|
||||
GET_JOB_BY_ID,
|
||||
CLEAR_TASKS_PARTSORDER_LINKS_BY_JOBIDS,
|
||||
DELETE_PARTS_ORDER_LINES_BY_JOB_IDS,
|
||||
DELETE_PARTS_ORDERS_BY_JOB_IDS,
|
||||
UPSERT_JOBLINES,
|
||||
GET_JOBLINE_IDS_BY_JOBID_UNQSEQ,
|
||||
UPDATE_JOBLINE_BY_PK
|
||||
};
|
||||
|
||||
@@ -252,35 +252,27 @@ const generatePaymentUrl = async (req, res) => {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const checkFee = async (req, res) => {
|
||||
const logResponseMeta = {
|
||||
bodyshop: {
|
||||
id: req.body?.bodyshop?.id,
|
||||
imexshopid: req.body?.bodyshop?.imexshopid,
|
||||
name: req.body?.bodyshop?.shopname,
|
||||
state: req.body?.bodyshop?.state
|
||||
},
|
||||
amount: req.body?.amount
|
||||
};
|
||||
const { bodyshop = {}, amount } = req.body || {};
|
||||
const { id, imexshopid, shopname, state } = bodyshop;
|
||||
const logResponseMeta = { bodyshop: { id, imexshopid, name: shopname, state }, amount };
|
||||
|
||||
logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||
|
||||
if (!isNumber(req.body?.amount) || req.body?.amount <= 0) {
|
||||
if (!isNumber(amount) || amount <= 0) {
|
||||
logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, {
|
||||
message: "Amount is zero or undefined, skipping fee check.",
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.json({ fee: 0 });
|
||||
}
|
||||
|
||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||
const shopCredentials = await getShopCredentials(bodyshop);
|
||||
|
||||
if (shopCredentials?.error) {
|
||||
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
|
||||
message: shopCredentials.error?.message,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
||||
}
|
||||
|
||||
@@ -292,13 +284,10 @@ const checkFee = async (req, res) => {
|
||||
{
|
||||
method: "fee",
|
||||
...shopCredentials,
|
||||
amount: req.body.amount,
|
||||
paymenttype: `CC`,
|
||||
amount: String(amount), // Type cast to string as required by API
|
||||
paymenttype: "CC",
|
||||
cardnum: "4111111111111111", // Required for compatibility with API
|
||||
state:
|
||||
req.body.bodyshop?.state && req.body.bodyshop.state.length === 2
|
||||
? req.body.bodyshop.state.toUpperCase()
|
||||
: "ZZ"
|
||||
state: state?.toUpperCase() || "ZZ"
|
||||
},
|
||||
{ sort: false } // Ensure query string order is preserved
|
||||
),
|
||||
@@ -310,46 +299,24 @@ const checkFee = async (req, res) => {
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
const response = await axios(options);
|
||||
const { data } = await axios(options);
|
||||
|
||||
if (response.data?.error) {
|
||||
logger.log("intellipay-checkfee-api-error", "ERROR", req.user?.email, null, {
|
||||
message: response.data?.error,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.status(400).json({
|
||||
error: response.data?.error,
|
||||
type: "intellipay-checkfee-api-error",
|
||||
...logResponseMeta
|
||||
});
|
||||
if (data?.error || data < 0) {
|
||||
const errorType = data?.error ? "intellipay-checkfee-api-error" : "intellipay-checkfee-negative-fee";
|
||||
const errorMessage = data?.error
|
||||
? data?.error
|
||||
: "Fee amount negative. Check API credentials & account configuration.";
|
||||
logger.log(errorType, "ERROR", req.user?.email, null, { message: errorMessage, data, ...logResponseMeta });
|
||||
return res.status(400).json({ error: errorMessage, type: errorType, data, ...logResponseMeta });
|
||||
}
|
||||
|
||||
if (response.data < 0) {
|
||||
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
|
||||
message: "Fee amount returned is negative.",
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.json({
|
||||
error: "Fee amount negative. Check API credentials & account configuration.",
|
||||
...logResponseMeta,
|
||||
type: "intellipay-checkfee-negative-fee"
|
||||
});
|
||||
}
|
||||
|
||||
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, {
|
||||
fee: response.data,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.json({ fee: response.data, ...logResponseMeta });
|
||||
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, { fee: data, ...logResponseMeta });
|
||||
return res.json({ fee: data, ...logResponseMeta });
|
||||
} catch (error) {
|
||||
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
|
||||
message: error?.message,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.status(500).json({ error: error?.message, logResponseMeta });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,37 +73,23 @@ const processCanvasRequest = async (req, res) => {
|
||||
// Default width and height
|
||||
const width = isNumber(w) && w > 0 ? w : 500;
|
||||
const height = isNumber(h) && h > 0 ? h : 275;
|
||||
|
||||
const configuration = getChartConfiguration(keys, values, override);
|
||||
|
||||
let canvas = null;
|
||||
let ctx = null;
|
||||
let chart = null;
|
||||
let chartImage = null;
|
||||
|
||||
try {
|
||||
// Create the canvas
|
||||
canvas = new Canvas(width, height);
|
||||
ctx = canvas.getContext("2d");
|
||||
const canvas = new Canvas(width, height);
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
// Render the chart
|
||||
chart = new Chart(ctx, configuration);
|
||||
|
||||
// Generate and send the image
|
||||
chartImage = (await canvas.toBuffer("image/png")).toString("base64");
|
||||
const chartImage = (await canvas.toBuffer("image/png")).toString("base64");
|
||||
res.status(200).send(`data:image/png;base64,${chartImage}`);
|
||||
} catch (error) {
|
||||
// Log the error and send the response
|
||||
logger.log("canvas-error", "error", "jsr", null, { error: error.message });
|
||||
res.status(500).send("Failed to generate canvas.");
|
||||
res.status(500).send("Error generating canvas");
|
||||
} finally {
|
||||
// Cleanup resources
|
||||
if (chart) {
|
||||
chart.destroy();
|
||||
}
|
||||
ctx = null;
|
||||
canvas = null;
|
||||
chartImage = null;
|
||||
chart?.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -118,15 +104,18 @@ const enqueueRequest = (req, res) => {
|
||||
};
|
||||
|
||||
const processNextInQueue = async () => {
|
||||
while (requestQueue.length > 0) {
|
||||
const { req, res } = requestQueue.shift();
|
||||
try {
|
||||
await processCanvasRequest(req, res);
|
||||
} catch (err) {
|
||||
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
|
||||
try {
|
||||
while (requestQueue.length > 0) {
|
||||
const { req, res } = requestQueue.shift();
|
||||
try {
|
||||
await processCanvasRequest(req, res);
|
||||
} catch (err) {
|
||||
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
}
|
||||
isProcessing = false;
|
||||
};
|
||||
|
||||
exports.canvastest = function (req, res) {
|
||||
@@ -134,7 +123,10 @@ exports.canvastest = function (req, res) {
|
||||
};
|
||||
|
||||
exports.canvas = async (req, res) => {
|
||||
if (isProcessing || !enqueueRequest(req, res)) return;
|
||||
isProcessing = true;
|
||||
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
|
||||
if (!enqueueRequest(req, res)) return;
|
||||
|
||||
if (!isProcessing) {
|
||||
isProcessing = true;
|
||||
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const logger = require("../../server/utils/logger");
|
||||
|
||||
// Pull secrets from env
|
||||
const { VSSTA_INTEGRATION_SECRET, PARTS_MANAGEMENT_INTEGRATION_SECRET } = process.env;
|
||||
@@ -11,7 +12,7 @@ if (typeof VSSTA_INTEGRATION_SECRET === "string" && VSSTA_INTEGRATION_SECRET.len
|
||||
|
||||
router.post("/vssta", vsstaMiddleware, vsstaIntegration);
|
||||
} else {
|
||||
console.warn("VSSTA_INTEGRATION_SECRET is not set — skipping /vssta integration route");
|
||||
logger.logger.warn("VSSTA_INTEGRATION_SECRET is not set — skipping /vssta integration route");
|
||||
}
|
||||
|
||||
// Only load Parts Management routes if that secret is set
|
||||
@@ -38,21 +39,24 @@ if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_
|
||||
* Route to handle Vehicle Damage Estimate Change Request
|
||||
*/
|
||||
router.post(
|
||||
"/parts-management/VehicleDamageEstimateChqRq",
|
||||
"/parts-management/VehicleDamageEstimateChgRq",
|
||||
express.raw({ type: "application/xml", limit: XML_BODY_LIMIT }), // Parse XML body
|
||||
partsManagementIntegrationMiddleware,
|
||||
partsManagementVehicleDamageEstimateChqRq
|
||||
);
|
||||
|
||||
// Deprovisioning route
|
||||
router.post("/parts-management/deprovision", partsManagementIntegrationMiddleware, partsManagementDeprovisioning);
|
||||
if (process.env.NODE_ENV !== "production" && !process.env.HOSTNAME?.endsWith("compute.internal")) {
|
||||
logger.logger.warn("Parts Management Deprovisioning route has been loaded.");
|
||||
router.post("/parts-management/deprovision", partsManagementIntegrationMiddleware, partsManagementDeprovisioning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Route to handle Parts Management Provisioning
|
||||
*/
|
||||
router.post("/parts-management/provision", partsManagementIntegrationMiddleware, partsManagementProvisioning);
|
||||
} else {
|
||||
console.warn("PARTS_MANAGEMENT_INTEGRATION_SECRET is not set — skipping /parts-management/provision route");
|
||||
logger.logger.warn("PARTS_MANAGEMENT_INTEGRATION_SECRET is not set — skipping /parts-management/provision route");
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -144,6 +144,9 @@ router.post("/emsupload", validateFirebaseIdTokenMiddleware, data.emsUpload);
|
||||
// Redis Cache Routes
|
||||
router.post("/bodyshop-cache", eventAuthorizationMiddleware, updateBodyshopCache);
|
||||
|
||||
// Estimate Scrubber Vehicle Type
|
||||
router.post("/es/vehicletype", data.vehicletype);
|
||||
|
||||
// Health Check for docker-compose-cluster load balancer, only available in development
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
router.get("/health", (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user