From e3b4620d0ca5ed80f3698409719280102a2f1a9d Mon Sep 17 00:00:00 2001 From: Dave Date: Wed, 12 Nov 2025 12:56:34 -0500 Subject: [PATCH] feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Refresh button inside VIN Enforced, add TOo Many OPen RO Handlers --- .../dms-customer-selector.component.jsx | 68 +++++++++++++++++-- client/src/pages/dms/dms.container.jsx | 46 ++++++++----- 2 files changed, 91 insertions(+), 23 deletions(-) diff --git a/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx b/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx index 7e731d559..225332756 100644 --- a/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx +++ b/client/src/components/dms-customer-selector/dms-customer-selector.component.jsx @@ -58,12 +58,13 @@ function rrAddressToString(addr) { return parts.join(", "); } -export function DmsCustomerSelector({ bodyshop, jobid }) { +export function DmsCustomerSelector({ bodyshop, jobid, rrOpenRoLimit = false, onRrOpenRoFinished }) { const { t } = useTranslation(); const [customerList, setcustomerList] = useState([]); const [open, setOpen] = useState(false); const [selectedCustomer, setSelectedCustomer] = useState(null); const [dmsType, setDmsType] = useState("cdk"); + const [refreshing, setRefreshing] = useState(false); const { treatments: { Fortellis } @@ -95,6 +96,7 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { setcustomerList(normalized); const firstOwner = normalized.find((r) => r.vinOwner)?.custNo; setSelectedCustomer(firstOwner ? String(firstOwner) : null); + setRefreshing(false); // stop any in-flight refresh spinner }; wsssocket.on("rr-select-customer", handleRrSelectCustomer); @@ -183,7 +185,6 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { const generic = bodyshop.cdk_configuration?.generic_customer_number || null; if (dmsType === "rr") { - // Not rendered in RR anymore return; } else if (Fortellis.treatment === "on") { setOpen(false); @@ -220,7 +221,30 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { setSelectedCustomer(null); }; - // ---------- Columns ---------- + // NEW: trigger a re-run of the RR combined search + const refreshRrSearch = () => { + if (dmsType !== "rr") return; + setRefreshing(true); + + // Safety timeout so the spinner can't hang forever + const to = setTimeout(() => { + setRefreshing(false); + }, 12000); + + // Stop spinner on either outcome + const stop = () => { + clearTimeout(to); + setRefreshing(false); + wsssocket.off("export-failed", stop); + wsssocket.off("rr-select-customer", stop); + }; + wsssocket.once("rr-select-customer", stop); + wsssocket.once("export-failed", stop); + + // This re-runs the name+VIN multi-search and emits rr-select-customer + wsssocket.emit("rr-export-job", { jobId: jobid }); + }; + const fortellisColumns = [ { title: t("jobs.fields.dms.id"), dataIndex: "customerId", key: "id" }, { @@ -342,8 +366,30 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { (
+ {/* Open RO limit banner (from parent flag) */} + {dmsType === "rr" && rrOpenRoLimit && ( + +
+ Reynolds has reached the maximum number of open Repair Orders for this Customer. Close or finalize + an RO in Reynolds, then click Finished to continue. +
+
+ +
+
+ } + /> + )} +
- @@ -359,12 +405,24 @@ export function DmsCustomerSelector({ bodyshop, jobid }) {
+ {/* NEW: VIN ownership enforced with Refresh */} {dmsType === "rr" && rrHasVinOwner && ( +
+ This VIN is already assigned in Reynolds. Only the VIN owner is selectable here. To use a + different customer, please change the vehicle ownership in Reynolds first, then return to complete + the export. +
+ + + } /> )} diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx index 00d3fa586..03f159068 100644 --- a/client/src/pages/dms/dms.container.jsx +++ b/client/src/pages/dms/dms.container.jsx @@ -79,29 +79,41 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse }); const logsRef = useRef(null); + // NEW: RR “open RO limit” UX hold + const [rrOpenRoLimit, setRrOpenRoLimit] = useState(false); + const clearRrOpenRoLimit = () => setRrOpenRoLimit(false); + const handleExportFailed = (payload = {}) => { const { title, friendlyMessage, error, severity, errorCode, vendorStatusCode } = payload; - // Prefer server-provided nice text; otherwise generic fallback const msg = friendlyMessage || error || t("dms.errors.exportfailedgeneric", "We couldn't complete the export. Please try again."); - // Title defaults by DMS const vendorTitle = title || (dms === "rr" ? "Reynolds" : "DMS"); - // Severity: warn for known soft-stops like 507, else error - const sev = - severity || (vendorStatusCode === 507 || (errorCode || "").includes("MAX_OPEN_ROS") ? "warning" : "error"); + // Detect the specific RR “max open ROs” case + const isRrOpenRoLimit = + dms === "rr" && + (vendorStatusCode === 507 || + /MAX_OPEN_ROS/i.test(String(errorCode || "")) || + /maximum number of open repair orders/i.test(String(msg || "").toLowerCase())); - const notifyKind = sev === "warning" && typeof notification.warning === "function" ? "warning" : "error"; + // Soft/warn default for known cases + const sev = severity || (isRrOpenRoLimit ? "warning" : "error"); - notification[notifyKind]({ - message: vendorTitle, - description: msg, - duration: 10 - }); + // Show toast for *other* failures; for the open RO limit, switch to blocking banner UX instead. + if (!isRrOpenRoLimit) { + const notifyKind = sev === "warning" && typeof notification.warning === "function" ? "warning" : "error"; + notification[notifyKind]({ + message: vendorTitle, + description: msg, + duration: 10 + }); + } else { + setRrOpenRoLimit(true); + } // Mirror to the on-screen log card setLogs((prev) => [ @@ -110,7 +122,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse timestamp: new Date(), level: (sev || "error").toUpperCase(), message: `${vendorTitle}: ${msg}`, - meta: { errorCode, vendorStatusCode, raw: payload } + meta: { errorCode, vendorStatusCode, raw: payload, blockedByOpenRoLimit: !!isRrOpenRoLimit } } ]); }; @@ -154,7 +166,8 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse const handleLogEvent = (payload) => setLogs((prev) => [...prev, payload]); const handleExportSuccess = (payload) => { - const jobId = payload?.jobId ?? payload; // RR sends object; legacy sends raw id notification.success({ message: t("jobs.successes.exported") }); + const jobId = payload?.jobId ?? payload; // RR sends object; legacy sends raw id + notification.success({ message: t("jobs.successes.exported") }); insertAuditTrail({ jobid: jobId, operation: AuditTrailMapping.jobexported(), @@ -283,11 +296,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse />
- + - - +
{ setLogs([]); - // Reconnect appropriate socket if (dms === "rr" || Fortellis.treatment === "on") { - // wsssocket is managed by provider; emit a ping wsssocket.emit("set-log-level", logLevel); } else { socket.disconnect();