diff --git a/client/src/pages/accounting-payments/accounting-payments.container.jsx b/client/src/pages/accounting-payments/accounting-payments.container.jsx
index cea80853f..0cf739f53 100644
--- a/client/src/pages/accounting-payments/accounting-payments.container.jsx
+++ b/client/src/pages/accounting-payments/accounting-payments.container.jsx
@@ -15,6 +15,7 @@ import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wr
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
import { Card } from "antd";
+import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -52,12 +53,10 @@ export function AccountingPaymentsContainer({ bodyshop, setBreadcrumbs, setSelec
});
if (error) return
;
+
const noPath =
- !partnerVersion?.qbpath &&
- !(
- bodyshop &&
- (bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid || bodyshop.accountingconfig.qbo)
- );
+ !partnerVersion?.qbpath && !(bodyshop && (bodyshopHasDmsKey(bodyshop) || bodyshop?.accountingconfig?.qbo));
+
return (
;
const noPath =
- !partnerVersion?.qbpath &&
- !(
- bodyshop &&
- (bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid || bodyshop.accountingconfig.qbo)
- );
+ !partnerVersion?.qbpath && !(bodyshop && (bodyshopHasDmsKey(bodyshop) || bodyshop?.accountingconfig?.qbo));
return (
diff --git a/client/src/pages/dms-payables/dms-payables.container.jsx b/client/src/pages/dms-payables/dms-payables.container.jsx
index c49b0985f..fa57109bb 100644
--- a/client/src/pages/dms-payables/dms-payables.container.jsx
+++ b/client/src/pages/dms-payables/dms-payables.container.jsx
@@ -90,6 +90,7 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
});
if (socket.disconnected) socket.connect();
+
return () => {
socket.removeAllListeners();
socket.disconnect();
@@ -139,7 +140,7 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
}
>
-
+
diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx
index 80c6a5e68..9650d912b 100644
--- a/client/src/pages/dms/dms.container.jsx
+++ b/client/src/pages/dms/dms.container.jsx
@@ -3,27 +3,23 @@ import { Link, useLocation, useNavigate } from "react-router-dom";
import { connect } from "react-redux";
import { useTranslation } from "react-i18next";
import { createStructuredSelector } from "reselect";
-import SocketIO from "socket.io-client";
import queryString from "query-string";
import { useQuery } from "@apollo/client";
import { Button, Card, Col, Result, Row, Select, Space, Switch } from "antd";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
-
-import { auth } from "../../firebase/firebase.utils";
-
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
-
import { selectBodyshop } from "../../redux/user/user.selectors";
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
-import { determineDmsType } from "../../utils/determineDmsType";
-import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
+import { bodyshopHasDmsKey, DMS_MAP, getDmsMode, isWssMode } from "../../utils/dmsUtils.js";
+import legacySocket from "../../utils/legacySocket";
+import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
import AlertComponent from "../../components/alert/alert.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
@@ -43,30 +39,37 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
-// Legacy /ws socket (CDK/PBS)
-export const socket = SocketIO(import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", {
- path: "/ws",
- withCredentials: true,
- auth: async (callback) => {
- const token = auth.currentUser && (await auth.currentUser.getIdToken());
- callback({ token });
+const DMS_SOCKET_EVENTS = {
+ [DMS_MAP.reynolds]: {
+ log: "rr-log-event",
+ partialResult: "rr-export-job:result",
+ cashierNeeded: "rr-cashiering-required",
+ exportSuccess: "export-success",
+ exportFailed: "export-failed"
+ },
+ [DMS_MAP.fortellis]: {
+ log: "fortellis-log-event",
+ exportSuccess: "export-success",
+ exportFailed: "export-failed"
+ },
+ [DMS_MAP.cdk]: {
+ log: "log-event",
+ exportSuccess: "export-success",
+ exportFailed: "export-failed"
+ },
+ [DMS_MAP.pbs]: {
+ log: "log-event",
+ exportSuccess: "export-success",
+ exportFailed: "export-failed"
}
-});
+};
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
const { t } = useTranslation();
- const dms = determineDmsType(bodyshop);
-
const history = useNavigate();
const search = queryString.parse(useLocation().search);
-
- const [logLevel, setLogLevel] = useState(dms === "pbs" ? "INFO" : "DEBUG");
- const [logs, setLogs] = useState([]);
- const [detailsOpen, setDetailsOpen] = useState(false); // false => button shows "Expand All"
- const [detailsNonce, setDetailsNonce] = useState(0); // forces child to react to toggles
- const [colorizeJson, setColorizeJson] = useState(false); // default: OFF
-
const { jobId } = search;
+
const notification = useNotification();
const {
@@ -77,11 +80,31 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
splitKey: bodyshop.imexshopid
});
- // New unified wss socket (Fortellis, RR)
+ // Compute a single normalized mode and pick the proper socket
+ const mode = getDmsMode(bodyshop, Fortellis.treatment); // "rr" | "fortellis" | "cdk" | "pbs" | "none"
+
const { socket: wsssocket } = useSocket();
- const activeSocket = useMemo(() => {
- return dms === "rr" || (dms === "cdk" && Fortellis.treatment === "on") ? wsssocket : socket;
- }, [dms, Fortellis.treatment, wsssocket, socket]);
+ const activeSocket = useMemo(() => (isWssMode(mode) ? wsssocket : legacySocket), [mode, wsssocket]);
+
+ const [isConnected, setIsConnected] = useState(!!activeSocket?.connected);
+
+ // One place to set log level
+ const [logLevel, setLogLevel] = useState(mode === DMS_MAP.pbs ? "INFO" : "DEBUG");
+
+ const setActiveLogLevel = (level) => {
+ if (!activeSocket) return;
+ activeSocket.emit("set-log-level", level);
+ };
+
+ const [logs, setLogs] = useState([]);
+ const [detailsOpen, setDetailsOpen] = useState(false);
+ const [detailsNonce, setDetailsNonce] = useState(0);
+ const [colorizeJson, setColorizeJson] = useState(false);
+
+ const [rrOpenRoLimit, setRrOpenRoLimit] = useState(false);
+ const clearRrOpenRoLimit = () => setRrOpenRoLimit(false);
+
+ const [rrCashierPending, setRrCashierPending] = useState(false);
const { loading, error, data } = useQuery(QUERY_JOB_EXPORT_DMS, {
variables: { id: jobId },
@@ -97,45 +120,51 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
setDetailsNonce((n) => n + 1);
};
- const [rrOpenRoLimit, setRrOpenRoLimit] = useState(false);
- const clearRrOpenRoLimit = () => setRrOpenRoLimit(false);
+ // Channel names per mode to avoid branching everywhere
+ const channels = useMemo(() => DMS_SOCKET_EVENTS[mode] || {}, [mode]);
- // NEW: RR “cashiering required” UX hold
- const [rrCashierPending, setRrCashierPending] = useState(false);
+ const providerLabel = useMemo(
+ () =>
+ ({
+ [DMS_MAP.reynolds]: "Reynolds",
+ [DMS_MAP.fortellis]: "Fortellis",
+ [DMS_MAP.cdk]: "CDK",
+ [DMS_MAP.pbs]: "PBS"
+ })[mode] || "DMS",
+ [mode]
+ );
+
+ const transportLabel = isWssMode(mode) ? "App Socket (WSS)" : "Legacy Socket (WS)";
+
+ const bannerMessage = `Posting to ${providerLabel} | ${transportLabel} | ${
+ isConnected ? "Connected" : "Disconnected"
+ }`;
const handleExportFailed = (payload = {}) => {
- const { title, friendlyMessage, error, severity, errorCode, vendorStatusCode } = payload;
+ const { title, friendlyMessage, error: errText, severity, errorCode, vendorStatusCode } = payload;
const msg =
friendlyMessage ||
- error ||
+ errText ||
t("dms.errors.exportfailedgeneric", "We couldn't complete the export. Please try again.");
- const vendorTitle = title || (dms === "rr" ? "Reynolds" : "DMS");
+ const vendorTitle = title || (mode === DMS_MAP.reynolds ? "Reynolds" : "DMS");
- // Detect the specific RR “max open ROs” case
const isRrOpenRoLimit =
- dms === "rr" &&
+ mode === DMS_MAP.reynolds &&
(vendorStatusCode === 507 ||
/MAX_OPEN_ROS/i.test(String(errorCode || "")) ||
/maximum number of open repair orders/i.test(String(msg || "").toLowerCase()));
- // Soft/warn default for known cases
const sev = severity || (isRrOpenRoLimit ? "warning" : "error");
- // Show toast for *other* failures; for the open RO limit, switch to blocking banner UX instead.
if (!isRrOpenRoLimit) {
const notifyKind = sev === "warning" && typeof notification.warning === "function" ? "warning" : "error";
- notification[notifyKind]({
- message: vendorTitle,
- description: msg,
- duration: 10
- });
+ notification[notifyKind]({ message: vendorTitle, description: msg, duration: 10 });
} else {
setRrOpenRoLimit(true);
}
- // Mirror to the on-screen log card
setLogs((prev) => [
...prev,
{
@@ -147,254 +176,218 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
]);
};
+ // keep this in sync if mode/socket flips
+ useEffect(() => {
+ setIsConnected(!!activeSocket?.connected);
+ }, [activeSocket]);
+
useEffect(() => {
document.title = t("titles.dms", {
- app: InstanceRenderManager({
- imex: "$t(titles.imexonline)",
- rome: "$t(titles.romeonline)"
- })
+ app: InstanceRenderManager({ imex: "$t(titles.imexonline)", rome: "$t(titles.romeonline)" })
});
setSelectedHeader("dms");
setBreadcrumbs([
- {
- link: "/manage/accounting/receivables",
- label: t("titles.bc.accounting-receivables")
- },
- {
- link: "/manage/dms",
- label: t("titles.bc.dms")
- }
+ { link: "/manage/accounting/receivables", label: t("titles.bc.accounting-receivables") },
+ { link: "/manage/dms", label: t("titles.bc.dms") }
]);
}, [t, setBreadcrumbs, setSelectedHeader]);
+ // Socket wiring (mode-aware)
useEffect(() => {
- // ✅ RR path uses WSS and has two-step flow
- if (dms === "rr") {
- // set log level on connect and immediately
- wsssocket.emit("set-log-level", logLevel);
+ if (!activeSocket) return;
- const handleConnect = () => wsssocket.emit("set-log-level", logLevel);
- const handleReconnect = () =>
- setLogs((prev) => [
- ...prev,
- { timestamp: new Date(), level: "warn", message: "Reconnected to RR Export Service" }
- ]);
- const handleConnectError = (err) => {
- console.log(`connect_error due to ${err}`, err);
- notification.error({ message: err.message });
- };
-
- const handleLogEvent = (payload = {}) => {
- const normalized = {
- timestamp: payload.timestamp ? new Date(payload.timestamp) : payload.ts ? new Date(payload.ts) : new Date(),
- level: (payload.level || "INFO").toUpperCase(),
- message: payload.message || payload.msg || "",
- // show details regardless of property name
- meta: payload.meta ?? payload.ctx ?? payload.details ?? null
- };
- setLogs((prev) => [...prev, normalized]);
- };
-
- // FINAL step only (emitted by server after rr-finalize-repair-order)
- const handleExportSuccess = (payload) => {
- const jobId = payload?.jobId ?? payload; // RR sends object; legacy sends raw id
- notification.success({ message: t("jobs.successes.exported") });
- setRrCashierPending(false);
- insertAuditTrail({
- jobid: jobId,
- operation: AuditTrailMapping.jobexported(),
- type: "jobexported"
- });
- history("/manage/accounting/receivables");
- };
-
- // STEP 1 result (RO created) – DO NOT navigate; wait for cashiering
- const handleRrExportResult = () => {
- // Be defensive: if the server didn't already set the banner yet, make it obvious
- setRrCashierPending(true);
- setLogs((prev) => [
- ...prev,
- {
- timestamp: new Date(),
- level: "INFO",
- message:
- "Repair Order created in Reynolds. Complete cashiering in Reynolds, then click Finished/Close to finalize."
- }
- ]);
- notification.info({
- message: "Reynolds RO created",
- description:
- "Complete cashiering in Reynolds, then click Finished/Close to finalize and mark this export complete.",
- duration: 8
- });
- // No routing here — we remain on the page for step 2
- };
-
- // NEW: cashier step required (after create, before finalize)
- const handleCashieringRequired = (payload) => {
- setRrCashierPending(true);
- setLogs((prev) => [
- ...prev,
- {
- timestamp: new Date(),
- level: "INFO",
- message:
- "Repair Order created in Reynolds. Complete cashiering in Reynolds, then click Finished/Close to finalize.",
- meta: { payload }
- }
- ]);
- };
-
- wsssocket.on("connect", handleConnect);
- wsssocket.on("reconnect", handleReconnect);
- wsssocket.on("connect_error", handleConnectError);
-
- // RR channels (over wss)
- wsssocket.on("rr-log-event", handleLogEvent);
- wsssocket.on("rr-export-job:result", handleRrExportResult);
-
- wsssocket.on("export-success", handleExportSuccess);
- wsssocket.on("export-failed", handleExportFailed);
-
- // NEW
- wsssocket.on("rr-cashiering-required", handleCashieringRequired);
-
- return () => {
- wsssocket.off("connect", handleConnect);
- wsssocket.off("reconnect", handleReconnect);
- wsssocket.off("connect_error", handleConnectError);
-
- wsssocket.off("rr-log-event", handleLogEvent);
- wsssocket.off("rr-export-job:result", handleRrExportResult);
-
- wsssocket.off("export-success", handleExportSuccess);
- wsssocket.off("export-failed", handleExportFailed);
-
- wsssocket.off("rr-cashiering-required", handleCashieringRequired);
- };
+ // Connect legacy socket if needed
+ if (!isWssMode(mode)) {
+ if (activeSocket.disconnected) activeSocket.connect();
}
- // Fortellis / CDK behavior (when not RR)
- if (Fortellis.treatment === "on") {
- wsssocket.emit("set-log-level", logLevel);
+ // Set log level now and on connect/reconnect
+ setActiveLogLevel(logLevel);
- const handleLogEvent = (payload) => setLogs((prev) => [...prev, payload]);
- const handleExportSuccess = (payload) => {
- notification.success({ message: t("jobs.successes.exported") });
- insertAuditTrail({
- jobid: payload,
- operation: AuditTrailMapping.jobexported(),
- type: "jobexported"
- });
- history("/manage/accounting/receivables");
- };
+ const onConnect = () => {
+ setIsConnected(true);
+ setActiveLogLevel(logLevel);
+ };
- // Fortellis logs (wss)
- wsssocket.on("fortellis-log-event", handleLogEvent);
- wsssocket.on("export-success", handleExportSuccess);
- wsssocket.on("export-failed", handleExportFailed);
+ const onDisconnect = () => setIsConnected(false);
- return () => {
- wsssocket.off("fortellis-log-event", handleLogEvent);
- wsssocket.off("export-success", handleExportSuccess);
- wsssocket.off("export-failed", handleExportFailed);
- };
- } else {
- // CDK/PBS via legacy /ws socket
- socket.on("export-failed", handleExportFailed);
+ const onReconnect = () => {
+ setIsConnected(true);
+ setLogs((prev) => [
+ ...prev,
+ {
+ timestamp: new Date(),
+ level: "warn",
+ message: `Reconnected to ${mode === DMS_MAP.reynolds ? "RR" : mode === DMS_MAP.fortellis ? "Fortellis" : "DMS"} Export Service`
+ }
+ ]);
+ };
- socket.on("connect", () => socket.emit("set-log-level", logLevel));
- socket.on("reconnect", () => {
- setLogs((prev) => [
- ...prev,
- { timestamp: new Date(), level: "warn", message: "Reconnected to CDK Export Service" }
- ]);
- });
- socket.on("connect_error", (err) => {
- console.log(`connect_error due to ${err}`, err);
- notification.error({ message: err.message });
- });
- socket.on("log-event", (payload) => setLogs((prev) => [...prev, payload]));
- socket.on("export-success", (payload) => {
- notification.success({ message: t("jobs.successes.exported") });
- insertAuditTrail({
- jobid: payload,
- operation: AuditTrailMapping.jobexported(),
- type: "jobexported"
- });
- history("/manage/accounting/receivables");
+ const onConnectError = (err) => {
+ // Legacy and WSS both emit this
+ console.log(`connect_error due to ${err}`, err);
+ notification.error({ message: err.message });
+ };
+
+ activeSocket.on("disconnect", onDisconnect);
+ activeSocket.on("connect", onConnect);
+ activeSocket.on("reconnect", onReconnect);
+ activeSocket.on("connect_error", onConnectError);
+
+ // Logs
+ const onLog =
+ mode === DMS_MAP.reynolds
+ ? (payload = {}) => {
+ const normalized = {
+ timestamp: payload.timestamp
+ ? new Date(payload.timestamp)
+ : payload.ts
+ ? new Date(payload.ts)
+ : new Date(),
+ level: (payload.level || "INFO").toUpperCase(),
+ message: payload.message || payload.msg || "",
+ meta: payload.meta ?? payload.ctx ?? payload.details ?? null
+ };
+ setLogs((prev) => [...prev, normalized]);
+ }
+ : (payload) => setLogs((prev) => [...prev, payload]);
+
+ if (channels.log) activeSocket.on(channels.log, onLog);
+
+ // Success / Failed
+ const onExportSuccess = (payload) => {
+ const jobIdResolved = payload?.jobId ?? payload;
+ notification.success({ message: t("jobs.successes.exported") });
+
+ // Clear RR cashier flag if any
+ setRrCashierPending(false);
+
+ insertAuditTrail({
+ jobid: jobIdResolved,
+ operation: AuditTrailMapping.jobexported(),
+ type: "jobexported"
});
+ history("/manage/accounting/receivables");
+ };
- if (socket.disconnected) socket.connect();
- return () => {
- socket.removeAllListeners();
- socket.disconnect();
- };
- }
- }, [dms, Fortellis?.treatment, logLevel, history, insertAuditTrail, notification, t, wsssocket]);
+ if (channels.exportSuccess) activeSocket.on(channels.exportSuccess, onExportSuccess);
+ if (channels.exportFailed) activeSocket.on(channels.exportFailed, handleExportFailed);
- // NEW: finalize button callback—emit finalize event
+ // RR-only extras
+
+ const onPartialResult = () => {
+ setRrCashierPending(true);
+ setLogs((prev) => [
+ ...prev,
+ {
+ timestamp: new Date(),
+ level: "INFO",
+ message:
+ "Repair Order created in Reynolds. Complete cashiering in Reynolds, then click Finished/Close to finalize."
+ }
+ ]);
+ notification.info({
+ message: "Reynolds RO created",
+ description:
+ "Complete cashiering in Reynolds, then click Finished/Close to finalize and mark this export complete.",
+ duration: 8
+ });
+ };
+
+ const onCashierRequired = (payload) => {
+ setRrCashierPending(true);
+ setLogs((prev) => [
+ ...prev,
+ {
+ timestamp: new Date(),
+ level: "INFO",
+ message:
+ "Repair Order created in Reynolds. Complete cashiering in Reynolds, then click Finished/Close to finalize.",
+ meta: { payload }
+ }
+ ]);
+ };
+
+ if (mode === DMS_MAP.reynolds && channels.partialResult) activeSocket.on(channels.partialResult, onPartialResult);
+ if (mode === DMS_MAP.reynolds && channels.cashierNeeded) activeSocket.on(channels.cashierNeeded, onCashierRequired);
+
+ return () => {
+ activeSocket.off("connect", onConnect);
+ activeSocket.off("reconnect", onReconnect);
+ activeSocket.off("connect_error", onConnectError);
+ activeSocket.off("disconnect", onDisconnect);
+
+ if (channels.log) activeSocket.off(channels.log, onLog);
+ if (channels.exportSuccess) activeSocket.off(channels.exportSuccess, onExportSuccess);
+ if (channels.exportFailed) activeSocket.off(channels.exportFailed, handleExportFailed);
+
+ if (mode === DMS_MAP.reynolds && channels.partialResult)
+ activeSocket.off(channels.partialResult, onPartialResult);
+ if (mode === DMS_MAP.reynolds && channels.cashierNeeded)
+ activeSocket.off(channels.cashierNeeded, onCashierRequired);
+
+ // Only tear down legacy socket listeners; don't disconnect WSS from here
+ if (!isWssMode(mode)) {
+ activeSocket.removeAllListeners();
+ activeSocket.disconnect();
+ }
+ };
+ }, [mode, activeSocket, channels, logLevel, notification, t, insertAuditTrail, history]);
+
+ // RR finalize callback (unchanged public behavior)
const handleRrCashierFinished = () => {
if (!jobId) return;
- wsssocket.emit("rr-finalize-repair-order", { jobId }, (ack) => {
- if (ack?.ok) {
- // success path handled by export-success listener
- return;
- }
- if (ack?.error) {
- notification.error({ message: ack.error });
- }
+ if (!isWssMode(mode)) return; // RR is WSS-only
+ activeSocket.emit("rr-finalize-repair-order", { jobId }, (ack) => {
+ if (ack?.ok) return;
+ if (ack?.error) notification.error({ message: ack.error });
});
};
if (loading) return
;
if (error) return
;
- if (!jobId || !(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid) || !data?.jobs_by_pk)
+ if (!jobId || !bodyshopHasDmsKey(bodyshop) || !data?.jobs_by_pk)
return
;
if (data.jobs_by_pk?.date_exported) return
;
return (
-
+
- {`${
- data?.jobs_by_pk && data.jobs_by_pk.ro_number
- }`}
- {` | ${OwnerNameDisplayFunction(data.jobs_by_pk)} | ${
- data.jobs_by_pk.v_model_yr || ""
- } ${data.jobs_by_pk.v_make_desc || ""} ${data.jobs_by_pk.v_model_desc || ""}`}
+ {`${data?.jobs_by_pk && data.jobs_by_pk.ro_number}`}
+ {` | ${OwnerNameDisplayFunction(data.jobs_by_pk)} | ${data.jobs_by_pk.v_model_yr || ""} ${data.jobs_by_pk.v_make_desc || ""} ${data.jobs_by_pk.v_model_desc || ""}`}
}
socket={activeSocket}
jobId={jobId}
+ mode={mode}
/>
-
+
- {/* NEW props for two-step RR flow banners */}
@@ -415,11 +408,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
value={logLevel}
onChange={(value) => {
setLogLevel(value);
- if (dms === "rr" || Fortellis.treatment === "on") {
- wsssocket.emit("set-log-level", value);
- } else {
- socket.emit("set-log-level", value);
- }
+ setActiveLogLevel(value);
}}
>
DEBUG
@@ -431,11 +420,11 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
{
setLogs([]);
- if (dms === "rr" || Fortellis.treatment === "on") {
- wsssocket.emit("set-log-level", logLevel);
+ if (isWssMode(mode)) {
+ setActiveLogLevel(logLevel);
} else {
- socket.disconnect();
- socket.connect();
+ activeSocket.disconnect();
+ activeSocket.connect();
}
}}
>
@@ -445,7 +434,6 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
}
>
- {(bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid || bodyshop.rr_dealerid) && (
+ {bodyshopHasDmsKey(bodyshop) && (
{t("jobs.actions.sendtodms")}
@@ -311,7 +314,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
)}
- {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid) && (
+ {hasDMSKey && (
)}
- {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid) && (
+ {hasDMSKey && (
)}
- {(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid) && (
+ {hasDMSKey && (
{
- const dmsMapping = {
- cdk_dealerid: "cdk",
- pbs_serialnumber: "pbs",
- rr_dealerid: "rr"
- };
-
- return Object.keys(dmsMapping).find((key) => bodyshop[key])
- ? dmsMapping[Object.keys(dmsMapping).find((key) => bodyshop[key])]
- : "pbs";
-};
diff --git a/client/src/utils/dmsUtils.js b/client/src/utils/dmsUtils.js
new file mode 100644
index 000000000..e4ae68f03
--- /dev/null
+++ b/client/src/utils/dmsUtils.js
@@ -0,0 +1,72 @@
+/**
+ * DMS type mapping constants.
+ * CAREFUL: the values here are used as canonical "mode" strings elsewhere in the app.
+ * @type {{reynolds: string, cdk: string, pbs: string, fortellis: string}}
+ */
+export const DMS_MAP = {
+ reynolds: "rr",
+ cdk: "cdk",
+ pbs: "pbs",
+ fortellis: "fortellis"
+};
+
+/**
+ * Determines the DMS type for a given bodyshop object.
+ * @param bodyshop
+ * @returns {*|string}
+ */
+export const determineDMSTypeByBodyshop = (bodyshop) => {
+ const dmsMapping = {
+ cdk_dealerid: DMS_MAP.cdk,
+ pbs_serialnumber: DMS_MAP.pbs,
+ rr_dealerid: DMS_MAP.reynolds
+ };
+
+ return Object.keys(dmsMapping).find((key) => bodyshop[key])
+ ? dmsMapping[Object.keys(dmsMapping).find((key) => bodyshop[key])]
+ : DMS_MAP.pbs;
+};
+
+/**
+ * Determines the translation key for a given DMS type.
+ * @param dmsType
+ * @returns {*|string}
+ */
+export const determineDmsTypeTranslationKey = (dmsType) => {
+ const dmsTypeMapping = {
+ [DMS_MAP.cdk]: "bodyshop.labels.dms.cdk",
+ [DMS_MAP.pbs]: "bodyshop.labels.dms.pbs",
+ [DMS_MAP.reynolds]: "bodyshop.labels.dms.rr"
+ };
+
+ return dmsTypeMapping[dmsType] || dmsTypeMapping[DMS_MAP.pbs];
+};
+
+/**
+ * Returns a normalized "mode" we can switch on:
+ * @param bodyshop
+ * @param fortellisTreatment
+ * @returns {*|string|string}
+ */
+export const getDmsMode = (bodyshop, fortellisTreatment) => {
+ const base = determineDMSTypeByBodyshop(bodyshop); // "rr" | "cdk" | "pbs" | undefined
+ if (base === DMS_MAP.cdk && fortellisTreatment === "on") return DMS_MAP.fortellis;
+ return base ?? "none";
+};
+
+/**
+ * Checks if the DMS mode uses WSS.
+ * @param mode
+ * @returns {boolean}
+ */
+export const isWssMode = (mode) => {
+ return mode === DMS_MAP.reynolds || mode === DMS_MAP.fortellis;
+};
+
+/**
+ * Checks if the bodyshop has any DMS key configured.
+ * @param bodyshop
+ * @returns {*|string}
+ */
+export const bodyshopHasDmsKey = (bodyshop) =>
+ bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid;
diff --git a/client/src/utils/legacySocket.js b/client/src/utils/legacySocket.js
new file mode 100644
index 000000000..c20f1cb6e
--- /dev/null
+++ b/client/src/utils/legacySocket.js
@@ -0,0 +1,16 @@
+// client/src/utils/legacySocket.js
+import SocketIO from "socket.io-client";
+import { auth } from "../firebase/firebase.utils";
+
+// Create once, reuse everywhere.
+const legacySocket = SocketIO(import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", {
+ path: "/ws",
+ withCredentials: true,
+ autoConnect: false,
+ auth: async (callback) => {
+ const token = auth.currentUser && (await auth.currentUser.getIdToken());
+ callback({ token });
+ }
+});
+
+export default legacySocket;