diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss
index 657520e49..409e30b38 100644
--- a/client/src/App/App.styles.scss
+++ b/client/src/App/App.styles.scss
@@ -443,6 +443,30 @@
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;
//}
diff --git a/client/src/components/dms-log-events/dms-log-events.component.jsx b/client/src/components/dms-log-events/dms-log-events.component.jsx
index d90623df6..916d577d1 100644
--- a/client/src/components/dms-log-events/dms-log-events.component.jsx
+++ b/client/src/components/dms-log-events/dms-log-events.component.jsx
@@ -4,6 +4,7 @@ import dayjs from "../../utils/day";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectDarkMode } from "../../redux/application/application.selectors.js";
+import { useTranslation } from "react-i18next";
const mapStateToProps = createStructuredSelector({
isDarkMode: selectDarkMode
@@ -21,23 +22,32 @@ export function DmsLogEvents({
colorizeJson = false,
showDetails = true
}) {
+ const { t } = useTranslation();
const [openSet, setOpenSet] = useState(() => new Set());
+ const [copiedKey, setCopiedKey] = useState(null);
// Inject JSON highlight styles once (only when colorize is enabled)
useEffect(() => {
if (!colorizeJson) return;
if (typeof document === "undefined") return;
- if (document.getElementById("json-highlight-styles")) return;
- const style = document.createElement("style");
- style.id = "json-highlight-styles";
+ let style = document.getElementById("json-highlight-styles");
+ if (!style) {
+ style = document.createElement("style");
+ style.id = "json-highlight-styles";
+ document.head.appendChild(style);
+ }
style.textContent = `
.json-key { color: #fa8c16; }
.json-string { color: #52c41a; }
.json-number { color: #722ed1; }
.json-boolean { color: #1890ff; }
.json-null { color: #faad14; }
+ .xml-tag { color: #1677ff; }
+ .xml-attr { color: #d46b08; }
+ .xml-value { color: #389e0d; }
+ .xml-decl { color: #7c3aed; }
+ .xml-comment { color: #8c8c8c; }
`;
- document.head.appendChild(style);
}, [colorizeJson]);
// Trim openSet if logs shrink
@@ -65,6 +75,13 @@ export function DmsLogEvents({
// Only treat meta as "present" when we are allowed to show details
const hasMeta = !isEmpty(meta) && showDetails;
const isOpen = hasMeta && openSet.has(idx);
+ const xml = hasMeta ? extractXmlFromMeta(meta) : { request: null, response: null };
+ const hasRequestXml = !!xml.request;
+ const hasResponseXml = !!xml.response;
+ const copyPayload = hasMeta ? getCopyPayload(meta) : null;
+ const copyPayloadKey = `copy-${idx}`;
+ const copyReqKey = `copy-req-${idx}`;
+ const copyResKey = `copy-res-${idx}`;
return {
key: idx,
@@ -92,10 +109,42 @@ export function DmsLogEvents({
return next;
})
}
- style={{ cursor: "pointer", userSelect: "none" }}
+ style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
>
- {isOpen ? "Hide details" : "Details"}
+ {isOpen ? t("dms.labels.hide_details") : t("dms.labels.details")}
+
+ handleCopyAction(copyPayloadKey, copyPayload, setCopiedKey)}
+ style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
+ >
+ {copiedKey === copyPayloadKey ? t("dms.labels.copied") : t("dms.labels.copy")}
+
+ {hasRequestXml && (
+ <>
+
+ handleCopyAction(copyReqKey, xml.request, setCopiedKey)}
+ style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
+ >
+ {copiedKey === copyReqKey ? t("dms.labels.copied") : t("dms.labels.copy_request")}
+
+ >
+ )}
+ {hasResponseXml && (
+ <>
+
+ handleCopyAction(copyResKey, xml.response, setCopiedKey)}
+ style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
+ >
+ {copiedKey === copyResKey ? t("dms.labels.copied") : t("dms.labels.copy_response")}
+
+ >
+ )}
>
)}
@@ -103,14 +152,30 @@ export function DmsLogEvents({
{/* Row 2: details body (only when open) */}
{hasMeta && isOpen && (
-
+
+ {hasRequestXml && (
+
+ )}
+ {hasResponseXml && (
+
+ )}
)}
)
};
}),
- [logs, openSet, colorizeJson, isDarkMode, showDetails]
+ [logs, openSet, colorizeJson, copiedKey, isDarkMode, showDetails, t]
);
return ;
@@ -179,6 +244,121 @@ const safeStringify = (obj, spaces = 2) => {
}
};
+/**
+ * Get request/response XML from various Reynolds log meta shapes.
+ * @param meta
+ * @returns {{request: string|null, response: string|null}}
+ */
+const extractXmlFromMeta = (meta) => {
+ const request =
+ firstString(meta?.requestXml) ||
+ firstString(meta?.xml?.request) ||
+ firstString(meta?.response?.xml?.request) ||
+ firstString(meta?.response?.requestXml);
+
+ const response =
+ firstString(meta?.responseXml) || firstString(meta?.xml?.response) || firstString(meta?.response?.xml?.response);
+
+ return { request, response };
+};
+
+/**
+ * Return the value to copy when clicking the "Copy" action.
+ * @param meta
+ * @returns {*}
+ */
+const getCopyPayload = (meta) => {
+ if (meta?.payload != null) return meta.payload;
+ return meta;
+};
+
+/**
+ * Remove bulky XML fields from object shown in JSON block (XML is rendered separately).
+ * @param meta
+ * @returns {*}
+ */
+const removeXmlFromMeta = (meta) => {
+ if (meta == null || typeof meta !== "object") return meta;
+ const cloned = safeClone(meta);
+ if (cloned == null || typeof cloned !== "object") return meta;
+
+ if (typeof cloned.requestXml === "string") delete cloned.requestXml;
+ if (typeof cloned.responseXml === "string") delete cloned.responseXml;
+
+ if (cloned.xml && typeof cloned.xml === "object") {
+ if (typeof cloned.xml.request === "string") delete cloned.xml.request;
+ if (typeof cloned.xml.response === "string") delete cloned.xml.response;
+ if (isEmpty(cloned.xml)) delete cloned.xml;
+ }
+
+ if (cloned.response?.xml && typeof cloned.response.xml === "object") {
+ if (typeof cloned.response.xml.request === "string") delete cloned.response.xml.request;
+ if (typeof cloned.response.xml.response === "string") delete cloned.response.xml.response;
+ if (isEmpty(cloned.response.xml)) delete cloned.response.xml;
+ }
+
+ return cloned;
+};
+
+/**
+ * Safe deep clone for plain JSON structures.
+ * @param value
+ * @returns {*}
+ */
+const safeClone = (value) => {
+ try {
+ return JSON.parse(JSON.stringify(value));
+ } catch {
+ return value;
+ }
+};
+
+/**
+ * First non-empty string helper.
+ * @param value
+ * @returns {string|null}
+ */
+const firstString = (value) => {
+ if (typeof value !== "string") return null;
+ const trimmed = value.trim();
+ return trimmed ? trimmed : null;
+};
+
+/**
+ * Copy arbitrary text/object to clipboard.
+ * @param key
+ * @param value
+ * @param setCopied
+ * @returns {Promise}
+ */
+const handleCopyAction = async (key, value, setCopied) => {
+ const text = typeof value === "string" ? value : safeStringify(value, 2);
+ if (!text) return;
+ const copied = await copyTextToClipboard(text);
+ if (!copied) return;
+ setCopied(key);
+ setTimeout(() => {
+ setCopied((prev) => (prev === key ? null : prev));
+ }, 1200);
+};
+
+/**
+ * Clipboard helper (modern async Clipboard API).
+ * @param text
+ * @returns {Promise}
+ */
+const copyTextToClipboard = async (text) => {
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
+ return false;
+ }
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch {
+ return false;
+ }
+};
+
/**
* JSON display block with optional syntax highlighting.
* @param data
@@ -210,6 +390,105 @@ const JsonBlock = ({ data, colorize, isDarkMode }) => {
return {jsonText};
};
+/**
+ * XML display block with normalized indentation.
+ * @param title
+ * @param xmlText
+ * @param isDarkMode
+ * @returns {JSX.Element}
+ * @constructor
+ */
+const XmlBlock = ({ title, xmlText, isDarkMode, colorize = false }) => {
+ const base = {
+ margin: "8px 0 0",
+ maxWidth: 720,
+ overflowX: "auto",
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
+ fontSize: 12,
+ lineHeight: 1.45,
+ padding: 8,
+ borderRadius: 6,
+ background: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.04)",
+ border: isDarkMode ? "1px solid rgba(255,255,255,0.12)" : "1px solid rgba(0,0,0,0.08)",
+ color: isDarkMode ? "var(--card-text-fallback)" : "#141414",
+ whiteSpace: "pre"
+ };
+
+ return (
+
+
{title}
+ {colorize ? (
+
+ ) : (
+
{formatXml(xmlText)}
+ )}
+
+ );
+};
+
+/**
+ * Basic XML pretty-printer.
+ * @param xml
+ * @returns {string}
+ */
+const formatXml = (xml) => {
+ if (typeof xml !== "string") return "";
+ const normalized = xml.replace(/\r\n/g, "\n").replace(/>\s*\n<").trim();
+ const lines = normalized.split("\n");
+ let indent = 0;
+ const out = [];
+
+ for (const rawLine of lines) {
+ const line = rawLine.trim();
+ if (!line) continue;
+
+ if (/^<\/[^>]+>/.test(line)) indent = Math.max(indent - 1, 0);
+ out.push(`${" ".repeat(indent)}${line}`);
+
+ const opens = (line.match(/<[^/!?][^>]*>/g) || []).length;
+ const closes = (line.match(/<\/[^>]+>/g) || []).length;
+ const selfClosing = (line.match(/<[^>]+\/>/g) || []).length;
+ const declaration = /^<\?xml/.test(line) ? 1 : 0;
+
+ indent += opens - closes - selfClosing - declaration;
+ if (indent < 0) indent = 0;
+ }
+
+ return out.join("\n");
+};
+
+/**
+ * Syntax highlight pretty-printed XML text for HTML display.
+ * @param xmlText
+ * @returns {string}
+ */
+const highlightXml = (xmlText) => {
+ const esc = String(xmlText || "")
+ .replace(/&/g, "&")
+ .replace(//g, ">");
+ const lines = esc.split("\n");
+
+ return lines
+ .map((line) => {
+ let out = line;
+
+ out = out.replace(/(<!--[\s\S]*?-->)/g, '');
+ out = out.replace(/(<\?xml[\s\S]*?\?>)/g, '$1');
+
+ out = out.replace(/(<\/?)([A-Za-z_][\w:.-]*)([\s\S]*?)(\/?>)/g, (_m, open, tag, attrs, close) => {
+ const coloredAttrs = attrs.replace(
+ /([A-Za-z_][\w:.-]*)(=)("[^"]*"|'[^']*'|"[\s\S]*?"|'[\s\S]*?')/g,
+ '$1$2$3'
+ );
+ return `${open}${tag}${coloredAttrs}${close}`;
+ });
+
+ return out;
+ })
+ .join("\n");
+};
+
/**
* Syntax highlight JSON text for HTML display.
* @param jsonText
diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx
index 22d79a0bb..d72de0194 100644
--- a/client/src/pages/dms/dms.container.jsx
+++ b/client/src/pages/dms/dms.container.jsx
@@ -163,19 +163,21 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
const providerLabel = useMemo(
() =>
({
- [DMS_MAP.reynolds]: "Reynolds",
- [DMS_MAP.fortellis]: "Fortellis",
- [DMS_MAP.cdk]: "CDK",
- [DMS_MAP.pbs]: "PBS"
- })[mode] || "DMS",
- [mode]
+ [DMS_MAP.reynolds]: t("dms.labels.provider_reynolds"),
+ [DMS_MAP.fortellis]: t("dms.labels.provider_fortellis"),
+ [DMS_MAP.cdk]: t("dms.labels.provider_cdk"),
+ [DMS_MAP.pbs]: t("dms.labels.provider_pbs")
+ })[mode] || t("dms.labels.provider_dms"),
+ [mode, t]
);
- const transportLabel = isWssMode(mode) ? "(WSS)" : "(WS)";
+ const transportLabel = isWssMode(mode) ? t("dms.labels.transport_wss") : t("dms.labels.transport_ws");
- const bannerMessage = `Posting to ${providerLabel} | ${transportLabel} | ${
- isConnected ? "Connected" : "Disconnected"
- }`;
+ const bannerMessage = t("dms.labels.banner_message", {
+ provider: providerLabel,
+ transport: transportLabel,
+ status: isConnected ? t("dms.labels.banner_status_connected") : t("dms.labels.banner_status_disconnected")
+ });
const resetKey = useMemo(() => `${mode || "none"}-${jobId || "none"}`, [mode, jobId]);
@@ -224,7 +226,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
errText ||
t("dms.errors.exportfailedgeneric", "We couldn't complete the export. Please try again.");
- const vendorTitle = title || (isRrMode ? "Reynolds" : "DMS");
+ const vendorTitle = title || (isRrMode ? t("dms.labels.provider_reynolds") : t("dms.labels.provider_dms"));
const isRrOpenRoLimit =
isRrMode &&
@@ -299,7 +301,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
{
timestamp: new Date(),
level: "warn",
- message: `Reconnected to ${isRrMode ? "RR" : mode === DMS_MAP.fortellis ? "Fortellis" : "DMS"} Export Service`
+ message: t("dms.labels.reconnected_export_service", {
+ provider: isRrMode ? t("dms.labels.provider_reynolds") : providerLabel
+ })
}
]);
};
@@ -358,14 +362,12 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
{
timestamp: new Date(),
level: "INFO",
- message:
- "Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize."
+ message: t("dms.labels.rr_validation_message")
}
]);
notification.info({
- title: "Reynolds RO created",
- description:
- "Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.",
+ title: t("dms.labels.rr_validation_notice_title"),
+ description: t("dms.labels.rr_validation_notice_description"),
duration: 8
});
};
@@ -377,8 +379,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
{
timestamp: new Date(),
level: "INFO",
- message:
- "Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.",
+ message: t("dms.labels.rr_validation_message"),
meta: { payload }
}
]);
@@ -406,7 +407,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
activeSocket.disconnect();
}
};
- }, [mode, activeSocket, channels, logLevel, notification, t, insertAuditTrail, history]);
+ }, [mode, activeSocket, channels, logLevel, notification, t, insertAuditTrail, history, isRrMode, providerLabel]);
// RR finalize callback (unchanged public behavior)
const handleRrValidationFinished = () => {
@@ -428,7 +429,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 (
-
+
{!isRrMode ? (
-
+
-
+
>
)}
-
+
}
diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json
index 626ab5e4c..677fcb6d8 100644
--- a/client/src/translations/en_us/common.json
+++ b/client/src/translations/en_us/common.json
@@ -1052,7 +1052,36 @@
"earlyrorequired.message": "This job requires an early Repair Order to be created before posting to Reynolds. Please use the admin panel to create the early RO first."
},
"labels": {
- "refreshallocations": "Refresh to see DMS Allocations."
+ "refreshallocations": "Refresh to see DMS Allocations.",
+ "provider_reynolds": "Reynolds",
+ "provider_fortellis": "Fortellis",
+ "provider_cdk": "CDK",
+ "provider_pbs": "PBS",
+ "provider_dms": "DMS",
+ "transport_wss": "(WSS)",
+ "transport_ws": "(WS)",
+ "banner_status_connected": "Connected",
+ "banner_status_disconnected": "Disconnected",
+ "banner_message": "Posting to {{provider}} | {{transport}} | {{status}}",
+ "reconnected_export_service": "Reconnected to {{provider}} Export Service",
+ "rr_validation_message": "Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.",
+ "rr_validation_notice_title": "Reynolds RO created",
+ "rr_validation_notice_description": "Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.",
+ "color_json": "Color JSON",
+ "plain_json": "Plain JSON",
+ "collapse_all": "Collapse All",
+ "expand_all": "Expand All",
+ "log_level": "Log Level",
+ "clear_logs": "Clear Logs",
+ "reconnect": "Reconnect",
+ "details": "Details",
+ "hide_details": "Hide details",
+ "copy": "Copy",
+ "copied": "Copied",
+ "copy_request": "Copy Request",
+ "copy_response": "Copy Response",
+ "request_xml": "Request XML",
+ "response_xml": "Response XML"
}
},
"documents": {
diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json
index 664c5081c..020e277f0 100644
--- a/client/src/translations/es/common.json
+++ b/client/src/translations/es/common.json
@@ -1052,7 +1052,36 @@
"earlyrorequired.message": ""
},
"labels": {
- "refreshallocations": ""
+ "refreshallocations": "",
+ "provider_reynolds": "",
+ "provider_fortellis": "",
+ "provider_cdk": "",
+ "provider_pbs": "",
+ "provider_dms": "",
+ "transport_wss": "",
+ "transport_ws": "",
+ "banner_status_connected": "",
+ "banner_status_disconnected": "",
+ "banner_message": "",
+ "reconnected_export_service": "",
+ "rr_validation_message": "",
+ "rr_validation_notice_title": "",
+ "rr_validation_notice_description": "",
+ "color_json": "",
+ "plain_json": "",
+ "collapse_all": "",
+ "expand_all": "",
+ "log_level": "",
+ "clear_logs": "",
+ "reconnect": "",
+ "details": "",
+ "hide_details": "",
+ "copy": "",
+ "copied": "",
+ "copy_request": "",
+ "copy_response": "",
+ "request_xml": "",
+ "response_xml": ""
}
},
"documents": {
diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json
index 67a14d5ff..21f986832 100644
--- a/client/src/translations/fr/common.json
+++ b/client/src/translations/fr/common.json
@@ -1052,7 +1052,36 @@
"earlyrorequired.message": ""
},
"labels": {
- "refreshallocations": ""
+ "refreshallocations": "",
+ "provider_reynolds": "",
+ "provider_fortellis": "",
+ "provider_cdk": "",
+ "provider_pbs": "",
+ "provider_dms": "",
+ "transport_wss": "",
+ "transport_ws": "",
+ "banner_status_connected": "",
+ "banner_status_disconnected": "",
+ "banner_message": "",
+ "reconnected_export_service": "",
+ "rr_validation_message": "",
+ "rr_validation_notice_title": "",
+ "rr_validation_notice_description": "",
+ "color_json": "",
+ "plain_json": "",
+ "collapse_all": "",
+ "expand_all": "",
+ "log_level": "",
+ "clear_logs": "",
+ "reconnect": "",
+ "details": "",
+ "hide_details": "",
+ "copy": "",
+ "copied": "",
+ "copy_request": "",
+ "copy_response": "",
+ "request_xml": "",
+ "response_xml": ""
}
},
"documents": {
diff --git a/server/rr/rr-register-socket-events.js b/server/rr/rr-register-socket-events.js
index 98897eaa7..bdb51580e 100644
--- a/server/rr/rr-register-socket-events.js
+++ b/server/rr/rr-register-socket-events.js
@@ -48,6 +48,46 @@ const resolveJobId = (explicit, payload, job) => explicit || payload?.jobId || j
*/
const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null;
+/**
+ * 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;
+
+ 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 (!responseXml && typeof rrObj?.responseXml === "string") responseXml = rrObj.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;
+};
+
/**
* Sort vehicle owners first in the list, preserving original order otherwise.
* @param list
@@ -154,15 +194,13 @@ const setJobDmsIdForSocket = async ({ socket, jobId, dmsId, dmsCustomerId, dmsAd
if (!token) throw new Error("Missing auth token for setJobDmsIdForSocket");
const client = new GraphQLClient(endpoint, {});
- await client
- .setHeaders({ Authorization: `Bearer ${token}` })
- .request(queries.SET_JOB_DMS_ID, {
- id: jobId,
- dms_id: String(dmsId),
- dms_customer_id: dmsCustomerId ? String(dmsCustomerId) : null,
- dms_advisor_id: dmsAdvisorId ? String(dmsAdvisorId) : null,
- kmin: mileageIn != null && mileageIn > 0 ? parseInt(mileageIn, 10) : null
- });
+ await client.setHeaders({ Authorization: `Bearer ${token}` }).request(queries.SET_JOB_DMS_ID, {
+ id: jobId,
+ dms_id: String(dmsId),
+ dms_customer_id: dmsCustomerId ? String(dmsCustomerId) : null,
+ dms_advisor_id: dmsAdvisorId ? String(dmsAdvisorId) : null,
+ kmin: mileageIn != null && mileageIn > 0 ? parseInt(mileageIn, 10) : null
+ });
CreateRRLogEvent(socket, "INFO", "Linked job.dms_id to RR RO", {
jobId,
@@ -511,7 +549,11 @@ const registerRREvents = ({ socket, redisHelpers }) => {
});
// Filter out invalid values
- if (selectedCustNo === "undefined" || selectedCustNo === "null" || (selectedCustNo && selectedCustNo.trim() === "")) {
+ if (
+ selectedCustNo === "undefined" ||
+ selectedCustNo === "null" ||
+ (selectedCustNo && selectedCustNo.trim() === "")
+ ) {
selectedCustNo = null;
}
@@ -705,42 +747,52 @@ const registerRREvents = ({ socket, redisHelpers }) => {
const outsdRoNo = data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;
- CreateRRLogEvent(socket, "DEBUG", "Early RO created - checking dmsRoNo", {
- dmsRoNo,
- resultRoNo: result?.roNo,
- dataRoNo: data?.dmsRoNo,
- jobId: rid
- });
+ CreateRRLogEvent(
+ socket,
+ "DEBUG",
+ "Early RO created - checking dmsRoNo",
+ withRRRequestXml(result, {
+ dmsRoNo,
+ resultRoNo: result?.roNo,
+ dataRoNo: data?.dmsRoNo,
+ jobId: rid
+ })
+ );
// ✅ Persist DMS RO number, customer ID, advisor ID, and mileage on the job
if (dmsRoNo) {
const mileageIn = txEnvelope?.kmin ?? null;
- CreateRRLogEvent(socket, "DEBUG", "Calling setJobDmsIdForSocket", {
- jobId: rid,
+ CreateRRLogEvent(socket, "DEBUG", "Calling setJobDmsIdForSocket", {
+ jobId: rid,
dmsId: dmsRoNo,
customerId: effectiveCustNo,
advisorId: String(advisorNo),
mileageIn
});
- await setJobDmsIdForSocket({
- socket,
- jobId: rid,
+ await setJobDmsIdForSocket({
+ socket,
+ jobId: rid,
dmsId: dmsRoNo,
dmsCustomerId: effectiveCustNo,
dmsAdvisorId: String(advisorNo),
mileageIn
});
} else {
- CreateRRLogEvent(socket, "WARN", "RR early RO creation succeeded but no DMS RO number was returned", {
- jobId: rid,
- resultPreview: {
- roNo: result?.roNo,
- data: {
- dmsRoNo: data?.dmsRoNo,
- outsdRoNo: data?.outsdRoNo
+ CreateRRLogEvent(
+ socket,
+ "WARN",
+ "RR early RO creation succeeded but no DMS RO number was returned",
+ withRRRequestXml(result, {
+ jobId: rid,
+ resultPreview: {
+ roNo: result?.roNo,
+ data: {
+ dmsRoNo: data?.dmsRoNo,
+ outsdRoNo: data?.outsdRoNo
+ }
}
- }
- });
+ })
+ );
}
await redisHelpers.setSessionTransactionData(
@@ -758,10 +810,15 @@ const registerRREvents = ({ socket, redisHelpers }) => {
defaultRRTTL
);
- CreateRRLogEvent(socket, "INFO", `{EARLY-5} Minimal RO created successfully`, {
- dmsRoNo: dmsRoNo || null,
- outsdRoNo: outsdRoNo || null
- });
+ CreateRRLogEvent(
+ socket,
+ "INFO",
+ `{EARLY-5} Minimal RO created successfully`,
+ withRRRequestXml(result, {
+ dmsRoNo: dmsRoNo || null,
+ outsdRoNo: outsdRoNo || null
+ })
+ );
// Mark success in export logs
await markRRExportSuccess({
@@ -810,11 +867,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
message: vendorMessage
});
- CreateRRLogEvent(socket, "ERROR", `Early RO creation failed`, {
- roStatus: result?.roStatus,
- statusBlocks: result?.statusBlocks,
- classification: cls
- });
+ CreateRRLogEvent(
+ socket,
+ "ERROR",
+ `Early RO creation failed`,
+ withRRRequestXml(result, {
+ roStatus: result?.roStatus,
+ statusBlocks: result?.statusBlocks,
+ classification: cls
+ })
+ );
await insertRRFailedExportLog({
socket,
@@ -940,14 +1002,14 @@ const registerRREvents = ({ socket, redisHelpers }) => {
// Check if this job already has an early RO - if so, use stored IDs and skip customer search
const hasEarlyRO = !!job?.dms_id;
-
+
if (hasEarlyRO) {
CreateRRLogEvent(socket, "DEBUG", `{2} Early RO exists - using stored customer/advisor`, {
dms_id: job.dms_id,
dms_customer_id: job.dms_customer_id,
dms_advisor_id: job.dms_advisor_id
});
-
+
// Cache the stored customer/advisor IDs for the next step
if (job.dms_customer_id) {
await redisHelpers.setSessionTransactionData(
@@ -967,18 +1029,18 @@ const registerRREvents = ({ socket, redisHelpers }) => {
defaultRRTTL
);
}
-
+
// Emit empty customer list to frontend (won't show modal)
socket.emit("rr-select-customer", []);
-
+
// Continue directly with the export by calling the selected customer handler logic inline
// This is essentially the same as if user selected the stored customer
const selectedCustNo = job.dms_customer_id;
-
+
if (!selectedCustNo) {
throw new Error("Early RO exists but no customer ID stored");
}
-
+
// Continue with ensureRRServiceVehicle and export (same as rr-selected-customer handler)
const { client, opts } = await buildClientAndOpts(bodyshop);
const routing = opts?.routing || client?.opts?.routing || null;
@@ -1011,7 +1073,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
redisHelpers
});
- const advisorNo = job.dms_advisor_id || readAdvisorNo({ txEnvelope }, await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo));
+ const advisorNo =
+ job.dms_advisor_id ||
+ readAdvisorNo(
+ { txEnvelope },
+ await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo)
+ );
if (!advisorNo) {
throw new Error("Advisor is required (advisorNo).");
@@ -1059,15 +1126,20 @@ const registerRREvents = ({ socket, redisHelpers }) => {
defaultRRTTL
);
- CreateRRLogEvent(socket, "INFO", `RR Repair Order updated successfully`, {
- dmsRoNo,
- jobId: rid
- });
+ CreateRRLogEvent(
+ socket,
+ "INFO",
+ `RR Repair Order updated successfully`,
+ withRRRequestXml(result, {
+ dmsRoNo,
+ jobId: rid
+ })
+ );
// For early RO flow, only emit validation-required (not export-job:result)
// since the export is not complete yet - we're just waiting for validation
socket.emit("rr-validation-required", { dmsRoNo, jobId: rid });
-
+
return ack?.({ ok: true, skipCustomerSelection: true, dmsRoNo });
}
@@ -1277,25 +1349,25 @@ const registerRREvents = ({ socket, redisHelpers }) => {
// When updating an early RO, use stored customer/advisor IDs
let finalEffectiveCustNo = effectiveCustNo;
let finalAdvisorNo = advisorNo;
-
+
if (shouldUpdate && job?.dms_customer_id) {
- CreateRRLogEvent(socket, "DEBUG", `Using stored customer ID from early RO`, {
+ CreateRRLogEvent(socket, "DEBUG", `Using stored customer ID from early RO`, {
storedCustomerId: job.dms_customer_id,
- originalCustomerId: effectiveCustNo
+ originalCustomerId: effectiveCustNo
});
finalEffectiveCustNo = String(job.dms_customer_id);
}
-
+
if (shouldUpdate && job?.dms_advisor_id) {
- CreateRRLogEvent(socket, "DEBUG", `Using stored advisor ID from early RO`, {
+ CreateRRLogEvent(socket, "DEBUG", `Using stored advisor ID from early RO`, {
storedAdvisorId: job.dms_advisor_id,
- originalAdvisorId: advisorNo
+ originalAdvisorId: advisorNo
});
finalAdvisorNo = String(job.dms_advisor_id);
}
let result;
-
+
if (shouldUpdate) {
// UPDATE existing RO with full data
CreateRRLogEvent(socket, "DEBUG", `{4} Updating existing RR RO with full data`, { dmsRoNo: existingDmsId });
@@ -1344,16 +1416,21 @@ const registerRREvents = ({ socket, redisHelpers }) => {
if (dmsRoNo) {
await setJobDmsIdForSocket({ socket, jobId: rid, dmsId: dmsRoNo });
} else {
- CreateRRLogEvent(socket, "WARN", "RR export succeeded but no DMS RO number was returned", {
- jobId: rid,
- resultPreview: {
- roNo: result?.roNo,
- data: {
- dmsRoNo: data?.dmsRoNo,
- outsdRoNo: data?.outsdRoNo
+ CreateRRLogEvent(
+ socket,
+ "WARN",
+ "RR export succeeded but no DMS RO number was returned",
+ withRRRequestXml(result, {
+ jobId: rid,
+ resultPreview: {
+ roNo: result?.roNo,
+ data: {
+ dmsRoNo: data?.dmsRoNo,
+ outsdRoNo: data?.outsdRoNo
+ }
}
- }
- });
+ })
+ );
}
await redisHelpers.setSessionTransactionData(
@@ -1370,10 +1447,15 @@ const registerRREvents = ({ socket, redisHelpers }) => {
defaultRRTTL
);
- CreateRRLogEvent(socket, "INFO", `{5} RO created. Waiting for validation.`, {
- dmsRoNo: dmsRoNo || null,
- outsdRoNo: outsdRoNo || null
- });
+ CreateRRLogEvent(
+ socket,
+ "INFO",
+ `{5} RO created. Waiting for validation.`,
+ withRRRequestXml(result, {
+ dmsRoNo: dmsRoNo || null,
+ outsdRoNo: outsdRoNo || null
+ })
+ );
// Tell FE to prompt for "Finished/Close"
socket.emit("rr-validation-required", { jobId: rid, dmsRoNo, outsdRoNo });
@@ -1412,11 +1494,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
message: vendorMessage
});
- CreateRRLogEvent(socket, "ERROR", `Export failed (step 1)`, {
- roStatus: result?.roStatus,
- statusBlocks: result?.statusBlocks,
- classification: cls
- });
+ CreateRRLogEvent(
+ socket,
+ "ERROR",
+ `Export failed (step 1)`,
+ withRRRequestXml(result, {
+ roStatus: result?.roStatus,
+ statusBlocks: result?.statusBlocks,
+ classification: cls
+ })
+ );
await insertRRFailedExportLog({
socket,
@@ -1541,7 +1628,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
});
if (finalizeResult?.success) {
- CreateRRLogEvent(socket, "INFO", `{7} Finalize success; marking exported`, { dmsRoNo, outsdRoNo });
+ CreateRRLogEvent(
+ socket,
+ "INFO",
+ `{7} Finalize success; marking exported`,
+ withRRRequestXml(finalizeResult, { dmsRoNo, outsdRoNo })
+ );
// ✅ Mark exported + success log
await markRRExportSuccess({
@@ -1584,6 +1676,17 @@ const registerRREvents = ({ socket, redisHelpers }) => {
message: vendorMessage
});
+ CreateRRLogEvent(
+ socket,
+ "ERROR",
+ "Finalize failed",
+ withRRRequestXml(finalizeResult, {
+ roStatus: finalizeResult?.roStatus,
+ statusBlocks: finalizeResult?.statusBlocks,
+ classification: cls
+ })
+ );
+
await insertRRFailedExportLog({
socket,
jobId: rid,