Merge remote-tracking branch 'origin/hotfix/2026-03-03' into release/2026-02-27
This commit is contained in:
@@ -443,6 +443,30 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* DMS top panels: prevent card/table overflow into adjacent column at desktop+zoom */
|
||||||
|
.dms-top-panel-col {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dms-top-panel-col > .ant-card {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dms-top-panel-col > .ant-card .ant-card-body {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dms-top-panel-col .ant-table-wrapper,
|
||||||
|
.dms-top-panel-col .ant-tabs,
|
||||||
|
.dms-top-panel-col .ant-tabs-content,
|
||||||
|
.dms-top-panel-col .ant-tabs-tabpane {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
//.rbc-time-header-gutter {
|
//.rbc-time-header-gutter {
|
||||||
// padding: 0;
|
// padding: 0;
|
||||||
//}
|
//}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import dayjs from "../../utils/day";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
isDarkMode: selectDarkMode
|
isDarkMode: selectDarkMode
|
||||||
@@ -21,23 +22,32 @@ export function DmsLogEvents({
|
|||||||
colorizeJson = false,
|
colorizeJson = false,
|
||||||
showDetails = true
|
showDetails = true
|
||||||
}) {
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [openSet, setOpenSet] = useState(() => new Set());
|
const [openSet, setOpenSet] = useState(() => new Set());
|
||||||
|
const [copiedKey, setCopiedKey] = useState(null);
|
||||||
|
|
||||||
// Inject JSON highlight styles once (only when colorize is enabled)
|
// Inject JSON highlight styles once (only when colorize is enabled)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!colorizeJson) return;
|
if (!colorizeJson) return;
|
||||||
if (typeof document === "undefined") return;
|
if (typeof document === "undefined") return;
|
||||||
if (document.getElementById("json-highlight-styles")) return;
|
let style = document.getElementById("json-highlight-styles");
|
||||||
const style = document.createElement("style");
|
if (!style) {
|
||||||
style.id = "json-highlight-styles";
|
style = document.createElement("style");
|
||||||
|
style.id = "json-highlight-styles";
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.json-key { color: #fa8c16; }
|
.json-key { color: #fa8c16; }
|
||||||
.json-string { color: #52c41a; }
|
.json-string { color: #52c41a; }
|
||||||
.json-number { color: #722ed1; }
|
.json-number { color: #722ed1; }
|
||||||
.json-boolean { color: #1890ff; }
|
.json-boolean { color: #1890ff; }
|
||||||
.json-null { color: #faad14; }
|
.json-null { color: #faad14; }
|
||||||
|
.xml-tag { color: #1677ff; }
|
||||||
|
.xml-attr { color: #d46b08; }
|
||||||
|
.xml-value { color: #389e0d; }
|
||||||
|
.xml-decl { color: #7c3aed; }
|
||||||
|
.xml-comment { color: #8c8c8c; }
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
|
||||||
}, [colorizeJson]);
|
}, [colorizeJson]);
|
||||||
|
|
||||||
// Trim openSet if logs shrink
|
// Trim openSet if logs shrink
|
||||||
@@ -65,6 +75,13 @@ export function DmsLogEvents({
|
|||||||
// Only treat meta as "present" when we are allowed to show details
|
// Only treat meta as "present" when we are allowed to show details
|
||||||
const hasMeta = !isEmpty(meta) && showDetails;
|
const hasMeta = !isEmpty(meta) && showDetails;
|
||||||
const isOpen = hasMeta && openSet.has(idx);
|
const isOpen = hasMeta && openSet.has(idx);
|
||||||
|
const xml = hasMeta ? extractXmlFromMeta(meta) : { request: null, response: null };
|
||||||
|
const hasRequestXml = !!xml.request;
|
||||||
|
const hasResponseXml = !!xml.response;
|
||||||
|
const copyPayload = hasMeta ? getCopyPayload(meta) : null;
|
||||||
|
const copyPayloadKey = `copy-${idx}`;
|
||||||
|
const copyReqKey = `copy-req-${idx}`;
|
||||||
|
const copyResKey = `copy-res-${idx}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: idx,
|
key: idx,
|
||||||
@@ -92,10 +109,42 @@ export function DmsLogEvents({
|
|||||||
return next;
|
return next;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
style={{ cursor: "pointer", userSelect: "none" }}
|
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
|
||||||
>
|
>
|
||||||
{isOpen ? "Hide details" : "Details"}
|
{isOpen ? t("dms.labels.hide_details") : t("dms.labels.details")}
|
||||||
</a>
|
</a>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={() => handleCopyAction(copyPayloadKey, copyPayload, setCopiedKey)}
|
||||||
|
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{copiedKey === copyPayloadKey ? t("dms.labels.copied") : t("dms.labels.copy")}
|
||||||
|
</a>
|
||||||
|
{hasRequestXml && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={() => handleCopyAction(copyReqKey, xml.request, setCopiedKey)}
|
||||||
|
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{copiedKey === copyReqKey ? t("dms.labels.copied") : t("dms.labels.copy_request")}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{hasResponseXml && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="vertical" />
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
onClick={() => handleCopyAction(copyResKey, xml.response, setCopiedKey)}
|
||||||
|
style={{ cursor: "pointer", userSelect: "none", fontSize: 11 }}
|
||||||
|
>
|
||||||
|
{copiedKey === copyResKey ? t("dms.labels.copied") : t("dms.labels.copy_response")}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -103,14 +152,30 @@ export function DmsLogEvents({
|
|||||||
{/* Row 2: details body (only when open) */}
|
{/* Row 2: details body (only when open) */}
|
||||||
{hasMeta && isOpen && (
|
{hasMeta && isOpen && (
|
||||||
<div style={{ marginLeft: 6 }}>
|
<div style={{ marginLeft: 6 }}>
|
||||||
<JsonBlock isDarkMode={isDarkMode} data={meta} colorize={colorizeJson} />
|
<JsonBlock isDarkMode={isDarkMode} data={removeXmlFromMeta(meta)} colorize={colorizeJson} />
|
||||||
|
{hasRequestXml && (
|
||||||
|
<XmlBlock
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
title={t("dms.labels.request_xml")}
|
||||||
|
xmlText={xml.request}
|
||||||
|
colorize={colorizeJson}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{hasResponseXml && (
|
||||||
|
<XmlBlock
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
|
title={t("dms.labels.response_xml")}
|
||||||
|
xmlText={xml.response}
|
||||||
|
colorize={colorizeJson}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
[logs, openSet, colorizeJson, isDarkMode, showDetails]
|
[logs, openSet, colorizeJson, copiedKey, isDarkMode, showDetails, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Timeline reverse items={items} />;
|
return <Timeline reverse items={items} />;
|
||||||
@@ -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<void>}
|
||||||
|
*/
|
||||||
|
const handleCopyAction = async (key, value, setCopied) => {
|
||||||
|
const text = typeof value === "string" ? value : safeStringify(value, 2);
|
||||||
|
if (!text) return;
|
||||||
|
const copied = await copyTextToClipboard(text);
|
||||||
|
if (!copied) return;
|
||||||
|
setCopied(key);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied((prev) => (prev === key ? null : prev));
|
||||||
|
}, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clipboard helper (modern async Clipboard API).
|
||||||
|
* @param text
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
const copyTextToClipboard = async (text) => {
|
||||||
|
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JSON display block with optional syntax highlighting.
|
* JSON display block with optional syntax highlighting.
|
||||||
* @param data
|
* @param data
|
||||||
@@ -210,6 +390,105 @@ const JsonBlock = ({ data, colorize, isDarkMode }) => {
|
|||||||
return <pre style={preStyle}>{jsonText}</pre>;
|
return <pre style={preStyle}>{jsonText}</pre>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* XML display block with normalized indentation.
|
||||||
|
* @param title
|
||||||
|
* @param xmlText
|
||||||
|
* @param isDarkMode
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
const XmlBlock = ({ title, xmlText, isDarkMode, colorize = false }) => {
|
||||||
|
const base = {
|
||||||
|
margin: "8px 0 0",
|
||||||
|
maxWidth: 720,
|
||||||
|
overflowX: "auto",
|
||||||
|
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
lineHeight: 1.45,
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 6,
|
||||||
|
background: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.04)",
|
||||||
|
border: isDarkMode ? "1px solid rgba(255,255,255,0.12)" : "1px solid rgba(0,0,0,0.08)",
|
||||||
|
color: isDarkMode ? "var(--card-text-fallback)" : "#141414",
|
||||||
|
whiteSpace: "pre"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 6 }}>
|
||||||
|
<div style={{ fontSize: 11, fontWeight: 600 }}>{title}</div>
|
||||||
|
{colorize ? (
|
||||||
|
<pre style={base} dangerouslySetInnerHTML={{ __html: highlightXml(formatXml(xmlText)) }} />
|
||||||
|
) : (
|
||||||
|
<pre style={base}>{formatXml(xmlText)}</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basic XML pretty-printer.
|
||||||
|
* @param xml
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const formatXml = (xml) => {
|
||||||
|
if (typeof xml !== "string") return "";
|
||||||
|
const normalized = xml.replace(/\r\n/g, "\n").replace(/>\s*</g, ">\n<").trim();
|
||||||
|
const lines = normalized.split("\n");
|
||||||
|
let indent = 0;
|
||||||
|
const out = [];
|
||||||
|
|
||||||
|
for (const rawLine of lines) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
if (/^<\/[^>]+>/.test(line)) indent = Math.max(indent - 1, 0);
|
||||||
|
out.push(`${" ".repeat(indent)}${line}`);
|
||||||
|
|
||||||
|
const opens = (line.match(/<[^/!?][^>]*>/g) || []).length;
|
||||||
|
const closes = (line.match(/<\/[^>]+>/g) || []).length;
|
||||||
|
const selfClosing = (line.match(/<[^>]+\/>/g) || []).length;
|
||||||
|
const declaration = /^<\?xml/.test(line) ? 1 : 0;
|
||||||
|
|
||||||
|
indent += opens - closes - selfClosing - declaration;
|
||||||
|
if (indent < 0) indent = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syntax highlight pretty-printed XML text for HTML display.
|
||||||
|
* @param xmlText
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
const highlightXml = (xmlText) => {
|
||||||
|
const esc = String(xmlText || "")
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
const lines = esc.split("\n");
|
||||||
|
|
||||||
|
return lines
|
||||||
|
.map((line) => {
|
||||||
|
let out = line;
|
||||||
|
|
||||||
|
out = out.replace(/(<!--[\s\S]*?-->)/g, '<span class="xml-comment">$1</span>');
|
||||||
|
out = out.replace(/(<\?xml[\s\S]*?\?>)/g, '<span class="xml-decl">$1</span>');
|
||||||
|
|
||||||
|
out = out.replace(/(<\/?)([A-Za-z_][\w:.-]*)([\s\S]*?)(\/?>)/g, (_m, open, tag, attrs, close) => {
|
||||||
|
const coloredAttrs = attrs.replace(
|
||||||
|
/([A-Za-z_][\w:.-]*)(=)("[^"]*"|'[^']*'|"[\s\S]*?"|'[\s\S]*?')/g,
|
||||||
|
'<span class="xml-attr">$1</span>$2<span class="xml-value">$3</span>'
|
||||||
|
);
|
||||||
|
return `${open}<span class="xml-tag">${tag}</span>${coloredAttrs}${close}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syntax highlight JSON text for HTML display.
|
* Syntax highlight JSON text for HTML display.
|
||||||
* @param jsonText
|
* @param jsonText
|
||||||
|
|||||||
@@ -164,19 +164,21 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
const providerLabel = useMemo(
|
const providerLabel = useMemo(
|
||||||
() =>
|
() =>
|
||||||
({
|
({
|
||||||
[DMS_MAP.reynolds]: "Reynolds",
|
[DMS_MAP.reynolds]: t("dms.labels.provider_reynolds"),
|
||||||
[DMS_MAP.fortellis]: "Fortellis",
|
[DMS_MAP.fortellis]: t("dms.labels.provider_fortellis"),
|
||||||
[DMS_MAP.cdk]: "CDK",
|
[DMS_MAP.cdk]: t("dms.labels.provider_cdk"),
|
||||||
[DMS_MAP.pbs]: "PBS"
|
[DMS_MAP.pbs]: t("dms.labels.provider_pbs")
|
||||||
})[mode] || "DMS",
|
})[mode] || t("dms.labels.provider_dms"),
|
||||||
[mode]
|
[mode, t]
|
||||||
);
|
);
|
||||||
|
|
||||||
const transportLabel = isWssMode(mode) ? "(WSS)" : "(WS)";
|
const transportLabel = isWssMode(mode) ? t("dms.labels.transport_wss") : t("dms.labels.transport_ws");
|
||||||
|
|
||||||
const bannerMessage = `Posting to ${providerLabel} | ${transportLabel} | ${
|
const bannerMessage = t("dms.labels.banner_message", {
|
||||||
isConnected ? "Connected" : "Disconnected"
|
provider: providerLabel,
|
||||||
}`;
|
transport: transportLabel,
|
||||||
|
status: isConnected ? t("dms.labels.banner_status_connected") : t("dms.labels.banner_status_disconnected")
|
||||||
|
});
|
||||||
|
|
||||||
const resetKey = useMemo(() => `${mode || "none"}-${jobId || "none"}`, [mode, jobId]);
|
const resetKey = useMemo(() => `${mode || "none"}-${jobId || "none"}`, [mode, jobId]);
|
||||||
const customerSelectorKey = useMemo(() => `${resetKey}-${reconnectNonce}`, [resetKey, reconnectNonce]);
|
const customerSelectorKey = useMemo(() => `${resetKey}-${reconnectNonce}`, [resetKey, reconnectNonce]);
|
||||||
@@ -246,7 +248,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
errText ||
|
errText ||
|
||||||
t("dms.errors.exportfailedgeneric", "We couldn't complete the export. Please try again.");
|
t("dms.errors.exportfailedgeneric", "We couldn't complete the export. Please try again.");
|
||||||
|
|
||||||
const vendorTitle = title || (isRrMode ? "Reynolds" : "DMS");
|
const vendorTitle = title || (isRrMode ? t("dms.labels.provider_reynolds") : t("dms.labels.provider_dms"));
|
||||||
|
|
||||||
const isRrOpenRoLimit =
|
const isRrOpenRoLimit =
|
||||||
isRrMode &&
|
isRrMode &&
|
||||||
@@ -321,7 +323,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
{
|
{
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
level: "warn",
|
level: "warn",
|
||||||
message: `Reconnected to ${isRrMode ? "RR" : mode === DMS_MAP.fortellis ? "Fortellis" : "DMS"} Export Service`
|
message: t("dms.labels.reconnected_export_service", {
|
||||||
|
provider: isRrMode ? t("dms.labels.provider_reynolds") : providerLabel
|
||||||
|
})
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
@@ -380,14 +384,12 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
{
|
{
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
level: "INFO",
|
level: "INFO",
|
||||||
message:
|
message: t("dms.labels.rr_validation_message")
|
||||||
"Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize."
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
notification.info({
|
notification.info({
|
||||||
title: "Reynolds RO created",
|
title: t("dms.labels.rr_validation_notice_title"),
|
||||||
description:
|
description: t("dms.labels.rr_validation_notice_description"),
|
||||||
"Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.",
|
|
||||||
duration: 8
|
duration: 8
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -399,8 +401,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
{
|
{
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
level: "INFO",
|
level: "INFO",
|
||||||
message:
|
message: t("dms.labels.rr_validation_message"),
|
||||||
"Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.",
|
|
||||||
meta: { payload }
|
meta: { payload }
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
@@ -428,7 +429,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
activeSocket.disconnect();
|
activeSocket.disconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [mode, activeSocket, channels, logLevel, notification, t, insertAuditTrail, history]);
|
}, [mode, activeSocket, channels, logLevel, notification, t, insertAuditTrail, history, isRrMode, providerLabel]);
|
||||||
|
|
||||||
// RR finalize callback (unchanged public behavior)
|
// RR finalize callback (unchanged public behavior)
|
||||||
const handleRrValidationFinished = () => {
|
const handleRrValidationFinished = () => {
|
||||||
@@ -471,7 +472,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
<AlertComponent style={{ marginBottom: 10 }} title={bannerMessage} type="warning" showIcon closable />
|
<AlertComponent style={{ marginBottom: 10 }} title={bannerMessage} type="warning" showIcon closable />
|
||||||
|
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col md={24} lg={10} className="dms-equal-height-col">
|
<Col xs={24} xxl={10} className="dms-equal-height-col dms-top-panel-col">
|
||||||
{!isRrMode ? (
|
{!isRrMode ? (
|
||||||
<DmsAllocationsSummary
|
<DmsAllocationsSummary
|
||||||
key={resetKey}
|
key={resetKey}
|
||||||
@@ -511,7 +512,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col md={24} lg={14} className="dms-equal-height-col">
|
<Col xs={24} xxl={14} className="dms-equal-height-col dms-top-panel-col">
|
||||||
<DmsPostForm
|
<DmsPostForm
|
||||||
key={resetKey}
|
key={resetKey}
|
||||||
socket={activeSocket}
|
socket={activeSocket}
|
||||||
@@ -550,15 +551,17 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
<Switch
|
<Switch
|
||||||
checked={colorizeJson}
|
checked={colorizeJson}
|
||||||
onChange={setColorizeJson}
|
onChange={setColorizeJson}
|
||||||
checkedChildren="Color JSON"
|
checkedChildren={t("dms.labels.color_json")}
|
||||||
unCheckedChildren="Plain JSON"
|
unCheckedChildren={t("dms.labels.plain_json")}
|
||||||
/>
|
/>
|
||||||
<Button onClick={toggleDetailsAll}>{detailsOpen ? "Collapse All" : "Expand All"}</Button>
|
<Button onClick={toggleDetailsAll}>
|
||||||
|
{detailsOpen ? t("dms.labels.collapse_all") : t("dms.labels.expand_all")}
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
placeholder="Log Level"
|
placeholder={t("dms.labels.log_level")}
|
||||||
value={logLevel}
|
value={logLevel}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setLogLevel(value);
|
setLogLevel(value);
|
||||||
@@ -572,8 +575,8 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
{ key: "ERROR", value: "ERROR", label: "ERROR" }
|
{ key: "ERROR", value: "ERROR", label: "ERROR" }
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Button onClick={() => setLogs([])}>Clear Logs</Button>
|
<Button onClick={() => setLogs([])}>{t("dms.labels.clear_logs")}</Button>
|
||||||
<Button onClick={handleReconnectClick}>Reconnect</Button>
|
<Button onClick={handleReconnectClick}> {t("dms.labels.reconnect")}</Button>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1074,7 +1074,36 @@
|
|||||||
"earlyrorequired.message": "This job requires an early Repair Order to be created before posting to Reynolds. Please use the admin panel to create the early RO first."
|
"earlyrorequired.message": "This job requires an early Repair Order to be created before posting to Reynolds. Please use the admin panel to create the early RO first."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"refreshallocations": "Refresh to see DMS Allocations."
|
"refreshallocations": "Refresh to see DMS Allocations.",
|
||||||
|
"provider_reynolds": "Reynolds",
|
||||||
|
"provider_fortellis": "Fortellis",
|
||||||
|
"provider_cdk": "CDK",
|
||||||
|
"provider_pbs": "PBS",
|
||||||
|
"provider_dms": "DMS",
|
||||||
|
"transport_wss": "(WSS)",
|
||||||
|
"transport_ws": "(WS)",
|
||||||
|
"banner_status_connected": "Connected",
|
||||||
|
"banner_status_disconnected": "Disconnected",
|
||||||
|
"banner_message": "Posting to {{provider}} | {{transport}} | {{status}}",
|
||||||
|
"reconnected_export_service": "Reconnected to {{provider}} Export Service",
|
||||||
|
"rr_validation_message": "Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.",
|
||||||
|
"rr_validation_notice_title": "Reynolds RO created",
|
||||||
|
"rr_validation_notice_description": "Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.",
|
||||||
|
"color_json": "Color JSON",
|
||||||
|
"plain_json": "Plain JSON",
|
||||||
|
"collapse_all": "Collapse All",
|
||||||
|
"expand_all": "Expand All",
|
||||||
|
"log_level": "Log Level",
|
||||||
|
"clear_logs": "Clear Logs",
|
||||||
|
"reconnect": "Reconnect",
|
||||||
|
"details": "Details",
|
||||||
|
"hide_details": "Hide details",
|
||||||
|
"copy": "Copy",
|
||||||
|
"copied": "Copied",
|
||||||
|
"copy_request": "Copy Request",
|
||||||
|
"copy_response": "Copy Response",
|
||||||
|
"request_xml": "Request XML",
|
||||||
|
"response_xml": "Response XML"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
|
|||||||
@@ -1074,7 +1074,36 @@
|
|||||||
"earlyrorequired.message": ""
|
"earlyrorequired.message": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"refreshallocations": ""
|
"refreshallocations": "",
|
||||||
|
"provider_reynolds": "",
|
||||||
|
"provider_fortellis": "",
|
||||||
|
"provider_cdk": "",
|
||||||
|
"provider_pbs": "",
|
||||||
|
"provider_dms": "",
|
||||||
|
"transport_wss": "",
|
||||||
|
"transport_ws": "",
|
||||||
|
"banner_status_connected": "",
|
||||||
|
"banner_status_disconnected": "",
|
||||||
|
"banner_message": "",
|
||||||
|
"reconnected_export_service": "",
|
||||||
|
"rr_validation_message": "",
|
||||||
|
"rr_validation_notice_title": "",
|
||||||
|
"rr_validation_notice_description": "",
|
||||||
|
"color_json": "",
|
||||||
|
"plain_json": "",
|
||||||
|
"collapse_all": "",
|
||||||
|
"expand_all": "",
|
||||||
|
"log_level": "",
|
||||||
|
"clear_logs": "",
|
||||||
|
"reconnect": "",
|
||||||
|
"details": "",
|
||||||
|
"hide_details": "",
|
||||||
|
"copy": "",
|
||||||
|
"copied": "",
|
||||||
|
"copy_request": "",
|
||||||
|
"copy_response": "",
|
||||||
|
"request_xml": "",
|
||||||
|
"response_xml": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
|
|||||||
@@ -1074,7 +1074,36 @@
|
|||||||
"earlyrorequired.message": ""
|
"earlyrorequired.message": ""
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"refreshallocations": ""
|
"refreshallocations": "",
|
||||||
|
"provider_reynolds": "",
|
||||||
|
"provider_fortellis": "",
|
||||||
|
"provider_cdk": "",
|
||||||
|
"provider_pbs": "",
|
||||||
|
"provider_dms": "",
|
||||||
|
"transport_wss": "",
|
||||||
|
"transport_ws": "",
|
||||||
|
"banner_status_connected": "",
|
||||||
|
"banner_status_disconnected": "",
|
||||||
|
"banner_message": "",
|
||||||
|
"reconnected_export_service": "",
|
||||||
|
"rr_validation_message": "",
|
||||||
|
"rr_validation_notice_title": "",
|
||||||
|
"rr_validation_notice_description": "",
|
||||||
|
"color_json": "",
|
||||||
|
"plain_json": "",
|
||||||
|
"collapse_all": "",
|
||||||
|
"expand_all": "",
|
||||||
|
"log_level": "",
|
||||||
|
"clear_logs": "",
|
||||||
|
"reconnect": "",
|
||||||
|
"details": "",
|
||||||
|
"hide_details": "",
|
||||||
|
"copy": "",
|
||||||
|
"copied": "",
|
||||||
|
"copy_request": "",
|
||||||
|
"copy_response": "",
|
||||||
|
"request_xml": "",
|
||||||
|
"response_xml": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documents": {
|
"documents": {
|
||||||
|
|||||||
@@ -48,6 +48,46 @@ const resolveJobId = (explicit, payload, job) => explicit || payload?.jobId || j
|
|||||||
*/
|
*/
|
||||||
const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null;
|
const resolveVin = ({ tx, job }) => tx?.jobData?.vin || job?.v_vin || null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
* Sort vehicle owners first in the list, preserving original order otherwise.
|
||||||
* @param list
|
* @param list
|
||||||
@@ -154,15 +194,13 @@ const setJobDmsIdForSocket = async ({ socket, jobId, dmsId, dmsCustomerId, dmsAd
|
|||||||
if (!token) throw new Error("Missing auth token for setJobDmsIdForSocket");
|
if (!token) throw new Error("Missing auth token for setJobDmsIdForSocket");
|
||||||
|
|
||||||
const client = new GraphQLClient(endpoint, {});
|
const client = new GraphQLClient(endpoint, {});
|
||||||
await client
|
await client.setHeaders({ Authorization: `Bearer ${token}` }).request(queries.SET_JOB_DMS_ID, {
|
||||||
.setHeaders({ Authorization: `Bearer ${token}` })
|
id: jobId,
|
||||||
.request(queries.SET_JOB_DMS_ID, {
|
dms_id: String(dmsId),
|
||||||
id: jobId,
|
dms_customer_id: dmsCustomerId ? String(dmsCustomerId) : null,
|
||||||
dms_id: String(dmsId),
|
dms_advisor_id: dmsAdvisorId ? String(dmsAdvisorId) : null,
|
||||||
dms_customer_id: dmsCustomerId ? String(dmsCustomerId) : null,
|
kmin: mileageIn != null && mileageIn > 0 ? parseInt(mileageIn, 10) : null
|
||||||
dms_advisor_id: dmsAdvisorId ? String(dmsAdvisorId) : null,
|
});
|
||||||
kmin: mileageIn != null && mileageIn > 0 ? parseInt(mileageIn, 10) : null
|
|
||||||
});
|
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", "Linked job.dms_id to RR RO", {
|
CreateRRLogEvent(socket, "INFO", "Linked job.dms_id to RR RO", {
|
||||||
jobId,
|
jobId,
|
||||||
@@ -511,7 +549,11 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Filter out invalid values
|
// Filter out invalid values
|
||||||
if (selectedCustNo === "undefined" || selectedCustNo === "null" || (selectedCustNo && selectedCustNo.trim() === "")) {
|
if (
|
||||||
|
selectedCustNo === "undefined" ||
|
||||||
|
selectedCustNo === "null" ||
|
||||||
|
(selectedCustNo && selectedCustNo.trim() === "")
|
||||||
|
) {
|
||||||
selectedCustNo = null;
|
selectedCustNo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,42 +747,52 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
|
|
||||||
const outsdRoNo = data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;
|
const outsdRoNo = data?.outsdRoNo ?? job?.ro_number ?? job?.id ?? null;
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "DEBUG", "Early RO created - checking dmsRoNo", {
|
CreateRRLogEvent(
|
||||||
dmsRoNo,
|
socket,
|
||||||
resultRoNo: result?.roNo,
|
"DEBUG",
|
||||||
dataRoNo: data?.dmsRoNo,
|
"Early RO created - checking dmsRoNo",
|
||||||
jobId: rid
|
withRRRequestXml(result, {
|
||||||
});
|
dmsRoNo,
|
||||||
|
resultRoNo: result?.roNo,
|
||||||
|
dataRoNo: data?.dmsRoNo,
|
||||||
|
jobId: rid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// ✅ Persist DMS RO number, customer ID, advisor ID, and mileage on the job
|
// ✅ Persist DMS RO number, customer ID, advisor ID, and mileage on the job
|
||||||
if (dmsRoNo) {
|
if (dmsRoNo) {
|
||||||
const mileageIn = txEnvelope?.kmin ?? null;
|
const mileageIn = txEnvelope?.kmin ?? null;
|
||||||
CreateRRLogEvent(socket, "DEBUG", "Calling setJobDmsIdForSocket", {
|
CreateRRLogEvent(socket, "DEBUG", "Calling setJobDmsIdForSocket", {
|
||||||
jobId: rid,
|
jobId: rid,
|
||||||
dmsId: dmsRoNo,
|
dmsId: dmsRoNo,
|
||||||
customerId: effectiveCustNo,
|
customerId: effectiveCustNo,
|
||||||
advisorId: String(advisorNo),
|
advisorId: String(advisorNo),
|
||||||
mileageIn
|
mileageIn
|
||||||
});
|
});
|
||||||
await setJobDmsIdForSocket({
|
await setJobDmsIdForSocket({
|
||||||
socket,
|
socket,
|
||||||
jobId: rid,
|
jobId: rid,
|
||||||
dmsId: dmsRoNo,
|
dmsId: dmsRoNo,
|
||||||
dmsCustomerId: effectiveCustNo,
|
dmsCustomerId: effectiveCustNo,
|
||||||
dmsAdvisorId: String(advisorNo),
|
dmsAdvisorId: String(advisorNo),
|
||||||
mileageIn
|
mileageIn
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
CreateRRLogEvent(socket, "WARN", "RR early RO creation succeeded but no DMS RO number was returned", {
|
CreateRRLogEvent(
|
||||||
jobId: rid,
|
socket,
|
||||||
resultPreview: {
|
"WARN",
|
||||||
roNo: result?.roNo,
|
"RR early RO creation succeeded but no DMS RO number was returned",
|
||||||
data: {
|
withRRRequestXml(result, {
|
||||||
dmsRoNo: data?.dmsRoNo,
|
jobId: rid,
|
||||||
outsdRoNo: data?.outsdRoNo
|
resultPreview: {
|
||||||
|
roNo: result?.roNo,
|
||||||
|
data: {
|
||||||
|
dmsRoNo: data?.dmsRoNo,
|
||||||
|
outsdRoNo: data?.outsdRoNo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await redisHelpers.setSessionTransactionData(
|
await redisHelpers.setSessionTransactionData(
|
||||||
@@ -758,10 +810,15 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", `{EARLY-5} Minimal RO created successfully`, {
|
CreateRRLogEvent(
|
||||||
dmsRoNo: dmsRoNo || null,
|
socket,
|
||||||
outsdRoNo: outsdRoNo || null
|
"INFO",
|
||||||
});
|
`{EARLY-5} Minimal RO created successfully`,
|
||||||
|
withRRRequestXml(result, {
|
||||||
|
dmsRoNo: dmsRoNo || null,
|
||||||
|
outsdRoNo: outsdRoNo || null
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Mark success in export logs
|
// Mark success in export logs
|
||||||
await markRRExportSuccess({
|
await markRRExportSuccess({
|
||||||
@@ -810,11 +867,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
message: vendorMessage
|
message: vendorMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Early RO creation failed`, {
|
CreateRRLogEvent(
|
||||||
roStatus: result?.roStatus,
|
socket,
|
||||||
statusBlocks: result?.statusBlocks,
|
"ERROR",
|
||||||
classification: cls
|
`Early RO creation failed`,
|
||||||
});
|
withRRRequestXml(result, {
|
||||||
|
roStatus: result?.roStatus,
|
||||||
|
statusBlocks: result?.statusBlocks,
|
||||||
|
classification: cls
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
socket,
|
socket,
|
||||||
@@ -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
|
// Check if this job already has an early RO - if so, use stored IDs and skip customer search
|
||||||
const hasEarlyRO = !!job?.dms_id;
|
const hasEarlyRO = !!job?.dms_id;
|
||||||
|
|
||||||
if (hasEarlyRO) {
|
if (hasEarlyRO) {
|
||||||
CreateRRLogEvent(socket, "DEBUG", `{2} Early RO exists - using stored customer/advisor`, {
|
CreateRRLogEvent(socket, "DEBUG", `{2} Early RO exists - using stored customer/advisor`, {
|
||||||
dms_id: job.dms_id,
|
dms_id: job.dms_id,
|
||||||
dms_customer_id: job.dms_customer_id,
|
dms_customer_id: job.dms_customer_id,
|
||||||
dms_advisor_id: job.dms_advisor_id
|
dms_advisor_id: job.dms_advisor_id
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cache the stored customer/advisor IDs for the next step
|
// Cache the stored customer/advisor IDs for the next step
|
||||||
if (job.dms_customer_id) {
|
if (job.dms_customer_id) {
|
||||||
await redisHelpers.setSessionTransactionData(
|
await redisHelpers.setSessionTransactionData(
|
||||||
@@ -967,18 +1029,18 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit empty customer list to frontend (won't show modal)
|
// Emit empty customer list to frontend (won't show modal)
|
||||||
socket.emit("rr-select-customer", []);
|
socket.emit("rr-select-customer", []);
|
||||||
|
|
||||||
// Continue directly with the export by calling the selected customer handler logic inline
|
// 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
|
// This is essentially the same as if user selected the stored customer
|
||||||
const selectedCustNo = job.dms_customer_id;
|
const selectedCustNo = job.dms_customer_id;
|
||||||
|
|
||||||
if (!selectedCustNo) {
|
if (!selectedCustNo) {
|
||||||
throw new Error("Early RO exists but no customer ID stored");
|
throw new Error("Early RO exists but no customer ID stored");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with ensureRRServiceVehicle and export (same as rr-selected-customer handler)
|
// Continue with ensureRRServiceVehicle and export (same as rr-selected-customer handler)
|
||||||
const { client, opts } = await buildClientAndOpts(bodyshop);
|
const { client, opts } = await buildClientAndOpts(bodyshop);
|
||||||
const routing = opts?.routing || client?.opts?.routing || null;
|
const routing = opts?.routing || client?.opts?.routing || null;
|
||||||
@@ -1011,7 +1073,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
redisHelpers
|
redisHelpers
|
||||||
});
|
});
|
||||||
|
|
||||||
const advisorNo = job.dms_advisor_id || readAdvisorNo({ txEnvelope }, await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo));
|
const advisorNo =
|
||||||
|
job.dms_advisor_id ||
|
||||||
|
readAdvisorNo(
|
||||||
|
{ txEnvelope },
|
||||||
|
await redisHelpers.getSessionTransactionData(socket.id, getTransactionType(rid), RRCacheEnums.AdvisorNo)
|
||||||
|
);
|
||||||
|
|
||||||
if (!advisorNo) {
|
if (!advisorNo) {
|
||||||
throw new Error("Advisor is required (advisorNo).");
|
throw new Error("Advisor is required (advisorNo).");
|
||||||
@@ -1059,15 +1126,20 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", `RR Repair Order updated successfully`, {
|
CreateRRLogEvent(
|
||||||
dmsRoNo,
|
socket,
|
||||||
jobId: rid
|
"INFO",
|
||||||
});
|
`RR Repair Order updated successfully`,
|
||||||
|
withRRRequestXml(result, {
|
||||||
|
dmsRoNo,
|
||||||
|
jobId: rid
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// For early RO flow, only emit validation-required (not export-job:result)
|
// For early RO flow, only emit validation-required (not export-job:result)
|
||||||
// since the export is not complete yet - we're just waiting for validation
|
// since the export is not complete yet - we're just waiting for validation
|
||||||
socket.emit("rr-validation-required", { dmsRoNo, jobId: rid });
|
socket.emit("rr-validation-required", { dmsRoNo, jobId: rid });
|
||||||
|
|
||||||
return ack?.({ ok: true, skipCustomerSelection: true, dmsRoNo });
|
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
|
// When updating an early RO, use stored customer/advisor IDs
|
||||||
let finalEffectiveCustNo = effectiveCustNo;
|
let finalEffectiveCustNo = effectiveCustNo;
|
||||||
let finalAdvisorNo = advisorNo;
|
let finalAdvisorNo = advisorNo;
|
||||||
|
|
||||||
if (shouldUpdate && job?.dms_customer_id) {
|
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,
|
storedCustomerId: job.dms_customer_id,
|
||||||
originalCustomerId: effectiveCustNo
|
originalCustomerId: effectiveCustNo
|
||||||
});
|
});
|
||||||
finalEffectiveCustNo = String(job.dms_customer_id);
|
finalEffectiveCustNo = String(job.dms_customer_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldUpdate && job?.dms_advisor_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,
|
storedAdvisorId: job.dms_advisor_id,
|
||||||
originalAdvisorId: advisorNo
|
originalAdvisorId: advisorNo
|
||||||
});
|
});
|
||||||
finalAdvisorNo = String(job.dms_advisor_id);
|
finalAdvisorNo = String(job.dms_advisor_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
if (shouldUpdate) {
|
if (shouldUpdate) {
|
||||||
// UPDATE existing RO with full data
|
// UPDATE existing RO with full data
|
||||||
CreateRRLogEvent(socket, "DEBUG", `{4} Updating existing RR RO with full data`, { dmsRoNo: existingDmsId });
|
CreateRRLogEvent(socket, "DEBUG", `{4} Updating existing RR RO with full data`, { dmsRoNo: existingDmsId });
|
||||||
@@ -1344,16 +1416,21 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
if (dmsRoNo) {
|
if (dmsRoNo) {
|
||||||
await setJobDmsIdForSocket({ socket, jobId: rid, dmsId: dmsRoNo });
|
await setJobDmsIdForSocket({ socket, jobId: rid, dmsId: dmsRoNo });
|
||||||
} else {
|
} else {
|
||||||
CreateRRLogEvent(socket, "WARN", "RR export succeeded but no DMS RO number was returned", {
|
CreateRRLogEvent(
|
||||||
jobId: rid,
|
socket,
|
||||||
resultPreview: {
|
"WARN",
|
||||||
roNo: result?.roNo,
|
"RR export succeeded but no DMS RO number was returned",
|
||||||
data: {
|
withRRRequestXml(result, {
|
||||||
dmsRoNo: data?.dmsRoNo,
|
jobId: rid,
|
||||||
outsdRoNo: data?.outsdRoNo
|
resultPreview: {
|
||||||
|
roNo: result?.roNo,
|
||||||
|
data: {
|
||||||
|
dmsRoNo: data?.dmsRoNo,
|
||||||
|
outsdRoNo: data?.outsdRoNo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await redisHelpers.setSessionTransactionData(
|
await redisHelpers.setSessionTransactionData(
|
||||||
@@ -1370,10 +1447,15 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
defaultRRTTL
|
defaultRRTTL
|
||||||
);
|
);
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "INFO", `{5} RO created. Waiting for validation.`, {
|
CreateRRLogEvent(
|
||||||
dmsRoNo: dmsRoNo || null,
|
socket,
|
||||||
outsdRoNo: outsdRoNo || null
|
"INFO",
|
||||||
});
|
`{5} RO created. Waiting for validation.`,
|
||||||
|
withRRRequestXml(result, {
|
||||||
|
dmsRoNo: dmsRoNo || null,
|
||||||
|
outsdRoNo: outsdRoNo || null
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Tell FE to prompt for "Finished/Close"
|
// Tell FE to prompt for "Finished/Close"
|
||||||
socket.emit("rr-validation-required", { jobId: rid, dmsRoNo, outsdRoNo });
|
socket.emit("rr-validation-required", { jobId: rid, dmsRoNo, outsdRoNo });
|
||||||
@@ -1412,11 +1494,16 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
message: vendorMessage
|
message: vendorMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
CreateRRLogEvent(socket, "ERROR", `Export failed (step 1)`, {
|
CreateRRLogEvent(
|
||||||
roStatus: result?.roStatus,
|
socket,
|
||||||
statusBlocks: result?.statusBlocks,
|
"ERROR",
|
||||||
classification: cls
|
`Export failed (step 1)`,
|
||||||
});
|
withRRRequestXml(result, {
|
||||||
|
roStatus: result?.roStatus,
|
||||||
|
statusBlocks: result?.statusBlocks,
|
||||||
|
classification: cls
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
socket,
|
socket,
|
||||||
@@ -1541,7 +1628,12 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (finalizeResult?.success) {
|
if (finalizeResult?.success) {
|
||||||
CreateRRLogEvent(socket, "INFO", `{7} Finalize success; marking exported`, { dmsRoNo, outsdRoNo });
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"INFO",
|
||||||
|
`{7} Finalize success; marking exported`,
|
||||||
|
withRRRequestXml(finalizeResult, { dmsRoNo, outsdRoNo })
|
||||||
|
);
|
||||||
|
|
||||||
// ✅ Mark exported + success log
|
// ✅ Mark exported + success log
|
||||||
await markRRExportSuccess({
|
await markRRExportSuccess({
|
||||||
@@ -1584,6 +1676,17 @@ const registerRREvents = ({ socket, redisHelpers }) => {
|
|||||||
message: vendorMessage
|
message: vendorMessage
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CreateRRLogEvent(
|
||||||
|
socket,
|
||||||
|
"ERROR",
|
||||||
|
"Finalize failed",
|
||||||
|
withRRRequestXml(finalizeResult, {
|
||||||
|
roStatus: finalizeResult?.roStatus,
|
||||||
|
statusBlocks: finalizeResult?.statusBlocks,
|
||||||
|
classification: cls
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
await insertRRFailedExportLog({
|
await insertRRFailedExportLog({
|
||||||
socket,
|
socket,
|
||||||
jobId: rid,
|
jobId: rid,
|
||||||
|
|||||||
Reference in New Issue
Block a user