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 6a6903880..dcfbad6d4 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 @@ -1,5 +1,5 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react"; -import { Button, Checkbox, Col, message, Table } from "antd"; +import { Alert, Button, Checkbox, Col, message, Table } from "antd"; import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -26,7 +26,9 @@ function normalizeRrList(list) { row.name || [row.CustomerName?.FirstName, row.CustomerName?.LastName].filter(Boolean).join(" ").trim() || (custNo ? String(custNo) : ""); - return custNo ? { custNo: String(custNo), name } : null; + if (!custNo) return null; + const vinOwner = !!(row.vinOwner ?? row.isVehicleOwner); + return { custNo: String(custNo), name, vinOwner }; }) .filter(Boolean); } @@ -49,14 +51,29 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { const { socket: wsssocket } = useSocket(); const dms = useMemo(() => determineDmsType(bodyshop), [bodyshop]); + // --- owner set (RR only) --- + const rrOwnerSet = useMemo(() => { + return new Set( + (Array.isArray(customerList) ? customerList : []) + .filter((c) => c?.vinOwner || c?.isVehicleOwner) + .map((c) => String(c.custNo)) + ); + }, [customerList]); + const rrHasVinOwner = rrOwnerSet.size > 0; + useEffect(() => { // RR takes precedence if (dms === "rr") { const handleRrSelectCustomer = (list) => { + const normalized = normalizeRrList(list); + setOpen(true); setDmsType("rr"); - setcustomerList(normalizeRrList(list)); - setSelectedCustomer(null); + setcustomerList(normalized); + + // PRESELECT VIN OWNER (first one if multiple) + const firstOwner = normalized.find((r) => r.vinOwner)?.custNo; + setSelectedCustomer(firstOwner ? String(firstOwner) : null); }; wsssocket.on("rr-select-customer", handleRrSelectCustomer); @@ -98,14 +115,30 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { } }, [dms, Fortellis?.treatment, wsssocket]); + // Safety: if owner info arrives later or list changes, keep the owner preselected. + useEffect(() => { + if (dmsType !== "rr" || !rrHasVinOwner) return; + const firstOwner = (customerList.find((c) => c.vinOwner) || {}).custNo; + if (firstOwner && String(selectedCustomer) !== String(firstOwner)) { + setSelectedCustomer(String(firstOwner)); + } + }, [dmsType, rrHasVinOwner, customerList]); + const onUseSelected = () => { if (!selectedCustomer) { message.warning(t("general.actions.select")); return; } + // If there is a VIN owner, only allow owner selection + if (dmsType === "rr" && rrHasVinOwner && !rrOwnerSet.has(String(selectedCustomer))) { + message.warning( + "This VIN is already assigned in Reynolds. Only the VIN owner can be selected. To choose a different customer, change ownership in Reynolds first." + ); + return; + } + if (dmsType === "rr") { - // RR now mirrors others: send selection and close wsssocket.emit("rr-selected-customer", { jobId: jobid, custNo: String(selectedCustomer) }, (ack) => { if (ack?.ok) { setOpen(false); @@ -127,6 +160,7 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { }; const onUseGeneric = () => { + if (dmsType === "rr" && rrHasVinOwner) return; const generic = bodyshop.cdk_configuration?.generic_customer_number || null; if (dmsType === "rr") { if (generic) { @@ -146,11 +180,11 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { }; const onCreateNew = () => { - // Exact parity with Fortellis: ask server to create immediately + if (dmsType === "rr" && rrHasVinOwner) return; + if (dmsType === "rr") { wsssocket.emit("rr-selected-customer", { jobId: jobid, create: true }, (ack) => { if (ack?.ok) { - // Optionally preselect returned custNo if (ack.custNo) setSelectedCustomer(String(ack.custNo)); setOpen(false); message.success(t("dms.messages.customerCreated")); @@ -161,7 +195,6 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { return; } - // Non-RR unchanged setOpen(false); if (Fortellis.treatment === "on") { wsssocket.emit("fortellis-selected-customer", { selectedCustomerId: null, jobid }); @@ -245,7 +278,18 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { const rrColumns = [ { title: t("jobs.fields.dms.id"), dataIndex: "custNo", key: "custNo" }, - { title: t("jobs.fields.dms.name1"), dataIndex: "name", key: "name", sorter: (a, b) => alphaSort(a?.name, b?.name) } + { + title: t("jobs.fields.dms.name1"), + dataIndex: "name", + key: "name", + sorter: (a, b) => alphaSort(a?.name, b?.name) + }, + { + title: t("jobs.fields.dms.vinowner"), + dataIndex: "vinOwner", + key: "vinOwner", + render: (_t, r) => + } ]; if (!open) return null; @@ -266,18 +310,40 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { ? (record) => record.id?.value || record.customerId : (record) => record.ContactId; + const rrDisableRow = (record) => { + if (dmsType !== "rr") return false; + if (!rrHasVinOwner) return false; + return !rrOwnerSet.has(String(record.custNo)); + }; + return ( ( -
- - - +
+
+ + + +
+ + {dmsType === "rr" && rrHasVinOwner && ( + + )}
)} pagination={{ position: "top" }} @@ -295,7 +361,10 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { setSelectedCustomer(key ? String(key) : null); }, type: "radio", - selectedRowKeys: selectedCustomer ? [selectedCustomer] : [] + selectedRowKeys: selectedCustomer ? [selectedCustomer] : [], + getCheckboxProps: (record) => ({ + disabled: rrDisableRow(record) + }) }} /> diff --git a/server/rr/rr-utils.js b/server/rr/rr-utils.js index 6da1caf15..0569b20f7 100644 --- a/server/rr/rr-utils.js +++ b/server/rr/rr-utils.js @@ -46,6 +46,7 @@ const makeVehicleSearchPayloadFromJob = (job) => { /** * Normalize customer candidates from VIN blocks + * Adds `vinOwner` (and keeps `isVehicleOwner` for backward compat). * @param res * @param ownersSet * @returns {any[]} @@ -66,19 +67,27 @@ const normalizeCustomerCandidates = (res, { ownersSet = null } = {}) => { for (const custNo of custNos) { const cno = String(custNo).trim(); - const item = { custNo: cno, name: name || `Customer ${cno}` }; - if (ownersSet && ownersSet.has(cno)) item.isVehicleOwner = true; + const isOwner = !!(ownersSet && ownersSet.has(cno)); + const item = { + custNo: cno, + name: name || `Customer ${cno}`, + vinOwner: isOwner, + isVehicleOwner: isOwner // legacy key kept for any older FE code + }; out.push(item); } } - // Dedup by custNo, keep isVehicleOwner if any + // Dedup by custNo, keep vinOwner/isVehicleOwner if any const seen = new Map(); for (const c of out) { const key = (c.custNo || "").trim(); if (!key) continue; const prev = seen.get(key); - if (!prev) seen.set(key, c); - else if (c.isVehicleOwner && !prev.isVehicleOwner) seen.set(key, { ...prev, isVehicleOwner: true }); + if (!prev) { + seen.set(key, c); + } else if ((c.vinOwner || c.isVehicleOwner) && !(prev.vinOwner || prev.isVehicleOwner)) { + seen.set(key, { ...prev, vinOwner: true, isVehicleOwner: true }); + } } return Array.from(seen.values()); }; diff --git a/server/web-sockets/rr-register-socket-events.js b/server/web-sockets/rr-register-socket-events.js index 81a38543f..a43d97426 100644 --- a/server/web-sockets/rr-register-socket-events.js +++ b/server/web-sockets/rr-register-socket-events.js @@ -119,9 +119,8 @@ async function rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }) { try { CreateRRLogEvent(socket, "DEBUG", `{RR-SEARCH} Executing ${q.kind} query`, { q }); const res = await rrCombinedSearch(bodyshop, q); - if (fromVin) { - const blocks = Array.isArray(res?.data) ? res.data : []; + const blocks = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : []; ownersSet = ownersFromVinBlocks(blocks, job?.v_vin); try { await redisHelpers.setSessionTransactionData( @@ -132,7 +131,7 @@ async function rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }) { defaultRRTTL ); } catch { - // + /* ignore cache write issues */ } } @@ -159,17 +158,19 @@ function registerRREvents({ socket, redisHelpers }) { let ownersSet = null; if ((params?.kind || "").toLowerCase() === "vin") { - const blocks = Array.isArray(res?.data) ? res.data : []; + const blocks = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : []; ownersSet = ownersFromVinBlocks(blocks); } const normalized = sortVehicleOwnerFirst(normalizeCustomerCandidates(res, { ownersSet })); const rid = resolveJobId(jobid, { jobid }, null); - cb?.({ jobid: rid, data: normalized }); - socket.emit("rr-select-customer", normalized); + const decorated = normalized.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner })); + + cb?.({ jobid: rid, data: decorated }); + socket.emit("rr-select-customer", decorated); CreateRRLogEvent(socket, "DEBUG", "rr-lookup-combined: emitted rr-select-customer", { - count: normalized.length + count: decorated.length }); } catch (e) { CreateRRLogEvent(socket, "ERROR", "RR combined lookup error", { error: e.message, jobid }); @@ -318,10 +319,11 @@ function registerRREvents({ socket, redisHelpers }) { CreateRRLogEvent(socket, "DEBUG", `{2} Running multi-search (Full Name + VIN)`); const candidates = await rrMultiCustomerSearch({ bodyshop, job, socket, redisHelpers }); - socket.emit("rr-select-customer", candidates); + const decorated = candidates.map((c) => (c.vinOwner != null ? c : { ...c, vinOwner: !!c.isVehicleOwner })); + socket.emit("rr-select-customer", decorated); CreateRRLogEvent(socket, "DEBUG", `{2.1} Emitted rr-select-customer`, { - count: candidates.length, - anyOwner: candidates.some((c) => c.isVehicleOwner) + count: decorated.length, + anyOwner: decorated.some((c) => c.vinOwner || c.isVehicleOwner) }); } catch (error) { CreateRRLogEvent(socket, "ERROR", `Error during RR export (prepare)`, { @@ -376,7 +378,7 @@ function registerRREvents({ socket, redisHelpers }) { const vehQ = makeVehicleSearchPayloadFromJob(job); if (vehQ && vehQ.kind === "vin" && job?.v_vin) { const resVin = await rrCombinedSearch(bodyshop, vehQ); - const blocksVin = Array.isArray(resVin?.data) ? resVin.data : []; + const blocksVin = Array.isArray(resVin?.data) ? resVin.data : Array.isArray(resVin) ? resVin : []; try { await redisHelpers.setSessionTransactionData( socket.id,