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:
@@ -58,12 +58,13 @@ function rrAddressToString(addr) {
|
|||||||
return parts.join(", ");
|
return parts.join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DmsCustomerSelector({ bodyshop, jobid }) {
|
export function DmsCustomerSelector({ bodyshop, jobid, rrOpenRoLimit = false, onRrOpenRoFinished }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [customerList, setcustomerList] = useState([]);
|
const [customerList, setcustomerList] = useState([]);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||||
const [dmsType, setDmsType] = useState("cdk");
|
const [dmsType, setDmsType] = useState("cdk");
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
treatments: { Fortellis }
|
treatments: { Fortellis }
|
||||||
@@ -95,6 +96,7 @@ export function DmsCustomerSelector({ bodyshop, jobid }) {
|
|||||||
setcustomerList(normalized);
|
setcustomerList(normalized);
|
||||||
const firstOwner = normalized.find((r) => r.vinOwner)?.custNo;
|
const firstOwner = normalized.find((r) => r.vinOwner)?.custNo;
|
||||||
setSelectedCustomer(firstOwner ? String(firstOwner) : null);
|
setSelectedCustomer(firstOwner ? String(firstOwner) : null);
|
||||||
|
setRefreshing(false); // stop any in-flight refresh spinner
|
||||||
};
|
};
|
||||||
|
|
||||||
wsssocket.on("rr-select-customer", handleRrSelectCustomer);
|
wsssocket.on("rr-select-customer", handleRrSelectCustomer);
|
||||||
@@ -183,7 +185,6 @@ export function DmsCustomerSelector({ bodyshop, jobid }) {
|
|||||||
const generic = bodyshop.cdk_configuration?.generic_customer_number || null;
|
const generic = bodyshop.cdk_configuration?.generic_customer_number || null;
|
||||||
|
|
||||||
if (dmsType === "rr") {
|
if (dmsType === "rr") {
|
||||||
// Not rendered in RR anymore
|
|
||||||
return;
|
return;
|
||||||
} else if (Fortellis.treatment === "on") {
|
} else if (Fortellis.treatment === "on") {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -220,7 +221,30 @@ export function DmsCustomerSelector({ bodyshop, jobid }) {
|
|||||||
setSelectedCustomer(null);
|
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 = [
|
const fortellisColumns = [
|
||||||
{ title: t("jobs.fields.dms.id"), dataIndex: "customerId", key: "id" },
|
{ title: t("jobs.fields.dms.id"), dataIndex: "customerId", key: "id" },
|
||||||
{
|
{
|
||||||
@@ -342,8 +366,30 @@ export function DmsCustomerSelector({ bodyshop, jobid }) {
|
|||||||
<Table
|
<Table
|
||||||
title={() => (
|
title={() => (
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
<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" }}>
|
<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")}
|
{t("jobs.actions.dms.useselected")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -359,12 +405,24 @@ export function DmsCustomerSelector({ bodyshop, jobid }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* NEW: VIN ownership enforced with Refresh */}
|
||||||
{dmsType === "rr" && rrHasVinOwner && (
|
{dmsType === "rr" && rrHasVinOwner && (
|
||||||
<Alert
|
<Alert
|
||||||
type="warning"
|
type="warning"
|
||||||
showIcon
|
showIcon
|
||||||
message="VIN ownership enforced"
|
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>
|
</div>
|
||||||
|
|||||||
@@ -79,29 +79,41 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
});
|
});
|
||||||
const logsRef = useRef(null);
|
const logsRef = useRef(null);
|
||||||
|
|
||||||
|
// NEW: RR “open RO limit” UX hold
|
||||||
|
const [rrOpenRoLimit, setRrOpenRoLimit] = useState(false);
|
||||||
|
const clearRrOpenRoLimit = () => setRrOpenRoLimit(false);
|
||||||
|
|
||||||
const handleExportFailed = (payload = {}) => {
|
const handleExportFailed = (payload = {}) => {
|
||||||
const { title, friendlyMessage, error, severity, errorCode, vendorStatusCode } = payload;
|
const { title, friendlyMessage, error, severity, errorCode, vendorStatusCode } = payload;
|
||||||
|
|
||||||
// Prefer server-provided nice text; otherwise generic fallback
|
|
||||||
const msg =
|
const msg =
|
||||||
friendlyMessage ||
|
friendlyMessage ||
|
||||||
error ||
|
error ||
|
||||||
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.");
|
||||||
|
|
||||||
// Title defaults by DMS
|
|
||||||
const vendorTitle = title || (dms === "rr" ? "Reynolds" : "DMS");
|
const vendorTitle = title || (dms === "rr" ? "Reynolds" : "DMS");
|
||||||
|
|
||||||
// Severity: warn for known soft-stops like 507, else error
|
// Detect the specific RR “max open ROs” case
|
||||||
const sev =
|
const isRrOpenRoLimit =
|
||||||
severity || (vendorStatusCode === 507 || (errorCode || "").includes("MAX_OPEN_ROS") ? "warning" : "error");
|
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]({
|
// Show toast for *other* failures; for the open RO limit, switch to blocking banner UX instead.
|
||||||
message: vendorTitle,
|
if (!isRrOpenRoLimit) {
|
||||||
description: msg,
|
const notifyKind = sev === "warning" && typeof notification.warning === "function" ? "warning" : "error";
|
||||||
duration: 10
|
notification[notifyKind]({
|
||||||
});
|
message: vendorTitle,
|
||||||
|
description: msg,
|
||||||
|
duration: 10
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setRrOpenRoLimit(true);
|
||||||
|
}
|
||||||
|
|
||||||
// Mirror to the on-screen log card
|
// Mirror to the on-screen log card
|
||||||
setLogs((prev) => [
|
setLogs((prev) => [
|
||||||
@@ -110,7 +122,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
level: (sev || "error").toUpperCase(),
|
level: (sev || "error").toUpperCase(),
|
||||||
message: `${vendorTitle}: ${msg}`,
|
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 handleLogEvent = (payload) => setLogs((prev) => [...prev, payload]);
|
||||||
|
|
||||||
const handleExportSuccess = (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({
|
insertAuditTrail({
|
||||||
jobid: jobId,
|
jobid: jobId,
|
||||||
operation: AuditTrailMapping.jobexported(),
|
operation: AuditTrailMapping.jobexported(),
|
||||||
@@ -283,11 +296,10 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col md={24} lg={14}>
|
<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>
|
</Col>
|
||||||
|
|
||||||
<DmsCustomerSelector jobid={jobId} />
|
<DmsCustomerSelector jobid={jobId} rrOpenRoLimit={rrOpenRoLimit} onRrOpenRoFinished={clearRrOpenRoLimit} />
|
||||||
|
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<div ref={logsRef}>
|
<div ref={logsRef}>
|
||||||
<Card
|
<Card
|
||||||
@@ -316,9 +328,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLogs([]);
|
setLogs([]);
|
||||||
// Reconnect appropriate socket
|
|
||||||
if (dms === "rr" || Fortellis.treatment === "on") {
|
if (dms === "rr" || Fortellis.treatment === "on") {
|
||||||
// wsssocket is managed by provider; emit a ping
|
|
||||||
wsssocket.emit("set-log-level", logLevel);
|
wsssocket.emit("set-log-level", logLevel);
|
||||||
} else {
|
} else {
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
|
|||||||
Reference in New Issue
Block a user