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, '$1'); + 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,