diff --git a/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx b/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx
index 914c64341..c04f2a62a 100644
--- a/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx
+++ b/client/src/components/dms-allocations-summary/rr-dms-allocations-summary.component.jsx
@@ -51,17 +51,20 @@ function normalizeJobAllocations(ack) {
* is now done on the backend via buildRogogFromAllocations/buildRolaborFromRogog.
* This component just renders the preview from `ack.rogg` / `ack.rolabor`.
*/
-export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocationsChange }) {
+export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocationsChange, opCode }) {
const { t } = useTranslation();
const [roggPreview, setRoggPreview] = useState(null);
const [rolaborPreview, setRolaborPreview] = useState(null);
const [error, setError] = useState(null);
+ // Prefer the user-selected OpCode (from DmsContainer), fall back to config default
+ const effectiveOpCode = useMemo(() => opCode || resolveRROpCodeFromBodyshop(bodyshop), [opCode, bodyshop]);
+
const fetchAllocations = useCallback(() => {
if (!socket || !jobId) return;
try {
- socket.emit("rr-calculate-allocations", jobId, (ack) => {
+ socket.emit("rr-calculate-allocations", { jobId, opCode: effectiveOpCode }, (ack) => {
if (ack && ack.ok === false) {
setRoggPreview(null);
setRolaborPreview(null);
@@ -101,14 +104,12 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
onAllocationsChange([]);
}
}
- }, [socket, jobId, t, onAllocationsChange]);
+ }, [socket, jobId, t, onAllocationsChange, effectiveOpCode]);
useEffect(() => {
fetchAllocations();
}, [fetchAllocations]);
- const opCode = resolveRROpCodeFromBodyshop(bodyshop);
-
const segmentLabelMap = {
partsExtras: "Parts/Extras",
laborTaxable: "Taxable Labor",
@@ -117,8 +118,11 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
const roggRows = useMemo(() => {
if (!roggPreview || !Array.isArray(roggPreview.ops)) return [];
+
const rows = [];
roggPreview.ops.forEach((op) => {
+ const rowOpCode = opCode || op.opCode;
+
(op.lines || []).forEach((line, idx) => {
const baseDesc = line.itemDesc;
const segmentKind = op.segmentKind;
@@ -128,7 +132,7 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
rows.push({
key: `${op.jobNo}-${idx}`,
- opCode: op.opCode,
+ opCode: rowOpCode,
jobNo: op.jobNo,
breakOut: line.breakOut,
itemType: line.itemType,
@@ -145,22 +149,27 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
});
});
return rows;
- }, [roggPreview]);
+ }, [roggPreview, opCode]);
const rolaborRows = useMemo(() => {
if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return [];
- return rolaborPreview.ops.map((op, idx) => ({
- key: `${op.jobNo}-${idx}`,
- opCode: op.opCode,
- jobNo: op.jobNo,
- custPayTypeFlag: op.custPayTypeFlag,
- custTxblNtxblFlag: op.custTxblNtxblFlag,
- payType: op.bill?.payType,
- amtType: op.amount?.amtType,
- custPrice: op.amount?.custPrice,
- totalAmt: op.amount?.totalAmt
- }));
- }, [rolaborPreview]);
+
+ return rolaborPreview.ops.map((op, idx) => {
+ const rowOpCode = opCode || op.opCode;
+
+ return {
+ key: `${op.jobNo}-${idx}`,
+ opCode: rowOpCode,
+ jobNo: op.jobNo,
+ custPayTypeFlag: op.custPayTypeFlag,
+ custTxblNtxblFlag: op.custTxblNtxblFlag,
+ payType: op.bill?.payType,
+ amtType: op.amount?.amtType,
+ custPrice: op.amount?.custPrice,
+ totalAmt: op.amount?.totalAmt
+ };
+ });
+ }, [rolaborPreview, opCode]);
// Totals for ROGOG (sum custPrice + dlrCost over all lines)
const roggTotals = useMemo(() => {
@@ -221,9 +230,10 @@ export function RrAllocationsSummary({ socket, bodyshop, jobId, title, onAllocat
children: (
<>
- OpCode: {opCode}. Only centers with RR GOG mapping (rr_gogcode & rr_item_type) are
- included. Totals below reflect exactly what will be sent in ROGOG.
+ OpCode: {effectiveOpCode}. Only centers with RR GOG mapping (rr_gogcode & rr_item_type)
+ are included. Totals below reflect exactly what will be sent in ROGOG.
+
);
diff --git a/client/src/components/dms-post-form/rr-dms-post-form.jsx b/client/src/components/dms-post-form/rr-dms-post-form.jsx
index eb05eb26a..669bac7fd 100644
--- a/client/src/components/dms-post-form/rr-dms-post-form.jsx
+++ b/client/src/components/dms-post-form/rr-dms-post-form.jsx
@@ -27,10 +27,20 @@ import dayjs from "../../utils/day";
* @param job
* @param logsRef
* @param allocationsSummary
+ * @param opCodeParts
+ * @param onChangeOpCodeParts
* @returns {JSX.Element}
* @constructor
*/
-export default function RRPostForm({ bodyshop, socket, job, logsRef, allocationsSummary }) {
+export default function RRPostForm({
+ bodyshop,
+ socket,
+ job,
+ logsRef,
+ allocationsSummary,
+ opCodeParts,
+ onChangeOpCodeParts
+}) {
const [form] = Form.useForm();
const { t } = useTranslation();
@@ -98,19 +108,54 @@ export default function RRPostForm({ bodyshop, socket, job, logsRef, allocations
: job.v_model_yr)) ||
2019
}-01-01`
- )
+ ),
+ opPrefix: opCodeParts?.prefix ?? "",
+ opBase: opCodeParts?.base ?? "",
+ opSuffix: opCodeParts?.suffix ?? ""
}),
- [job, t]
+ [job, t, opCodeParts]
);
+ // Keep the RR OpCode parts in sync with DmsContainer state
+ const opPrefixWatch = Form.useWatch("opPrefix", form);
+ const opBaseWatch = Form.useWatch("opBase", form);
+ const opSuffixWatch = Form.useWatch("opSuffix", form);
+
+ useEffect(() => {
+ if (!onChangeOpCodeParts) return;
+
+ onChangeOpCodeParts({
+ prefix: opPrefixWatch || "",
+ base: opBaseWatch || "",
+ suffix: opSuffixWatch || ""
+ });
+ }, [opPrefixWatch, opBaseWatch, opSuffixWatch, onChangeOpCodeParts]);
+
const handleFinish = (values) => {
if (!socket) return;
+
+ const { opPrefix, opBase, opSuffix, ...rest } = values;
+
+ const combinedOpCode = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
+
+ const txEnvelope = {
+ ...rest,
+ opPrefix,
+ opBase,
+ opSuffix
+ };
+
+ if (combinedOpCode) {
+ txEnvelope.opCode = combinedOpCode;
+ }
+
socket.emit("rr-export-job", {
bodyshopId: bodyshop?.id,
jobId: job.id,
job,
- txEnvelope: values
+ txEnvelope
});
+
logsRef?.current?.scrollIntoView({ behavior: "smooth" });
};
@@ -177,10 +222,39 @@ export default function RRPostForm({ bodyshop, socket, job, logsRef, allocations
- {/* Make Override */}
+ {/* RR OpCode (prefix / base / suffix) */}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/pages/dms/dms.container.jsx b/client/src/pages/dms/dms.container.jsx
index 31b527832..abaace018 100644
--- a/client/src/pages/dms/dms.container.jsx
+++ b/client/src/pages/dms/dms.container.jsx
@@ -66,16 +66,6 @@ const DMS_SOCKET_EVENTS = {
};
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
- const { t } = useTranslation();
- const [resetAfterReconnect, setResetAfterReconnect] = useState(false);
- const [allocationsSummary, setAllocationsSummary] = useState(null);
-
- const history = useNavigate();
- const search = queryString.parse(useLocation().search);
- const { jobId } = search;
-
- const notification = useNotification();
-
const {
treatments: { Fortellis }
} = useSplitTreatments({
@@ -84,10 +74,46 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
splitKey: bodyshop.imexshopid
});
+ const { t } = useTranslation();
+ const [resetAfterReconnect, setResetAfterReconnect] = useState(false);
+ const [allocationsSummary, setAllocationsSummary] = useState(null);
+
// Compute a single normalized mode and pick the proper socket
const mode = getDmsMode(bodyshop, Fortellis.treatment); // "rr" | "fortellis" | "cdk" | "pbs" | "none"
+
+ // RR-only: derive default OpCode parts from bodyshop RR configuration
const isRrMode = mode === DMS_MAP.reynolds;
+ const deriveDefaultRrOpCodeParts = () => {
+ if (!isRrMode) return null;
+
+ const cfg = bodyshop?.rr_configuration || {};
+
+ // Adjust these paths to match your real schema.
+ const defaults =
+ cfg.opCodeDefault ||
+ cfg.op_code_default ||
+ cfg.op_codes?.default ||
+ cfg.defaults?.opCode ||
+ cfg.defaults ||
+ cfg.default ||
+ {};
+
+ const prefix = defaults.prefix ?? defaults.opCodePrefix ?? "";
+ const base = defaults.base ?? defaults.opCodeBase ?? "";
+ const suffix = defaults.suffix ?? defaults.opCodeSuffix ?? "";
+
+ return { prefix, base, suffix };
+ };
+
+ const [rrOpCodeParts, setRrOpCodeParts] = useState(() => deriveDefaultRrOpCodeParts());
+
+ const history = useNavigate();
+ const search = queryString.parse(useLocation().search);
+ const { jobId } = search;
+
+ const notification = useNotification();
+
const { socket: wsssocket } = useSocket();
const activeSocket = useMemo(() => (isWssMode(mode) ? wsssocket : legacySocket), [mode, wsssocket]);
@@ -96,6 +122,12 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
// One place to set log level
const [logLevel, setLogLevel] = useState(mode === DMS_MAP.pbs ? "INFO" : "DEBUG");
+ const rrOpCodeCombined = useMemo(() => {
+ if (!rrOpCodeParts || !rrOpCodeParts.base) return "";
+ const { prefix, base, suffix } = rrOpCodeParts;
+ return `${prefix || ""}${base}${suffix || ""}`;
+ }, [rrOpCodeParts]);
+
const setActiveLogLevel = (level) => {
if (!activeSocket) return;
activeSocket.emit("set-log-level", level);
@@ -155,6 +187,9 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
setrrValidationPending(false);
setAllocationsSummary(null);
+ // RR OpCode parts: reset to config defaults when job/mode changes
+ setRrOpCodeParts(deriveDefaultRrOpCodeParts());
+
if (!activeSocket) return;
const emitReset = () => {
@@ -431,6 +466,7 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
}
socket={activeSocket}
jobId={jobId}
+ opCode={rrOpCodeCombined}
/>
)}
@@ -443,6 +479,8 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
logsRef={logsRef}
mode={mode}
allocationsSummary={allocationsSummary}
+ rrOpCodeParts={rrOpCodeParts}
+ onChangeRrOpCodeParts={setRrOpCodeParts}
/>
diff --git a/server/rr/rr-job-export.js b/server/rr/rr-job-export.js
index 498f0334a..96c69e5c5 100644
--- a/server/rr/rr-job-export.js
+++ b/server/rr/rr-job-export.js
@@ -90,11 +90,15 @@ const exportJobToRR = async (args) => {
const story = txEnvelope?.story ? String(txEnvelope.story).trim() : null;
const makeOverride = txEnvelope?.makeOverride ? String(txEnvelope.makeOverride).trim() : null;
+ // Optional RR OpCode segments coming from the FE (RRPostForm)
+ const opPrefix = txEnvelope?.opPrefix ?? txEnvelope?.op_prefix ?? null;
+ const opBase = txEnvelope?.opBase ?? txEnvelope?.op_base ?? null;
+ const opSuffix = txEnvelope?.opSuffix ?? txEnvelope?.op_suffix ?? null;
+
// RR-only extras
let rrCentersConfig = null;
let allocations = null;
let opCode = null;
- // let taxCode = null;
// 1) Responsibility center config (for visibility / debugging)
try {
@@ -139,7 +143,15 @@ const exportJobToRR = async (args) => {
const resolvedBaseOpCode = resolveRROpCodeFromBodyshop(bodyshop);
- const opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
+ let opCodeOverride = txEnvelope?.opCode || txEnvelope?.opcode || txEnvelope?.op_code || null;
+
+ // If the FE only sends segments, combine them here.
+ if (!opCodeOverride && (opPrefix || opBase || opSuffix)) {
+ const combined = `${opPrefix || ""}${opBase || ""}${opSuffix || ""}`.trim();
+ if (combined) {
+ opCodeOverride = combined;
+ }
+ }
if (opCodeOverride || resolvedBaseOpCode) {
opCode = String(opCodeOverride || resolvedBaseOpCode).trim() || null;
@@ -147,7 +159,10 @@ const exportJobToRR = async (args) => {
CreateRRLogEvent(socket, "SILLY", "RR OP config resolved", {
opCode,
- baseFromConfig: resolvedBaseOpCode
+ baseFromConfig: resolvedBaseOpCode,
+ opPrefix,
+ opBase,
+ opSuffix
});
// Build RO payload for create.
diff --git a/server/rr/rr-register-socket-events.js b/server/rr/rr-register-socket-events.js
index 026577afe..138e96f13 100644
--- a/server/rr/rr-register-socket-events.js
+++ b/server/rr/rr-register-socket-events.js
@@ -896,9 +896,26 @@ const registerRREvents = ({ socket, redisHelpers }) => {
}
});
- socket.on("rr-calculate-allocations", async (jobid, cb) => {
+ // RR allocations preview (RR-only)
+ // Accepts either:
+ // - legacy: (jobid, cb)
+ // - new: ({ jobId, opCode, opPrefix, opBase, opSuffix }, cb)
+ socket.on("rr-calculate-allocations", async (payload, cb) => {
+ // Normalize arguments
+ const isObjectPayload = payload && typeof payload === "object";
+ const jobid = isObjectPayload ? payload.jobId || payload.jobid || payload.id : payload;
+
+ const opCodeFromClient =
+ isObjectPayload &&
+ (payload.opCode ||
+ payload.opcode ||
+ payload.op_code ||
+ (payload.opPrefix || payload.opBase || payload.opSuffix
+ ? `${payload.opPrefix || ""}${payload.opBase || ""}${payload.opSuffix || ""}`.trim()
+ : null));
+
try {
- CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: begin", { jobid });
+ CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: begin", { jobid, opCodeFromClient });
const raw = await RRCalculateAllocations(socket, jobid);
@@ -925,20 +942,24 @@ const registerRREvents = ({ socket, redisHelpers }) => {
jobAllocations = Array.isArray(ack.jobAllocations) ? ack.jobAllocations : [];
}
- // Try to derive OpCode from bodyshop.rr_configuration.defaults; fall back to default
- let opCode;
+ // Start with client-supplied OpCode (if any); fall back to defaults.
+ let opCode = opCodeFromClient || null;
try {
const { bodyshopId } = await getSessionOrSocket(redisHelpers, socket);
const bodyshop = await getBodyshopForSocket({ bodyshopId, socket });
+
+ // resolveRROpCodeFromBodyshop(bodyshop, existingOverride?)
opCode = resolveRROpCodeFromBodyshop(bodyshop, opCode);
CreateRRLogEvent(socket, "DEBUG", "rr-calculate-allocations: resolved OpCode", {
- opCode
+ opCode,
+ opCodeFromClient
});
} catch (e) {
- CreateRRLogEvent(socket, "WARN", "rr-calculate-allocations: bodyshop lookup failed, using default OpCode", {
- error: e.message
+ CreateRRLogEvent(socket, "WARN", "rr-calculate-allocations: bodyshop lookup failed, using existing OpCode", {
+ error: e.message,
+ opCodeFromClient
});
}