Merged in release/2025-08-29 (pull request #2518)

Release/2025 08 29
This commit is contained in:
Dave Richer
2025-08-27 19:27:22 +00:00
47 changed files with 6046 additions and 2150 deletions

View File

@@ -14,3 +14,5 @@ VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX VITE_APP_INSTANCE=IMEX
TEST_USERNAME="test@imex.dev" TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123" TEST_PASSWORD="test123"
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -16,3 +16,5 @@ VITE_APP_COUNTRY=USA
VITE_APP_INSTANCE=ROME VITE_APP_INSTANCE=ROME
TEST_USERNAME="test@imex.dev" TEST_USERNAME="test@imex.dev"
TEST_PASSWORD="test123" TEST_PASSWORD="test123"
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -13,3 +13,5 @@ VITE_APP_AXIOS_BASE_API_URL=https://api.imex.online/
VITE_APP_REPORTS_SERVER_URL=https://reports.imex.online VITE_APP_REPORTS_SERVER_URL=https://reports.imex.online
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
VITE_APP_INSTANCE=IMEX VITE_APP_INSTANCE=IMEX
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -13,3 +13,5 @@ VITE_APP_AXIOS_BASE_API_URL=https://api.romeonline.io/
VITE_APP_REPORTS_SERVER_URL=https://reports.romeonline.io VITE_APP_REPORTS_SERVER_URL=https://reports.romeonline.io
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
VITE_APP_INSTANCE=ROME VITE_APP_INSTANCE=ROME
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -13,3 +13,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.imex.online
VITE_APP_IS_TEST=true VITE_APP_IS_TEST=true
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX VITE_APP_INSTANCE=IMEX
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

View File

@@ -13,3 +13,5 @@ VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
VITE_APP_IS_TEST=true VITE_APP_IS_TEST=true
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=ROME VITE_APP_INSTANCE=ROME
VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien
VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com

2968
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@
"private": true, "private": true,
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@amplitude/analytics-browser": "^2.23.1",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^3.13.9", "@apollo/client": "^3.13.9",
"@emotion/is-prop-valid": "^1.3.1", "@emotion/is-prop-valid": "^1.3.1",
@@ -38,16 +39,17 @@
"env-cmd": "^10.1.0", "env-cmd": "^10.1.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"i18next": "^25.3.6", "i18next": "^25.4.0",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.12.12", "libphonenumber-js": "^1.12.13",
"logrocket": "^9.0.2", "logrocket": "^9.0.2",
"markerjs2": "^2.32.6", "markerjs2": "^2.32.6",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"normalize-url": "^8.0.2", "normalize-url": "^8.0.2",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"phone": "^3.1.67", "phone": "^3.1.67",
"posthog-js": "^1.260.2",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^9.2.2", "query-string": "^9.2.2",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",
@@ -59,7 +61,7 @@
"react-drag-listview": "^2.0.0", "react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1", "react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4", "react-grid-layout": "1.3.4",
"react-i18next": "^15.6.1", "react-i18next": "^15.7.1",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -137,10 +139,10 @@
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@eslint/js": "^9.33.0", "@eslint/js": "^9.33.0",
"@playwright/test": "^1.54.2", "@playwright/test": "^1.55.0",
"@sentry/webpack-plugin": "^4.1.1", "@sentry/webpack-plugin": "^4.1.1",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.7.0", "@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^4.6.0", "@vitejs/plugin-react": "^4.6.0",
"browserslist": "^4.25.3", "browserslist": "^4.25.3",
@@ -150,9 +152,10 @@
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^15.15.0", "globals": "^15.15.0",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"lightningcss": "^1.30.1",
"memfs": "^4.36.3", "memfs": "^4.36.3",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"playwright": "^1.54.2", "playwright": "^1.55.0",
"react-error-overlay": "^6.1.0", "react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3", "source-map-explorer": "^2.5.3",
@@ -160,7 +163,7 @@
"vite-plugin-babel": "^1.3.2", "vite-plugin-babel": "^1.3.2",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.24.0", "vite-plugin-node-polyfills": "^0.24.0",
"vite-plugin-pwa": "^1.0.2", "vite-plugin-pwa": "^1.0.3",
"vite-plugin-style-import": "^2.0.0", "vite-plugin-style-import": "^2.0.0",
"vitest": "^3.2.4", "vitest": "^3.2.4",
"workbox-window": "^7.3.0" "workbox-window": "^7.3.0"

View File

@@ -1,20 +1,20 @@
import { ApolloProvider } from "@apollo/client"; import { ApolloProvider } from "@apollo/client";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react"; import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd"; import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US"; import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect, useSelector } from "react-redux"; import { connect, useSelector } from "react-redux";
import { createStructuredSelector } from "reselect";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component"; 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 client from "../utils/GraphQLClient";
import App from "./App"; import App from "./App";
import * as Sentry from "@sentry/react";
import getTheme from "./themeProvider"; 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 // Base Split configuration
const config = { const config = {
@@ -85,7 +85,7 @@ function AppContainer({ currentUser, setDarkMode }) {
theme={theme} theme={theme}
form={{ form={{
validateMessages: { validateMessages: {
required: t("general.validation.required", { label: "{{label}}" }) required: t("general.validation.required", { label: "${label}" })
} }
}} }}
> >

View File

@@ -22,9 +22,9 @@ export function BreadCrumbs({ breadcrumbs, bodyshop, isPartsEntry }) {
} = useSplitTreatments({ } = useSplitTreatments({
attributes: {}, attributes: {},
names: ["OpenSearch"], names: ["OpenSearch"],
splitKey: bodyshop && bodyshop.imexshopid splitKey: bodyshop?.imexshopid
}); });
// TODO - Client Update - Technically key is not doing anything here
return ( return (
<Row className="breadcrumb-container"> <Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}> <Col xs={24} sm={24} md={16}>
@@ -35,7 +35,7 @@ export function BreadCrumbs({ breadcrumbs, bodyshop, isPartsEntry }) {
key: "home", key: "home",
title: ( title: (
<Link to={isPartsEntry ? `/parts/` : `/manage/`}> <Link to={isPartsEntry ? `/parts/` : `/manage/`}>
<HomeFilled /> {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || ""} <HomeFilled /> {(bodyshop?.shopname && `(${bodyshop.shopname})`) || ""}
</Link> </Link>
) )
}, },

View File

@@ -4,13 +4,8 @@ import parsePhoneNumber from "libphonenumber-js";
import { forwardRef } from "react"; import { forwardRef } from "react";
import "./phone-form-item.styles.scss"; import "./phone-form-item.styles.scss";
function FormItemPhone(props) { function FormItemPhone(props, ref) {
return ( return <Input ref={ref} {...props} />;
<Input
// country="ca" ref={ref} className="ant-input"
{...props}
/>
);
} }
export default forwardRef(FormItemPhone); export default forwardRef(FormItemPhone);

View File

@@ -1,6 +1,5 @@
import { AlertOutlined, BulbOutlined } from "@ant-design/icons"; import { AlertOutlined, BulbOutlined } from "@ant-design/icons";
import { Button, Layout, Space } from "antd"; import { Button, Layout, Space } from "antd";
import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@@ -17,18 +16,7 @@ const mapStateToProps = createStructuredSelector({
export function GlobalFooter({ isPartsEntry }) { export function GlobalFooter({ isPartsEntry }) {
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
// Canny Not Required on Parts Entry
if (isPartsEntry) return;
window.Canny("initChangelog", {
appID: "680bd2c7ee501290377f6686",
position: "top",
align: "left",
theme: "light" // options: light [default], dark, auto
});
}, [isPartsEntry]);
if (isPartsEntry) { if (isPartsEntry) {
return ( return (
<Footer> <Footer>

View File

@@ -8,19 +8,20 @@ import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries"; import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; 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 { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
jobRO: selectJobReadOnly jobRO: selectJobReadOnly,
isPartsEntry: selectIsPartsEntry
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })) 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 { t } = useTranslation();
const [availableStatuses, setAvailableStatuses] = useState([]); 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(() => { useEffect(() => {
//Figure out what scenario were in, populate accodingly if (!job || !bodyshop) return;
if (job && bodyshop) {
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) { const { md_ro_statuses } = bodyshop;
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses); const {
} else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) { parts_statuses,
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses); pre_production_statuses,
} else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) { production_statuses,
setAvailableStatuses( post_production_statuses,
bodyshop.md_ro_statuses.post_production_statuses.filter( statuses,
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported default_invoiced,
) default_exported
); } = md_ro_statuses;
} else {
console.log("Status didn't match any restrictions. Allowing all status changes."); if (isPartsEntry) {
setAvailableStatuses(bodyshop.md_ro_statuses.statuses); // 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 = { const statusMenu = {
items: [ items: [

View File

@@ -37,7 +37,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
.filter( .filter(
(temp) => (temp) =>
(!temp.regions || (!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.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)) &&
(!temp.dms || temp.dms === false) (!temp.dms || temp.dms === false)
) )
@@ -46,7 +46,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
.filter( .filter(
(temp) => (temp) =>
!temp.regions || !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.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
); );
@@ -82,7 +82,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
variables: { id: jobId } variables: { id: jobId }
}, },
{ {
to: job && job.ownr_ea, to: job?.ownr_ea,
subject: cards.find((c) => c.key === key)?.subject subject: cards.find((c) => c.key === key)?.subject
}, },
"e", "e",
@@ -129,7 +129,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
const columns = `repeat(${actions.length}, 1fr)`; const columns = `repeat(${actions.length}, 1fr)`;
return ( 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 }}> <Card hoverable style={{ minHeight: 100 }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}> <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ flex: "1 1 70%", minWidth: 0 }}> <div style={{ flex: "1 1 70%", minWidth: 0 }}>

View File

@@ -64,7 +64,7 @@ export default function ShopInfoContainer() {
onFinish={handleFinish} onFinish={handleFinish}
initialValues={ initialValues={
data data
? data.bodyshops[0].accountingconfig.ClosingPeriod ? data?.bodyshops?.[0]?.accountingconfig?.ClosingPeriod
? { ? {
...data.bodyshops[0], ...data.bodyshops[0],
accountingconfig: { accountingconfig: {

View File

@@ -96,13 +96,15 @@ export function SimplifiedPartsJobsListComponent({
key: "status", key: "status",
ellipsis: true, 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, sortOrder: sortcolumn === "status" && sortorder,
render: (text, record) => { render: (text, record) => {
return record.status || t("general.labels.na"); return record.status || t("general.labels.na");
}, },
filteredValue: filter?.status || null, 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] }; return { text: s, value: [s] };
}), }),
onFilter: (value, record) => value.includes(record.status) onFilter: (value, record) => value.includes(record.status)

View File

@@ -167,7 +167,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
socketRef.current = socketInstance; socketRef.current = socketInstance;
const handleBodyshopMessage = (message) => { const handleBodyshopMessage = (message) => {
if (!message || !message.type) return; if (!message?.type) return;
switch (message.type) { switch (message.type) {
case "alert-update": case "alert-update":
store.dispatch(addAlerts(message.payload)); store.dispatch(addAlerts(message.payload));
@@ -512,21 +512,20 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
}; };
const unsubscribe = auth.onIdTokenChanged(async (user) => { const unsubscribe = auth.onIdTokenChanged(async (user) => {
if (user) { if (!user) {
const token = await user.getIdToken(); socketRef.current?.disconnect();
if (socketRef.current) { socketRef.current = null;
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id }); setIsConnected(false);
} else { return;
initializeSocket(token).catch((err) => }
console.error(`Something went wrong Initializing Sockets: ${err?.message || ""}`)
); const token = await user.getIdToken();
} if (socketRef.current) {
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id });
} else { } else {
if (socketRef.current) { initializeSocket(token).catch((err) =>
socketRef.current.disconnect(); console.error("Something went wrong Initializing Sockets:", err?.message || "")
socketRef.current = null; );
setIsConnected(false);
}
} }
}); });

View File

@@ -4,6 +4,8 @@ import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
import { getFirestore } from "@firebase/firestore"; import { getFirestore } from "@firebase/firestore";
import { getMessaging, getToken, onMessage } from "@firebase/messaging"; import { getMessaging, getToken, onMessage } from "@firebase/messaging";
import { store } from "../redux/store"; 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); const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
initializeApp(config); initializeApp(config);
@@ -71,25 +73,33 @@ onMessage(messaging, (payload) => {
}); });
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => { export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
const state = stateProp || store.getState(); try {
const eventParams = {
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null, const state = stateProp || store.getState();
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null, const eventParams = {
...additionalParams shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
}; user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
// axios.post("/ioevent", { ...additionalParams
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null, };
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null, // axios.post("/ioevent", {
// operationName: eventName, // useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
// variables: additionalParams, // bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
// dbevent: false, // operationName: eventName,
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}` // variables: additionalParams,
// }); // dbevent: false,
// console.log( // env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
// "%c[Analytics]", // });
// "background-color: green ;font-weight:bold;", // console.log(
// eventName, // "%c[Analytics]",
// eventParams // "background-color: green ;font-weight:bold;",
// ); // eventName,
logEvent(analytics, eventName, eventParams); // eventParams
// );
logEvent(analytics, eventName, eventParams);
amplitude.track(eventName, eventParams);
posthog.capture(eventName, eventParams);
} finally {
//If it fails, just keep going.
}
}; };

View File

@@ -14,6 +14,8 @@ import { persistor, store } from "./redux/store";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
import "./translations/i18n"; import "./translations/i18n";
import "./utils/CleanAxios"; import "./utils/CleanAxios";
import * as amplitude from "@amplitude/analytics-browser";
import { PostHogProvider } from "posthog-js/react";
window.global ||= window; window.global ||= window;
@@ -23,10 +25,10 @@ registerSW({ immediate: true });
// Dinero.globalLocale = "en-CA"; // Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN"; Dinero.globalRoundingMode = "HALF_EVEN";
amplitude.init("6228a598e57cd66875cfd41604f1f891", {});
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter); const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />)); const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
let styles = 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) "; "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 +39,12 @@ function App() {
return ( return (
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}> <PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
<Provider store={store}> <Provider store={store}>
<RouterProvider router={router} /> <PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={{ autocapture: false, capture_exceptions: true }}
>
<RouterProvider router={router} />
</PostHogProvider>
</Provider> </Provider>
</PersistGate> </PersistGate>
); );

View File

@@ -2,7 +2,7 @@ import { FloatButton, Layout, Spin } from "antd";
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
// import preval from "preval.macro"; // import preval from "preval.macro";
import { lazy, Suspense, useEffect, useState } from "react"; import { lazy, Suspense, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -23,6 +23,7 @@ import UpdateAlert from "../../components/update-alert/update-alert.component";
import { selectBodyshop, selectInstanceConflict, selectPartsManagementOnly } from "../../redux/user/user.selectors"; import { selectBodyshop, selectInstanceConflict, selectPartsManagementOnly } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx"; import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
import { selectDarkMode } from "../../redux/application/application.selectors.js";
const PrintCenterModalContainer = lazy( const PrintCenterModalContainer = lazy(
() => import("../../components/print-center-modal/print-center-modal.container") () => import("../../components/print-center-modal/print-center-modal.container")
@@ -107,22 +108,26 @@ const { Content } = Layout;
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
conflict: selectInstanceConflict, conflict: selectInstanceConflict,
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
partsManagementOnly: selectPartsManagementOnly partsManagementOnly: selectPartsManagementOnly,
isDarkMode: selectDarkMode
}); });
export function Manage({ conflict, bodyshop, partsManagementOnly }) { export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [chatVisible] = useState(false); const [chatVisible] = useState(false);
const didMount = useRef(false);
// Centralized alerts handling (fetch + dedupe + notifications) // Centralized alerts handling (fetch + dedupe + notifications)
useAlertsNotifications(); useAlertsNotifications();
useEffect(() => { useEffect(() => {
if (didMount.current) return; // prevents dev StrictMode double-run
didMount.current = true;
window.Canny("initChangelog", { window.Canny("initChangelog", {
appID: "680bd2c7ee501290377f6686", appID: "680bd2c7ee501290377f6686",
position: "top", position: "top",
align: "left", align: "left",
theme: "light" // options: light [default], dark, auto theme: !isDarkMode ? "light" : "dark"
}); });
}, []); }, []);

View File

@@ -60,19 +60,19 @@ function SimplifiedPartsJobsDetailContainer({ setBreadcrumbs, addRecentItem, set
imex: "$t(titles.imexonline)", imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)" rome: "$t(titles.romeonline)"
}), }),
ro_number: (data.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na") ro_number: data.jobs_by_pk?.ro_number || t("general.labels.na")
}); });
setBreadcrumbs([ setBreadcrumbs([
{ link: "/parts/", label: t("titles.bc.jobs") }, { link: "/parts", label: t("titles.bc.parts") },
{ {
link: `/parts/jobs/${jobId}`, link: `/parts/jobs/${jobId}`,
label: t("titles.bc.jobs-detail", { label: t("titles.bc.jobs-detail", {
number: (data && data.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na") number: (data?.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na")
}) })
} }
]); ]);
if (data && data.jobs_by_pk) { if (data?.jobs_by_pk) {
setJobReadOnly(IsJobReadOnly(data.jobs_by_pk)); setJobReadOnly(IsJobReadOnly(data.jobs_by_pk));
addRecentItem( addRecentItem(

View File

@@ -22,7 +22,7 @@ export function SimplifiedPartsJobsPage({ setBreadcrumbs, setSelectedHeader }) {
}) })
}); });
setSelectedHeader("parts-queue"); setSelectedHeader("parts-queue");
setBreadcrumbs([{ link: "/parts", label: t("titles.bc.simplified-parts-jobs") }]); setBreadcrumbs([{ link: "/parts", label: t("titles.bc.parts") }]);
}, [setBreadcrumbs, t, setSelectedHeader]); }, [setBreadcrumbs, t, setSelectedHeader]);
return ( return (

View File

@@ -3,7 +3,7 @@ import { FloatButton, Layout, Spin } from "antd";
import { lazy, Suspense, useEffect } from "react"; import { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Route, Routes } from "react-router-dom"; import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import BreadCrumbs from "../../components/breadcrumbs/breadcrumbs.component.jsx"; import BreadCrumbs from "../../components/breadcrumbs/breadcrumbs.component.jsx";
import ConflictComponent from "../../components/conflict/conflict.component.jsx"; import ConflictComponent from "../../components/conflict/conflict.component.jsx";
@@ -39,6 +39,15 @@ const mapStateToProps = createStructuredSelector({
export function SimplifiedPartsPage({ conflict, bodyshop }) { export function SimplifiedPartsPage({ conflict, bodyshop }) {
const { t } = useTranslation(); const { t } = useTranslation();
// Redirector to strip '/parts/jobs' from path for non-detail routes
function JobsStripRedirect() {
// lazy import to avoid top-level import churn
const location = useLocation();
const { pathname, search, hash } = location;
const target = pathname.replace("/parts/jobs", "/parts") + (search || "") + (hash || "");
return <Navigate to={target} replace />;
}
// Centralized alerts handling (fetch + dedupe + notifications) // Centralized alerts handling (fetch + dedupe + notifications)
useAlertsNotifications(); useAlertsNotifications();
@@ -67,6 +76,10 @@ export function SimplifiedPartsPage({ conflict, bodyshop }) {
<EmailOverlayContainer /> <EmailOverlayContainer />
<PrintCenterModalContainer /> <PrintCenterModalContainer />
<Routes> <Routes>
{/* Redirect legacy or relative routes that include '/jobs' segment */}
<Route path="jobs" element={<JobsStripRedirect />} />
<Route path="jobs/*" element={<JobsStripRedirect />} />
<Route <Route
path="/" path="/"
element={ element={

View File

@@ -42,33 +42,31 @@ export function VehicleDetailContainer({ setBreadcrumbs, addRecentItem, setSelec
imex: "$t(titles.imexonline)", imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)" rome: "$t(titles.romeonline)"
}), }),
vehicle: vehicle: data?.vehicles_by_pk
data && data.vehicles_by_pk ? `${data.vehicles_by_pk?.v_model_yr || ""} ${
? `${(data.vehicles_by_pk && data.vehicles_by_pk.v_model_yr) || ""} ${ data.vehicles_by_pk?.v_make_desc || ""
(data.vehicles_by_pk && data.vehicles_by_pk.v_make_desc) || "" } ${data.vehicles_by_pk?.v_model_desc || ""}`
} ${(data.vehicles_by_pk && data.vehicles_by_pk.v_model_desc) || ""}` : ""
: ""
}); });
setSelectedHeader("vehicles"); setSelectedHeader("vehicles");
const crumbs = []; const crumbs = [];
if (isPartsEntry) crumbs.push({ link: "/parts/", label: t("titles.bc.jobs") }); if (isPartsEntry) crumbs.push({ link: "/parts", label: t("titles.bc.parts") });
crumbs.push({ link: `${basePath}/vehicles`, label: t("titles.bc.vehicles") }); crumbs.push({ link: `${basePath}/vehicles`, label: t("titles.bc.vehicles") });
crumbs.push({ crumbs.push({
link: `${basePath}/vehicles/${vehId}`, link: `${basePath}/vehicles/${vehId}`,
label: t("titles.bc.vehicle-details", { label: t("titles.bc.vehicle-details", {
vehicle: vehicle: data?.vehicles_by_pk
data && data.vehicles_by_pk ? `${data.vehicles_by_pk?.v_model_yr || ""} ${
? `${(data.vehicles_by_pk && data.vehicles_by_pk.v_model_yr) || ""} ${ data.vehicles_by_pk?.v_make_desc || ""
(data.vehicles_by_pk && data.vehicles_by_pk.v_make_desc) || "" } ${data.vehicles_by_pk?.v_model_desc || ""}`
} ${(data.vehicles_by_pk && data.vehicles_by_pk.v_model_desc) || ""}` : ""
: ""
}) })
}); });
setBreadcrumbs(crumbs); setBreadcrumbs(crumbs);
if (data && data.vehicles_by_pk) if (data?.vehicles_by_pk)
addRecentItem( addRecentItem(
CreateRecentItem( CreateRecentItem(
vehId, vehId,

View File

@@ -33,7 +33,7 @@ export function VehiclesPageContainer({ setBreadcrumbs, setSelectedHeader, isPar
if (isPartsEntry) { if (isPartsEntry) {
setBreadcrumbs([ setBreadcrumbs([
{ link: "/parts/", label: t("titles.bc.jobs") }, { link: "/parts", label: t("titles.bc.parts") },
{ link: `${basePath}/vehicles`, label: t("titles.bc.vehicles") } { link: `${basePath}/vehicles`, label: t("titles.bc.vehicles") }
]); ]);
} else { } else {

View File

@@ -49,6 +49,8 @@ import {
validatePasswordResetSuccess validatePasswordResetSuccess
} from "./user.actions"; } from "./user.actions";
import UserActionTypes from "./user.types"; import UserActionTypes from "./user.types";
import * as amplitude from '@amplitude/analytics-browser';
import posthog from 'posthog-js';
const fpPromise = FingerprintJS.load(); const fpPromise = FingerprintJS.load();
@@ -83,8 +85,6 @@ export function* onCheckUserSession() {
export function* isUserAuthenticated() { export function* isUserAuthenticated() {
try { try {
logImEXEvent("redux_auth_check");
const user = yield getCurrentUser(); const user = yield getCurrentUser();
if (!user) { if (!user) {
yield put(unauthorizedUser()); yield put(unauthorizedUser());
@@ -92,6 +92,8 @@ export function* isUserAuthenticated() {
} }
LogRocket.identify(user.email); LogRocket.identify(user.email);
amplitude.setUserId(user.email);
posthog.identify(user.email);
const eulaQuery = yield client.query({ const eulaQuery = yield client.query({
query: QUERY_EULA, query: QUERY_EULA,
@@ -137,6 +139,7 @@ export function* signOutStart() {
imexshopid: state.user.bodyshop.imexshopid, imexshopid: state.user.bodyshop.imexshopid,
type: "messaging" type: "messaging"
}); });
amplitude.reset();
} catch { } catch {
console.log("No FCM token. Skipping unsubscribe."); console.log("No FCM token. Skipping unsubscribe.");
} }
@@ -266,11 +269,11 @@ export function* signInSuccessSaga({ payload }) {
instanceSeg, instanceSeg,
...(isParts ...(isParts
? [ ? [
InstanceRenderManager({ InstanceRenderManager({
imex: "ImexPartsManagement", imex: "ImexPartsManagement",
rome: "RomePartsManagement" rome: "RomePartsManagement"
}) })
] ]
: []) : [])
]; ];
window.$crisp.push(["set", "session:segments", [segs]]); window.$crisp.push(["set", "session:segments", [segs]]);
@@ -295,7 +298,6 @@ export function* signInSuccessSaga({ payload }) {
setUserId(analytics, payload.email); setUserId(analytics, payload.email);
setUserProperties(analytics, payload); setUserProperties(analytics, payload);
yield logImEXEvent("redux_sign_in_success");
} }
export function* onSendPasswordResetStart() { export function* onSendPasswordResetStart() {
@@ -362,6 +364,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
} }
try { try {
amplitude.setGroup('Shop', payload.shopname);
window.$crisp.push(["set", "user:company", [payload.shopname]]); window.$crisp.push(["set", "user:company", [payload.shopname]]);
if (authRecord[0] && authRecord[0].user.validemail) { if (authRecord[0] && authRecord[0].user.validemail) {
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]); window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);

View File

@@ -3523,6 +3523,8 @@
} }
}, },
"titles": { "titles": {
"parts_settings": "Parts Management Settings | {{app}}",
"simplified-parts-jobs": "Parts Management | {{app}}",
"accounting-payables": "Payables | {{app}}", "accounting-payables": "Payables | {{app}}",
"accounting-payments": "Payments | {{app}}", "accounting-payments": "Payments | {{app}}",
"accounting-receivables": "Receivables | {{app}}", "accounting-receivables": "Receivables | {{app}}",
@@ -3530,7 +3532,7 @@
"app": "", "app": "",
"bc": { "bc": {
"simplified-parts-jobs": "Jobs", "simplified-parts-jobs": "Jobs",
"parts": "Jobs", "parts": "Parts",
"parts_settings": "Settings", "parts_settings": "Settings",
"accounting-payables": "Payables", "accounting-payables": "Payables",
"accounting-payments": "Payments", "accounting-payments": "Payments",

View File

@@ -3523,6 +3523,9 @@
} }
}, },
"titles": { "titles": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",

View File

@@ -3523,6 +3523,9 @@
} }
}, },
"titles": { "titles": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "", "accounting-payables": "",
"accounting-payments": "", "accounting-payments": "",
"accounting-receivables": "", "accounting-receivables": "",

View File

@@ -1,5 +1,4 @@
/* eslint-disable */ /* eslint-disable */
import { sentryVitePlugin } from "@sentry/vite-plugin"; import { sentryVitePlugin } from "@sentry/vite-plugin";
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import chalk from "chalk"; import chalk from "chalk";
@@ -10,21 +9,23 @@ import { ViteEjsPlugin } from "vite-plugin-ejs";
import eslint from "vite-plugin-eslint"; import eslint from "vite-plugin-eslint";
import { VitePWA } from "vite-plugin-pwa"; import { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr"; import InstanceRenderManager from "./src/utils/instanceRenderMgr";
import browserslist from "browserslist";
import { browserslistToTargets } from "lightningcss";
// Ensure your environment variables are set correctly for Vite 6 process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", { timeZone: "America/Los_Angeles" });
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
timeZone: "America/Los_Angeles"
});
const commitHash = child.execSync("git rev-parse HEAD").toString().trimEnd(); const commitHash = child.execSync("git rev-parse HEAD").toString().trimEnd();
process.env.VITE_GIT_COMMIT_HASH = commitHash; process.env.VITE_GIT_COMMIT_HASH = commitHash;
const currentDatePST = new Date() // Resolve browserslist from package.json and map to Lightning CSS targets
.toLocaleDateString("en-US", { const lightningCssTargets = browserslistToTargets(
timeZone: "America/Los_Angeles", browserslist(undefined, {
year: "numeric", path: process.cwd(),
month: "2-digit", env: process.env.NODE_ENV === "production" ? "production" : "development"
day: "2-digit"
}) })
);
const currentDatePST = new Date()
.toLocaleDateString("en-US", { timeZone: "America/Los_Angeles", year: "numeric", month: "2-digit", day: "2-digit" })
.split("/") .split("/")
.reverse() .reverse()
.join("-"); .join("-");
@@ -32,18 +33,21 @@ const currentDatePST = new Date()
const getFormattedTimestamp = () => const getFormattedTimestamp = () =>
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m."); new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
export const logger = createLogger("info", { export const logger = createLogger("info", { allowClearScreen: false });
allowClearScreen: false
});
export default defineConfig({ export default defineConfig({
base: "/", base: "/",
plugins: [ plugins: [
// Ensure all plugins are Vite 6 compatible
ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })), ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })),
// PWA only for production builds (faster dev)
VitePWA({ VitePWA({
apply: "build",
injectRegister: "auto", injectRegister: "auto",
registerType: "prompt", registerType: "prompt",
workbox: {
navigateFallbackDenylist: [/^\/api\//] // prevent caching API routes
},
manifest: { manifest: {
short_name: InstanceRenderManager({ short_name: InstanceRenderManager({
instance: process.env.VITE_APP_INSTANCE, instance: process.env.VITE_APP_INSTANCE,
@@ -94,13 +98,15 @@ export default defineConfig({
gcm_sender_id: "103953800507" gcm_sender_id: "103953800507"
} }
}), }),
react(), react(),
eslint(), eslint(),
// Sentry only for production builds (no dev overhead)
sentryVitePlugin({ sentryVitePlugin({
apply: "build",
org: "imex", org: "imex",
reactComponentAnnotation: { reactComponentAnnotation: { enabled: true },
enabled: true
},
release: { release: {
name: `${process.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${commitHash}`.trim() name: `${process.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${commitHash}`.trim()
}, },
@@ -111,10 +117,12 @@ export default defineConfig({
}) })
}) })
], ],
define: { define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version), APP_VERSION: JSON.stringify(process.env.npm_package_version),
__COMMIT_HASH__: JSON.stringify(commitHash) __COMMIT_HASH__: JSON.stringify(commitHash)
}, },
server: { server: {
host: true, host: true,
port: 3000, port: 3000,
@@ -122,13 +130,11 @@ export default defineConfig({
proxy: { proxy: {
"/ws": { "/ws": {
target: "ws://localhost:4000", target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false, secure: false,
ws: true ws: true
}, },
"/wss": { "/wss": {
target: "ws://localhost:4000", target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false, secure: false,
ws: true ws: true
}, },
@@ -148,18 +154,17 @@ export default defineConfig({
}, },
https: { https: {
key: await fsPromises.readFile("../certs/key.pem"), key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem"), cert: await fsPromises.readFile("../certs/cert.pem")
allowHTTP1: false // Force HTTP/2
} }
}, },
preview: { preview: {
port: 6000, port: 6000,
host: true, host: true,
open: true, open: true,
https: { https: {
key: await fsPromises.readFile("../certs/key.pem"), key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem"), cert: await fsPromises.readFile("../certs/cert.pem")
allowHTTP1: false // Force HTTP/2
}, },
proxy: { proxy: {
"/ws": { "/ws": {
@@ -182,7 +187,10 @@ export default defineConfig({
} }
} }
}, },
build: { build: {
sourcemap: true,
rollupOptions: { rollupOptions: {
output: { output: {
manualChunks: { manualChunks: {
@@ -207,8 +215,14 @@ export default defineConfig({
} }
}, },
sourcemap: true cssMinify: "lightningcss"
}, },
// Strip console/debugger in prod to shrink bundles
esbuild: {
drop: ["console", "debugger"]
},
optimizeDeps: { optimizeDeps: {
include: [ include: [
"react", "react",
@@ -231,19 +245,17 @@ export default defineConfig({
"@firebase/util" "@firebase/util"
], ],
esbuildOptions: { esbuildOptions: {
// Update for Vite 6: Use proper file extensions loader: { ".jsx": "jsx", ".tsx": "tsx" }
loader: {
".jsx": "jsx",
".tsx": "tsx"
}
} }
}, },
css: { css: {
transformer: "lightningcss",
lightningcss: {
targets: lightningCssTargets
},
preprocessorOptions: { preprocessorOptions: {
scss: { scss: { quietDeps: true }
api: "modern-compiler",
quietDeps: true // Quite Deprecation Warnings, should be disabled occasionally before major upgrades
}
} }
} }
}); });

2965
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,14 +18,14 @@
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js" "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.864.0", "@aws-sdk/client-cloudwatch-logs": "^3.873.0",
"@aws-sdk/client-elasticache": "^3.864.0", "@aws-sdk/client-elasticache": "^3.872.0",
"@aws-sdk/client-s3": "^3.864.0", "@aws-sdk/client-s3": "^3.872.0",
"@aws-sdk/client-secrets-manager": "^3.864.0", "@aws-sdk/client-secrets-manager": "^3.872.0",
"@aws-sdk/client-ses": "^3.864.0", "@aws-sdk/client-ses": "^3.872.0",
"@aws-sdk/credential-provider-node": "^3.864.0", "@aws-sdk/credential-provider-node": "^3.873.0",
"@aws-sdk/lib-storage": "^3.864.0", "@aws-sdk/lib-storage": "^3.872.0",
"@aws-sdk/s3-request-presigner": "^3.864.0", "@aws-sdk/s3-request-presigner": "^3.872.0",
"@opensearch-project/opensearch": "^2.13.0", "@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1", "@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
@@ -41,7 +41,7 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"crisp-status-reporter": "^1.2.2", "crisp-status-reporter": "^1.2.2",
"dd-trace": "^5.63.1", "dd-trace": "^5.63.3",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"express": "^4.21.1", "express": "^4.21.1",
@@ -63,7 +63,7 @@
"query-string": "7.1.3", "query-string": "7.1.3",
"recursive-diff": "^1.0.9", "recursive-diff": "^1.0.9",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"skia-canvas": "^2.0.2", "skia-canvas": "^3.0.3",
"soap": "^1.3.0", "soap": "^1.3.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"socket.io-adapter": "^2.5.5", "socket.io-adapter": "^2.5.5",

View File

@@ -6,4 +6,5 @@ exports.kaizen = require("./kaizen").default;
exports.usageReport = require("./usageReport").default; exports.usageReport = require("./usageReport").default;
exports.podium = require("./podium").default; exports.podium = require("./podium").default;
exports.emsUpload = require("./emsUpload").default; exports.emsUpload = require("./emsUpload").default;
exports.carfax = require("./carfax").default; exports.carfax = require("./carfax").default;
exports.vehicletype = require("./vehicletype/vehicletype").default;

View 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"
]

View 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"
]

View 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"
]

View 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"
]

View 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;

View File

@@ -35,6 +35,11 @@
"headerMargin": "135" "headerMargin": "135"
}, },
"md_ro_statuses": { "md_ro_statuses": {
"parts_statuses": [
"Open",
"In Progress",
"Completed"
],
"statuses": [ "statuses": [
"Open", "Open",
"Scheduled", "Scheduled",
@@ -54,6 +59,10 @@
"Void" "Void"
], ],
"default_void": "Void", "default_void": "Void",
"parts_active_statuses": [
"Open",
"In Progress"
],
"active_statuses": [ "active_statuses": [
"Open", "Open",
"Scheduled", "Scheduled",

View File

@@ -181,7 +181,7 @@ const partsManagementProvisioning = async (req, res) => {
headerMargin: DefaultNewShop.logo_img_path.headerMargin headerMargin: DefaultNewShop.logo_img_path.headerMargin
}, },
features: { features: {
allAccess: true, // TODO: should be false? allAccess: false,
partsManagementOnly: true partsManagementOnly: true
}, },
md_ro_statuses: DefaultNewShop.md_ro_statuses, md_ro_statuses: DefaultNewShop.md_ro_statuses,
@@ -191,14 +191,14 @@ const partsManagementProvisioning = async (req, res) => {
md_messaging_presets: DefaultNewShop.md_messaging_presets, md_messaging_presets: DefaultNewShop.md_messaging_presets,
md_rbac: DefaultNewShop.md_rbac, md_rbac: DefaultNewShop.md_rbac,
md_classes: DefaultNewShop.md_classes, md_classes: DefaultNewShop.md_classes,
md_ins_cos: DefaultNewShop.md_ins_cos, // TODO need? md_ins_cos: DefaultNewShop.md_ins_cos,
md_categories: DefaultNewShop.md_categories, // TODO need? md_categories: DefaultNewShop.md_categories,
md_labor_rates: DefaultNewShop.md_labor_rates, // TODO need? md_labor_rates: DefaultNewShop.md_labor_rates,
md_payment_types: DefaultNewShop.md_payment_types, // TODO need? md_payment_types: DefaultNewShop.md_payment_types,
md_hour_split: DefaultNewShop.md_hour_split, // TODO need? md_hour_split: DefaultNewShop.md_hour_split,
md_ccc_rates: DefaultNewShop.md_ccc_rates, // TODO need? md_ccc_rates: DefaultNewShop.md_ccc_rates,
appt_alt_transport: DefaultNewShop.appt_alt_transport, // TODO need? appt_alt_transport: DefaultNewShop.appt_alt_transport,
md_jobline_presets: DefaultNewShop.md_jobline_presets, // TODO need? md_jobline_presets: DefaultNewShop.md_jobline_presets,
vendors: { vendors: {
data: p.vendors.map((v) => ({ data: p.vendors.map((v) => ({
name: v.name, name: v.name,

View File

@@ -14,10 +14,8 @@ const {
} = require("../partsManagement.queries"); } = require("../partsManagement.queries");
// Defaults // Defaults
const FALLBACK_DEFAULT_ORDER_STATUS = "OPEN"; const FALLBACK_DEFAULT_ORDER_STATUS = "Open";
// Config: include labor lines and labor in totals (default true)
const INCLUDE_LABOR = true;
/** /**
* Fetches the default order status for a bodyshop. * Fetches the default order status for a bodyshop.
* @param {string} shopId - The bodyshop UUID. * @param {string} shopId - The bodyshop UUID.
@@ -76,8 +74,6 @@ const extractJobData = (rq) => {
category: doc.DocumentType || null, category: doc.DocumentType || null,
classType: doc.DocumentStatus || null, classType: doc.DocumentStatus || null,
comment: doc.Comment || null, comment: doc.Comment || null,
// TODO: This causes the job to be read only in the UI
// date_exported: doc.TransmitDateTime || null,
asgn_no: asgn.AssignmentNumber || null, asgn_no: asgn.AssignmentNumber || null,
asgn_type: asgn.AssignmentType || null, asgn_type: asgn.AssignmentType || null,
asgn_date: asgn.AssignmentDate || null, asgn_date: asgn.AssignmentDate || null,
@@ -85,7 +81,8 @@ const extractJobData = (rq) => {
scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null, scheduled_in: ev.RepairEvent?.RequestedPickUpDateTime || null,
scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null, scheduled_completion: ev.RepairEvent?.TargetCompletionDateTime || null,
clm_no: ci.ClaimNum || null, clm_no: ci.ClaimNum || null,
status: ci.ClaimStatus || null, // status: ci.ClaimStatus || null, Proper, setting it default for now
status: FALLBACK_DEFAULT_ORDER_STATUS,
policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null, policy_no: ci.PolicyInfo?.PolicyInfo?.PolicyNum || ci.PolicyInfo?.PolicyNum || null,
ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0) ded_amt: parseFloat(ci.PolicyInfo?.CoverageInfo?.Coverage?.DeductibleInfo?.DeductibleAmt || 0)
}; };
@@ -329,10 +326,6 @@ const extractJobLines = (rq) => {
const refinishInfo = line.RefinishLaborInfo || {}; const refinishInfo = line.RefinishLaborInfo || {};
const subletInfo = line.SubletInfo || {}; const subletInfo = line.SubletInfo || {};
let jobLineType = "PART";
if (Object.keys(subletInfo).length > 0) jobLineType = "SUBLET";
else if (Object.keys(laborInfo).length > 0 && Object.keys(partInfo).length === 0) jobLineType = "LABOR";
const base = { const base = {
line_no: parseInt(line.LineNum || 0, 10), line_no: parseInt(line.LineNum || 0, 10),
unq_seq: parseInt(line.UniqueSequenceNum || 0, 10), unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
@@ -341,134 +334,87 @@ const extractJobLines = (rq) => {
notes: line.LineMemo || null notes: line.LineMemo || null
}; };
if (jobLineType === "PART") { 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;
}
// 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); const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
// Push the part line with ONLY part pricing/fields lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
out.push({ lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
...base, lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
part_type: partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null, lineOut.db_price = isNaN(price) ? 0 : price;
part_qty: parseFloat(partInfo.Quantity || 0) || 1, lineOut.act_price = isNaN(price) ? 0 : price;
oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null,
db_price: price, // Tax flag from PartInfo.TaxableInd when provided
act_price: price, if (
// Tax flag from PartInfo.TaxableInd when provided partInfo.TaxableInd !== undefined &&
...(partInfo.TaxableInd !== undefined &&
(typeof partInfo.TaxableInd === "string" || (typeof partInfo.TaxableInd === "string" ||
typeof partInfo.TaxableInd === "number" || typeof partInfo.TaxableInd === "number" ||
typeof partInfo.TaxableInd === "boolean") typeof partInfo.TaxableInd === "boolean")
? { ) {
tax_part: lineOut.tax_part =
partInfo.TaxableInd === true || partInfo.TaxableInd === true ||
partInfo.TaxableInd === 1 || partInfo.TaxableInd === 1 ||
partInfo.TaxableInd === "1" || partInfo.TaxableInd === "1" ||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y") (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
}
: {}),
// Manual line flag coercion
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
// If labor is present on the same damage line, split it to a separate LABOR jobline
// TODO: Verify with patrick this is desired.
if (INCLUDE_LABOR) {
const hrs = parseFloat(laborInfo.LaborHours || 0);
const amt = parseFloat(laborInfo.LaborAmt || 0);
const hasLabor =
(!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) ||
(!isNaN(hrs) && hrs !== 0) ||
(!isNaN(amt) && amt !== 0);
if (hasLabor) {
out.push({
...base,
// tweak unq_seq to avoid collisions in later upserts
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 400000,
mod_lbr_ty: laborInfo.LaborType || null,
mod_lb_hrs: isNaN(hrs) ? 0 : hrs,
lbr_op: laborInfo.LaborOperation || null,
lbr_amt: isNaN(amt) ? 0 : amt,
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
}
} }
} else if (jobLineType === "SUBLET") { } else if (hasSublet) {
out.push({ const amt = parseFloat(subletInfo.SubletAmount || 0);
...base, lineOut.part_type = "PAS"; // Sublet as parts-as-service
part_type: "PAS", lineOut.part_qty = 1;
part_qty: 1, lineOut.act_price = isNaN(amt) ? 0 : amt;
act_price: parseFloat(subletInfo.SubletAmount || 0),
// Manual line flag
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
} else if (INCLUDE_LABOR) {
// Labor-only line (only when enabled)
out.push({
...base,
mod_lbr_ty: laborInfo.LaborType || null,
mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0),
lbr_op: laborInfo.LaborOperation || null,
lbr_amt: parseFloat(laborInfo.LaborAmt || 0),
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
} }
// Add a separate refinish labor line if present and enabled // Primary labor (if present) recorded on the same line
if (INCLUDE_LABOR && Object.keys(refinishInfo).length > 0) { const hrs = parseFloat(laborInfo.LaborHours || 0);
const hrs = parseFloat(refinishInfo.LaborHours || 0); const amt = parseFloat(laborInfo.LaborAmt || 0);
const amt = parseFloat(refinishInfo.LaborAmt || 0); const hasLabor =
if (!isNaN(hrs) || !isNaN(amt)) { (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) ||
out.push({ (!isNaN(hrs) && hrs !== 0) ||
...base, (!isNaN(amt) && amt !== 0);
// tweak unq_seq to avoid collisions in later upserts if (hasLabor) {
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000, lineOut.mod_lbr_ty = laborInfo.LaborType || null;
line_desc: base.line_desc || "Refinish", lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
mod_lbr_ty: "LAR", lineOut.lbr_op = laborInfo.LaborOperation || null;
mod_lb_hrs: isNaN(hrs) ? 0 : hrs, lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
lbr_op: refinishInfo.LaborOperation || null,
lbr_amt: isNaN(amt) ? 0 : amt,
...(line.ManualLineInd !== undefined
? {
manual_line:
line.ManualLineInd === true ||
line.ManualLineInd === 1 ||
line.ManualLineInd === "1" ||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y")
}
: { manual_line: null })
});
}
} }
// 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 =
Object.keys(refinishInfo).length > 0 &&
((refinishInfo.LaborType && String(refinishInfo.LaborType).length > 0) ||
!isNaN(rHrs) ||
!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;
// 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;
}
if (refinishInfo.PaintStagesNum !== undefined) lineOut.paint_stg = refinishInfo.PaintStagesNum;
if (refinishInfo.PaintTonesNum !== undefined) lineOut.paint_tone = refinishInfo.PaintTonesNum;
}
out.push(lineOut);
} }
return out; return out;
@@ -517,14 +463,14 @@ const computeLinesTotal = (joblines = []) => {
let parts = 0; let parts = 0;
let labor = 0; let labor = 0;
for (const jl of joblines) { for (const jl of joblines) {
if (jl && jl.part_type) { if (jl?.part_type) {
const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1; const qty = Number.isFinite(jl.part_qty) ? jl.part_qty : 1;
const price = Number.isFinite(jl.act_price) ? jl.act_price : 0; const price = Number.isFinite(jl.act_price) ? jl.act_price : 0;
parts += price * (qty || 1); parts += price * (qty || 1);
} else if (!jl.part_type && Number.isFinite(jl.act_price)) { } else if (!jl.part_type && Number.isFinite(jl.act_price)) {
parts += jl.act_price; parts += jl.act_price;
} }
if (INCLUDE_LABOR && Number.isFinite(jl.lbr_amt)) { if (Number.isFinite(jl.lbr_amt)) {
labor += jl.lbr_amt; labor += jl.lbr_amt;
} }
} }

View File

@@ -6,22 +6,23 @@ const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates"); const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
const { const {
GET_JOB_BY_CLAIM, GET_JOB_BY_ID,
UPDATE_JOB_BY_ID, UPDATE_JOB_BY_ID,
DELETE_JOBLINES_BY_IDS, SOFT_DELETE_JOBLINES_BY_IDS,
INSERT_JOBLINES INSERT_JOBLINES,
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ
} = require("../partsManagement.queries"); } = require("../partsManagement.queries");
/** /**
* Finds a job by shop ID and claim number. * Finds a job by shop ID and claim number.
* @param shopId * @param shopId
* @param claimNum * @param jobId
* @param logger * @param logger
* @returns {Promise<*|null>} * @returns {Promise<*|null>}
*/ */
const findJob = async (shopId, claimNum, logger) => { const findJob = async (shopId, jobId, logger) => {
try { 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; return jobs?.[0] || null;
} catch (err) { } catch (err) {
logger.log("parts-job-lookup-failed", "error", null, null, { error: err }); logger.log("parts-job-lookup-failed", "error", null, null, { error: err });
@@ -56,13 +57,13 @@ const extractUpdatedJobData = (rq) => {
}; };
/** /**
* Extracts updated job lines from the request payload, mirroring the AddRq splitting rules: * Extracts updated job lines from the request payload without splitting parts and labor:
* - PART lines carry only part pricing (act_price) and related fields * - Keep part and labor on the same jobline
* - If LaborInfo exists on a part line, add a separate LABOR line at unq_seq + 400000 * - Aggregate RefinishLabor into secondary labor fields and add its amount to lbr_amt
* - If RefinishLaborInfo exists, add a separate LABOR line at unq_seq + 500000 with mod_lbr_ty=LAR * - SUBLET-only lines become PAS part_type with act_price = SubletAmount
* - SUBLET lines become PAS part_type with act_price=SubletAmount * Accepts currentJobLineNotes map for notes merging.
*/ */
const extractUpdatedJobLines = (addsChgs = {}, jobId) => { const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {}) => {
const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}]; const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}];
const coerceManual = (val) => const coerceManual = (val) =>
@@ -84,78 +85,99 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId) => {
unq_seq: parseInt(line.UniqueSequenceNum || 0, 10), unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
status: line.LineStatusCode || null, status: line.LineStatusCode || null,
line_desc: line.LineDesc || null, line_desc: line.LineDesc || null,
notes: line.LineMemo || null, // notes will be set below
manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
}; };
const lineOut = { ...base };
// --- Notes merge logic ---
const unqSeq = lineOut.unq_seq;
const currentNotes = currentJobLineNotes?.[unqSeq] || null;
const newNotes = line.LineMemo || null;
if (newNotes && currentNotes) {
if (currentNotes === newNotes) {
lineOut.notes = currentNotes;
} else if (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 logic ---
const hasPart = Object.keys(partInfo).length > 0; const hasPart = Object.keys(partInfo).length > 0;
const hasLaborOnly = Object.keys(laborInfo).length > 0 && !hasPart && Object.keys(subletInfo).length === 0;
const hasSublet = Object.keys(subletInfo).length > 0; const hasSublet = Object.keys(subletInfo).length > 0;
if (hasPart) { if (hasPart) {
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0); const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
out.push({ lineOut.part_type = partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null;
...base, lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
part_type: partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null, lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
part_qty: parseFloat(partInfo.Quantity || 0) || 1, lineOut.db_price = isNaN(price) ? 0 : price;
oem_partno: partInfo.OEMPartNum || partInfo.PartNum || null, lineOut.act_price = isNaN(price) ? 0 : price;
db_price: isNaN(price) ? 0 : price,
act_price: isNaN(price) ? 0 : price
});
// Split any attached labor on the part line into a derived labor jobline // Optional: taxability flag for parts
const hrs = parseFloat(laborInfo.LaborHours || 0); if (
const amt = parseFloat(laborInfo.LaborAmt || 0); partInfo.TaxableInd !== undefined &&
const hasLabor = (typeof partInfo.TaxableInd === "string" ||
(!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) || typeof partInfo.TaxableInd === "number" ||
(!isNaN(hrs) && hrs !== 0) || typeof partInfo.TaxableInd === "boolean")
(!isNaN(amt) && amt !== 0); ) {
if (hasLabor) { lineOut.tax_part =
out.push({ partInfo.TaxableInd === true ||
...base, partInfo.TaxableInd === 1 ||
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 400000, partInfo.TaxableInd === "1" ||
mod_lbr_ty: laborInfo.LaborType || null, (typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
mod_lb_hrs: isNaN(hrs) ? 0 : hrs,
lbr_op: laborInfo.LaborOperation || null,
lbr_amt: isNaN(amt) ? 0 : amt
});
} }
} else if (hasSublet) { } else if (hasSublet) {
out.push({ const amt = parseFloat(subletInfo.SubletAmount || 0);
...base, lineOut.part_type = "PAS";
part_type: "PAS", lineOut.part_qty = 1;
part_qty: 1, lineOut.act_price = isNaN(amt) ? 0 : amt;
act_price: parseFloat(subletInfo.SubletAmount || 0) || 0
});
} }
// Labor-only line (no PartInfo): still upsert as a labor entry // Primary labor on same line
if (hasLaborOnly) { const hrs = parseFloat(laborInfo.LaborHours || 0);
out.push({ const amt = parseFloat(laborInfo.LaborAmt || 0);
...base, const hasLabor =
mod_lbr_ty: laborInfo.LaborType || null, (!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) ||
mod_lb_hrs: parseFloat(laborInfo.LaborHours || 0) || 0, (!isNaN(hrs) && hrs !== 0) ||
lbr_op: laborInfo.LaborOperation || null, (!isNaN(amt) && amt !== 0);
lbr_amt: parseFloat(laborInfo.LaborAmt || 0) || 0 if (hasLabor) {
}); lineOut.mod_lbr_ty = laborInfo.LaborType || null;
lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
lineOut.lbr_op = laborInfo.LaborOperation || null;
lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
} }
// Separate refinish labor line // Refinish labor on same line using secondary fields; aggregate amount into lbr_amt
if (Object.keys(refinishInfo).length > 0) { const rHrs = parseFloat(refinishInfo.LaborHours || 0);
const rHrs = parseFloat(refinishInfo.LaborHours || 0); const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
const rAmt = parseFloat(refinishInfo.LaborAmt || 0); const hasRefinish =
if (!isNaN(rHrs) || !isNaN(rAmt)) { Object.keys(refinishInfo).length > 0 &&
out.push({ ((refinishInfo.LaborType && String(refinishInfo.LaborType).length > 0) ||
...base, !isNaN(rHrs) ||
unq_seq: (parseInt(line.UniqueSequenceNum || 0, 10) || 0) + 500000, !isNaN(rAmt) ||
line_desc: base.line_desc || "Refinish", !!refinishInfo.LaborOperation);
mod_lbr_ty: "LAR", if (hasRefinish) {
mod_lb_hrs: isNaN(rHrs) ? 0 : rHrs, lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR";
lbr_op: refinishInfo.LaborOperation || null, lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;
lbr_amt: isNaN(rAmt) ? 0 : rAmt lineOut.lbr_op_j = refinishInfo.LaborOperation || null;
}); 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;
} }
out.push(lineOut);
} }
return out; return out;
@@ -192,28 +214,42 @@ const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
if (!rq) return res.status(400).send("Missing <VehicleDamageEstimateChgRq>"); if (!rq) return res.status(400).send("Missing <VehicleDamageEstimateChgRq>");
const shopId = rq.ShopID; const shopId = rq.ShopID;
const claimNum = rq.ClaimInfo?.ClaimNum; const jobId = rq.JobID;
if (!shopId || !claimNum) return res.status(400).send("Missing ShopID or ClaimNum"); if (!shopId || !jobId) return res.status(400).send("Missing ShopID or JobID");
const job = await findJob(shopId, jobId, logger);
const job = await findJob(shopId, claimNum, logger);
if (!job) return res.status(404).send("Job not found"); if (!job) return res.status(404).send("Job not found");
// --- Get updated lines and their unq_seq ---
const linesIn = Array.isArray(rq.AddsChgs?.DamageLineInfo)
? rq.AddsChgs.DamageLineInfo
: [rq.AddsChgs?.DamageLineInfo || {}];
const updatedSeqs = Array.from(
new Set((linesIn || []).map((l) => parseInt(l?.UniqueSequenceNum || 0, 10)).filter((v) => Number.isInteger(v)))
);
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;
}
}
}
// --- End fetch current notes ---
const updatedJobData = extractUpdatedJobData(rq); const updatedJobData = extractUpdatedJobData(rq);
const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id); const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id, currentJobLineNotes);
const deletedLineIds = extractDeletions(rq.Deletions); const deletedLineIds = extractDeletions(rq.Deletions);
await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData }); 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. if (deletedLineIds?.length || updatedSeqs?.length) {
const updatedSeqs = Array.from(
new Set((updatedLines || []).map((l) => l && l.unq_seq).filter((v) => Number.isInteger(v)))
);
if ((deletedLineIds && deletedLineIds.length) || (updatedSeqs && updatedSeqs.length)) {
const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])])); const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])]));
if (allToDelete.length) { if (allToDelete.length) {
await client.request(DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete }); await client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete });
} }
} }

View File

@@ -44,6 +44,18 @@ const GET_JOB_BY_CLAIM = `
} }
`; `;
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 UPDATE_JOB_BY_ID = ` const UPDATE_JOB_BY_ID = `
mutation UpdateJobById($id: uuid!, $job: jobs_set_input!) { mutation UpdateJobById($id: uuid!, $job: jobs_set_input!) {
update_jobs_by_pk(pk_columns: { id: $id }, _set: $job) { update_jobs_by_pk(pk_columns: { id: $id }, _set: $job) {
@@ -52,50 +64,12 @@ const UPDATE_JOB_BY_ID = `
} }
`; `;
const UPSERT_JOBLINES = ` // Soft delete joblines by marking removed=true instead of hard-deleting
mutation UpsertJoblines($joblines: [joblines_insert_input!]!) { const SOFT_DELETE_JOBLINES_BY_IDS = `
insert_joblines( mutation SoftDeleteJoblinesByIds($jobid: uuid!, $unqSeqs: [Int!]!) {
objects: $joblines update_joblines(
on_conflict: { where: { jobid: { _eq: $jobid }, unq_seq: { _in: $unqSeqs } },
constraint: joblines_pkey _set: { removed: true }
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 affected_rows
} }
@@ -233,6 +207,15 @@ 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
}
}
`;
module.exports = { module.exports = {
GET_BODYSHOP_STATUS, GET_BODYSHOP_STATUS,
GET_VEHICLE_BY_SHOP_VIN, GET_VEHICLE_BY_SHOP_VIN,
@@ -240,9 +223,7 @@ module.exports = {
INSERT_JOB_WITH_LINES, INSERT_JOB_WITH_LINES,
GET_JOB_BY_CLAIM, GET_JOB_BY_CLAIM,
UPDATE_JOB_BY_ID, UPDATE_JOB_BY_ID,
UPSERT_JOBLINES, SOFT_DELETE_JOBLINES_BY_IDS,
DELETE_JOBLINES_BY_JOBID,
DELETE_JOBLINES_BY_IDS,
INSERT_JOBLINES, INSERT_JOBLINES,
CHECK_EXTERNAL_SHOP_ID, CHECK_EXTERNAL_SHOP_ID,
CREATE_SHOP, CREATE_SHOP,
@@ -258,5 +239,7 @@ module.exports = {
GET_JOBS_BY_SHOP, GET_JOBS_BY_SHOP,
DELETE_JOBLINES_BY_JOB_IDS, DELETE_JOBLINES_BY_JOB_IDS,
DELETE_JOBS_BY_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
}; };

View File

@@ -44,11 +44,20 @@ const generateSignedUploadUrls = async (req, res) => {
for (const filename of filenames) { for (const filename of filenames) {
const key = filename; const key = filename;
const client = new S3Client({ region: InstanceRegion() }); const client = new S3Client({ region: InstanceRegion() });
const command = new PutObjectCommand({
// Check if filename indicates PDF and set content type accordingly
const isPdf = filename.toLowerCase().endsWith('.pdf');
const commandParams = {
Bucket: imgproxyDestinationBucket, Bucket: imgproxyDestinationBucket,
Key: key, Key: key,
StorageClass: "INTELLIGENT_TIERING" StorageClass: "INTELLIGENT_TIERING"
}); };
if (isPdf) {
commandParams.ContentType = "application/pdf";
}
const command = new PutObjectCommand(commandParams);
const presignedUrl = await getSignedUrl(client, command, { expiresIn: 360 }); const presignedUrl = await getSignedUrl(client, command, { expiresIn: 360 });
signedUrls.push({ filename, presignedUrl, key }); signedUrls.push({ filename, presignedUrl, key });
} }

View File

@@ -73,37 +73,23 @@ const processCanvasRequest = async (req, res) => {
// Default width and height // Default width and height
const width = isNumber(w) && w > 0 ? w : 500; const width = isNumber(w) && w > 0 ? w : 500;
const height = isNumber(h) && h > 0 ? h : 275; const height = isNumber(h) && h > 0 ? h : 275;
const configuration = getChartConfiguration(keys, values, override); const configuration = getChartConfiguration(keys, values, override);
let canvas = null;
let ctx = null;
let chart = null; let chart = null;
let chartImage = null;
try { try {
// Create the canvas const canvas = new Canvas(width, height);
canvas = new Canvas(width, height); const ctx = canvas.getContext("2d");
ctx = canvas.getContext("2d");
// Render the chart
chart = new Chart(ctx, configuration); chart = new Chart(ctx, configuration);
// Generate and send the image const chartImage = (await canvas.toBuffer("image/png")).toString("base64");
chartImage = (await canvas.toBuffer("image/png")).toString("base64");
res.status(200).send(`data:image/png;base64,${chartImage}`); res.status(200).send(`data:image/png;base64,${chartImage}`);
} catch (error) { } catch (error) {
// Log the error and send the response
logger.log("canvas-error", "error", "jsr", null, { error: error.message }); 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 { } finally {
// Cleanup resources chart?.destroy();
if (chart) {
chart.destroy();
}
ctx = null;
canvas = null;
chartImage = null;
} }
}; };
@@ -118,15 +104,18 @@ const enqueueRequest = (req, res) => {
}; };
const processNextInQueue = async () => { const processNextInQueue = async () => {
while (requestQueue.length > 0) { try {
const { req, res } = requestQueue.shift(); while (requestQueue.length > 0) {
try { const { req, res } = requestQueue.shift();
await processCanvasRequest(req, res); try {
} catch (err) { await processCanvasRequest(req, res);
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message }); } catch (err) {
console.error("canvas-queue-error", "error", "jsr", null, { error: err.message });
}
} }
} finally {
isProcessing = false;
} }
isProcessing = false;
}; };
exports.canvastest = function (req, res) { exports.canvastest = function (req, res) {
@@ -134,7 +123,10 @@ exports.canvastest = function (req, res) {
}; };
exports.canvas = async (req, res) => { exports.canvas = async (req, res) => {
if (isProcessing || !enqueueRequest(req, res)) return; if (!enqueueRequest(req, res)) return;
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message })); if (!isProcessing) {
isProcessing = true;
processNextInQueue().catch((err) => console.error("canvas-processing-error", { error: err.message }));
}
}; };

View File

@@ -38,7 +38,7 @@ if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_
* Route to handle Vehicle Damage Estimate Change Request * Route to handle Vehicle Damage Estimate Change Request
*/ */
router.post( router.post(
"/parts-management/VehicleDamageEstimateChqRq", "/parts-management/VehicleDamageEstimateChgRq",
express.raw({ type: "application/xml", limit: XML_BODY_LIMIT }), // Parse XML body express.raw({ type: "application/xml", limit: XML_BODY_LIMIT }), // Parse XML body
partsManagementIntegrationMiddleware, partsManagementIntegrationMiddleware,
partsManagementVehicleDamageEstimateChqRq partsManagementVehicleDamageEstimateChqRq

View File

@@ -144,6 +144,9 @@ router.post("/emsupload", validateFirebaseIdTokenMiddleware, data.emsUpload);
// Redis Cache Routes // Redis Cache Routes
router.post("/bodyshop-cache", eventAuthorizationMiddleware, updateBodyshopCache); 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 // Health Check for docker-compose-cluster load balancer, only available in development
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
router.get("/health", (req, res) => { router.get("/health", (req, res) => {