Compare commits
30 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
784378a999 | ||
|
|
0d502d4dd4 | ||
|
|
8980d3716b | ||
|
|
764ec5f8f9 | ||
|
|
a7a7551dae | ||
|
|
571536a7ec | ||
|
|
20e56fff6a | ||
|
|
8f132ca14d | ||
|
|
99c002dac1 | ||
|
|
0cd30ccdec | ||
|
|
acd69276a5 | ||
|
|
faf5878bdf | ||
|
|
f56a540b2f | ||
|
|
e251e5f8f6 | ||
|
|
5a55798d2d | ||
|
|
c9e41ba72a | ||
|
|
522f2b9e26 | ||
|
|
be9267ddd4 | ||
|
|
e4a79b51c7 | ||
|
|
47a9a963fa | ||
|
|
f3c7a831a1 | ||
|
|
6ac9310e81 | ||
|
|
b91e65be0e | ||
|
|
3f2358e30c | ||
|
|
ce02d90c3c | ||
|
|
95a71bea6e | ||
|
|
3b27120d77 | ||
|
|
f350163056 | ||
|
|
57cfecb7b8 | ||
|
|
5f8a08b0a7 |
184
client/src/App/App.container.backup-2026-03-04.jsx
Normal file
184
client/src/App/App.container.backup-2026-03-04.jsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { ApolloProvider } from "@apollo/client/react";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||||
|
import { ConfigProvider, Grid } from "antd";
|
||||||
|
import enLocale from "antd/es/locale/en_US";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { CookiesProvider } from "react-cookie";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
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 { signOutStart } from "../redux/user/user.actions";
|
||||||
|
import client from "../utils/GraphQLClient";
|
||||||
|
import App from "./App";
|
||||||
|
import getTheme from "./themeProvider";
|
||||||
|
|
||||||
|
// Base Split configuration
|
||||||
|
const config = {
|
||||||
|
core: {
|
||||||
|
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||||
|
key: "anon"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function SplitClientProvider({ children }) {
|
||||||
|
const imexshopid = useSelector((state) => state.user.imexshopid);
|
||||||
|
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (import.meta.env.DEV && splitClient && imexshopid) {
|
||||||
|
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
||||||
|
}
|
||||||
|
}, [splitClient, imexshopid]);
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppContainer() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const currentUser = useSelector(selectCurrentUser);
|
||||||
|
const isDarkMode = useSelector(selectDarkMode);
|
||||||
|
const screens = Grid.useBreakpoint();
|
||||||
|
const isPhone = !screens.md;
|
||||||
|
const isUltraWide = Boolean(screens.xxxl);
|
||||||
|
|
||||||
|
const theme = useMemo(() => {
|
||||||
|
const baseTheme = getTheme(isDarkMode);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseTheme,
|
||||||
|
token: {
|
||||||
|
...(baseTheme.token || {}),
|
||||||
|
screenXXXL: 2160
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
...(baseTheme.components || {}),
|
||||||
|
Table: {
|
||||||
|
...(baseTheme.components?.Table || {}),
|
||||||
|
cellFontSizeSM: isPhone ? 12 : 13,
|
||||||
|
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
|
||||||
|
cellFontSize: isUltraWide ? 15 : 14,
|
||||||
|
cellPaddingInlineSM: isPhone ? 8 : 10,
|
||||||
|
cellPaddingInlineMD: isPhone ? 10 : 14,
|
||||||
|
cellPaddingInline: isUltraWide ? 20 : 16,
|
||||||
|
cellPaddingBlockSM: isPhone ? 8 : 10,
|
||||||
|
cellPaddingBlockMD: isPhone ? 10 : 12,
|
||||||
|
cellPaddingBlock: isUltraWide ? 14 : 12,
|
||||||
|
selectionColumnWidth: isPhone ? 44 : 52
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isDarkMode, isPhone, isUltraWide]);
|
||||||
|
|
||||||
|
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
|
||||||
|
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
|
||||||
|
const antdPagination = useMemo(
|
||||||
|
() => ({
|
||||||
|
showSizeChanger: !isPhone,
|
||||||
|
totalBoundaryShowSizeChanger: 100
|
||||||
|
}),
|
||||||
|
[isPhone]
|
||||||
|
);
|
||||||
|
|
||||||
|
const antdForm = useMemo(
|
||||||
|
() => ({
|
||||||
|
validateMessages: {
|
||||||
|
required: t("general.validation.required", { label: "${label}" })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Global seamless logout listener with redirect to /signin
|
||||||
|
useEffect(() => {
|
||||||
|
const handleSeamlessLogout = (event) => {
|
||||||
|
if (event.data?.type !== "seamlessLogoutRequest") return;
|
||||||
|
|
||||||
|
// Only accept messages from the parent window
|
||||||
|
if (event.source !== window.parent) return;
|
||||||
|
|
||||||
|
const targetOrigin = event.origin || "*";
|
||||||
|
|
||||||
|
if (currentUser?.authorized !== true) {
|
||||||
|
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(signOutStart());
|
||||||
|
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", handleSeamlessLogout);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handleSeamlessLogout);
|
||||||
|
};
|
||||||
|
}, [dispatch, currentUser?.authorized]);
|
||||||
|
|
||||||
|
// Update data-theme attribute (no cleanup to avoid transient style churn)
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
|
||||||
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
// Sync darkMode with localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
const uid = currentUser?.uid;
|
||||||
|
|
||||||
|
if (!uid) {
|
||||||
|
dispatch(setDarkMode(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `dark-mode-${uid}`;
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (raw == null) {
|
||||||
|
dispatch(setDarkMode(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
|
||||||
|
} catch {
|
||||||
|
dispatch(setDarkMode(false));
|
||||||
|
}
|
||||||
|
}, [currentUser?.uid, dispatch]);
|
||||||
|
|
||||||
|
// Persist darkMode
|
||||||
|
useEffect(() => {
|
||||||
|
const uid = currentUser?.uid;
|
||||||
|
if (!uid) return;
|
||||||
|
|
||||||
|
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
|
||||||
|
}, [isDarkMode, currentUser?.uid]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CookiesProvider>
|
||||||
|
<ApolloProvider client={client}>
|
||||||
|
<ConfigProvider
|
||||||
|
input={antdInput}
|
||||||
|
locale={enLocale}
|
||||||
|
theme={theme}
|
||||||
|
form={antdForm}
|
||||||
|
table={antdTable}
|
||||||
|
pagination={antdPagination}
|
||||||
|
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
|
||||||
|
popupOverflow="viewport"
|
||||||
|
>
|
||||||
|
<GlobalLoadingBar />
|
||||||
|
<SplitFactoryProvider config={config}>
|
||||||
|
<SplitClientProvider>
|
||||||
|
<App />
|
||||||
|
</SplitClientProvider>
|
||||||
|
</SplitFactoryProvider>
|
||||||
|
</ConfigProvider>
|
||||||
|
</ApolloProvider>
|
||||||
|
</CookiesProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sentry.withProfiler(AppContainer);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ApolloProvider } from "@apollo/client/react";
|
import { ApolloProvider } from "@apollo/client/react";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||||
import { ConfigProvider, Grid } 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 { CookiesProvider } from "react-cookie";
|
||||||
@@ -43,47 +43,10 @@ function AppContainer() {
|
|||||||
|
|
||||||
const currentUser = useSelector(selectCurrentUser);
|
const currentUser = useSelector(selectCurrentUser);
|
||||||
const isDarkMode = useSelector(selectDarkMode);
|
const isDarkMode = useSelector(selectDarkMode);
|
||||||
const screens = Grid.useBreakpoint();
|
|
||||||
const isPhone = !screens.md;
|
|
||||||
const isUltraWide = Boolean(screens.xxxl);
|
|
||||||
|
|
||||||
const theme = useMemo(() => {
|
const theme = useMemo(() => getTheme(isDarkMode), [isDarkMode]);
|
||||||
const baseTheme = getTheme(isDarkMode);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...baseTheme,
|
|
||||||
token: {
|
|
||||||
...(baseTheme.token || {}),
|
|
||||||
screenXXXL: 2160
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
...(baseTheme.components || {}),
|
|
||||||
Table: {
|
|
||||||
...(baseTheme.components?.Table || {}),
|
|
||||||
cellFontSizeSM: isPhone ? 12 : 13,
|
|
||||||
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
|
|
||||||
cellFontSize: isUltraWide ? 15 : 14,
|
|
||||||
cellPaddingInlineSM: isPhone ? 8 : 10,
|
|
||||||
cellPaddingInlineMD: isPhone ? 10 : 14,
|
|
||||||
cellPaddingInline: isUltraWide ? 20 : 16,
|
|
||||||
cellPaddingBlockSM: isPhone ? 8 : 10,
|
|
||||||
cellPaddingBlockMD: isPhone ? 10 : 12,
|
|
||||||
cellPaddingBlock: isUltraWide ? 14 : 12,
|
|
||||||
selectionColumnWidth: isPhone ? 44 : 52
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [isDarkMode, isPhone, isUltraWide]);
|
|
||||||
|
|
||||||
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
|
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
|
||||||
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
|
|
||||||
const antdPagination = useMemo(
|
|
||||||
() => ({
|
|
||||||
showSizeChanger: !isPhone,
|
|
||||||
totalBoundaryShowSizeChanger: 100
|
|
||||||
}),
|
|
||||||
[isPhone]
|
|
||||||
);
|
|
||||||
|
|
||||||
const antdForm = useMemo(
|
const antdForm = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -159,16 +122,7 @@ function AppContainer() {
|
|||||||
return (
|
return (
|
||||||
<CookiesProvider>
|
<CookiesProvider>
|
||||||
<ApolloProvider client={client}>
|
<ApolloProvider client={client}>
|
||||||
<ConfigProvider
|
<ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}>
|
||||||
input={antdInput}
|
|
||||||
locale={enLocale}
|
|
||||||
theme={theme}
|
|
||||||
form={antdForm}
|
|
||||||
table={antdTable}
|
|
||||||
pagination={antdPagination}
|
|
||||||
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
|
|
||||||
popupOverflow="viewport"
|
|
||||||
>
|
|
||||||
<GlobalLoadingBar />
|
<GlobalLoadingBar />
|
||||||
<SplitFactoryProvider config={config}>
|
<SplitFactoryProvider config={config}>
|
||||||
<SplitClientProvider>
|
<SplitClientProvider>
|
||||||
|
|||||||
184
client/src/App/App.container.pre-rollback-2026-03-04.jsx
Normal file
184
client/src/App/App.container.pre-rollback-2026-03-04.jsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { ApolloProvider } from "@apollo/client/react";
|
||||||
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
|
||||||
|
import { ConfigProvider, Grid } from "antd";
|
||||||
|
import enLocale from "antd/es/locale/en_US";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { CookiesProvider } from "react-cookie";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
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 { signOutStart } from "../redux/user/user.actions";
|
||||||
|
import client from "../utils/GraphQLClient";
|
||||||
|
import App from "./App";
|
||||||
|
import getTheme from "./themeProvider";
|
||||||
|
|
||||||
|
// Base Split configuration
|
||||||
|
const config = {
|
||||||
|
core: {
|
||||||
|
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||||
|
key: "anon"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function SplitClientProvider({ children }) {
|
||||||
|
const imexshopid = useSelector((state) => state.user.imexshopid);
|
||||||
|
const splitClient = useSplitClient({ key: imexshopid || "anon" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (import.meta.env.DEV && splitClient && imexshopid) {
|
||||||
|
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
|
||||||
|
}
|
||||||
|
}, [splitClient, imexshopid]);
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppContainer() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const currentUser = useSelector(selectCurrentUser);
|
||||||
|
const isDarkMode = useSelector(selectDarkMode);
|
||||||
|
const screens = Grid.useBreakpoint();
|
||||||
|
const isPhone = !screens.md;
|
||||||
|
const isUltraWide = Boolean(screens.xxxl);
|
||||||
|
|
||||||
|
const theme = useMemo(() => {
|
||||||
|
const baseTheme = getTheme(isDarkMode);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseTheme,
|
||||||
|
token: {
|
||||||
|
...(baseTheme.token || {}),
|
||||||
|
screenXXXL: 2160
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
...(baseTheme.components || {}),
|
||||||
|
Table: {
|
||||||
|
...(baseTheme.components?.Table || {}),
|
||||||
|
cellFontSizeSM: isPhone ? 12 : 13,
|
||||||
|
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
|
||||||
|
cellFontSize: isUltraWide ? 15 : 14,
|
||||||
|
cellPaddingInlineSM: isPhone ? 8 : 10,
|
||||||
|
cellPaddingInlineMD: isPhone ? 10 : 14,
|
||||||
|
cellPaddingInline: isUltraWide ? 20 : 16,
|
||||||
|
cellPaddingBlockSM: isPhone ? 8 : 10,
|
||||||
|
cellPaddingBlockMD: isPhone ? 10 : 12,
|
||||||
|
cellPaddingBlock: isUltraWide ? 14 : 12,
|
||||||
|
selectionColumnWidth: isPhone ? 44 : 52
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isDarkMode, isPhone, isUltraWide]);
|
||||||
|
|
||||||
|
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
|
||||||
|
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
|
||||||
|
const antdPagination = useMemo(
|
||||||
|
() => ({
|
||||||
|
showSizeChanger: !isPhone,
|
||||||
|
totalBoundaryShowSizeChanger: 100
|
||||||
|
}),
|
||||||
|
[isPhone]
|
||||||
|
);
|
||||||
|
|
||||||
|
const antdForm = useMemo(
|
||||||
|
() => ({
|
||||||
|
validateMessages: {
|
||||||
|
required: t("general.validation.required", { label: "${label}" })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Global seamless logout listener with redirect to /signin
|
||||||
|
useEffect(() => {
|
||||||
|
const handleSeamlessLogout = (event) => {
|
||||||
|
if (event.data?.type !== "seamlessLogoutRequest") return;
|
||||||
|
|
||||||
|
// Only accept messages from the parent window
|
||||||
|
if (event.source !== window.parent) return;
|
||||||
|
|
||||||
|
const targetOrigin = event.origin || "*";
|
||||||
|
|
||||||
|
if (currentUser?.authorized !== true) {
|
||||||
|
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(signOutStart());
|
||||||
|
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", handleSeamlessLogout);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handleSeamlessLogout);
|
||||||
|
};
|
||||||
|
}, [dispatch, currentUser?.authorized]);
|
||||||
|
|
||||||
|
// Update data-theme attribute (no cleanup to avoid transient style churn)
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
|
||||||
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
// Sync darkMode with localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
const uid = currentUser?.uid;
|
||||||
|
|
||||||
|
if (!uid) {
|
||||||
|
dispatch(setDarkMode(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = `dark-mode-${uid}`;
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (raw == null) {
|
||||||
|
dispatch(setDarkMode(false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
|
||||||
|
} catch {
|
||||||
|
dispatch(setDarkMode(false));
|
||||||
|
}
|
||||||
|
}, [currentUser?.uid, dispatch]);
|
||||||
|
|
||||||
|
// Persist darkMode
|
||||||
|
useEffect(() => {
|
||||||
|
const uid = currentUser?.uid;
|
||||||
|
if (!uid) return;
|
||||||
|
|
||||||
|
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
|
||||||
|
}, [isDarkMode, currentUser?.uid]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CookiesProvider>
|
||||||
|
<ApolloProvider client={client}>
|
||||||
|
<ConfigProvider
|
||||||
|
input={antdInput}
|
||||||
|
locale={enLocale}
|
||||||
|
theme={theme}
|
||||||
|
form={antdForm}
|
||||||
|
table={antdTable}
|
||||||
|
pagination={antdPagination}
|
||||||
|
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
|
||||||
|
popupOverflow="viewport"
|
||||||
|
>
|
||||||
|
<GlobalLoadingBar />
|
||||||
|
<SplitFactoryProvider config={config}>
|
||||||
|
<SplitClientProvider>
|
||||||
|
<App />
|
||||||
|
</SplitClientProvider>
|
||||||
|
</SplitFactoryProvider>
|
||||||
|
</ConfigProvider>
|
||||||
|
</ApolloProvider>
|
||||||
|
</CookiesProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Sentry.withProfiler(AppContainer);
|
||||||
@@ -443,38 +443,62 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* DMS top panels: prevent card/table overflow into adjacent column at desktop+zoom */
|
||||||
|
.dms-top-panel-col {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dms-top-panel-col > .ant-card {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dms-top-panel-col > .ant-card .ant-card-body {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dms-top-panel-col .ant-table-wrapper,
|
||||||
|
.dms-top-panel-col .ant-tabs,
|
||||||
|
.dms-top-panel-col .ant-tabs-content,
|
||||||
|
.dms-top-panel-col .ant-tabs-tabpane {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
//.rbc-time-header-gutter {
|
//.rbc-time-header-gutter {
|
||||||
// padding: 0;
|
// padding: 0;
|
||||||
//}
|
//}
|
||||||
|
|
||||||
/* globally allow shrink inside table cells */
|
///* globally allow shrink inside table cells */
|
||||||
.prod-list-table .ant-table-cell,
|
//.prod-list-table .ant-table-cell,
|
||||||
.prod-list-table .ant-table-cell > * {
|
//.prod-list-table .ant-table-cell > * {
|
||||||
min-width: 0;
|
// min-width: 0;
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/* common AntD offenders */
|
///* common AntD offenders */
|
||||||
.prod-list-table > .ant-table-cell .ant-space,
|
//.prod-list-table > .ant-table-cell .ant-space,
|
||||||
.ant-table-cell .ant-space-item {
|
//.ant-table-cell .ant-space-item {
|
||||||
min-width: 0;
|
// min-width: 0;
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
/* Keep your custom header content on the left, push AntD sorter to the far right */
|
///* Keep your custom header content on the left, push AntD sorter to the far right */
|
||||||
.prod-list-table .ant-table-column-sorters {
|
//.prod-list-table .ant-table-column-sorters {
|
||||||
display: flex !important;
|
// display: flex !important;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
width: 100%;
|
// width: 100%;
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
.prod-list-table .ant-table-column-title {
|
//.prod-list-table .ant-table-column-title {
|
||||||
flex: 1 1 auto;
|
// flex: 1 1 auto;
|
||||||
min-width: 0; /* allows ellipsis to work */
|
// min-width: 0; /* allows ellipsis to work */
|
||||||
}
|
//}
|
||||||
|
//
|
||||||
.prod-list-table .ant-table-column-sorter {
|
//.prod-list-table .ant-table-column-sorter {
|
||||||
margin-left: auto;
|
// margin-left: auto;
|
||||||
flex: 0 0 auto;
|
// flex: 0 0 auto;
|
||||||
}
|
//}
|
||||||
|
|
||||||
|
|
||||||
.global-search-autocomplete-fix {
|
.global-search-autocomplete-fix {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ function BillEnterAiScan({
|
|||||||
setIsAiScan(true);
|
setIsAiScan(true);
|
||||||
const formdata = new FormData();
|
const formdata = new FormData();
|
||||||
formdata.append("billScan", file);
|
formdata.append("billScan", file);
|
||||||
formdata.append("jobid", billEnterModal.context.job?.id);
|
formdata.append("jobid", form.getFieldValue("jobid") || billEnterModal.context.job?.id);
|
||||||
formdata.append("bodyshopid", bodyshop.id);
|
formdata.append("bodyshopid", bodyshop.id);
|
||||||
formdata.append("partsorderid", billEnterModal.context.parts_order?.id);
|
formdata.append("partsorderid", billEnterModal.context.parts_order?.id);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import dayjs from "../../utils/day";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
isDarkMode: selectDarkMode
|
isDarkMode: selectDarkMode
|
||||||
@@ -19,25 +20,35 @@ export function DmsLogEvents({
|
|||||||
detailsNonce,
|
detailsNonce,
|
||||||
isDarkMode,
|
isDarkMode,
|
||||||
colorizeJson = false,
|
colorizeJson = false,
|
||||||
showDetails = true
|
showDetails = true,
|
||||||
|
allowXmlPayload = true
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [openSet, setOpenSet] = useState(() => new Set());
|
const [openSet, setOpenSet] = useState(() => new Set());
|
||||||
|
const [copiedKey, setCopiedKey] = useState(null);
|
||||||
|
|
||||||
// Inject JSON highlight styles once (only when colorize is enabled)
|
// Inject JSON highlight styles once (only when colorize is enabled)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!colorizeJson) return;
|
if (!colorizeJson) return;
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
if (document.getElementById("json-highlight-styles")) return;
|
let style = document.getElementById("json-highlight-styles");
|
||||||
const style = document.createElement("style");
|
if (!style) {
|
||||||
style.id = "json-highlight-styles";
|
style = document.createElement("style");
|
||||||
|
style.id = "json-highlight-styles";
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.json-key { color: #fa8c16; }
|
.json-key { color: #fa8c16; }
|
||||||
.json-string { color: #52c41a; }
|
.json-string { color: #52c41a; }
|
||||||
.json-number { color: #722ed1; }
|
.json-number { color: #722ed1; }
|
||||||
.json-boolean { color: #1890ff; }
|
.json-boolean { color: #1890ff; }
|
||||||
.json-null { color: #faad14; }
|
.json-null { color: #faad14; }
|
||||||
|
.xml-tag { color: #1677ff; }
|
||||||
|
.xml-attr { color: #d46b08; }
|
||||||
|
.xml-value { color: #389e0d; }
|
||||||
|
.xml-decl { color: #7c3aed; }
|
||||||
|
.xml-comment { color: #8c8c8c; }
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
|
||||||
}, [colorizeJson]);
|
}, [colorizeJson]);
|
||||||
|
|
||||||
// Trim openSet if logs shrink
|
// Trim openSet if logs shrink
|
||||||
@@ -65,6 +76,13 @@ export function DmsLogEvents({
|
|||||||
// Only treat meta as "present" when we are allowed to show details
|
// Only treat meta as "present" when we are allowed to show details
|
||||||
const hasMeta = !isEmpty(meta) && showDetails;
|
const hasMeta = !isEmpty(meta) && showDetails;
|
||||||
const isOpen = hasMeta && openSet.has(idx);
|
const isOpen = hasMeta && openSet.has(idx);
|
||||||
|
const xml = hasMeta && allowXmlPayload ? extractXmlFromMeta(meta) : { request: null, response: null };
|
||||||
|
const hasRequestXml = !!xml.request;
|
||||||
|
const hasResponseXml = !!xml.response;
|
||||||
|
const copyPayload = hasMeta ? getCopyPayload(meta) : null;
|
||||||
|
const copyPayloadKey = `copy-${idx}`;
|
||||||
|
const copyReqKey = `copy-req-${idx}`;
|
||||||
|
const copyResKey = `copy-res-${idx}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: idx,
|
key: idx,
|
||||||
@@ -92,10 +110,42 @@ export function DmsLogEvents({
|
|||||||
return next;
|
return next;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
style={{ cursor: "pointer", userSelect: "none" }}
|
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
|
||||||
>
|
>
|
||||||
{isOpen ? "Hide details" : "Details"}
|
{isOpen ? t("dms.labels.hide_details") : t("dms.labels.details")}
|
||||||
</a>
|
</a>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={() => handleCopyAction(copyPayloadKey, copyPayload, setCopiedKey)}
|
||||||
|
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{copiedKey === copyPayloadKey ? t("dms.labels.copied") : t("dms.labels.copy")}
|
||||||
|
</a>
|
||||||
|
{hasRequestXml && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={() => handleCopyAction(copyReqKey, xml.request, setCopiedKey)}
|
||||||
|
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{copiedKey === copyReqKey ? t("dms.labels.copied") : t("dms.labels.copy_request")}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{hasResponseXml && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={() => handleCopyAction(copyResKey, xml.response, setCopiedKey)}
|
||||||
|
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{copiedKey === copyResKey ? t("dms.labels.copied") : t("dms.labels.copy_response")}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -103,14 +153,30 @@ export function DmsLogEvents({
|
|||||||
{/* Row 2: details body (only when open) */}
|
{/* Row 2: details body (only when open) */}
|
||||||
{hasMeta && isOpen && (
|
{hasMeta && isOpen && (
|
||||||
<div style={{ marginLeft: 6 }}>
|
<div style={{ marginLeft: 6 }}>
|
||||||
<JsonBlock isDarkMode={isDarkMode} data={meta} colorize={colorizeJson} />
|
<JsonBlock isDarkMode={isDarkMode} data={removeXmlFromMeta(meta)} colorize={colorizeJson} />
|
||||||
|
{hasRequestXml && (
|
||||||
|
<XmlBlock
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
title={t("dms.labels.request_xml")}
|
||||||
|
xmlText={xml.request}
|
||||||
|
colorize={colorizeJson}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{hasResponseXml && (
|
||||||
|
<XmlBlock
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
title={t("dms.labels.response_xml")}
|
||||||
|
xmlText={xml.response}
|
||||||
|
colorize={colorizeJson}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
[logs, openSet, colorizeJson, isDarkMode, showDetails]
|
[logs, openSet, colorizeJson, copiedKey, isDarkMode, showDetails, allowXmlPayload, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Timeline reverse items={items} />;
|
return <Timeline reverse items={items} />;
|
||||||
@@ -179,6 +245,121 @@ const safeStringify = (obj, spaces = 2) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get request/response XML from various Reynolds log meta shapes.
|
||||||
|
* @param meta
|
||||||
|
* @returns {{request: string|null, response: string|null}}
|
||||||
|
*/
|
||||||
|
const extractXmlFromMeta = (meta) => {
|
||||||
|
const request =
|
||||||
|
firstString(meta?.requestXml) ||
|
||||||
|
firstString(meta?.xml?.request) ||
|
||||||
|
firstString(meta?.response?.xml?.request) ||
|
||||||
|
firstString(meta?.response?.requestXml);
|
||||||
|
|
||||||
|
const response =
|
||||||
|
firstString(meta?.responseXml) || firstString(meta?.xml?.response) || firstString(meta?.response?.xml?.response);
|
||||||
|
|
||||||
|
return { request, response };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the value to copy when clicking the "Copy" action.
|
||||||
|
* @param meta
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const getCopyPayload = (meta) => {
|
||||||
|
if (meta?.payload != null) return meta.payload;
|
||||||
|
return meta;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove bulky XML fields from object shown in JSON block (XML is rendered separately).
|
||||||
|
* @param meta
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const removeXmlFromMeta = (meta) => {
|
||||||
|
if (meta == null || typeof meta !== "object") return meta;
|
||||||
|
const cloned = safeClone(meta);
|
||||||
|
if (cloned == null || typeof cloned !== "object") return meta;
|
||||||
|
|
||||||
|
if (typeof cloned.requestXml === "string") delete cloned.requestXml;
|
||||||
|
if (typeof cloned.responseXml === "string") delete cloned.responseXml;
|
||||||
|
|
||||||
|
if (cloned.xml && typeof cloned.xml === "object") {
|
||||||
|
if (typeof cloned.xml.request === "string") delete cloned.xml.request;
|
||||||
|
if (typeof cloned.xml.response === "string") delete cloned.xml.response;
|
||||||
|
if (isEmpty(cloned.xml)) delete cloned.xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cloned.response?.xml && typeof cloned.response.xml === "object") {
|
||||||
|
if (typeof cloned.response.xml.request === "string") delete cloned.response.xml.request;
|
||||||
|
if (typeof cloned.response.xml.response === "string") delete cloned.response.xml.response;
|
||||||
|
if (isEmpty(cloned.response.xml)) delete cloned.response.xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloned;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe deep clone for plain JSON structures.
|
||||||
|
* @param value
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const safeClone = (value) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First non-empty string helper.
|
||||||
|
* @param value
|
||||||
|
* @returns {string|null}
|
||||||
|
*/
|
||||||
|
const firstString = (value) => {
|
||||||
|
if (typeof value !== "string") return null;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed ? trimmed : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy arbitrary text/object to clipboard.
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @param setCopied
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const handleCopyAction = async (key, value, setCopied) => {
|
||||||
|
const text = typeof value === "string" ? value : safeStringify(value, 2);
|
||||||
|
if (!text) return;
|
||||||
|
const copied = await copyTextToClipboard(text);
|
||||||
|
if (!copied) return;
|
||||||
|
setCopied(key);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied((prev) => (prev === key ? null : prev));
|
||||||
|
}, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clipboard helper (modern async Clipboard API).
|
||||||
|
* @param text
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
const copyTextToClipboard = async (text) => {
|
||||||
|
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON display block with optional syntax highlighting.
|
* JSON display block with optional syntax highlighting.
|
||||||
* @param data
|
* @param data
|
||||||
@@ -210,6 +391,105 @@ const JsonBlock = ({ data, colorize, isDarkMode }) => {
|
|||||||
return <pre style={preStyle}>{jsonText}</pre>;
|
return <pre style={preStyle}>{jsonText}</pre>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XML display block with normalized indentation.
|
||||||
|
* @param title
|
||||||
|
* @param xmlText
|
||||||
|
* @param isDarkMode
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
const XmlBlock = ({ title, xmlText, isDarkMode, colorize = false }) => {
|
||||||
|
const base = {
|
||||||
|
margin: "8px 0 0",
|
||||||
|
maxWidth: 720,
|
||||||
|
overflowX: "auto",
|
||||||
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 1.45,
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.04)",
|
||||||
|
border: isDarkMode ? "1px solid rgba(255,255,255,0.12)" : "1px solid rgba(0,0,0,0.08)",
|
||||||
|
color: isDarkMode ? "var(--card-text-fallback)" : "#141414",
|
||||||
|
whiteSpace: "pre"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6 }}>
|
||||||
|
<div style={{ fontSize: 11, fontWeight: 600 }}>{title}</div>
|
||||||
|
{colorize ? (
|
||||||
|
<pre style={base} dangerouslySetInnerHTML={{ __html: highlightXml(formatXml(xmlText)) }} />
|
||||||
|
) : (
|
||||||
|
<pre style={base}>{formatXml(xmlText)}</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic XML pretty-printer.
|
||||||
|
* @param xml
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const formatXml = (xml) => {
|
||||||
|
if (typeof xml !== "string") return "";
|
||||||
|
const normalized = xml.replace(/\r\n/g, "\n").replace(/>\s*</g, ">\n<").trim();
|
||||||
|
const lines = normalized.split("\n");
|
||||||
|
let indent = 0;
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
for (const rawLine of lines) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
if (/^<\/[^>]+>/.test(line)) indent = Math.max(indent - 1, 0);
|
||||||
|
out.push(`${" ".repeat(indent)}${line}`);
|
||||||
|
|
||||||
|
const opens = (line.match(/<[^/!?][^>]*>/g) || []).length;
|
||||||
|
const closes = (line.match(/<\/[^>]+>/g) || []).length;
|
||||||
|
const selfClosing = (line.match(/<[^>]+\/>/g) || []).length;
|
||||||
|
const declaration = /^<\?xml/.test(line) ? 1 : 0;
|
||||||
|
|
||||||
|
indent += opens - closes - selfClosing - declaration;
|
||||||
|
if (indent < 0) indent = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syntax highlight pretty-printed XML text for HTML display.
|
||||||
|
* @param xmlText
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const highlightXml = (xmlText) => {
|
||||||
|
const esc = String(xmlText || "")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
const lines = esc.split("\n");
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map((line) => {
|
||||||
|
let out = line;
|
||||||
|
|
||||||
|
out = out.replace(/(<!--[\s\S]*?-->)/g, '<span class="xml-comment">$1</span>');
|
||||||
|
out = out.replace(/(<\?xml[\s\S]*?\?>)/g, '<span class="xml-decl">$1</span>');
|
||||||
|
|
||||||
|
out = out.replace(/(<\/?)([A-Za-z_][\w:.-]*)([\s\S]*?)(\/?>)/g, (_m, open, tag, attrs, close) => {
|
||||||
|
const coloredAttrs = attrs.replace(
|
||||||
|
/([A-Za-z_][\w:.-]*)(=)("[^"]*"|'[^']*'|"[\s\S]*?"|'[\s\S]*?')/g,
|
||||||
|
'<span class="xml-attr">$1</span>$2<span class="xml-value">$3</span>'
|
||||||
|
);
|
||||||
|
return `${open}<span class="xml-tag">${tag}</span>${coloredAttrs}${close}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syntax highlight JSON text for HTML display.
|
* Syntax highlight JSON text for HTML display.
|
||||||
* @param jsonText
|
* @param jsonText
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Grid, Table } from "antd";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import "./responsive-table.styles.scss";
|
||||||
|
|
||||||
|
function ResponsiveTable({ className, columns, mobileColumnKeys, scroll, tableLayout, ...rest }) {
|
||||||
|
const screens = Grid.useBreakpoint();
|
||||||
|
const isPhone = !screens.md;
|
||||||
|
const isCompactViewport = !screens.lg;
|
||||||
|
const prefersHorizontalScroll = isPhone || isCompactViewport;
|
||||||
|
const isResponsiveFilteringEnabled = ["1", "true", "yes", "on"].includes(
|
||||||
|
String(import.meta.env.VITE_APP_ENABLE_RESPONSIVE_TABLE_FILTERING || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvedColumns = useMemo(() => {
|
||||||
|
if (
|
||||||
|
!isResponsiveFilteringEnabled ||
|
||||||
|
!Array.isArray(columns) ||
|
||||||
|
!isPhone ||
|
||||||
|
!Array.isArray(mobileColumnKeys) ||
|
||||||
|
mobileColumnKeys.length === 0
|
||||||
|
) {
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleColumnKeys = new Set(mobileColumnKeys);
|
||||||
|
const filteredColumns = columns.filter((column) => {
|
||||||
|
const key = column?.key ?? column?.dataIndex;
|
||||||
|
|
||||||
|
// Keep columns with no stable key to avoid accidental loss.
|
||||||
|
if (key == null) return true;
|
||||||
|
|
||||||
|
if (Array.isArray(key)) {
|
||||||
|
return key.some((part) => visibleColumnKeys.has(part));
|
||||||
|
}
|
||||||
|
|
||||||
|
return visibleColumnKeys.has(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
return filteredColumns.length > 0 ? filteredColumns : columns;
|
||||||
|
}, [columns, isPhone, isResponsiveFilteringEnabled, mobileColumnKeys]);
|
||||||
|
|
||||||
|
const resolvedScroll = useMemo(() => {
|
||||||
|
if (prefersHorizontalScroll) {
|
||||||
|
if (scroll == null) {
|
||||||
|
return { x: "max-content" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof scroll !== "object" || Array.isArray(scroll)) {
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, ...baseScroll } = scroll;
|
||||||
|
|
||||||
|
return { ...baseScroll, x: x ?? "max-content" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scroll == null) {
|
||||||
|
// Explicitly override ConfigProvider table.scroll desktop defaults.
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof scroll !== "object" || Array.isArray(scroll)) {
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { x, ...desktopScroll } = scroll;
|
||||||
|
|
||||||
|
// On desktop we prefer fitting columns with ellipsis over forced horizontal scroll.
|
||||||
|
if (x == null) {
|
||||||
|
return desktopScroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
return desktopScroll;
|
||||||
|
}, [prefersHorizontalScroll, scroll]);
|
||||||
|
|
||||||
|
const resolvedTableLayout = tableLayout ?? (prefersHorizontalScroll ? "auto" : "fixed");
|
||||||
|
const responsiveClassName = prefersHorizontalScroll ? undefined : "responsive-table-fit";
|
||||||
|
const resolvedClassName = [responsiveClassName, className].filter(Boolean).join(" ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
className={resolvedClassName}
|
||||||
|
columns={resolvedColumns}
|
||||||
|
scroll={resolvedScroll}
|
||||||
|
tableLayout={resolvedTableLayout}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponsiveTable.Summary = Table.Summary;
|
||||||
|
ResponsiveTable.Column = Table.Column;
|
||||||
|
ResponsiveTable.ColumnGroup = Table.ColumnGroup;
|
||||||
|
ResponsiveTable.SELECTION_COLUMN = Table.SELECTION_COLUMN;
|
||||||
|
ResponsiveTable.EXPAND_COLUMN = Table.EXPAND_COLUMN;
|
||||||
|
|
||||||
|
export default ResponsiveTable;
|
||||||
@@ -1,93 +1,7 @@
|
|||||||
import { Grid, Table } from "antd";
|
import { Table } from "antd";
|
||||||
import { useMemo } from "react";
|
|
||||||
import "./responsive-table.styles.scss";
|
|
||||||
|
|
||||||
function ResponsiveTable({ className, columns, mobileColumnKeys, scroll, tableLayout, ...rest }) {
|
function ResponsiveTable(props) {
|
||||||
const screens = Grid.useBreakpoint();
|
return <Table {...props} />;
|
||||||
const isPhone = !screens.md;
|
|
||||||
const isCompactViewport = !screens.lg;
|
|
||||||
const prefersHorizontalScroll = isPhone || isCompactViewport;
|
|
||||||
const isResponsiveFilteringEnabled = ["1", "true", "yes", "on"].includes(
|
|
||||||
String(import.meta.env.VITE_APP_ENABLE_RESPONSIVE_TABLE_FILTERING || "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
const resolvedColumns = useMemo(() => {
|
|
||||||
if (
|
|
||||||
!isResponsiveFilteringEnabled ||
|
|
||||||
!Array.isArray(columns) ||
|
|
||||||
!isPhone ||
|
|
||||||
!Array.isArray(mobileColumnKeys) ||
|
|
||||||
mobileColumnKeys.length === 0
|
|
||||||
) {
|
|
||||||
return columns;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibleColumnKeys = new Set(mobileColumnKeys);
|
|
||||||
const filteredColumns = columns.filter((column) => {
|
|
||||||
const key = column?.key ?? column?.dataIndex;
|
|
||||||
|
|
||||||
// Keep columns with no stable key to avoid accidental loss.
|
|
||||||
if (key == null) return true;
|
|
||||||
|
|
||||||
if (Array.isArray(key)) {
|
|
||||||
return key.some((part) => visibleColumnKeys.has(part));
|
|
||||||
}
|
|
||||||
|
|
||||||
return visibleColumnKeys.has(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredColumns.length > 0 ? filteredColumns : columns;
|
|
||||||
}, [columns, isPhone, isResponsiveFilteringEnabled, mobileColumnKeys]);
|
|
||||||
|
|
||||||
const resolvedScroll = useMemo(() => {
|
|
||||||
if (prefersHorizontalScroll) {
|
|
||||||
if (scroll == null) {
|
|
||||||
return { x: "max-content" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof scroll !== "object" || Array.isArray(scroll)) {
|
|
||||||
return scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { x, ...baseScroll } = scroll;
|
|
||||||
|
|
||||||
return { ...baseScroll, x: x ?? "max-content" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scroll == null) {
|
|
||||||
// Explicitly override ConfigProvider table.scroll desktop defaults.
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof scroll !== "object" || Array.isArray(scroll)) {
|
|
||||||
return scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { x, ...desktopScroll } = scroll;
|
|
||||||
|
|
||||||
// On desktop we prefer fitting columns with ellipsis over forced horizontal scroll.
|
|
||||||
if (x == null) {
|
|
||||||
return desktopScroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
return desktopScroll;
|
|
||||||
}, [prefersHorizontalScroll, scroll]);
|
|
||||||
|
|
||||||
const resolvedTableLayout = tableLayout ?? (prefersHorizontalScroll ? "auto" : "fixed");
|
|
||||||
const responsiveClassName = prefersHorizontalScroll ? undefined : "responsive-table-fit";
|
|
||||||
const resolvedClassName = [responsiveClassName, className].filter(Boolean).join(" ");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table
|
|
||||||
className={resolvedClassName}
|
|
||||||
columns={resolvedColumns}
|
|
||||||
scroll={resolvedScroll}
|
|
||||||
tableLayout={resolvedTableLayout}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponsiveTable.Summary = Table.Summary;
|
ResponsiveTable.Summary = Table.Summary;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function TimeTicketModalComponent({
|
|||||||
} = useTreatmentsWithConfig({
|
} = useTreatmentsWithConfig({
|
||||||
attributes: {},
|
attributes: {},
|
||||||
names: ["Enhanced_Payroll"],
|
names: ["Enhanced_Payroll"],
|
||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop?.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loadLineTicketData, { loading, data: lineTicketData, refetch }] = useLazyQuery(GET_LINE_TICKET_BY_PK, {
|
const [loadLineTicketData, { loading, data: lineTicketData, refetch }] = useLazyQuery(GET_LINE_TICKET_BY_PK, {
|
||||||
@@ -347,7 +347,7 @@ export function LaborAllocationContainer({
|
|||||||
} = useTreatmentsWithConfig({
|
} = useTreatmentsWithConfig({
|
||||||
attributes: {},
|
attributes: {},
|
||||||
names: ["Enhanced_Payroll"],
|
names: ["Enhanced_Payroll"],
|
||||||
splitKey: bodyshop.imexshopid
|
splitKey: bodyshop?.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading) return <LoadingSkeleton />;
|
if (loading) return <LoadingSkeleton />;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
|||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
|
|
||||||
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
|
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
|
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
@@ -29,7 +29,8 @@ import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-
|
|||||||
import RrAllocationsSummary from "../../components/dms-allocations-summary/rr-dms-allocations-summary.component";
|
import RrAllocationsSummary from "../../components/dms-allocations-summary/rr-dms-allocations-summary.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
@@ -65,7 +66,41 @@ const DMS_SOCKET_EVENTS = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
|
const stripRrXmlFromPayload = (input) => {
|
||||||
|
if (input == null || typeof input !== "object") return input;
|
||||||
|
|
||||||
|
let target = null;
|
||||||
|
try {
|
||||||
|
target = JSON.parse(JSON.stringify(input));
|
||||||
|
} catch {
|
||||||
|
// Fallback to in-place scrub if cloning fails.
|
||||||
|
target = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrub = (node) => {
|
||||||
|
if (node == null || typeof node !== "object") return;
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
node.forEach(scrub);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete node.requestXml;
|
||||||
|
delete node.responseXml;
|
||||||
|
|
||||||
|
if (node.xml && typeof node.xml === "object") {
|
||||||
|
delete node.xml.request;
|
||||||
|
delete node.xml.response;
|
||||||
|
if (Object.keys(node.xml).length === 0) delete node.xml;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(node).forEach(scrub);
|
||||||
|
};
|
||||||
|
|
||||||
|
scrub(target);
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DmsContainer({ bodyshop, currentUser, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
|
||||||
const {
|
const {
|
||||||
treatments: { Fortellis }
|
treatments: { Fortellis }
|
||||||
} = useTreatmentsWithConfig({
|
} = useTreatmentsWithConfig({
|
||||||
@@ -79,6 +114,15 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
const [allocationsSummary, setAllocationsSummary] = useState(null);
|
const [allocationsSummary, setAllocationsSummary] = useState(null);
|
||||||
const [reconnectNonce, setReconnectNonce] = useState(0);
|
const [reconnectNonce, setReconnectNonce] = useState(0);
|
||||||
|
|
||||||
|
const isDevEnv = import.meta.env.DEV;
|
||||||
|
const isProdEnv = import.meta.env.PROD;
|
||||||
|
const userEmail = (currentUser?.email || "").toLowerCase();
|
||||||
|
|
||||||
|
const devEmails = ["imex.dev", "rome.dev"];
|
||||||
|
const prodEmails = ["imex.prod", "rome.prod", "imex.test", "rome.test"];
|
||||||
|
const hasValidEmail = (emails) => emails.some((email) => userEmail.endsWith(email));
|
||||||
|
const canViewSensitiveRrXml = (isDevEnv && hasValidEmail(devEmails)) || (isProdEnv && hasValidEmail(prodEmails));
|
||||||
|
|
||||||
// Compute a single normalized mode and pick the proper socket
|
// Compute a single normalized mode and pick the proper socket
|
||||||
const mode = getDmsMode(bodyshop, Fortellis.treatment); // "rr" | "fortellis" | "cdk" | "pbs" | "none"
|
const mode = getDmsMode(bodyshop, Fortellis.treatment); // "rr" | "fortellis" | "cdk" | "pbs" | "none"
|
||||||
|
|
||||||
@@ -164,19 +208,21 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
const providerLabel = useMemo(
|
const providerLabel = useMemo(
|
||||||
() =>
|
() =>
|
||||||
({
|
({
|
||||||
[DMS_MAP.reynolds]: "Reynolds",
|
[DMS_MAP.reynolds]: t("dms.labels.provider_reynolds"),
|
||||||
[DMS_MAP.fortellis]: "Fortellis",
|
[DMS_MAP.fortellis]: t("dms.labels.provider_fortellis"),
|
||||||
[DMS_MAP.cdk]: "CDK",
|
[DMS_MAP.cdk]: t("dms.labels.provider_cdk"),
|
||||||
[DMS_MAP.pbs]: "PBS"
|
[DMS_MAP.pbs]: t("dms.labels.provider_pbs")
|
||||||
})[mode] || "DMS",
|
})[mode] || t("dms.labels.provider_dms"),
|
||||||
[mode]
|
[mode, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const transportLabel = isWssMode(mode) ? "(WSS)" : "(WS)";
|
const transportLabel = isWssMode(mode) ? t("dms.labels.transport_wss") : t("dms.labels.transport_ws");
|
||||||
|
|
||||||
const bannerMessage = `Posting to ${providerLabel} | ${transportLabel} | ${
|
const bannerMessage = t("dms.labels.banner_message", {
|
||||||
isConnected ? "Connected" : "Disconnected"
|
provider: providerLabel,
|
||||||
}`;
|
transport: transportLabel,
|
||||||
|
status: isConnected ? t("dms.labels.banner_status_connected") : t("dms.labels.banner_status_disconnected")
|
||||||
|
});
|
||||||
|
|
||||||
const resetKey = useMemo(() => `${mode || "none"}-${jobId || "none"}`, [mode, jobId]);
|
const resetKey = useMemo(() => `${mode || "none"}-${jobId || "none"}`, [mode, jobId]);
|
||||||
const customerSelectorKey = useMemo(() => `${resetKey}-${reconnectNonce}`, [resetKey, reconnectNonce]);
|
const customerSelectorKey = useMemo(() => `${resetKey}-${reconnectNonce}`, [resetKey, reconnectNonce]);
|
||||||
@@ -239,6 +285,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
}, [jobId, mode, activeSocket]);
|
}, [jobId, mode, activeSocket]);
|
||||||
|
|
||||||
const handleExportFailed = (payload = {}) => {
|
const handleExportFailed = (payload = {}) => {
|
||||||
|
const safePayload = canViewSensitiveRrXml ? payload : stripRrXmlFromPayload(payload);
|
||||||
const { title, friendlyMessage, error: errText, severity, errorCode, vendorStatusCode } = payload;
|
const { title, friendlyMessage, error: errText, severity, errorCode, vendorStatusCode } = payload;
|
||||||
|
|
||||||
const msg =
|
const msg =
|
||||||
@@ -246,7 +293,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
errText ||
|
errText ||
|
||||||
t("dms.errors.exportfailedgeneric", "We couldn't complete the export. Please try again.");
|
t("dms.errors.exportfailedgeneric", "We couldn't complete the export. Please try again.");
|
||||||
|
|
||||||
const vendorTitle = title || (isRrMode ? "Reynolds" : "DMS");
|
const vendorTitle = title || (isRrMode ? t("dms.labels.provider_reynolds") : t("dms.labels.provider_dms"));
|
||||||
|
|
||||||
const isRrOpenRoLimit =
|
const isRrOpenRoLimit =
|
||||||
isRrMode &&
|
isRrMode &&
|
||||||
@@ -269,7 +316,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
level: (sev || "error").toUpperCase(),
|
level: (sev || "error").toUpperCase(),
|
||||||
message: `${vendorTitle}: ${msg}`,
|
message: `${vendorTitle}: ${msg}`,
|
||||||
meta: { errorCode, vendorStatusCode, raw: payload, blockedByOpenRoLimit: !!isRrOpenRoLimit }
|
meta: { errorCode, vendorStatusCode, raw: safePayload, blockedByOpenRoLimit: !!isRrOpenRoLimit }
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -321,7 +368,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
{
|
{
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
level: "warn",
|
level: "warn",
|
||||||
message: `Reconnected to ${isRrMode ? "RR" : mode === DMS_MAP.fortellis ? "Fortellis" : "DMS"} Export Service`
|
message: t("dms.labels.reconnected_export_service", {
|
||||||
|
provider: isRrMode ? t("dms.labels.provider_reynolds") : providerLabel
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -340,11 +389,16 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
// Logs
|
// Logs
|
||||||
const onLog = isRrMode
|
const onLog = isRrMode
|
||||||
? (payload = {}) => {
|
? (payload = {}) => {
|
||||||
|
const safePayload = canViewSensitiveRrXml ? payload : stripRrXmlFromPayload(payload);
|
||||||
const normalized = {
|
const normalized = {
|
||||||
timestamp: payload.timestamp ? new Date(payload.timestamp) : payload.ts ? new Date(payload.ts) : new Date(),
|
timestamp: safePayload.timestamp
|
||||||
level: (payload.level || "INFO").toUpperCase(),
|
? new Date(safePayload.timestamp)
|
||||||
message: payload.message || payload.msg || "",
|
: safePayload.ts
|
||||||
meta: payload.meta ?? payload.ctx ?? payload.details ?? null
|
? new Date(safePayload.ts)
|
||||||
|
: new Date(),
|
||||||
|
level: (safePayload.level || "INFO").toUpperCase(),
|
||||||
|
message: safePayload.message || safePayload.msg || "",
|
||||||
|
meta: safePayload.meta ?? safePayload.ctx ?? safePayload.details ?? null
|
||||||
};
|
};
|
||||||
setLogs((prev) => [...prev, normalized]);
|
setLogs((prev) => [...prev, normalized]);
|
||||||
}
|
}
|
||||||
@@ -380,14 +434,12 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
{
|
{
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
level: "INFO",
|
level: "INFO",
|
||||||
message:
|
message: t("dms.labels.rr_validation_message")
|
||||||
"Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize."
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
notification.info({
|
notification.info({
|
||||||
title: "Reynolds RO created",
|
title: t("dms.labels.rr_validation_notice_title"),
|
||||||
description:
|
description: t("dms.labels.rr_validation_notice_description"),
|
||||||
"Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.",
|
|
||||||
duration: 8
|
duration: 8
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -399,8 +451,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
{
|
{
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
level: "INFO",
|
level: "INFO",
|
||||||
message:
|
message: t("dms.labels.rr_validation_message"),
|
||||||
"Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.",
|
|
||||||
meta: { payload }
|
meta: { payload }
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
@@ -428,7 +479,19 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
activeSocket.disconnect();
|
activeSocket.disconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [mode, activeSocket, channels, logLevel, notification, t, insertAuditTrail, history]);
|
}, [
|
||||||
|
mode,
|
||||||
|
activeSocket,
|
||||||
|
channels,
|
||||||
|
logLevel,
|
||||||
|
notification,
|
||||||
|
t,
|
||||||
|
insertAuditTrail,
|
||||||
|
history,
|
||||||
|
isRrMode,
|
||||||
|
providerLabel,
|
||||||
|
canViewSensitiveRrXml
|
||||||
|
]);
|
||||||
|
|
||||||
// RR finalize callback (unchanged public behavior)
|
// RR finalize callback (unchanged public behavior)
|
||||||
const handleRrValidationFinished = () => {
|
const handleRrValidationFinished = () => {
|
||||||
@@ -471,7 +534,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
<AlertComponent style={{ marginBottom: 10 }} title={bannerMessage} type="warning" showIcon closable />
|
<AlertComponent style={{ marginBottom: 10 }} title={bannerMessage} type="warning" showIcon closable />
|
||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col md={24} lg={10} className="dms-equal-height-col">
|
<Col xs={24} xxl={10} className="dms-equal-height-col dms-top-panel-col">
|
||||||
{!isRrMode ? (
|
{!isRrMode ? (
|
||||||
<DmsAllocationsSummary
|
<DmsAllocationsSummary
|
||||||
key={resetKey}
|
key={resetKey}
|
||||||
@@ -511,7 +574,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col md={24} lg={14} className="dms-equal-height-col">
|
<Col xs={24} xxl={14} className="dms-equal-height-col dms-top-panel-col">
|
||||||
<DmsPostForm
|
<DmsPostForm
|
||||||
key={resetKey}
|
key={resetKey}
|
||||||
socket={activeSocket}
|
socket={activeSocket}
|
||||||
@@ -550,15 +613,17 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
<Switch
|
<Switch
|
||||||
checked={colorizeJson}
|
checked={colorizeJson}
|
||||||
onChange={setColorizeJson}
|
onChange={setColorizeJson}
|
||||||
checkedChildren="Color JSON"
|
checkedChildren={t("dms.labels.color_json")}
|
||||||
unCheckedChildren="Plain JSON"
|
unCheckedChildren={t("dms.labels.plain_json")}
|
||||||
/>
|
/>
|
||||||
<Button onClick={toggleDetailsAll}>{detailsOpen ? "Collapse All" : "Expand All"}</Button>
|
<Button onClick={toggleDetailsAll}>
|
||||||
|
{detailsOpen ? t("dms.labels.collapse_all") : t("dms.labels.expand_all")}
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
placeholder="Log Level"
|
placeholder={t("dms.labels.log_level")}
|
||||||
value={logLevel}
|
value={logLevel}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setLogLevel(value);
|
setLogLevel(value);
|
||||||
@@ -572,8 +637,8 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
{ key: "ERROR", value: "ERROR", label: "ERROR" }
|
{ key: "ERROR", value: "ERROR", label: "ERROR" }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => setLogs([])}>Clear Logs</Button>
|
<Button onClick={() => setLogs([])}>{t("dms.labels.clear_logs")}</Button>
|
||||||
<Button onClick={handleReconnectClick}>Reconnect</Button>
|
<Button onClick={handleReconnectClick}> {t("dms.labels.reconnect")}</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -585,6 +650,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
detailsNonce={detailsNonce}
|
detailsNonce={detailsNonce}
|
||||||
colorizeJson={isRrMode ? colorizeJson : false}
|
colorizeJson={isRrMode ? colorizeJson : false}
|
||||||
showDetails={isRrMode}
|
showDetails={isRrMode}
|
||||||
|
allowXmlPayload={canViewSensitiveRrXml}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,14 +10,12 @@ import JobsCreateOwnerInfoContainer from "../../components/jobs-create-owner-inf
|
|||||||
import JobsCreateVehicleInfoContainer from "../../components/jobs-create-vehicle-info/jobs-create-vehicle-info.container";
|
import JobsCreateVehicleInfoContainer from "../../components/jobs-create-vehicle-info/jobs-create-vehicle-info.container";
|
||||||
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
|
||||||
|
|
||||||
export default function JobsCreateComponent({ form }) {
|
export default function JobsCreateComponent({ form, isSubmitting }) {
|
||||||
const [pageIndex, setPageIndex] = useState(0);
|
const [pageIndex, setPageIndex] = useState(0);
|
||||||
|
|
||||||
const [errorMessage, setErrorMessage] = useState(null);
|
const [errorMessage, setErrorMessage] = useState(null);
|
||||||
|
|
||||||
const [state] = useContext(JobCreateContext);
|
const [state] = useContext(JobCreateContext);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
title: t("jobs.labels.create.vehicleinfo"),
|
title: t("jobs.labels.create.vehicleinfo"),
|
||||||
@@ -42,11 +40,9 @@ export default function JobsCreateComponent({ form }) {
|
|||||||
|
|
||||||
const next = () => {
|
const next = () => {
|
||||||
setPageIndex(pageIndex + 1);
|
setPageIndex(pageIndex + 1);
|
||||||
console.log("Next");
|
|
||||||
};
|
};
|
||||||
const prev = () => {
|
const prev = () => {
|
||||||
setPageIndex(pageIndex - 1);
|
setPageIndex(pageIndex - 1);
|
||||||
console.log("Previous");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProgressButtons = ({ top }) => {
|
const ProgressButtons = ({ top }) => {
|
||||||
@@ -79,17 +75,19 @@ export default function JobsCreateComponent({ form }) {
|
|||||||
{pageIndex === steps.length - 1 && (
|
{pageIndex === steps.length - 1 && (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
loading={isSubmitting}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form
|
form
|
||||||
.validateFields()
|
.validateFields()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// NO OP
|
form.submit();
|
||||||
})
|
})
|
||||||
.catch((error) => console.log("error", error));
|
.catch((error) => {
|
||||||
form.submit();
|
console.log("error", error);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Done
|
{t("general.actions.done")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -146,13 +144,11 @@ export default function JobsCreateComponent({ form }) {
|
|||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<ProgressButtons top />
|
<ProgressButtons top />
|
||||||
|
|
||||||
{errorMessage ? (
|
{errorMessage ? (
|
||||||
<div>
|
<div>
|
||||||
<AlertComponent title={errorMessage} type="error" />
|
<AlertComponent title={errorMessage} type="error" />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{steps.map((item, idx) => (
|
{steps.map((item, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr
|
|||||||
});
|
});
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [state, setState] = contextState;
|
const [state, setState] = contextState;
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [insertJob] = useMutation(INSERT_NEW_JOB);
|
const [insertJob] = useMutation(INSERT_NEW_JOB);
|
||||||
const [loadOwner, remoteOwnerData] = useLazyQuery(QUERY_OWNER_FOR_JOB_CREATION);
|
const [loadOwner, remoteOwnerData] = useLazyQuery(QUERY_OWNER_FOR_JOB_CREATION);
|
||||||
|
|
||||||
@@ -83,16 +84,19 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr
|
|||||||
newJobId: resp.data.insert_jobs.returning[0].id
|
newJobId: resp.data.insert_jobs.returning[0].id
|
||||||
});
|
});
|
||||||
logImEXEvent("manual_job_create_completed", {});
|
logImEXEvent("manual_job_create_completed", {});
|
||||||
|
setIsSubmitting(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
notification.error({
|
notification.error({
|
||||||
title: t("jobs.errors.creating", { error: error })
|
title: t("jobs.errors.creating", { error: error })
|
||||||
});
|
});
|
||||||
setState({ ...state, error: error });
|
setState({ ...state, error: error });
|
||||||
|
setIsSubmitting(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = (values) => {
|
||||||
|
setIsSubmitting(true);
|
||||||
let job = Object.assign(
|
let job = Object.assign(
|
||||||
{},
|
{},
|
||||||
values,
|
values,
|
||||||
@@ -297,7 +301,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr
|
|||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<JobsCreateComponent form={form} />
|
<JobsCreateComponent form={form} isSubmitting={isSubmitting} />
|
||||||
</Form>
|
</Form>
|
||||||
</RbacWrapper>
|
</RbacWrapper>
|
||||||
</JobCreateContext.Provider>
|
</JobCreateContext.Provider>
|
||||||
|
|||||||
@@ -1074,7 +1074,36 @@
|
|||||||
"earlyrorequired.message": "This job requires an early Repair Order to be created before posting to Reynolds. Please use the admin panel to create the early RO first."
|
"earlyrorequired.message": "This job requires an early Repair Order to be created before posting to Reynolds. Please use the admin panel to create the early RO first."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"refreshallocations": "Refresh to see DMS Allocations."
|
"refreshallocations": "Refresh to see DMS Allocations.",
|
||||||
|
"provider_reynolds": "Reynolds",
|
||||||
|
"provider_fortellis": "Fortellis",
|
||||||
|
"provider_cdk": "CDK",
|
||||||
|
"provider_pbs": "PBS",
|
||||||
|
"provider_dms": "DMS",
|
||||||
|
"transport_wss": "(WSS)",
|
||||||
|
"transport_ws": "(WS)",
|
||||||
|
"banner_status_connected": "Connected",
|
||||||
|
"banner_status_disconnected": "Disconnected",
|
||||||
|
"banner_message": "Posting to {{provider}} | {{transport}} | {{status}}",
|
||||||
|
"reconnected_export_service": "Reconnected to {{provider}} Export Service",
|
||||||
|
"rr_validation_message": "Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.",
|
||||||
|
"rr_validation_notice_title": "Reynolds RO created",
|
||||||
|
"rr_validation_notice_description": "Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.",
|
||||||
|
"color_json": "Color JSON",
|
||||||
|
"plain_json": "Plain JSON",
|
||||||
|
"collapse_all": "Collapse All",
|
||||||
|
"expand_all": "Expand All",
|
||||||
|
"log_level": "Log Level",
|
||||||
|
"clear_logs": "Clear Logs",
|
||||||
|
"reconnect": "Reconnect",
|
||||||
|
"details": "Details",
|
||||||
|
"hide_details": "Hide details",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"copy_request": "Copy Request",
|
||||||
|
"copy_response": "Copy Response",
|
||||||
|
"request_xml": "Request XML",
|
||||||
|
"response_xml": "Response XML"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
@@ -1266,6 +1295,7 @@
|
|||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"deleteall": "Delete All",
|
"deleteall": "Delete All",
|
||||||
"deselectall": "Deselect All",
|
"deselectall": "Deselect All",
|
||||||
|
"done": "Done",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"gotoadmin": "Go to Admin Panel",
|
"gotoadmin": "Go to Admin Panel",
|
||||||
|
|||||||
@@ -1074,7 +1074,36 @@
|
|||||||
"earlyrorequired.message": ""
|
"earlyrorequired.message": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"refreshallocations": ""
|
"refreshallocations": "",
|
||||||
|
"provider_reynolds": "",
|
||||||
|
"provider_fortellis": "",
|
||||||
|
"provider_cdk": "",
|
||||||
|
"provider_pbs": "",
|
||||||
|
"provider_dms": "",
|
||||||
|
"transport_wss": "",
|
||||||
|
"transport_ws": "",
|
||||||
|
"banner_status_connected": "",
|
||||||
|
"banner_status_disconnected": "",
|
||||||
|
"banner_message": "",
|
||||||
|
"reconnected_export_service": "",
|
||||||
|
"rr_validation_message": "",
|
||||||
|
"rr_validation_notice_title": "",
|
||||||
|
"rr_validation_notice_description": "",
|
||||||
|
"color_json": "",
|
||||||
|
"plain_json": "",
|
||||||
|
"collapse_all": "",
|
||||||
|
"expand_all": "",
|
||||||
|
"log_level": "",
|
||||||
|
"clear_logs": "",
|
||||||
|
"reconnect": "",
|
||||||
|
"details": "",
|
||||||
|
"hide_details": "",
|
||||||
|
"copy": "",
|
||||||
|
"copied": "",
|
||||||
|
"copy_request": "",
|
||||||
|
"copy_response": "",
|
||||||
|
"request_xml": "",
|
||||||
|
"response_xml": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
@@ -1266,6 +1295,7 @@
|
|||||||
"delete": "Borrar",
|
"delete": "Borrar",
|
||||||
"deleteall": "",
|
"deleteall": "",
|
||||||
"deselectall": "",
|
"deselectall": "",
|
||||||
|
"done": "",
|
||||||
"download": "",
|
"download": "",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"gotoadmin": "",
|
"gotoadmin": "",
|
||||||
|
|||||||
@@ -1074,7 +1074,36 @@
|
|||||||
"earlyrorequired.message": ""
|
"earlyrorequired.message": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"refreshallocations": ""
|
"refreshallocations": "",
|
||||||
|
"provider_reynolds": "",
|
||||||
|
"provider_fortellis": "",
|
||||||
|
"provider_cdk": "",
|
||||||
|
"provider_pbs": "",
|
||||||
|
"provider_dms": "",
|
||||||
|
"transport_wss": "",
|
||||||
|
"transport_ws": "",
|
||||||
|
"banner_status_connected": "",
|
||||||
|
"banner_status_disconnected": "",
|
||||||
|
"banner_message": "",
|
||||||
|
"reconnected_export_service": "",
|
||||||
|
"rr_validation_message": "",
|
||||||
|
"rr_validation_notice_title": "",
|
||||||
|
"rr_validation_notice_description": "",
|
||||||
|
"color_json": "",
|
||||||
|
"plain_json": "",
|
||||||
|
"collapse_all": "",
|
||||||
|
"expand_all": "",
|
||||||
|
"log_level": "",
|
||||||
|
"clear_logs": "",
|
||||||
|
"reconnect": "",
|
||||||
|
"details": "",
|
||||||
|
"hide_details": "",
|
||||||
|
"copy": "",
|
||||||
|
"copied": "",
|
||||||
|
"copy_request": "",
|
||||||
|
"copy_response": "",
|
||||||
|
"request_xml": "",
|
||||||
|
"response_xml": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
@@ -1266,6 +1295,7 @@
|
|||||||
"delete": "Effacer",
|
"delete": "Effacer",
|
||||||
"deleteall": "",
|
"deleteall": "",
|
||||||
"deselectall": "",
|
"deselectall": "",
|
||||||
|
"done": "",
|
||||||
"download": "",
|
"download": "",
|
||||||
"edit": "modifier",
|
"edit": "modifier",
|
||||||
"gotoadmin": "",
|
"gotoadmin": "",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
|
|
||||||
const Fuse = require('fuse.js');
|
const Fuse = require('fuse.js');
|
||||||
const { has } = require("lodash");
|
|
||||||
const { standardizedFieldsnames } = require('./bill-ocr-normalize');
|
const { standardizedFieldsnames } = require('./bill-ocr-normalize');
|
||||||
const InstanceManager = require("../../utils/instanceMgr").default;
|
const InstanceManager = require("../../utils/instanceMgr").default;
|
||||||
|
|
||||||
const PRICE_PERCENT_MARGIN_TOLERANCE = 0.5; //Used to make sure prices and costs are likely.
|
const PRICE_PERCENT_MARGIN_TOLERANCE = 0.5; //Used to make sure prices and costs are likely.
|
||||||
|
const PRICE_QUANTITY_MARGIN_TOLERANCE = 0.03; //Used to make sure that if there is a quantity, the price is likely a unit price.
|
||||||
// Helper function to normalize fields
|
// Helper function to normalize fields
|
||||||
const normalizePartNumber = (str) => {
|
const normalizePartNumber = (str) => {
|
||||||
return str.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
return str.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
|
||||||
@@ -17,7 +17,38 @@ const normalizeText = (str) => {
|
|||||||
};
|
};
|
||||||
const normalizePrice = (str) => {
|
const normalizePrice = (str) => {
|
||||||
if (typeof str !== 'string') return str;
|
if (typeof str !== 'string') return str;
|
||||||
return str.replace(/[^0-9.-]+/g, "");
|
|
||||||
|
let value = str.trim();
|
||||||
|
|
||||||
|
// Handle European-style decimal comma like "292,37".
|
||||||
|
// Only treat the *last* comma as a decimal separator when:
|
||||||
|
// - there's no '.' anywhere (so we don't fight normal US formatting like "1,234.56")
|
||||||
|
// - and the suffix after the last comma is 1-2 digits (so "1,234" stays 1234)
|
||||||
|
if (!value.includes('.') && value.includes(',')) {
|
||||||
|
const lastCommaIndex = value.lastIndexOf(',');
|
||||||
|
const decimalSuffix = value.slice(lastCommaIndex + 1).trim();
|
||||||
|
|
||||||
|
if (/^\d{1,2}$/.test(decimalSuffix)) {
|
||||||
|
const before = value.slice(0, lastCommaIndex).replace(/,/g, '');
|
||||||
|
value = `${before}.${decimalSuffix}`;
|
||||||
|
} else {
|
||||||
|
// Treat commas as thousands separators (or noise) and drop them.
|
||||||
|
value = value.replace(/,/g, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.replace(/[^0-9.-]+/g, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const roundToIncrement = (value, increment) => {
|
||||||
|
if (typeof value !== 'number' || !isFinite(value) || typeof increment !== 'number' || !isFinite(increment) || increment <= 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rounded = Math.round((value + Number.EPSILON) / increment) * increment;
|
||||||
|
// Prevent float artifacts (e.g. 0.20500000000000002)
|
||||||
|
const decimals = Math.max(0, Math.ceil(-Math.log10(increment)));
|
||||||
|
return parseFloat(rounded.toFixed(decimals));
|
||||||
};
|
};
|
||||||
|
|
||||||
//More complex function. Not necessary at the moment, keeping for reference.
|
//More complex function. Not necessary at the moment, keeping for reference.
|
||||||
@@ -134,6 +165,7 @@ const calculateTextractConfidence = (textractLineItem) => {
|
|||||||
const hasActualCost = Object.values(textractLineItem).some(field => field.normalizedLabel === standardizedFieldsnames.actual_cost);
|
const hasActualCost = Object.values(textractLineItem).some(field => field.normalizedLabel === standardizedFieldsnames.actual_cost);
|
||||||
const hasActualPrice = Object.values(textractLineItem).some(field => field.normalizedLabel === standardizedFieldsnames.actual_price);
|
const hasActualPrice = Object.values(textractLineItem).some(field => field.normalizedLabel === standardizedFieldsnames.actual_price);
|
||||||
const hasLineDesc = Object.values(textractLineItem).some(field => field.normalizedLabel === standardizedFieldsnames.line_desc);
|
const hasLineDesc = Object.values(textractLineItem).some(field => field.normalizedLabel === standardizedFieldsnames.line_desc);
|
||||||
|
const hasQuantity = textractLineItem?.QUANTITY?.value; //We don't normalize quantity, we just use what textract gives us.
|
||||||
|
|
||||||
// Calculate weighted average, giving more weight to important fields
|
// Calculate weighted average, giving more weight to important fields
|
||||||
// If we can identify key fields (ITEM, PRODUCT_CODE, PRICE), weight them higher
|
// If we can identify key fields (ITEM, PRODUCT_CODE, PRICE), weight them higher
|
||||||
@@ -173,10 +205,11 @@ const calculateTextractConfidence = (textractLineItem) => {
|
|||||||
if (!hasActualCost) missingCount++;
|
if (!hasActualCost) missingCount++;
|
||||||
if (!hasActualPrice) missingCount++;
|
if (!hasActualPrice) missingCount++;
|
||||||
if (!hasLineDesc) missingCount++;
|
if (!hasLineDesc) missingCount++;
|
||||||
|
if (!hasQuantity) missingCount++;
|
||||||
|
|
||||||
// Each missing field reduces confidence by 15%
|
// Each missing field reduces confidence by 20%
|
||||||
if (missingCount > 0) {
|
if (missingCount > 0) {
|
||||||
missingFieldsPenalty = 1.0 - (missingCount * 0.15);
|
missingFieldsPenalty = 1.0 - (missingCount * 0.20);
|
||||||
}
|
}
|
||||||
|
|
||||||
avgConfidence = avgConfidence * missingFieldsPenalty;
|
avgConfidence = avgConfidence * missingFieldsPenalty;
|
||||||
@@ -361,16 +394,16 @@ async function generateBillFormData({ processedData, jobid: jobidFromProps, body
|
|||||||
const joblineMatches = joblineFuzzySearch({ fuseToSearch: jobLineDescFuse, processedData });
|
const joblineMatches = joblineFuzzySearch({ fuseToSearch: jobLineDescFuse, processedData });
|
||||||
|
|
||||||
const vendorFuse = new Fuse(
|
const vendorFuse = new Fuse(
|
||||||
jobData.vendors,
|
jobData.vendors.map(v => ({ ...v, name_normalized: normalizeText(v.name) })),
|
||||||
{
|
{
|
||||||
keys: ['name'],
|
keys: [{ name: "name", weight: 3 }, { name: 'name_normalized', weight: 2 }],
|
||||||
threshold: 0.4, //Adjust as needed for matching sensitivity,
|
threshold: 0.4,
|
||||||
includeScore: true,
|
includeScore: true,
|
||||||
|
},
|
||||||
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const vendorMatches = vendorFuse.search(processedData.summary?.VENDOR_NAME?.value || processedData.summary?.NAME?.value);
|
const vendorMatches = vendorFuse.search(normalizeText(processedData.summary?.VENDOR_NAME?.value || processedData.summary?.NAME?.value));
|
||||||
|
|
||||||
let vendorid;
|
let vendorid;
|
||||||
if (vendorMatches.length > 0) {
|
if (vendorMatches.length > 0) {
|
||||||
@@ -381,6 +414,21 @@ async function generateBillFormData({ processedData, jobid: jobidFromProps, body
|
|||||||
throw new Error('Job not found for bill form data generation.');
|
throw new Error('Job not found for bill form data generation.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Is there a subtotal level discount? If there is, we need to figure out what the percentage is, and apply that to the actual cost as a reduction
|
||||||
|
const subtotalDiscountValueRaw = processedData.summary?.DISCOUNT?.value || processedData.summary?.SUBTOTAL_DISCOUNT?.value || 0;
|
||||||
|
let discountPercentageDecimal = 0;
|
||||||
|
if (subtotalDiscountValueRaw) {
|
||||||
|
const subtotal = parseFloat(normalizePrice(processedData.summary?.SUBTOTAL?.value || 0)) || 0;
|
||||||
|
const subtotalDiscountValue = parseFloat(normalizePrice(subtotalDiscountValueRaw)) || 0;
|
||||||
|
if (subtotal > 0 && subtotalDiscountValue) {
|
||||||
|
// Store discount percentage as a decimal (e.g. 20.5% => 0.205),
|
||||||
|
// but only allow half-percent increments (0.005 steps).
|
||||||
|
discountPercentageDecimal = Math.abs(subtotalDiscountValue / subtotal);
|
||||||
|
discountPercentageDecimal = roundToIncrement(discountPercentageDecimal, 0.005);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: How do we handle freight lines and core charges?
|
//TODO: How do we handle freight lines and core charges?
|
||||||
//Create the form data structure for the bill posting screen.
|
//Create the form data structure for the bill posting screen.
|
||||||
const billFormData = {
|
const billFormData = {
|
||||||
@@ -448,6 +496,31 @@ async function generateBillFormData({ processedData, jobid: jobidFromProps, body
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//If there's nothing, just fall back to seeing if there's a price object from textract.
|
||||||
|
|
||||||
|
if (!actualPrice && textractLineItem.PRICE) {
|
||||||
|
actualPrice = textractLineItem.PRICE.value;
|
||||||
|
}
|
||||||
|
if (!actualCost && textractLineItem.PRICE) {
|
||||||
|
actualCost = textractLineItem.PRICE.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
//If quantity greater than 1, check if the actual cost is a multiple of the actual price, if so, divide it out to get the unit price.
|
||||||
|
const quantity = parseInt(textractLineItem?.QUANTITY?.value);
|
||||||
|
if (quantity && quantity > 1) {
|
||||||
|
if (actualPrice && quantity && Math.abs((actualPrice / quantity) - (parseFloat(matchToUse?.item?.act_price) || 0)) / ((parseFloat(matchToUse?.item?.act_price) || 1)) < PRICE_QUANTITY_MARGIN_TOLERANCE) {
|
||||||
|
actualPrice = actualPrice / quantity;
|
||||||
|
}
|
||||||
|
if (actualCost && quantity && Math.abs((actualCost / quantity) - (parseFloat(matchToUse?.item?.act_price) || 0)) / ((parseFloat(matchToUse?.item?.act_price) || 1)) < PRICE_QUANTITY_MARGIN_TOLERANCE) {
|
||||||
|
actualCost = actualCost / quantity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (discountPercentageDecimal > 0) {
|
||||||
|
actualCost = actualCost * (1 - discountPercentageDecimal);
|
||||||
|
}
|
||||||
|
|
||||||
const responsibilityCenters = job.bodyshop.md_responsibility_centers
|
const responsibilityCenters = job.bodyshop.md_responsibility_centers
|
||||||
//TODO: Do we need to verify the lines to see if it is a unit price or total price (i.e. quantity * price)
|
//TODO: Do we need to verify the lines to see if it is a unit price or total price (i.e. quantity * price)
|
||||||
const lineObject = {
|
const lineObject = {
|
||||||
@@ -714,5 +787,6 @@ const bodyshopHasDmsKey = (bodyshop) =>
|
|||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
generateBillFormData
|
generateBillFormData,
|
||||||
|
normalizePrice
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,10 +50,12 @@ function normalizeLabelName(labelText) {
|
|||||||
'unit_price': standardizedFieldsnames.actual_price,
|
'unit_price': standardizedFieldsnames.actual_price,
|
||||||
'list': standardizedFieldsnames.actual_price,
|
'list': standardizedFieldsnames.actual_price,
|
||||||
'retail_price': standardizedFieldsnames.actual_price,
|
'retail_price': standardizedFieldsnames.actual_price,
|
||||||
|
'retail': standardizedFieldsnames.actual_price,
|
||||||
'net': standardizedFieldsnames.actual_cost,
|
'net': standardizedFieldsnames.actual_cost,
|
||||||
'selling_price': standardizedFieldsnames.actual_cost,
|
'selling_price': standardizedFieldsnames.actual_cost,
|
||||||
'net_price': standardizedFieldsnames.actual_cost,
|
'net_price': standardizedFieldsnames.actual_cost,
|
||||||
'net_cost': standardizedFieldsnames.actual_cost,
|
'net_cost': standardizedFieldsnames.actual_cost,
|
||||||
|
'total': standardizedFieldsnames.actual_cost,
|
||||||
'po_no': standardizedFieldsnames.ro_number,
|
'po_no': standardizedFieldsnames.ro_number,
|
||||||
'customer_po_no': standardizedFieldsnames.ro_number,
|
'customer_po_no': standardizedFieldsnames.ro_number,
|
||||||
'customer_po_no_': standardizedFieldsnames.ro_number
|
'customer_po_no_': standardizedFieldsnames.ro_number
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { getTextractJobKey, setTextractJob, getTextractJob, getFileType, getPdfPa
|
|||||||
const { extractInvoiceData, processScanData } = require("./bill-ocr-normalize");
|
const { extractInvoiceData, processScanData } = require("./bill-ocr-normalize");
|
||||||
const { generateBillFormData } = require("./bill-ocr-generator");
|
const { generateBillFormData } = require("./bill-ocr-generator");
|
||||||
const logger = require("../../utils/logger");
|
const logger = require("../../utils/logger");
|
||||||
|
const _ = require("lodash");
|
||||||
|
|
||||||
// Initialize AWS clients
|
// Initialize AWS clients
|
||||||
const awsConfig = {
|
const awsConfig = {
|
||||||
@@ -66,7 +67,7 @@ async function handleBillOcr(req, res) {
|
|||||||
if (fileType === 'image') {
|
if (fileType === 'image') {
|
||||||
const processedData = await processSinglePageDocument(uploadedFile.buffer);
|
const processedData = await processSinglePageDocument(uploadedFile.buffer);
|
||||||
const billForm = await generateBillFormData({ processedData: processedData, jobid, bodyshopid, partsorderid, req: req });
|
const billForm = await generateBillFormData({ processedData: processedData, jobid, bodyshopid, partsorderid, req: req });
|
||||||
logger.log("bill-ocr-single-complete", "DEBUG", req.user.email, jobid, { ...processedData, billForm });
|
logger.log("bill-ocr-single-complete", "DEBUG", req.user.email, jobid, { ..._.omit(processedData, "originalTextractResponse"), billForm });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -82,7 +83,7 @@ async function handleBillOcr(req, res) {
|
|||||||
// Process synchronously for single-page documents
|
// Process synchronously for single-page documents
|
||||||
const processedData = await processSinglePageDocument(uploadedFile.buffer);
|
const processedData = await processSinglePageDocument(uploadedFile.buffer);
|
||||||
const billForm = await generateBillFormData({ processedData: processedData, jobid, bodyshopid, partsorderid, req: req });
|
const billForm = await generateBillFormData({ processedData: processedData, jobid, bodyshopid, partsorderid, req: req });
|
||||||
logger.log("bill-ocr-single-complete", "DEBUG", req.user.email, jobid, { ...processedData, billForm });
|
logger.log("bill-ocr-single-complete", "DEBUG", req.user.email, jobid, { ..._.omit(processedData, "originalTextractResponse"), billForm });
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
status: 'COMPLETED',
|
status: 'COMPLETED',
|
||||||
|
|||||||
@@ -250,6 +250,8 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
|||||||
},
|
},
|
||||||
InsuranceCompany: job.ins_co_nm || "",
|
InsuranceCompany: job.ins_co_nm || "",
|
||||||
Claim: job.clm_no || "",
|
Claim: job.clm_no || "",
|
||||||
|
Deductible: job.ded_amt || 0,
|
||||||
|
PolicyNo: job.policy_no || "",
|
||||||
DMSAllocation: job.dms_allocation || "",
|
DMSAllocation: job.dms_allocation || "",
|
||||||
Contacts: {
|
Contacts: {
|
||||||
CSR: job.employee_csr_rel
|
CSR: job.employee_csr_rel
|
||||||
|
|||||||
@@ -1285,6 +1285,7 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
|
|||||||
date_repairstarted
|
date_repairstarted
|
||||||
date_void
|
date_void
|
||||||
dms_allocation
|
dms_allocation
|
||||||
|
ded_amt
|
||||||
employee_body_rel {
|
employee_body_rel {
|
||||||
first_name
|
first_name
|
||||||
last_name
|
last_name
|
||||||
@@ -1380,6 +1381,7 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
|
|||||||
}
|
}
|
||||||
parts_tax_rates
|
parts_tax_rates
|
||||||
plate_no
|
plate_no
|
||||||
|
policy_no
|
||||||
rate_la1
|
rate_la1
|
||||||
rate_la2
|
rate_la2
|
||||||
rate_la3
|
rate_la3
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
|||||||
const { RRClient } = require("./lib/index.cjs");
|
const { RRClient } = require("./lib/index.cjs");
|
||||||
const { getRRConfigFromBodyshop } = require("./rr-config");
|
const { getRRConfigFromBodyshop } = require("./rr-config");
|
||||||
const CreateRRLogEvent = require("./rr-logger-event");
|
const CreateRRLogEvent = require("./rr-logger-event");
|
||||||
|
const { withRRRequestXml } = require("./rr-log-xml");
|
||||||
const InstanceManager = require("../utils/instanceMgr").default;
|
const InstanceManager = require("../utils/instanceMgr").default;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,14 +218,24 @@ const createRRCustomer = async ({ bodyshop, job, overrides = {}, socket }) => {
|
|||||||
try {
|
try {
|
||||||
response = await client.insertCustomer(safePayload, opts);
|
response = await client.insertCustomer(safePayload, opts);
|
||||||
// Very noisy; only show when log level is cranked to SILLY
|
// Very noisy; only show when log level is cranked to SILLY
|
||||||
CreateRRLogEvent(socket, "SILLY", "{CU} insertCustomer: raw response", { response });
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"SILLY",
|
||||||
|
"{CU} insertCustomer: raw response",
|
||||||
|
withRRRequestXml(response, { response })
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
CreateRRLogEvent(socket, "ERROR", "RR insertCustomer transport error", {
|
CreateRRLogEvent(
|
||||||
message: e?.message,
|
socket,
|
||||||
code: e?.code,
|
"ERROR",
|
||||||
status: e?.meta?.status || e?.status,
|
"RR insertCustomer transport error",
|
||||||
payload: safePayload
|
withRRRequestXml(e, {
|
||||||
});
|
message: e?.message,
|
||||||
|
code: e?.code,
|
||||||
|
status: e?.meta?.status || e?.status,
|
||||||
|
payload: safePayload
|
||||||
|
})
|
||||||
|
);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,12 +244,17 @@ const createRRCustomer = async ({ bodyshop, job, overrides = {}, socket }) => {
|
|||||||
|
|
||||||
let customerNo = data?.dmsRecKey;
|
let customerNo = data?.dmsRecKey;
|
||||||
if (!customerNo) {
|
if (!customerNo) {
|
||||||
CreateRRLogEvent(socket, "ERROR", "RR insertCustomer returned no dmsRecKey/custNo", {
|
CreateRRLogEvent(
|
||||||
status: trx?.status,
|
socket,
|
||||||
statusCode: trx?.statusCode,
|
"ERROR",
|
||||||
message: trx?.message,
|
"RR insertCustomer returned no dmsRecKey/custNo",
|
||||||
data
|
withRRRequestXml(response, {
|
||||||
});
|
status: trx?.status,
|
||||||
|
statusCode: trx?.statusCode,
|
||||||
|
message: trx?.message,
|
||||||
|
data
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`RR insertCustomer returned no dmsRecKey (status=${trx?.status ?? "?"} code=${trx?.statusCode ?? "?"}${
|
`RR insertCustomer returned no dmsRecKey (status=${trx?.status ?? "?"} code=${trx?.statusCode ?? "?"}${
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* @returns {number|null}
|
* @returns {number|null}
|
||||||
*/
|
*/
|
||||||
const parseVendorStatusCode = (err) => {
|
const parseVendorStatusCode = (err) => {
|
||||||
// Prefer explicit numeric props when available
|
// Prefer explicit numeric props when available.
|
||||||
const codeProp = err?.code ?? err?.statusCode ?? err?.meta?.status?.StatusCode ?? err?.status?.StatusCode;
|
const codeProp = err?.code ?? err?.statusCode ?? err?.meta?.status?.StatusCode ?? err?.status?.StatusCode;
|
||||||
const num = Number(codeProp);
|
const num = Number(codeProp);
|
||||||
if (!Number.isNaN(num) && num > 0) return num;
|
if (!Number.isNaN(num) && num > 0) return num;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { GraphQLClient } = require("graphql-request");
|
const { GraphQLClient } = require("graphql-request");
|
||||||
const queries = require("../graphql-client/queries");
|
const queries = require("../graphql-client/queries");
|
||||||
const CreateRRLogEvent = require("./rr-logger-event");
|
const CreateRRLogEvent = require("./rr-logger-event");
|
||||||
|
const { extractRRXmlPair } = require("./rr-log-xml");
|
||||||
|
|
||||||
/** Get bearer token from the socket (same approach used elsewhere) */
|
/** Get bearer token from the socket (same approach used elsewhere) */
|
||||||
const getAuthToken = (socket) =>
|
const getAuthToken = (socket) =>
|
||||||
@@ -178,11 +179,23 @@ const insertRRFailedExportLog = async ({ socket, jobId, job, bodyshop, error, cl
|
|||||||
const client = new GraphQLClient(endpoint, {});
|
const client = new GraphQLClient(endpoint, {});
|
||||||
client.setHeaders({ Authorization: `Bearer ${token}` });
|
client.setHeaders({ Authorization: `Bearer ${token}` });
|
||||||
|
|
||||||
|
const { requestXml, responseXml } = extractRRXmlPair(error);
|
||||||
|
const xmlFromError =
|
||||||
|
requestXml || responseXml
|
||||||
|
? {
|
||||||
|
...(requestXml ? { request: requestXml } : {}),
|
||||||
|
...(responseXml ? { response: responseXml } : {})
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const meta = buildRRExportMeta({
|
const meta = buildRRExportMeta({
|
||||||
result,
|
result,
|
||||||
extra: {
|
extra: {
|
||||||
error: error?.message || String(error),
|
error: error?.message || String(error),
|
||||||
classification: classification || undefined
|
classification: classification || undefined,
|
||||||
|
...(requestXml ? { requestXml } : {}),
|
||||||
|
...(responseXml ? { responseXml } : {}),
|
||||||
|
...(xmlFromError && !result?.xml ? { xml: xmlFromError } : {})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
const { buildRRRepairOrderPayload } = require("./rr-job-helpers");
|
||||||
const { buildClientAndOpts } = require("./rr-lookup");
|
const { buildClientAndOpts } = require("./rr-lookup");
|
||||||
const CreateRRLogEvent = require("./rr-logger-event");
|
const CreateRRLogEvent = require("./rr-logger-event");
|
||||||
|
const { withRRRequestXml } = require("./rr-log-xml");
|
||||||
const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers");
|
const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers");
|
||||||
const CdkCalculateAllocations = require("./rr-calculate-allocations").default;
|
const CdkCalculateAllocations = require("./rr-calculate-allocations").default;
|
||||||
const { resolveRROpCodeFromBodyshop } = require("./rr-utils");
|
const { resolveRROpCodeFromBodyshop } = require("./rr-utils");
|
||||||
@@ -147,10 +148,7 @@ const createMinimalRRRepairOrder = async (args) => {
|
|||||||
|
|
||||||
const response = await client.createRepairOrder(payload, finalOpts);
|
const response = await client.createRepairOrder(payload, finalOpts);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", "RR minimal Repair Order created", {
|
CreateRRLogEvent(socket, "INFO", "RR minimal Repair Order created", withRRRequestXml(response, { payload, response }));
|
||||||
payload,
|
|
||||||
response
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = response?.data || null;
|
const data = response?.data || null;
|
||||||
const statusBlocks = response?.statusBlocks || {};
|
const statusBlocks = response?.statusBlocks || {};
|
||||||
@@ -327,7 +325,7 @@ const updateRRRepairOrderWithFullData = async (args) => {
|
|||||||
// Without this, Reynolds won't recognize the OpCode when we send rogg operations
|
// Without this, Reynolds won't recognize the OpCode when we send rogg operations
|
||||||
// The rolabor section tells Reynolds "these jobs exist" even with minimal data
|
// The rolabor section tells Reynolds "these jobs exist" even with minimal data
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", "Sending full data for early RO (using create with roNo)", {
|
CreateRRLogEvent(socket, "INFO", "Preparing full data for early RO (using create with roNo)", {
|
||||||
roNo: String(roNo),
|
roNo: String(roNo),
|
||||||
hasRolabor: !!payload.rolabor,
|
hasRolabor: !!payload.rolabor,
|
||||||
hasRogg: !!payload.rogg,
|
hasRogg: !!payload.rogg,
|
||||||
@@ -338,10 +336,18 @@ const updateRRRepairOrderWithFullData = async (args) => {
|
|||||||
// Reynolds will merge this with the existing RO header
|
// Reynolds will merge this with the existing RO header
|
||||||
const response = await client.createRepairOrder(payload, finalOpts);
|
const response = await client.createRepairOrder(payload, finalOpts);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", "RR Repair Order full data sent", {
|
CreateRRLogEvent(
|
||||||
payload,
|
socket,
|
||||||
response
|
"INFO",
|
||||||
});
|
"Sending full data for early RO (using create with roNo)",
|
||||||
|
withRRRequestXml(response, {
|
||||||
|
roNo: String(roNo),
|
||||||
|
hasRolabor: !!payload.rolabor,
|
||||||
|
hasRogg: !!payload.rogg,
|
||||||
|
payload,
|
||||||
|
response
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const data = response?.data || null;
|
const data = response?.data || null;
|
||||||
const statusBlocks = response?.statusBlocks || {};
|
const statusBlocks = response?.statusBlocks || {};
|
||||||
@@ -501,10 +507,7 @@ const exportJobToRR = async (args) => {
|
|||||||
|
|
||||||
const response = await client.createRepairOrder(payload, finalOpts);
|
const response = await client.createRepairOrder(payload, finalOpts);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", "RR raw Repair Order created", {
|
CreateRRLogEvent(socket, "INFO", "RR raw Repair Order created", withRRRequestXml(response, { payload, response }));
|
||||||
payload,
|
|
||||||
response
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = response?.data || null;
|
const data = response?.data || null;
|
||||||
const statusBlocks = response?.statusBlocks || {};
|
const statusBlocks = response?.statusBlocks || {};
|
||||||
@@ -603,10 +606,15 @@ const finalizeRRRepairOrder = async (args) => {
|
|||||||
|
|
||||||
const rrRes = await client.updateRepairOrder(payload, finalOpts);
|
const rrRes = await client.updateRepairOrder(payload, finalOpts);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "SILLY", "RR Repair Order finalized", {
|
CreateRRLogEvent(
|
||||||
payload,
|
socket,
|
||||||
response: rrRes
|
"SILLY",
|
||||||
});
|
"RR Repair Order finalized",
|
||||||
|
withRRRequestXml(rrRes, {
|
||||||
|
payload,
|
||||||
|
response: rrRes
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const data = rrRes?.data || null;
|
const data = rrRes?.data || null;
|
||||||
const statusBlocks = rrRes?.statusBlocks || {};
|
const statusBlocks = rrRes?.statusBlocks || {};
|
||||||
|
|||||||
63
server/rr/rr-log-xml.js
Normal file
63
server/rr/rr-log-xml.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* Extract request/response XML from RR response/result shapes.
|
||||||
|
* @param rrObj
|
||||||
|
* @returns {{requestXml: string|null, responseXml: string|null}}
|
||||||
|
*/
|
||||||
|
const extractRRXmlPair = (rrObj) => {
|
||||||
|
const xml = rrObj?.xml ?? rrObj?.meta?.xml;
|
||||||
|
|
||||||
|
let requestXml = null;
|
||||||
|
let responseXml = null;
|
||||||
|
|
||||||
|
if (typeof xml === "string") {
|
||||||
|
requestXml = xml;
|
||||||
|
} else {
|
||||||
|
if (typeof xml?.request === "string") requestXml = xml.request;
|
||||||
|
else if (typeof xml?.req === "string") requestXml = xml.req;
|
||||||
|
else if (typeof xml?.starXml === "string") requestXml = xml.starXml;
|
||||||
|
if (typeof xml?.response === "string") responseXml = xml.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requestXml && typeof rrObj?.requestXml === "string") requestXml = rrObj.requestXml;
|
||||||
|
if (!requestXml && typeof rrObj?.meta?.requestXml === "string") requestXml = rrObj.meta.requestXml;
|
||||||
|
if (!requestXml && typeof rrObj?.meta?.reqXml === "string") requestXml = rrObj.meta.reqXml;
|
||||||
|
if (!requestXml && typeof rrObj?.meta?.request === "string") requestXml = rrObj.meta.request;
|
||||||
|
if (!responseXml && typeof rrObj?.responseXml === "string") responseXml = rrObj.responseXml;
|
||||||
|
if (!responseXml && typeof rrObj?.meta?.responseXml === "string") responseXml = rrObj.meta.responseXml;
|
||||||
|
if (!responseXml && typeof rrObj?.meta?.resXml === "string") responseXml = rrObj.meta.resXml;
|
||||||
|
if (!responseXml && typeof rrObj?.meta?.response === "string") responseXml = rrObj.meta.response;
|
||||||
|
|
||||||
|
// If wrapped HTTP response data contains raw XML, surface it.
|
||||||
|
if (!responseXml && typeof rrObj?.response?.data === "string") {
|
||||||
|
const xmlData = rrObj.response.data.trim();
|
||||||
|
if (xmlData.startsWith("<")) responseXml = xmlData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try one level down when errors are wrapped.
|
||||||
|
if ((!requestXml || !responseXml) && rrObj?.cause && rrObj.cause !== rrObj) {
|
||||||
|
const nested = extractRRXmlPair(rrObj.cause);
|
||||||
|
if (!requestXml) requestXml = nested.requestXml;
|
||||||
|
if (!responseXml) responseXml = nested.responseXml;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { requestXml, responseXml };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Reynolds request/response XML to RR log metadata when available.
|
||||||
|
* @param rrObj
|
||||||
|
* @param meta
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const withRRRequestXml = (rrObj, meta = {}) => {
|
||||||
|
const { requestXml, responseXml } = extractRRXmlPair(rrObj);
|
||||||
|
const xmlMeta = {};
|
||||||
|
if (requestXml) xmlMeta.requestXml = requestXml;
|
||||||
|
if (responseXml) xmlMeta.responseXml = responseXml;
|
||||||
|
return Object.keys(xmlMeta).length ? { ...meta, ...xmlMeta } : meta;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
extractRRXmlPair,
|
||||||
|
withRRRequestXml
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ const { createRRCustomer } = require("./rr-customers");
|
|||||||
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
const { ensureRRServiceVehicle } = require("./rr-service-vehicles");
|
||||||
const { classifyRRVendorError } = require("./rr-errors");
|
const { classifyRRVendorError } = require("./rr-errors");
|
||||||
const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs");
|
const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs");
|
||||||
|
const { withRRRequestXml, extractRRXmlPair } = require("./rr-log-xml");
|
||||||
const {
|
const {
|
||||||
makeVehicleSearchPayloadFromJob,
|
makeVehicleSearchPayloadFromJob,
|
||||||
ownersFromVinBlocks,
|
ownersFromVinBlocks,
|
||||||
@@ -48,6 +49,21 @@ const resolveJobId = (explicit, payload, job) => explicit || payload?.jobId || j
|
|||||||
*/
|
*/
|
||||||
const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null;
|
const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add request/response XML to socket event payloads when available.
|
||||||
|
* @param rrObj
|
||||||
|
* @param payload
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
const withRRXmlSocketPayload = (rrObj, payload = {}) => {
|
||||||
|
const { requestXml, responseXml } = extractRRXmlPair(rrObj);
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
...(requestXml ? { requestXml } : {}),
|
||||||
|
...(responseXml ? { responseXml } : {})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort vehicle owners first in the list, preserving original order otherwise.
|
* Sort vehicle owners first in the list, preserving original order otherwise.
|
||||||
* @param list
|
* @param list
|
||||||
@@ -154,15 +170,13 @@ const setJobDmsIdForSocket = async ({ socket, jobId, dmsId, dmsCustomerId, dmsAd
|
|||||||
if (!token) throw new Error("Missing auth token for setJobDmsIdForSocket");
|
if (!token) throw new Error("Missing auth token for setJobDmsIdForSocket");
|
||||||
|
|
||||||
const client = new GraphQLClient(endpoint, {});
|
const client = new GraphQLClient(endpoint, {});
|
||||||
await client
|
await client.setHeaders({ Authorization: `Bearer ${token}` }).request(queries.SET_JOB_DMS_ID, {
|
||||||
.setHeaders({ Authorization: `Bearer ${token}` })
|
id: jobId,
|
||||||
.request(queries.SET_JOB_DMS_ID, {
|
dms_id: String(dmsId),
|
||||||
id: jobId,
|
dms_customer_id: dmsCustomerId ? String(dmsCustomerId) : null,
|
||||||
dms_id: String(dmsId),
|
dms_advisor_id: dmsAdvisorId ? String(dmsAdvisorId) : null,
|
||||||
dms_customer_id: dmsCustomerId ? String(dmsCustomerId) : null,
|
kmin: mileageIn != null && mileageIn > 0 ? parseInt(mileageIn, 10) : null
|
||||||
dms_advisor_id: dmsAdvisorId ? String(dmsAdvisorId) : null,
|
});
|
||||||
kmin: mileageIn != null && mileageIn > 0 ? parseInt(mileageIn, 10) : null
|
|
||||||
});
|
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", "Linked job.dms_id to RR RO", {
|
CreateRRLogEvent(socket, "INFO", "Linked job.dms_id to RR RO", {
|
||||||
jobId,
|
jobId,
|
||||||
@@ -241,7 +255,12 @@ const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) =>
|
|||||||
|
|
||||||
const multiResponse = await rrCombinedSearch(bodyshop, q);
|
const multiResponse = await rrCombinedSearch(bodyshop, q);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "SILLY", "Multi Customer Search - raw combined search", { response: multiResponse });
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"SILLY",
|
||||||
|
"Multi Customer Search - raw combined search",
|
||||||
|
withRRRequestXml(multiResponse, { response: multiResponse })
|
||||||
|
);
|
||||||
|
|
||||||
if (fromVin) {
|
if (fromVin) {
|
||||||
const multiBlocks = Array.isArray(multiResponse?.data) ? multiResponse.data : [];
|
const multiBlocks = Array.isArray(multiResponse?.data) ? multiResponse.data : [];
|
||||||
@@ -262,7 +281,7 @@ const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) =>
|
|||||||
const norm = normalizeCustomerCandidates(multiResponse, { ownersSet });
|
const norm = normalizeCustomerCandidates(multiResponse, { ownersSet });
|
||||||
merged.push(...norm);
|
merged.push(...norm);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
CreateRRLogEvent(socket, "WARN", "Multi-search subquery failed", { kind: q.kind, error: e.message });
|
CreateRRLogEvent(socket, "WARN", "Multi-search subquery failed", withRRRequestXml(e, { kind: q.kind, error: e.message }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +329,7 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
count: decorated.length
|
count: decorated.length
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
CreateRRLogEvent(socket, "ERROR", "RR combined lookup error", { error: e.message, jobid });
|
CreateRRLogEvent(socket, "ERROR", "RR combined lookup error", withRRRequestXml(e, { error: e.message, jobid }));
|
||||||
cb?.({ jobid, error: e.message });
|
cb?.({ jobid, error: e.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -387,7 +406,7 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
fromCache
|
fromCache
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
CreateRRLogEvent(socket, "ERROR", "rr-get-advisors: failed", { error: err?.message });
|
CreateRRLogEvent(socket, "ERROR", "rr-get-advisors: failed", withRRRequestXml(err, { error: err?.message }));
|
||||||
ack?.({ ok: false, error: err?.message || "get advisors failed" });
|
ack?.({ ok: false, error: err?.message || "get advisors failed" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -458,14 +477,26 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
|
anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CreateRRLogEvent(socket, "ERROR", `Error during RR early RO creation (prepare)`, {
|
CreateRRLogEvent(
|
||||||
error: error.message,
|
socket,
|
||||||
stack: error.stack,
|
"ERROR",
|
||||||
jobid: rid
|
`Error during RR early RO creation (prepare)`,
|
||||||
});
|
withRRRequestXml(error, {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
jobid: rid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
|
socket.emit(
|
||||||
|
"export-failed",
|
||||||
|
withRRXmlSocketPayload(error, {
|
||||||
|
vendor: "rr",
|
||||||
|
jobId: rid,
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@@ -511,7 +542,11 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Filter out invalid values
|
// Filter out invalid values
|
||||||
if (selectedCustNo === "undefined" || selectedCustNo === "null" || (selectedCustNo && selectedCustNo.trim() === "")) {
|
if (
|
||||||
|
selectedCustNo === "undefined" ||
|
||||||
|
selectedCustNo === "null" ||
|
||||||
|
(selectedCustNo && selectedCustNo.trim() === "")
|
||||||
|
) {
|
||||||
selectedCustNo = null;
|
selectedCustNo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +590,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
|
if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
|
||||||
const vinResponse = await rrCombinedSearch(bodyshop, vehQ);
|
const vinResponse = await rrCombinedSearch(bodyshop, vehQ);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "SILLY", `VIN owner pre-check response (early RO)`, { response: vinResponse });
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"SILLY",
|
||||||
|
`VIN owner pre-check response (early RO)`,
|
||||||
|
withRRRequestXml(vinResponse, { response: vinResponse })
|
||||||
|
);
|
||||||
|
|
||||||
const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : [];
|
const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : [];
|
||||||
|
|
||||||
@@ -588,9 +628,14 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
CreateRRLogEvent(socket, "WARN", `VIN owner pre-check failed; continuing with selected customer (early RO)`, {
|
CreateRRLogEvent(
|
||||||
error: e?.message
|
socket,
|
||||||
});
|
"WARN",
|
||||||
|
`VIN owner pre-check failed; continuing with selected customer (early RO)`,
|
||||||
|
withRRRequestXml(e, {
|
||||||
|
error: e?.message
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache final/effective customer selection
|
// Cache final/effective customer selection
|
||||||
@@ -705,12 +750,17 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
|
|
||||||
const outsdRoNo = data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;
|
const outsdRoNo = data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "DEBUG", "Early RO created - checking dmsRoNo", {
|
CreateRRLogEvent(
|
||||||
dmsRoNo,
|
socket,
|
||||||
resultRoNo: result?.roNo,
|
"DEBUG",
|
||||||
dataRoNo: data?.dmsRoNo,
|
"Early RO created - checking dmsRoNo",
|
||||||
jobId: rid
|
withRRRequestXml(result, {
|
||||||
});
|
dmsRoNo,
|
||||||
|
resultRoNo: result?.roNo,
|
||||||
|
dataRoNo: data?.dmsRoNo,
|
||||||
|
jobId: rid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// ✅ Persist DMS RO number, customer ID, advisor ID, and mileage on the job
|
// ✅ Persist DMS RO number, customer ID, advisor ID, and mileage on the job
|
||||||
if (dmsRoNo) {
|
if (dmsRoNo) {
|
||||||
@@ -731,16 +781,21 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
mileageIn
|
mileageIn
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
CreateRRLogEvent(socket, "WARN", "RR early RO creation succeeded but no DMS RO number was returned", {
|
CreateRRLogEvent(
|
||||||
jobId: rid,
|
socket,
|
||||||
resultPreview: {
|
"WARN",
|
||||||
roNo: result?.roNo,
|
"RR early RO creation succeeded but no DMS RO number was returned",
|
||||||
data: {
|
withRRRequestXml(result, {
|
||||||
dmsRoNo: data?.dmsRoNo,
|
jobId: rid,
|
||||||
outsdRoNo: data?.outsdRoNo
|
resultPreview: {
|
||||||
|
roNo: result?.roNo,
|
||||||
|
data: {
|
||||||
|
dmsRoNo: data?.dmsRoNo,
|
||||||
|
outsdRoNo: data?.outsdRoNo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await redisHelpers.setSessionTransactionData(
|
await redisHelpers.setSessionTransactionData(
|
||||||
@@ -758,10 +813,15 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", `{EARLY-5} Minimal RO created successfully`, {
|
CreateRRLogEvent(
|
||||||
dmsRoNo: dmsRoNo || null,
|
socket,
|
||||||
outsdRoNo: outsdRoNo || null
|
"INFO",
|
||||||
});
|
`{EARLY-5} Minimal RO created successfully`,
|
||||||
|
withRRRequestXml(result, {
|
||||||
|
dmsRoNo: dmsRoNo || null,
|
||||||
|
outsdRoNo: outsdRoNo || null
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Mark success in export logs
|
// Mark success in export logs
|
||||||
await markRRExportSuccess({
|
await markRRExportSuccess({
|
||||||
@@ -810,11 +870,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
message: vendorMessage
|
message: vendorMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Early RO creation failed`, {
|
CreateRRLogEvent(
|
||||||
roStatus: result?.roStatus,
|
socket,
|
||||||
statusBlocks: result?.statusBlocks,
|
"ERROR",
|
||||||
classification: cls
|
`Early RO creation failed`,
|
||||||
});
|
withRRRequestXml(result, {
|
||||||
|
roStatus: result?.roStatus,
|
||||||
|
statusBlocks: result?.statusBlocks,
|
||||||
|
classification: cls
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
socket,
|
socket,
|
||||||
@@ -827,9 +892,11 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.emit("export-failed", {
|
socket.emit("export-failed", {
|
||||||
vendor: "rr",
|
...withRRXmlSocketPayload(result, {
|
||||||
jobId: rid,
|
vendor: "rr",
|
||||||
error: cls?.friendlyMessage || result?.error || "RR early RO creation failed",
|
jobId: rid,
|
||||||
|
error: cls?.friendlyMessage || result?.error || "RR early RO creation failed"
|
||||||
|
}),
|
||||||
...cls
|
...cls
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -843,14 +910,19 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const cls = classifyRRVendorError(error);
|
const cls = classifyRRVendorError(error);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Error during RR early RO creation (customer-selected)`, {
|
CreateRRLogEvent(
|
||||||
error: error.message,
|
socket,
|
||||||
vendorStatusCode: cls.vendorStatusCode,
|
"ERROR",
|
||||||
code: cls.errorCode,
|
`Error during RR early RO creation (customer-selected)`,
|
||||||
friendly: cls.friendlyMessage,
|
withRRRequestXml(error, {
|
||||||
stack: error.stack,
|
error: error.message,
|
||||||
jobid: rid
|
vendorStatusCode: cls.vendorStatusCode,
|
||||||
});
|
code: cls.errorCode,
|
||||||
|
friendly: cls.friendlyMessage,
|
||||||
|
stack: error.stack,
|
||||||
|
jobid: rid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!bodyshop || !job) {
|
if (!bodyshop || !job) {
|
||||||
@@ -875,9 +947,11 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
socket.emit("export-failed", {
|
socket.emit("export-failed", {
|
||||||
vendor: "rr",
|
...withRRXmlSocketPayload(error, {
|
||||||
jobId: rid,
|
vendor: "rr",
|
||||||
error: error.message,
|
jobId: rid,
|
||||||
|
error: error.message
|
||||||
|
}),
|
||||||
...cls
|
...cls
|
||||||
});
|
});
|
||||||
socket.emit("rr-user-notice", { jobId: rid, ...cls });
|
socket.emit("rr-user-notice", { jobId: rid, ...cls });
|
||||||
@@ -1011,7 +1085,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
redisHelpers
|
redisHelpers
|
||||||
});
|
});
|
||||||
|
|
||||||
const advisorNo = job.dms_advisor_id || readAdvisorNo({ txEnvelope }, await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo));
|
const advisorNo =
|
||||||
|
job.dms_advisor_id ||
|
||||||
|
readAdvisorNo(
|
||||||
|
{ txEnvelope },
|
||||||
|
await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo)
|
||||||
|
);
|
||||||
|
|
||||||
if (!advisorNo) {
|
if (!advisorNo) {
|
||||||
throw new Error("Advisor is required (advisorNo).");
|
throw new Error("Advisor is required (advisorNo).");
|
||||||
@@ -1030,7 +1109,28 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
roNo: job.dms_id
|
roNo: job.dms_id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"SILLY",
|
||||||
|
"{4.1} RR RO update response received",
|
||||||
|
withRRRequestXml(result, {
|
||||||
|
dmsRoNo: job.dms_id,
|
||||||
|
success: !!result?.success
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (!result?.success) {
|
if (!result?.success) {
|
||||||
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"ERROR",
|
||||||
|
"RR Repair Order update failed",
|
||||||
|
withRRRequestXml(result, {
|
||||||
|
jobId: rid,
|
||||||
|
dmsRoNo: job.dms_id,
|
||||||
|
roStatus: result?.roStatus,
|
||||||
|
statusBlocks: result?.statusBlocks
|
||||||
|
})
|
||||||
|
);
|
||||||
throw new Error(result?.roStatus?.message || "Failed to update RR Repair Order");
|
throw new Error(result?.roStatus?.message || "Failed to update RR Repair Order");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1059,10 +1159,15 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", `RR Repair Order updated successfully`, {
|
CreateRRLogEvent(
|
||||||
dmsRoNo,
|
socket,
|
||||||
jobId: rid
|
"INFO",
|
||||||
});
|
`RR Repair Order updated successfully`,
|
||||||
|
withRRRequestXml(result, {
|
||||||
|
dmsRoNo,
|
||||||
|
jobId: rid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// For early RO flow, only emit validation-required (not export-job:result)
|
// For early RO flow, only emit validation-required (not export-job:result)
|
||||||
// since the export is not complete yet - we're just waiting for validation
|
// since the export is not complete yet - we're just waiting for validation
|
||||||
@@ -1082,14 +1187,26 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
|
anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
CreateRRLogEvent(socket, "ERROR", `Error during RR export (prepare)`, {
|
CreateRRLogEvent(
|
||||||
error: error.message,
|
socket,
|
||||||
stack: error.stack,
|
"ERROR",
|
||||||
jobid: rid
|
`Error during RR export (prepare)`,
|
||||||
});
|
withRRRequestXml(error, {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
jobid: rid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
|
socket.emit(
|
||||||
|
"export-failed",
|
||||||
|
withRRXmlSocketPayload(error, {
|
||||||
|
vendor: "rr",
|
||||||
|
jobId: rid,
|
||||||
|
error: error.message
|
||||||
|
})
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
@@ -1148,7 +1265,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
|
if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
|
||||||
const vinResponse = await rrCombinedSearch(bodyshop, vehQ);
|
const vinResponse = await rrCombinedSearch(bodyshop, vehQ);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "SILLY", `VIN owner pre-check response`, { response: vinResponse });
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"SILLY",
|
||||||
|
`VIN owner pre-check response`,
|
||||||
|
withRRRequestXml(vinResponse, { response: vinResponse })
|
||||||
|
);
|
||||||
|
|
||||||
const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : [];
|
const vinBlocks = Array.isArray(vinResponse?.data) ? vinResponse.data : [];
|
||||||
|
|
||||||
@@ -1181,9 +1303,14 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
CreateRRLogEvent(socket, "WARN", `VIN owner pre-check failed; continuing with selected customer`, {
|
CreateRRLogEvent(
|
||||||
error: e?.message
|
socket,
|
||||||
});
|
"WARN",
|
||||||
|
`VIN owner pre-check failed; continuing with selected customer`,
|
||||||
|
withRRRequestXml(e, {
|
||||||
|
error: e?.message
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache final/effective customer selection
|
// Cache final/effective customer selection
|
||||||
@@ -1344,16 +1471,21 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
if (dmsRoNo) {
|
if (dmsRoNo) {
|
||||||
await setJobDmsIdForSocket({ socket, jobId: rid, dmsId: dmsRoNo });
|
await setJobDmsIdForSocket({ socket, jobId: rid, dmsId: dmsRoNo });
|
||||||
} else {
|
} else {
|
||||||
CreateRRLogEvent(socket, "WARN", "RR export succeeded but no DMS RO number was returned", {
|
CreateRRLogEvent(
|
||||||
jobId: rid,
|
socket,
|
||||||
resultPreview: {
|
"WARN",
|
||||||
roNo: result?.roNo,
|
"RR export succeeded but no DMS RO number was returned",
|
||||||
data: {
|
withRRRequestXml(result, {
|
||||||
dmsRoNo: data?.dmsRoNo,
|
jobId: rid,
|
||||||
outsdRoNo: data?.outsdRoNo
|
resultPreview: {
|
||||||
|
roNo: result?.roNo,
|
||||||
|
data: {
|
||||||
|
dmsRoNo: data?.dmsRoNo,
|
||||||
|
outsdRoNo: data?.outsdRoNo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await redisHelpers.setSessionTransactionData(
|
await redisHelpers.setSessionTransactionData(
|
||||||
@@ -1370,10 +1502,15 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", `{5} RO created. Waiting for validation.`, {
|
CreateRRLogEvent(
|
||||||
dmsRoNo: dmsRoNo || null,
|
socket,
|
||||||
outsdRoNo: outsdRoNo || null
|
"INFO",
|
||||||
});
|
`{5} RO created. Waiting for validation.`,
|
||||||
|
withRRRequestXml(result, {
|
||||||
|
dmsRoNo: dmsRoNo || null,
|
||||||
|
outsdRoNo: outsdRoNo || null
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Tell FE to prompt for "Finished/Close"
|
// Tell FE to prompt for "Finished/Close"
|
||||||
socket.emit("rr-validation-required", { jobId: rid, dmsRoNo, outsdRoNo });
|
socket.emit("rr-validation-required", { jobId: rid, dmsRoNo, outsdRoNo });
|
||||||
@@ -1412,11 +1549,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
message: vendorMessage
|
message: vendorMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Export failed (step 1)`, {
|
CreateRRLogEvent(
|
||||||
roStatus: result?.roStatus,
|
socket,
|
||||||
statusBlocks: result?.statusBlocks,
|
"ERROR",
|
||||||
classification: cls
|
`Export failed (step 1)`,
|
||||||
});
|
withRRRequestXml(result, {
|
||||||
|
roStatus: result?.roStatus,
|
||||||
|
statusBlocks: result?.statusBlocks,
|
||||||
|
classification: cls
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
socket,
|
socket,
|
||||||
@@ -1429,9 +1571,11 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.emit("export-failed", {
|
socket.emit("export-failed", {
|
||||||
vendor: "rr",
|
...withRRXmlSocketPayload(result, {
|
||||||
jobId: rid,
|
vendor: "rr",
|
||||||
error: cls?.friendlyMessage || result?.error || "RR export failed",
|
jobId: rid,
|
||||||
|
error: cls?.friendlyMessage || result?.error || "RR export failed"
|
||||||
|
}),
|
||||||
...cls
|
...cls
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1445,14 +1589,19 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const cls = classifyRRVendorError(error);
|
const cls = classifyRRVendorError(error);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Error during RR export (selected-customer)`, {
|
CreateRRLogEvent(
|
||||||
error: error.message,
|
socket,
|
||||||
vendorStatusCode: cls.vendorStatusCode,
|
"ERROR",
|
||||||
code: cls.errorCode,
|
`Error during RR export (selected-customer)`,
|
||||||
friendly: cls.friendlyMessage,
|
withRRRequestXml(error, {
|
||||||
stack: error.stack,
|
error: error.message,
|
||||||
jobid: rid
|
vendorStatusCode: cls.vendorStatusCode,
|
||||||
});
|
code: cls.errorCode,
|
||||||
|
friendly: cls.friendlyMessage,
|
||||||
|
stack: error.stack,
|
||||||
|
jobid: rid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!bodyshop || !job) {
|
if (!bodyshop || !job) {
|
||||||
@@ -1477,9 +1626,11 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
socket.emit("export-failed", {
|
socket.emit("export-failed", {
|
||||||
vendor: "rr",
|
...withRRXmlSocketPayload(error, {
|
||||||
jobId: rid,
|
vendor: "rr",
|
||||||
error: error.message,
|
jobId: rid,
|
||||||
|
error: error.message
|
||||||
|
}),
|
||||||
...cls
|
...cls
|
||||||
});
|
});
|
||||||
socket.emit("rr-user-notice", { jobId: rid, ...cls });
|
socket.emit("rr-user-notice", { jobId: rid, ...cls });
|
||||||
@@ -1541,7 +1692,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (finalizeResult?.success) {
|
if (finalizeResult?.success) {
|
||||||
CreateRRLogEvent(socket, "INFO", `{7} Finalize success; marking exported`, { dmsRoNo, outsdRoNo });
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"INFO",
|
||||||
|
`{7} Finalize success; marking exported`,
|
||||||
|
withRRRequestXml(finalizeResult, { dmsRoNo, outsdRoNo })
|
||||||
|
);
|
||||||
|
|
||||||
// ✅ Mark exported + success log
|
// ✅ Mark exported + success log
|
||||||
await markRRExportSuccess({
|
await markRRExportSuccess({
|
||||||
@@ -1584,6 +1740,17 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
message: vendorMessage
|
message: vendorMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"ERROR",
|
||||||
|
"Finalize failed",
|
||||||
|
withRRRequestXml(finalizeResult, {
|
||||||
|
roStatus: finalizeResult?.roStatus,
|
||||||
|
statusBlocks: finalizeResult?.statusBlocks,
|
||||||
|
classification: cls
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
socket,
|
socket,
|
||||||
jobId: rid,
|
jobId: rid,
|
||||||
@@ -1595,23 +1762,30 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.emit("export-failed", {
|
socket.emit("export-failed", {
|
||||||
vendor: "rr",
|
...withRRXmlSocketPayload(finalizeResult, {
|
||||||
jobId: rid,
|
vendor: "rr",
|
||||||
error: cls?.friendlyMessage || finalizeResult?.error || "RR finalize failed",
|
jobId: rid,
|
||||||
|
error: cls?.friendlyMessage || finalizeResult?.error || "RR finalize failed"
|
||||||
|
}),
|
||||||
...cls
|
...cls
|
||||||
});
|
});
|
||||||
ack?.({ ok: false, error: cls.friendlyMessage || "RR finalize failed", classification: cls });
|
ack?.({ ok: false, error: cls.friendlyMessage || "RR finalize failed", classification: cls });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const cls = classifyRRVendorError(error);
|
const cls = classifyRRVendorError(error);
|
||||||
CreateRRLogEvent(socket, "ERROR", `Error during RR finalize`, {
|
CreateRRLogEvent(
|
||||||
error: error.message,
|
socket,
|
||||||
vendorStatusCode: cls.vendorStatusCode,
|
"ERROR",
|
||||||
code: cls.errorCode,
|
`Error during RR finalize`,
|
||||||
friendly: cls.friendlyMessage,
|
withRRRequestXml(error, {
|
||||||
stack: error.stack,
|
error: error.message,
|
||||||
jobid: rid
|
vendorStatusCode: cls.vendorStatusCode,
|
||||||
});
|
code: cls.errorCode,
|
||||||
|
friendly: cls.friendlyMessage,
|
||||||
|
stack: error.stack,
|
||||||
|
jobid: rid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!bodyshop || !job) {
|
if (!bodyshop || !job) {
|
||||||
@@ -1635,7 +1809,17 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message, ...cls });
|
socket.emit(
|
||||||
|
"export-failed",
|
||||||
|
{
|
||||||
|
...withRRXmlSocketPayload(error, {
|
||||||
|
vendor: "rr",
|
||||||
|
jobId: rid,
|
||||||
|
error: error.message
|
||||||
|
}),
|
||||||
|
...cls
|
||||||
|
}
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup");
|
const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup");
|
||||||
const CreateRRLogEvent = require("./rr-logger-event");
|
const CreateRRLogEvent = require("./rr-logger-event");
|
||||||
|
const { withRRRequestXml } = require("./rr-log-xml");
|
||||||
/**
|
/**
|
||||||
* Pick and normalize VIN from inputs
|
* Pick and normalize VIN from inputs
|
||||||
* @param vin
|
* @param vin
|
||||||
@@ -168,9 +169,12 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
|||||||
if (bodyshop) {
|
if (bodyshop) {
|
||||||
const combinedSearchResponse = await rrCombinedSearch(bodyshop, { kind: "vin", vin: vinStr, maxResults: 50 });
|
const combinedSearchResponse = await rrCombinedSearch(bodyshop, { kind: "vin", vin: vinStr, maxResults: 50 });
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "silly", "{SV} Preflight combined search by VIN: raw response", {
|
CreateRRLogEvent(
|
||||||
response: combinedSearchResponse
|
socket,
|
||||||
});
|
"silly",
|
||||||
|
"{SV} Preflight combined search by VIN: raw response",
|
||||||
|
withRRRequestXml(combinedSearchResponse, { response: combinedSearchResponse })
|
||||||
|
);
|
||||||
|
|
||||||
owners = ownersFromCombined(combinedSearchResponse, vinStr);
|
owners = ownersFromCombined(combinedSearchResponse, vinStr);
|
||||||
}
|
}
|
||||||
@@ -194,10 +198,15 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Preflight shouldn't be fatal; log and continue to insert (idempotency will still be handled)
|
// Preflight shouldn't be fatal; log and continue to insert (idempotency will still be handled)
|
||||||
CreateRRLogEvent(socket, "warn", "{SV} VIN preflight lookup failed; continuing to insert", {
|
CreateRRLogEvent(
|
||||||
vin: vinStr,
|
socket,
|
||||||
error: e?.message
|
"warn",
|
||||||
});
|
"{SV} VIN preflight lookup failed; continuing to insert",
|
||||||
|
withRRRequestXml(e, {
|
||||||
|
vin: vinStr,
|
||||||
|
error: e?.message
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vendor says: MODEL DESCRIPTION HAS MAXIMUM LENGTH OF 20
|
// Vendor says: MODEL DESCRIPTION HAS MAXIMUM LENGTH OF 20
|
||||||
@@ -271,7 +280,7 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
|||||||
try {
|
try {
|
||||||
const res = await client.insertServiceVehicle(insertPayload, insertOpts);
|
const res = await client.insertServiceVehicle(insertPayload, insertOpts);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "silly", "{SV} insertServiceVehicle: raw response", { res });
|
CreateRRLogEvent(socket, "silly", "{SV} insertServiceVehicle: raw response", withRRRequestXml(res, { res }));
|
||||||
|
|
||||||
const data = res?.data ?? {};
|
const data = res?.data ?? {};
|
||||||
const svId = data?.dmsRecKey || data?.svId || undefined;
|
const svId = data?.dmsRecKey || data?.svId || undefined;
|
||||||
@@ -309,11 +318,16 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "error", "{SV} insertServiceVehicle: failure", {
|
CreateRRLogEvent(
|
||||||
message: e?.message,
|
socket,
|
||||||
code: e?.code,
|
"error",
|
||||||
status: e?.meta?.status || e?.status
|
"{SV} insertServiceVehicle: failure",
|
||||||
});
|
withRRRequestXml(e, {
|
||||||
|
message: e?.message,
|
||||||
|
code: e?.code,
|
||||||
|
status: e?.meta?.status || e?.status
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user