feature/IO-3357-Reynolds-and-Reynolds-DMS-API-Integration - Refresh button inside VIN Enforced, add TOo Many OPen RO Handlers

This commit is contained in:
Dave
2025-11-12 12:56:34 -05:00
parent cbfda822c6
commit e3b4620d0c
2 changed files with 91 additions and 23 deletions

View File

@@ -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 }) {
<Table
title={() => (
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{/* Open RO limit banner (from parent flag) */}
{dmsType === "rr" && rrOpenRoLimit && (
<Alert
type="error"
showIcon
message="Open RO limit reached in Reynolds"
description={
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<div>
Reynolds has reached the maximum number of open Repair Orders for this Customer. Close or finalize
an RO in Reynolds, then click <strong>Finished</strong> to continue.
</div>
<div>
<Button type="primary" danger onClick={onRrOpenRoFinished}>
Finished
</Button>
</div>
</div>
}
/>
)}
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
<Button onClick={onUseSelected} disabled={!selectedCustomer || (dmsType === "rr" && rrOpenRoLimit)}>
{t("jobs.actions.dms.useselected")}
</Button>
@@ -359,12 +405,24 @@ export function DmsCustomerSelector({ bodyshop, jobid }) {
</Button>
</div>
{/* NEW: VIN ownership enforced with Refresh */}
{dmsType === "rr" && rrHasVinOwner && (
<Alert
type="warning"
showIcon
message="VIN ownership enforced"
description="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."
description={
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12 }}>
<div>
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.
</div>
<Button onClick={refreshRrSearch} loading={refreshing}>
Refresh
</Button>
</div>
}
/>
)}
</div>

View File

@@ -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
/>
</Col>
<Col md={24} lg={14}>
<DmsPostForm socket={activeSocket} jobId={jobId} job={data?.jobs_by_pk} logsRef={logsRef} />
<DmsPostForm socket={activeSocket} job={data?.jobs_by_pk} logsRef={logsRef} />
</Col>
<DmsCustomerSelector jobid={jobId} />
<DmsCustomerSelector jobid={jobId} rrOpenRoLimit={rrOpenRoLimit} onRrOpenRoFinished={clearRrOpenRoLimit} />
<Col span={24}>
<div ref={logsRef}>
<Card
@@ -316,9 +328,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
<Button
onClick={() => {
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();