Compare commits
100 Commits
feature/IO
...
release/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c33d743fe3 | ||
|
|
f56a540b2f | ||
|
|
e251e5f8f6 | ||
|
|
5a55798d2d | ||
|
|
c9e41ba72a | ||
|
|
522f2b9e26 | ||
|
|
be9267ddd4 | ||
|
|
e4a79b51c7 | ||
|
|
47a9a963fa | ||
|
|
f3c7a831a1 | ||
|
|
6ac9310e81 | ||
|
|
b91e65be0e | ||
|
|
3f2358e30c | ||
|
|
ce02d90c3c | ||
|
|
95a71bea6e | ||
|
|
3b27120d77 | ||
|
|
f350163056 | ||
|
|
db4d286a86 | ||
|
|
57cfecb7b8 | ||
|
|
56c24e3450 | ||
|
|
9a41cfd6af | ||
|
|
2934da4be9 | ||
|
|
c2fb010a59 | ||
|
|
ccba7b0137 | ||
|
|
c116007042 | ||
|
|
31c7abab39 | ||
|
|
589e537c94 | ||
|
|
b2f471fe9a | ||
|
|
7ea4f96664 | ||
|
|
fd6f46e39d | ||
|
|
0b505b3b4b | ||
|
|
226cc801ae | ||
|
|
67396afeb7 | ||
|
|
dab66b4d66 | ||
|
|
20d51431e7 | ||
|
|
15bb1e72a2 | ||
|
|
5edab6d040 | ||
|
|
48017e7471 | ||
|
|
acb1cc6367 | ||
|
|
77befd5d93 | ||
|
|
c93b8ed961 | ||
|
|
4d58c46a33 | ||
|
|
7299020bd8 | ||
|
|
f16a0c491b | ||
|
|
ae52f12bae | ||
|
|
11475afdb1 | ||
|
|
7a5e722ec1 | ||
|
|
7c686e38da | ||
|
|
9eaf45ac88 | ||
|
|
8cd2e65305 | ||
|
|
da9744da6f | ||
|
|
947ded4b5e | ||
|
|
6e6304124b | ||
|
|
2f694c2638 | ||
|
|
5f8a08b0a7 | ||
|
|
fd7970df2c | ||
|
|
03ad66b2a2 | ||
|
|
6f80e6dcbf | ||
|
|
21f43285bc | ||
|
|
b2bc19c5c9 | ||
|
|
e075361e23 | ||
|
|
a6327912ab | ||
|
|
ae1408012f | ||
|
|
c8b7d7461a | ||
|
|
48755dfa58 | ||
|
|
3be344b595 | ||
|
|
5d53d09af9 | ||
|
|
d4bbdd7383 | ||
|
|
8b55df8624 | ||
|
|
8422ea83ae | ||
|
|
e5f930b8c8 | ||
|
|
6d94265081 | ||
|
|
d9e75fe775 | ||
|
|
94c3ab6e1b | ||
|
|
1b84087ef8 | ||
|
|
a9fdf3da18 | ||
|
|
6ae4e228ce | ||
|
|
49fb2caac0 | ||
|
|
673670eeb4 | ||
|
|
d9b3730db9 | ||
|
|
313a90e8f3 | ||
|
|
2a352b60a0 | ||
|
|
e6100851b8 | ||
|
|
e9795072d5 | ||
|
|
64454dce2a | ||
|
|
c59acb1b72 | ||
|
|
773f3d4c84 | ||
|
|
5ae0e8e4d5 | ||
|
|
40d5e02415 | ||
|
|
5b891281d1 | ||
|
|
71043313d6 | ||
|
|
c9620a3f6f | ||
|
|
cdfae5a429 | ||
|
|
20dad2caba | ||
|
|
96731a29e1 | ||
|
|
83be45a40b | ||
|
|
55de16281d | ||
|
|
ad7e85a578 | ||
|
|
2a6d0446f0 | ||
|
|
c3718fff87 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -132,10 +132,3 @@ server/job/test/fixtures
|
||||
.github
|
||||
_reference/ragmate/.ragmate.env
|
||||
docker_data
|
||||
/.cursorrules
|
||||
/AGENTS.md
|
||||
/AI_CONTEXT.md
|
||||
/CLAUDE.md
|
||||
/COPILOT.md
|
||||
/GEMINI.md
|
||||
/_reference/select-component-test-plan.md
|
||||
|
||||
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);
|
||||
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,35 +443,69 @@
|
||||
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 {
|
||||
// padding: 0;
|
||||
//}
|
||||
|
||||
/* globally allow shrink inside table cells */
|
||||
.prod-list-table .ant-table-cell,
|
||||
.prod-list-table .ant-table-cell > * {
|
||||
min-width: 0;
|
||||
}
|
||||
///* globally allow shrink inside table cells */
|
||||
//.prod-list-table .ant-table-cell,
|
||||
//.prod-list-table .ant-table-cell > * {
|
||||
// min-width: 0;
|
||||
//}
|
||||
//
|
||||
///* common AntD offenders */
|
||||
//.prod-list-table > .ant-table-cell .ant-space,
|
||||
//.ant-table-cell .ant-space-item {
|
||||
// min-width: 0;
|
||||
//}
|
||||
//
|
||||
///* Keep your custom header content on the left, push AntD sorter to the far right */
|
||||
//.prod-list-table .ant-table-column-sorters {
|
||||
// display: flex !important;
|
||||
// align-items: center;
|
||||
// width: 100%;
|
||||
//}
|
||||
//
|
||||
//.prod-list-table .ant-table-column-title {
|
||||
// flex: 1 1 auto;
|
||||
// min-width: 0; /* allows ellipsis to work */
|
||||
//}
|
||||
//
|
||||
//.prod-list-table .ant-table-column-sorter {
|
||||
// margin-left: auto;
|
||||
// flex: 0 0 auto;
|
||||
//}
|
||||
|
||||
/* common AntD offenders */
|
||||
.prod-list-table > .ant-table-cell .ant-space,
|
||||
.ant-table-cell .ant-space-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Keep your custom header content on the left, push AntD sorter to the far right */
|
||||
.prod-list-table .ant-table-column-sorters {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prod-list-table .ant-table-column-title {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0; /* allows ellipsis to work */
|
||||
}
|
||||
|
||||
.prod-list-table .ant-table-column-sorter {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
.global-search-autocomplete-fix {
|
||||
// This is the extra value render that causes the “duplicate text”
|
||||
.ant-select-selection-item {
|
||||
position: absolute !important;
|
||||
left: -10000px !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Table } from "antd";
|
||||
|
||||
function ResponsiveTable(props) {
|
||||
return <Table {...props} />;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -21,6 +21,7 @@ import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
|
||||
import JobEmployeeAssignmentsContainer from "./../job-employee-assignments/job-employee-assignments.container";
|
||||
import { PayrollLaborAllocationsTable } from "../labor-allocations-table/labor-allocations-table.payroll.component.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -46,10 +47,10 @@ export function TimeTicketModalComponent({
|
||||
} = useTreatmentsWithConfig({
|
||||
attributes: {},
|
||||
names: ["Enhanced_Payroll"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
splitKey: bodyshop?.imexshopid
|
||||
});
|
||||
|
||||
const [loadLineTicketData, { loading, data: lineTicketData }] = useLazyQuery(GET_LINE_TICKET_BY_PK, {
|
||||
const [loadLineTicketData, { loading, data: lineTicketData, refetch }] = useLazyQuery(GET_LINE_TICKET_BY_PK, {
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
@@ -91,21 +92,22 @@ export function TimeTicketModalComponent({
|
||||
value={value === "timetickets.labels.shift" ? t(value) : value}
|
||||
{...props}
|
||||
disabled={value === "timetickets.labels.shift" || disabled}
|
||||
>
|
||||
{emps &&
|
||||
emps.rates.map((item) => (
|
||||
<Select.Option key={item.cost_center} value={item.cost_center}>
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
options={
|
||||
emps &&
|
||||
emps.rates.map((item) => ({
|
||||
value: item.cost_center,
|
||||
label:
|
||||
item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
: item.cost_center
|
||||
}))
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
const MemoInput = ({ value, ...props }) => (
|
||||
@@ -320,13 +322,34 @@ export function TimeTicketModalComponent({
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
|
||||
<LaborAllocationContainer jobid={watchedJobId || null} loading={loading} lineTicketData={lineTicketData} />
|
||||
<LaborAllocationContainer
|
||||
jobid={watchedJobId || null}
|
||||
loading={loading}
|
||||
lineTicketData={lineTicketData}
|
||||
bodyshop={bodyshop}
|
||||
refetch={refetch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideTimeTickets = false }) {
|
||||
export function LaborAllocationContainer({
|
||||
jobid,
|
||||
loading,
|
||||
lineTicketData,
|
||||
hideTimeTickets = false,
|
||||
bodyshop,
|
||||
refetch
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
treatments: { Enhanced_Payroll }
|
||||
} = useTreatmentsWithConfig({
|
||||
attributes: {},
|
||||
names: ["Enhanced_Payroll"],
|
||||
splitKey: bodyshop?.imexshopid
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (!lineTicketData) return null;
|
||||
if (!jobid) return null;
|
||||
@@ -337,12 +360,23 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
|
||||
<JobEmployeeAssignmentsContainer job={lineTicketData.jobs_by_pk} />
|
||||
</Card>
|
||||
|
||||
<LaborAllocationsTable
|
||||
jobId={jobid}
|
||||
joblines={lineTicketData.joblines}
|
||||
timetickets={lineTicketData.timetickets}
|
||||
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
|
||||
/>
|
||||
{Enhanced_Payroll.treatment === "on" ? (
|
||||
<PayrollLaborAllocationsTable
|
||||
jobId={jobid}
|
||||
joblines={lineTicketData.joblines}
|
||||
timetickets={lineTicketData.timetickets}
|
||||
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
|
||||
refetch={refetch}
|
||||
bodyshop={bodyshop}
|
||||
/>
|
||||
) : (
|
||||
<LaborAllocationsTable
|
||||
jobId={jobid}
|
||||
joblines={lineTicketData.joblines}
|
||||
timetickets={lineTicketData.timetickets}
|
||||
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hideTimeTickets && (
|
||||
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import SocketIO from "socket.io-client";
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import { store } from "../../redux/store";
|
||||
@@ -18,7 +18,6 @@ import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
|
||||
import { INITIAL_NOTIFICATIONS, SocketContext } from "./useSocket.js";
|
||||
|
||||
const LIMIT = INITIAL_NOTIFICATIONS;
|
||||
const TOKEN_SYNC_INTERVAL_MS = 10 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Socket Provider - Scenario Notifications / Web Socket related items
|
||||
@@ -31,7 +30,6 @@ const TOKEN_SYNC_INTERVAL_MS = 10 * 60 * 1000;
|
||||
*/
|
||||
const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
const socketRef = useRef(null);
|
||||
const tokenSyncIntervalRef = useRef(null);
|
||||
const [clientId, setClientId] = useState(null);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const notification = useNotification();
|
||||
@@ -149,30 +147,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
onError: (err) => console.error("MARK_ALL_NOTIFICATIONS_READ error:", err)
|
||||
});
|
||||
|
||||
const reconnectSocket = useCallback(
|
||||
async ({ forceRefreshToken = true } = {}) => {
|
||||
const socketInstance = socketRef.current;
|
||||
if (!socketInstance || !auth.currentUser || !bodyshop?.id) return false;
|
||||
|
||||
try {
|
||||
const token = await auth.currentUser.getIdToken(forceRefreshToken);
|
||||
socketInstance.auth = { token, bodyshopId: bodyshop.id };
|
||||
|
||||
if (socketInstance.connected) {
|
||||
socketInstance.emit("update-token", { token, bodyshopId: bodyshop.id });
|
||||
}
|
||||
|
||||
socketInstance.disconnect();
|
||||
socketInstance.connect();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Socket reconnect failed:", error?.message || error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[bodyshop?.id]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const initializeSocket = async (token) => {
|
||||
if (!bodyshop?.id || socketRef.current) return;
|
||||
@@ -280,60 +254,25 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const syncCurrentTokenToSocket = async () => {
|
||||
try {
|
||||
if (!auth.currentUser || !bodyshop?.id) return;
|
||||
const token = await auth.currentUser.getIdToken();
|
||||
socketInstance.auth = { token, bodyshopId: bodyshop.id };
|
||||
socketInstance.emit("update-token", { token, bodyshopId: bodyshop.id });
|
||||
} catch (error) {
|
||||
console.error("Failed to sync token to socket:", error?.message || error);
|
||||
}
|
||||
};
|
||||
|
||||
const forceRefreshAndSyncToken = async () => {
|
||||
try {
|
||||
if (!auth.currentUser || !bodyshop?.id) return;
|
||||
const token = await auth.currentUser.getIdToken(true);
|
||||
socketInstance.auth = { token, bodyshopId: bodyshop.id };
|
||||
socketInstance.emit("update-token", { token, bodyshopId: bodyshop.id });
|
||||
} catch (error) {
|
||||
console.error("Failed to force-refresh token for socket:", error?.message || error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnect = () => {
|
||||
socketInstance.emit("join-bodyshop-room", bodyshop.id);
|
||||
syncCurrentTokenToSocket();
|
||||
setClientId(socketInstance.id);
|
||||
setIsConnected(true);
|
||||
store.dispatch(setWssStatus("connected"));
|
||||
};
|
||||
|
||||
const handleReconnect = () => {
|
||||
forceRefreshAndSyncToken();
|
||||
setIsConnected(true);
|
||||
store.dispatch(setWssStatus("connected"));
|
||||
};
|
||||
|
||||
const handleTokenUpdated = ({ success, error }) => {
|
||||
if (success) return;
|
||||
const err = String(error || "");
|
||||
if (/stale token|id-token-expired/i.test(err)) {
|
||||
forceRefreshAndSyncToken();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnectionError = (err) => {
|
||||
console.error("Socket connection error:", err);
|
||||
setIsConnected(false);
|
||||
if (err?.message?.includes("auth/id-token-expired")) {
|
||||
if (err.message.includes("auth/id-token-expired")) {
|
||||
console.warn("Token expired, refreshing...");
|
||||
auth.currentUser?.getIdToken(true).then((newToken) => {
|
||||
socketInstance.auth = { token: newToken, bodyshopId: bodyshop.id };
|
||||
if (socketInstance.connected) {
|
||||
socketInstance.emit("update-token", { token: newToken, bodyshopId: bodyshop.id });
|
||||
}
|
||||
socketInstance.auth = { token: newToken };
|
||||
socketInstance.connect();
|
||||
});
|
||||
} else {
|
||||
@@ -574,23 +513,10 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
socketInstance.on("notification", handleNotification);
|
||||
socketInstance.on("sync-notification-read", handleSyncNotificationRead);
|
||||
socketInstance.on("sync-all-notifications-read", handleSyncAllNotificationsRead);
|
||||
socketInstance.on("token-updated", handleTokenUpdated);
|
||||
|
||||
if (tokenSyncIntervalRef.current) {
|
||||
clearInterval(tokenSyncIntervalRef.current);
|
||||
}
|
||||
tokenSyncIntervalRef.current = setInterval(() => {
|
||||
if (!socketInstance.connected) return;
|
||||
syncCurrentTokenToSocket();
|
||||
}, TOKEN_SYNC_INTERVAL_MS);
|
||||
};
|
||||
|
||||
const unsubscribe = auth.onIdTokenChanged(async (user) => {
|
||||
if (!user) {
|
||||
if (tokenSyncIntervalRef.current) {
|
||||
clearInterval(tokenSyncIntervalRef.current);
|
||||
tokenSyncIntervalRef.current = null;
|
||||
}
|
||||
socketRef.current?.disconnect();
|
||||
socketRef.current = null;
|
||||
setIsConnected(false);
|
||||
@@ -599,10 +525,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
|
||||
const token = await user.getIdToken();
|
||||
if (socketRef.current) {
|
||||
socketRef.current.auth = { token, bodyshopId: bodyshop.id };
|
||||
if (socketRef.current.connected) {
|
||||
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id });
|
||||
}
|
||||
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id });
|
||||
} else {
|
||||
initializeSocket(token).catch((err) =>
|
||||
console.error("Something went wrong Initializing Sockets:", err?.message || "")
|
||||
@@ -612,10 +535,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
if (tokenSyncIntervalRef.current) {
|
||||
clearInterval(tokenSyncIntervalRef.current);
|
||||
tokenSyncIntervalRef.current = null;
|
||||
}
|
||||
if (socketRef.current) {
|
||||
socketRef.current.disconnect();
|
||||
socketRef.current = null;
|
||||
@@ -630,7 +549,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
socket: socketRef.current,
|
||||
clientId,
|
||||
isConnected,
|
||||
reconnectSocket,
|
||||
markNotificationRead,
|
||||
markAllNotificationsRead,
|
||||
scenarioNotificationsOn: Realtime_Notifications_UI?.treatment === "on"
|
||||
|
||||
@@ -77,7 +77,6 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
const { t } = useTranslation();
|
||||
const [resetAfterReconnect, setResetAfterReconnect] = useState(false);
|
||||
const [allocationsSummary, setAllocationsSummary] = useState(null);
|
||||
const [reconnectNonce, setReconnectNonce] = useState(0);
|
||||
|
||||
// Compute a single normalized mode and pick the proper socket
|
||||
const mode = getDmsMode(bodyshop, Fortellis.treatment); // "rr" | "fortellis" | "cdk" | "pbs" | "none"
|
||||
@@ -115,7 +114,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
|
||||
const notification = useNotification();
|
||||
|
||||
const { socket: wsssocket, reconnectSocket } = useSocket();
|
||||
const { socket: wsssocket } = useSocket();
|
||||
const activeSocket = useMemo(() => (isWssMode(mode) ? wsssocket : legacySocket), [mode, wsssocket]);
|
||||
|
||||
const [isConnected, setIsConnected] = useState(!!activeSocket?.connected);
|
||||
@@ -179,27 +178,6 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
}`;
|
||||
|
||||
const resetKey = useMemo(() => `${mode || "none"}-${jobId || "none"}`, [mode, jobId]);
|
||||
const customerSelectorKey = useMemo(() => `${resetKey}-${reconnectNonce}`, [resetKey, reconnectNonce]);
|
||||
|
||||
const handleReconnectClick = async () => {
|
||||
setResetAfterReconnect(true);
|
||||
setReconnectNonce((n) => n + 1);
|
||||
|
||||
if (!activeSocket) return;
|
||||
|
||||
if (isWssMode(mode)) {
|
||||
setActiveLogLevel(logLevel);
|
||||
const didReconnect = await reconnectSocket?.({ forceRefreshToken: true });
|
||||
if (!didReconnect) {
|
||||
activeSocket.disconnect();
|
||||
setTimeout(() => activeSocket.connect(), 100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
activeSocket.disconnect();
|
||||
setTimeout(() => activeSocket.connect(), 100);
|
||||
};
|
||||
|
||||
// 🔄 Hard reset of local + server-side DMS context when the page/job loads
|
||||
useEffect(() => {
|
||||
@@ -450,7 +428,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
|
||||
// Check if Reynolds mode requires early RO
|
||||
const hasEarlyRO = !!(data.jobs_by_pk?.dms_id && data.jobs_by_pk?.dms_customer_id && data.jobs_by_pk?.dms_advisor_id);
|
||||
|
||||
|
||||
if (isRrMode && !hasEarlyRO) {
|
||||
return (
|
||||
<Result
|
||||
@@ -525,7 +503,6 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
</Col>
|
||||
|
||||
<DmsCustomerSelector
|
||||
key={customerSelectorKey}
|
||||
jobid={jobId}
|
||||
job={data?.jobs_by_pk}
|
||||
bodyshop={bodyshop}
|
||||
@@ -572,7 +549,21 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
<Select.Option key="ERROR">ERROR</Select.Option>
|
||||
</Select>
|
||||
<Button onClick={() => setLogs([])}>Clear Logs</Button>
|
||||
<Button onClick={handleReconnectClick}>Reconnect</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLogs([]);
|
||||
setResetAfterReconnect(true);
|
||||
if (isWssMode(mode)) {
|
||||
setActiveLogLevel(logLevel);
|
||||
}
|
||||
if (activeSocket) {
|
||||
activeSocket.disconnect();
|
||||
setTimeout(() => activeSocket.connect(), 100);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reconnect
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -86,9 +86,6 @@ async function FetchSubscriptions({ redisHelpers, socket, jobid, SubscriptionObj
|
||||
logRequest: false
|
||||
});
|
||||
const SubscriptionMeta = subscriptions.data.subscriptions.find((s) => s.subscriptionId === SubscriptionID);
|
||||
if (!SubscriptionMeta) {
|
||||
throw new Error(`Subscription metadata not found for SubscriptionID: ${SubscriptionID}`);
|
||||
}
|
||||
if (setSessionTransactionData) {
|
||||
await setSessionTransactionData(
|
||||
socket.id,
|
||||
@@ -105,15 +102,11 @@ async function FetchSubscriptions({ redisHelpers, socket, jobid, SubscriptionObj
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function GetDepartmentId({ apiName, debug = false, SubscriptionMeta, overrideDepartmentId }) {
|
||||
if (!apiName) throw new Error("apiName not provided. Unable to get department without apiName.");
|
||||
if (!SubscriptionMeta || !Array.isArray(SubscriptionMeta.apiDmsInfo)) {
|
||||
throw new Error("Subscription metadata missing apiDmsInfo.");
|
||||
}
|
||||
if (debug) {
|
||||
console.log("API Names & Departments ");
|
||||
console.log("===========");
|
||||
@@ -125,8 +118,9 @@ async function GetDepartmentId({ apiName, debug = false, SubscriptionMeta, overr
|
||||
.find((info) => info.name === apiName)?.departments; //Departments are categorized by API name and have an array of departments.
|
||||
|
||||
if (overrideDepartmentId) {
|
||||
return departmentIds && departmentIds.find((d) => d.id === overrideDepartmentId)?.id;
|
||||
return departmentIds && departmentIds.find(d => d.id === overrideDepartmentId)?.id
|
||||
} else {
|
||||
|
||||
return departmentIds && departmentIds[0] && departmentIds[0].id; //TODO: This makes the assumption that there is only 1 department.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,52 +180,22 @@ async function FortellisSelectedCustomer({ socket, redisHelpers, selectedCustome
|
||||
getTransactionType(jobid),
|
||||
FortellisCacheEnums.txEnvelope
|
||||
);
|
||||
if (!JobData || !txEnvelope) {
|
||||
const friendlyMessage =
|
||||
"Fortellis export context was lost after reconnect. Click Post again to restart the Fortellis flow.";
|
||||
CreateFortellisLogEvent(socket, "WARN", friendlyMessage, {
|
||||
jobid,
|
||||
hasJobData: !!JobData,
|
||||
hasTxEnvelope: !!txEnvelope
|
||||
});
|
||||
socket.emit("export-failed", {
|
||||
title: "Fortellis",
|
||||
severity: "warning",
|
||||
errorCode: "FORTELLIS_CONTEXT_MISSING",
|
||||
friendlyMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const DMSVid = await redisHelpers.getSessionTransactionData(
|
||||
socket.id,
|
||||
getTransactionType(JobData.id),
|
||||
FortellisCacheEnums.DMSVid
|
||||
);
|
||||
try {
|
||||
const DMSVid = await redisHelpers.getSessionTransactionData(
|
||||
socket.id,
|
||||
getTransactionType(JobData.id),
|
||||
FortellisCacheEnums.DMSVid
|
||||
);
|
||||
if (!DMSVid) {
|
||||
const friendlyMessage =
|
||||
"Fortellis vehicle context is missing after reconnect. Click Post again to restart the Fortellis flow.";
|
||||
CreateFortellisLogEvent(socket, "WARN", friendlyMessage, {
|
||||
jobid,
|
||||
hasDMSVid: !!DMSVid
|
||||
});
|
||||
socket.emit("export-failed", {
|
||||
title: "Fortellis",
|
||||
severity: "warning",
|
||||
errorCode: "FORTELLIS_CONTEXT_MISSING",
|
||||
friendlyMessage
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let DMSCust;
|
||||
if (selectedCustomerId) {
|
||||
CreateFortellisLogEvent(socket, "DEBUG", `{3.1} Querying the Customer using Customer ID: ${selectedCustomerId}`);
|
||||
|
||||
//Get cust list from Redis. Return the item
|
||||
const DMSCustList =
|
||||
(await getSessionTransactionData(socket.id, getTransactionType(jobid), FortellisCacheEnums.DMSCustList)) || [];
|
||||
const DMSCustList = await getSessionTransactionData(
|
||||
socket.id,
|
||||
getTransactionType(jobid),
|
||||
FortellisCacheEnums.DMSCustList
|
||||
);
|
||||
const existingCustomerInDMSCustList = DMSCustList.find((c) => c.customerId === selectedCustomerId);
|
||||
DMSCust = existingCustomerInDMSCustList || {
|
||||
customerId: selectedCustomerId //This is the fall back in case it is the generic customer.
|
||||
@@ -336,7 +306,7 @@ async function FortellisSelectedCustomer({ socket, redisHelpers, selectedCustome
|
||||
//There was something wrong. Throw an error to trigger clean up.
|
||||
//throw new Error("Error posting DMS Batch Transaction");
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
//Clean up the transaction and insert a faild error code
|
||||
// //Get the error code
|
||||
CreateFortellisLogEvent(socket, "DEBUG", `{6.1} Getting errors for Transaction ID ${DMSTransHeader.transID}`);
|
||||
@@ -366,12 +336,6 @@ async function FortellisSelectedCustomer({ socket, redisHelpers, selectedCustome
|
||||
stack: error.stack,
|
||||
data: error.errorData
|
||||
});
|
||||
socket.emit("export-failed", {
|
||||
title: "Fortellis",
|
||||
severity: "error",
|
||||
error: error.message,
|
||||
friendlyMessage: "Fortellis export failed. Please click Post again to retry."
|
||||
});
|
||||
await InsertFailedExportLog({
|
||||
socket,
|
||||
JobData,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { RRClient } = require("./lib/index.cjs");
|
||||
const { getRRConfigFromBodyshop } = require("./rr-config");
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
const { withRRRequestXml } = require("./rr-log-xml");
|
||||
const InstanceManager = require("../utils/instanceMgr").default;
|
||||
|
||||
/**
|
||||
@@ -217,14 +218,24 @@ const createRRCustomer = async ({ bodyshop, job, overrides = {}, socket }) => {
|
||||
try {
|
||||
response = await client.insertCustomer(safePayload, opts);
|
||||
// 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) {
|
||||
CreateRRLogEvent(socket, "ERROR", "RR insertCustomer transport error", {
|
||||
message: e?.message,
|
||||
code: e?.code,
|
||||
status: e?.meta?.status || e?.status,
|
||||
payload: safePayload
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"ERROR",
|
||||
"RR insertCustomer transport error",
|
||||
withRRRequestXml(e, {
|
||||
message: e?.message,
|
||||
code: e?.code,
|
||||
status: e?.meta?.status || e?.status,
|
||||
payload: safePayload
|
||||
})
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -233,12 +244,17 @@ const createRRCustomer = async ({ bodyshop, job, overrides = {}, socket }) => {
|
||||
|
||||
let customerNo = data?.dmsRecKey;
|
||||
if (!customerNo) {
|
||||
CreateRRLogEvent(socket, "ERROR", "RR insertCustomer returned no dmsRecKey/custNo", {
|
||||
status: trx?.status,
|
||||
statusCode: trx?.statusCode,
|
||||
message: trx?.message,
|
||||
data
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"ERROR",
|
||||
"RR insertCustomer returned no dmsRecKey/custNo",
|
||||
withRRRequestXml(response, {
|
||||
status: trx?.status,
|
||||
statusCode: trx?.statusCode,
|
||||
message: trx?.message,
|
||||
data
|
||||
})
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`RR insertCustomer returned no dmsRecKey (status=${trx?.status ?? "?"} code=${trx?.statusCode ?? "?"}${
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { GraphQLClient } = require("graphql-request");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
const { extractRRXmlPair } = require("./rr-log-xml");
|
||||
|
||||
/** Get bearer token from the socket (same approach used elsewhere) */
|
||||
const getAuthToken = (socket) =>
|
||||
@@ -178,11 +179,23 @@ const insertRRFailedExportLog = async ({ socket, jobId, job, bodyshop, error, cl
|
||||
const client = new GraphQLClient(endpoint, {});
|
||||
client.setHeaders({ Authorization: `Bearer ${token}` });
|
||||
|
||||
const { requestXml, responseXml } = extractRRXmlPair(error);
|
||||
const xmlFromError =
|
||||
requestXml || responseXml
|
||||
? {
|
||||
...(requestXml ? { request: requestXml } : {}),
|
||||
...(responseXml ? { response: responseXml } : {})
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const meta = buildRRExportMeta({
|
||||
result,
|
||||
extra: {
|
||||
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 { buildClientAndOpts } = require("./rr-lookup");
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
const { withRRRequestXml } = require("./rr-log-xml");
|
||||
const { extractRrResponsibilityCenters } = require("./rr-responsibility-centers");
|
||||
const CdkCalculateAllocations = require("./rr-calculate-allocations").default;
|
||||
const { resolveRROpCodeFromBodyshop } = require("./rr-utils");
|
||||
@@ -147,10 +148,7 @@ const createMinimalRRRepairOrder = async (args) => {
|
||||
|
||||
const response = await client.createRepairOrder(payload, finalOpts);
|
||||
|
||||
CreateRRLogEvent(socket, "INFO", "RR minimal Repair Order created", {
|
||||
payload,
|
||||
response
|
||||
});
|
||||
CreateRRLogEvent(socket, "INFO", "RR minimal Repair Order created", withRRRequestXml(response, { payload, response }));
|
||||
|
||||
const data = response?.data || null;
|
||||
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
|
||||
// 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),
|
||||
hasRolabor: !!payload.rolabor,
|
||||
hasRogg: !!payload.rogg,
|
||||
@@ -338,10 +336,18 @@ const updateRRRepairOrderWithFullData = async (args) => {
|
||||
// Reynolds will merge this with the existing RO header
|
||||
const response = await client.createRepairOrder(payload, finalOpts);
|
||||
|
||||
CreateRRLogEvent(socket, "INFO", "RR Repair Order full data sent", {
|
||||
payload,
|
||||
response
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"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 statusBlocks = response?.statusBlocks || {};
|
||||
@@ -501,10 +507,7 @@ const exportJobToRR = async (args) => {
|
||||
|
||||
const response = await client.createRepairOrder(payload, finalOpts);
|
||||
|
||||
CreateRRLogEvent(socket, "INFO", "RR raw Repair Order created", {
|
||||
payload,
|
||||
response
|
||||
});
|
||||
CreateRRLogEvent(socket, "INFO", "RR raw Repair Order created", withRRRequestXml(response, { payload, response }));
|
||||
|
||||
const data = response?.data || null;
|
||||
const statusBlocks = response?.statusBlocks || {};
|
||||
@@ -603,10 +606,15 @@ const finalizeRRRepairOrder = async (args) => {
|
||||
|
||||
const rrRes = await client.updateRepairOrder(payload, finalOpts);
|
||||
|
||||
CreateRRLogEvent(socket, "SILLY", "RR Repair Order finalized", {
|
||||
payload,
|
||||
response: rrRes
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
"RR Repair Order finalized",
|
||||
withRRRequestXml(rrRes, {
|
||||
payload,
|
||||
response: rrRes
|
||||
})
|
||||
);
|
||||
|
||||
const data = rrRes?.data || null;
|
||||
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 { classifyRRVendorError } = require("./rr-errors");
|
||||
const { markRRExportSuccess, insertRRFailedExportLog } = require("./rr-export-logs");
|
||||
const { withRRRequestXml } = require("./rr-log-xml");
|
||||
const {
|
||||
makeVehicleSearchPayloadFromJob,
|
||||
ownersFromVinBlocks,
|
||||
@@ -241,7 +242,12 @@ const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) =>
|
||||
|
||||
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) {
|
||||
const multiBlocks = Array.isArray(multiResponse?.data) ? multiResponse.data : [];
|
||||
@@ -262,7 +268,7 @@ const rrMultiCustomerSearch = async ({ bodyshop, job, socket, redisHelpers }) =>
|
||||
const norm = normalizeCustomerCandidates(multiResponse, { ownersSet });
|
||||
merged.push(...norm);
|
||||
} 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 +316,7 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
count: decorated.length
|
||||
});
|
||||
} 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 });
|
||||
}
|
||||
});
|
||||
@@ -387,7 +393,7 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
fromCache
|
||||
});
|
||||
} 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" });
|
||||
}
|
||||
});
|
||||
@@ -458,11 +464,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
|
||||
});
|
||||
} catch (error) {
|
||||
CreateRRLogEvent(socket, "ERROR", `Error during RR early RO creation (prepare)`, {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"ERROR",
|
||||
`Error during RR early RO creation (prepare)`,
|
||||
withRRRequestXml(error, {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
|
||||
@@ -555,7 +566,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
|
||||
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 : [];
|
||||
|
||||
@@ -588,9 +604,14 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
CreateRRLogEvent(socket, "WARN", `VIN owner pre-check failed; continuing with selected customer (early RO)`, {
|
||||
error: e?.message
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"WARN",
|
||||
`VIN owner pre-check failed; continuing with selected customer (early RO)`,
|
||||
withRRRequestXml(e, {
|
||||
error: e?.message
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Cache final/effective customer selection
|
||||
@@ -843,14 +864,19 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
} catch (error) {
|
||||
const cls = classifyRRVendorError(error);
|
||||
|
||||
CreateRRLogEvent(socket, "ERROR", `Error during RR early RO creation (customer-selected)`, {
|
||||
error: error.message,
|
||||
vendorStatusCode: cls.vendorStatusCode,
|
||||
code: cls.errorCode,
|
||||
friendly: cls.friendlyMessage,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"ERROR",
|
||||
`Error during RR early RO creation (customer-selected)`,
|
||||
withRRRequestXml(error, {
|
||||
error: error.message,
|
||||
vendorStatusCode: cls.vendorStatusCode,
|
||||
code: cls.errorCode,
|
||||
friendly: cls.friendlyMessage,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
if (!bodyshop || !job) {
|
||||
@@ -1030,7 +1056,28 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
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) {
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -1082,11 +1129,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner)
|
||||
});
|
||||
} catch (error) {
|
||||
CreateRRLogEvent(socket, "ERROR", `Error during RR export (prepare)`, {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"ERROR",
|
||||
`Error during RR export (prepare)`,
|
||||
withRRRequestXml(error, {
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
socket.emit("export-failed", { vendor: "rr", jobId: rid, error: error.message });
|
||||
@@ -1148,7 +1200,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
if (vehQ && vehQ.kind === "vin" && job?.v_vin) {
|
||||
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 : [];
|
||||
|
||||
@@ -1181,9 +1238,14 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
CreateRRLogEvent(socket, "WARN", `VIN owner pre-check failed; continuing with selected customer`, {
|
||||
error: e?.message
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"WARN",
|
||||
`VIN owner pre-check failed; continuing with selected customer`,
|
||||
withRRRequestXml(e, {
|
||||
error: e?.message
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Cache final/effective customer selection
|
||||
@@ -1445,14 +1507,19 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
} catch (error) {
|
||||
const cls = classifyRRVendorError(error);
|
||||
|
||||
CreateRRLogEvent(socket, "ERROR", `Error during RR export (selected-customer)`, {
|
||||
error: error.message,
|
||||
vendorStatusCode: cls.vendorStatusCode,
|
||||
code: cls.errorCode,
|
||||
friendly: cls.friendlyMessage,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"ERROR",
|
||||
`Error during RR export (selected-customer)`,
|
||||
withRRRequestXml(error, {
|
||||
error: error.message,
|
||||
vendorStatusCode: cls.vendorStatusCode,
|
||||
code: cls.errorCode,
|
||||
friendly: cls.friendlyMessage,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
if (!bodyshop || !job) {
|
||||
@@ -1604,14 +1671,19 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
||||
}
|
||||
} catch (error) {
|
||||
const cls = classifyRRVendorError(error);
|
||||
CreateRRLogEvent(socket, "ERROR", `Error during RR finalize`, {
|
||||
error: error.message,
|
||||
vendorStatusCode: cls.vendorStatusCode,
|
||||
code: cls.errorCode,
|
||||
friendly: cls.friendlyMessage,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"ERROR",
|
||||
`Error during RR finalize`,
|
||||
withRRRequestXml(error, {
|
||||
error: error.message,
|
||||
vendorStatusCode: cls.vendorStatusCode,
|
||||
code: cls.errorCode,
|
||||
friendly: cls.friendlyMessage,
|
||||
stack: error.stack,
|
||||
jobid: rid
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
if (!bodyshop || !job) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const { buildClientAndOpts, rrCombinedSearch } = require("./rr-lookup");
|
||||
const CreateRRLogEvent = require("./rr-logger-event");
|
||||
const { withRRRequestXml } = require("./rr-log-xml");
|
||||
/**
|
||||
* Pick and normalize VIN from inputs
|
||||
* @param vin
|
||||
@@ -168,9 +169,12 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
||||
if (bodyshop) {
|
||||
const combinedSearchResponse = await rrCombinedSearch(bodyshop, { kind: "vin", vin: vinStr, maxResults: 50 });
|
||||
|
||||
CreateRRLogEvent(socket, "silly", "{SV} Preflight combined search by VIN: raw response", {
|
||||
response: combinedSearchResponse
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"silly",
|
||||
"{SV} Preflight combined search by VIN: raw response",
|
||||
withRRRequestXml(combinedSearchResponse, { response: combinedSearchResponse })
|
||||
);
|
||||
|
||||
owners = ownersFromCombined(combinedSearchResponse, vinStr);
|
||||
}
|
||||
@@ -194,10 +198,15 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
||||
}
|
||||
} catch (e) {
|
||||
// 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", {
|
||||
vin: vinStr,
|
||||
error: e?.message
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"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
|
||||
@@ -271,7 +280,7 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
||||
try {
|
||||
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 svId = data?.dmsRecKey || data?.svId || undefined;
|
||||
@@ -309,11 +318,16 @@ const ensureRRServiceVehicle = async (args = {}) => {
|
||||
};
|
||||
}
|
||||
|
||||
CreateRRLogEvent(socket, "error", "{SV} insertServiceVehicle: failure", {
|
||||
message: e?.message,
|
||||
code: e?.code,
|
||||
status: e?.meta?.status || e?.status
|
||||
});
|
||||
CreateRRLogEvent(
|
||||
socket,
|
||||
"error",
|
||||
"{SV} insertServiceVehicle: failure",
|
||||
withRRRequestXml(e, {
|
||||
message: e?.message,
|
||||
code: e?.code,
|
||||
status: e?.meta?.status || e?.status
|
||||
})
|
||||
);
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -68,33 +68,12 @@ const fetchBodyshopFromDB = async (bodyshopId, logger) => {
|
||||
* @param logger
|
||||
*/
|
||||
const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
const toRedisJson = (value) => JSON.stringify(value === undefined ? null : value);
|
||||
|
||||
// Store session data in Redis
|
||||
const setSessionData = async (socketId, key, value, ttl) => {
|
||||
try {
|
||||
const sessionKey = `socket:${socketId}`;
|
||||
|
||||
// Supports both forms:
|
||||
// 1) setSessionData(socketId, "field", value, ttl)
|
||||
// 2) setSessionData(socketId, { fieldA: valueA, fieldB: valueB }, ttl)
|
||||
if (key && typeof key === "object" && !Array.isArray(key)) {
|
||||
const entries = Object.entries(key).flatMap(([field, fieldValue]) => [field, toRedisJson(fieldValue)]);
|
||||
|
||||
if (entries.length > 0) {
|
||||
await pubClient.hset(sessionKey, ...entries);
|
||||
}
|
||||
|
||||
const objectTtl = typeof value === "number" ? value : typeof ttl === "number" ? ttl : null;
|
||||
if (objectTtl) {
|
||||
await pubClient.expire(sessionKey, objectTtl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await pubClient.hset(sessionKey, key, toRedisJson(value)); // Use Redis pubClient
|
||||
await pubClient.hset(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient
|
||||
if (ttl && typeof ttl === "number") {
|
||||
await pubClient.expire(sessionKey, ttl);
|
||||
await pubClient.expire(`socket:${socketId}`, ttl);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log(`Error Setting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
@@ -109,26 +88,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
*/
|
||||
const getSessionData = async (socketId, key) => {
|
||||
try {
|
||||
const sessionKey = `socket:${socketId}`;
|
||||
|
||||
// Supports:
|
||||
// 1) getSessionData(socketId, "field") -> parsed field value
|
||||
// 2) getSessionData(socketId) -> parsed object of all fields
|
||||
if (typeof key === "undefined") {
|
||||
const raw = await pubClient.hgetall(sessionKey);
|
||||
if (!raw || Object.keys(raw).length === 0) return null;
|
||||
|
||||
return Object.entries(raw).reduce((acc, [field, rawValue]) => {
|
||||
try {
|
||||
acc[field] = JSON.parse(rawValue);
|
||||
} catch {
|
||||
acc[field] = rawValue;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const data = await pubClient.hget(sessionKey, key);
|
||||
const data = await pubClient.hget(`socket:${socketId}`, key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
} catch (error) {
|
||||
logger.log(`Error Getting Session Data for socket ${socketId}: ${error}`, "ERROR", "redis");
|
||||
@@ -146,7 +106,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
*/
|
||||
const setSessionTransactionData = async (socketId, transactionType, key, value, ttl) => {
|
||||
try {
|
||||
await pubClient.hset(getSocketTransactionkey({ socketId, transactionType }), key, toRedisJson(value)); // Use Redis pubClient
|
||||
await pubClient.hset(getSocketTransactionkey({ socketId, transactionType }), key, JSON.stringify(value)); // Use Redis pubClient
|
||||
if (ttl && typeof ttl === "number") {
|
||||
await pubClient.expire(getSocketTransactionkey({ socketId, transactionType }), ttl);
|
||||
}
|
||||
@@ -200,17 +160,7 @@ const applyRedisHelpers = ({ pubClient, app, logger }) => {
|
||||
*/
|
||||
const clearSessionTransactionData = async (socketId, transactionType) => {
|
||||
try {
|
||||
if (transactionType) {
|
||||
await pubClient.del(getSocketTransactionkey({ socketId, transactionType }));
|
||||
return;
|
||||
}
|
||||
|
||||
// If no transactionType is provided, clear all transaction namespaces for this socket.
|
||||
const pattern = getSocketTransactionkey({ socketId, transactionType: "*" });
|
||||
const keys = await pubClient.keys(pattern);
|
||||
if (Array.isArray(keys) && keys.length > 0) {
|
||||
await pubClient.del(...keys);
|
||||
}
|
||||
await pubClient.del(getSocketTransactionkey({ socketId, transactionType }));
|
||||
} catch (error) {
|
||||
logger.log(
|
||||
`Error Clearing Session Transaction Data for socket ${socketId}:${transactionType}: ${error}`,
|
||||
|
||||
@@ -4,14 +4,11 @@ const { FortellisJobExport, FortellisSelectedCustomer } = require("../fortellis/
|
||||
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||
const registerRREvents = require("../rr/rr-register-socket-events");
|
||||
|
||||
const SOCKET_SESSION_TTL_SECONDS = 60 * 60 * 24;
|
||||
|
||||
const redisSocketEvents = ({ io, redisHelpers, ioHelpers, logger }) => {
|
||||
// Destructure helpers locally, but keep full objects available for downstream modules
|
||||
const {
|
||||
setSessionData,
|
||||
getSessionData,
|
||||
clearSessionData,
|
||||
addUserSocketMapping,
|
||||
removeUserSocketMapping,
|
||||
refreshUserSocketTTL,
|
||||
@@ -54,16 +51,12 @@ const redisSocketEvents = ({ io, redisHelpers, ioHelpers, logger }) => {
|
||||
}
|
||||
|
||||
// NEW: seed a base session for this socket so downstream handlers can read it
|
||||
await setSessionData(
|
||||
socket.id,
|
||||
{
|
||||
bodyshopId,
|
||||
email: user.email,
|
||||
uid: user.user_id || user.uid,
|
||||
seededAt: Date.now()
|
||||
},
|
||||
SOCKET_SESSION_TTL_SECONDS
|
||||
);
|
||||
await setSessionData(socket.id, {
|
||||
bodyshopId,
|
||||
email: user.email,
|
||||
uid: user.user_id || user.uid,
|
||||
seededAt: Date.now()
|
||||
});
|
||||
|
||||
await addUserSocketMapping(user.email, socket.id, bodyshopId);
|
||||
next();
|
||||
@@ -133,18 +126,14 @@ const redisSocketEvents = ({ io, redisHelpers, ioHelpers, logger }) => {
|
||||
}
|
||||
|
||||
// NEW: refresh (or create) the base session with the latest info
|
||||
await setSessionData(
|
||||
socket.id,
|
||||
{
|
||||
bodyshopId,
|
||||
email: user.email,
|
||||
uid: user.user_id || user.uid,
|
||||
refreshedAt: Date.now()
|
||||
},
|
||||
SOCKET_SESSION_TTL_SECONDS
|
||||
);
|
||||
await setSessionData(socket.id, {
|
||||
bodyshopId,
|
||||
email: user.email,
|
||||
uid: user.user_id || user.uid,
|
||||
refreshedAt: Date.now()
|
||||
});
|
||||
|
||||
await refreshUserSocketTTL(user.email);
|
||||
await refreshUserSocketTTL(user.email, bodyshopId);
|
||||
socket.emit("token-updated", { success: true });
|
||||
} catch (error) {
|
||||
if (error.code === "auth/id-token-expired") {
|
||||
@@ -200,11 +189,6 @@ const redisSocketEvents = ({ io, redisHelpers, ioHelpers, logger }) => {
|
||||
if (socket.user?.email) {
|
||||
await removeUserSocketMapping(socket.user.email, socket.id);
|
||||
}
|
||||
try {
|
||||
await clearSessionData(socket.id);
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
// Optional: clear transactional session
|
||||
try {
|
||||
await clearSessionTransactionData(socket.id);
|
||||
|
||||
Reference in New Issue
Block a user