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 }); }