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 4ae308f57..7e731d559 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 @@ -29,7 +29,6 @@ function normalizeRrList(list) { if (!custNo) return null; const vinOwner = !!(row.vinOwner ?? row.isVehicleOwner); - // Pass through address from backend if present; tolerate various shapes const address = row.address && typeof row.address === "object" ? { @@ -47,7 +46,6 @@ function normalizeRrList(list) { .filter(Boolean); } -// Small formatter used by the RR Address column render function rrAddressToString(addr) { if (!addr) return ""; const parts = [ @@ -89,16 +87,12 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { const rrHasVinOwner = rrOwnerSet.size > 0; useEffect(() => { - // RR takes precedence if (dms === "rr") { const handleRrSelectCustomer = (list) => { const normalized = normalizeRrList(list); - setOpen(true); setDmsType("rr"); setcustomerList(normalized); - - // PRESELECT VIN OWNER (first one if multiple) const firstOwner = normalized.find((r) => r.vinOwner)?.custNo; setSelectedCustomer(firstOwner ? String(firstOwner) : null); }; @@ -142,7 +136,6 @@ 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; @@ -157,7 +150,6 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { 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." @@ -187,15 +179,12 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { }; const onUseGeneric = () => { - if (dmsType === "rr" && rrHasVinOwner) return; + if (dmsType === "rr" && rrHasVinOwner) return; // not rendered in RR, but keep guard const generic = bodyshop.cdk_configuration?.generic_customer_number || null; + if (dmsType === "rr") { - if (generic) { - wsssocket.emit("rr-selected-customer", { jobId: jobid, custNo: String(generic) }, (ack) => { - if (!ack?.ok && ack?.error) message.error(ack.error); - }); - } - setOpen(false); + // Not rendered in RR anymore + return; } else if (Fortellis.treatment === "on") { setOpen(false); wsssocket.emit("fortellis-selected-customer", { selectedCustomerId: generic, jobid }); @@ -357,12 +346,14 @@ export function DmsCustomerSelector({ bodyshop, jobid }) { - + + {/* Hide "Use Generic" entirely in RR mode */} + {dmsType !== "rr" && ( + + )} + diff --git a/client/src/components/dms-post-form/dms-post-form.component.jsx b/client/src/components/dms-post-form/dms-post-form.component.jsx index 6370dccf6..f10071aa4 100644 --- a/client/src/components/dms-post-form/dms-post-form.component.jsx +++ b/client/src/components/dms-post-form/dms-post-form.component.jsx @@ -1,17 +1,19 @@ -// DmsPostForm updated import { DeleteFilled, DownOutlined, ReloadOutlined } from "@ant-design/icons"; import { Button, Card, + Col, Divider, Dropdown, Form, Input, InputNumber, + Row, Select, Space, Statistic, Switch, + Tooltip, Typography } from "antd"; import Dinero from "dinero.js"; @@ -26,7 +28,6 @@ import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component"; import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; -import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { useSocket } from "../../contexts/SocketIO/useSocket"; import { useSplitTreatments } from "@splitsoftware/splitio-react"; import { useEffect, useMemo, useState } from "react"; @@ -53,13 +54,13 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) { // Figure out DMS once and reuse const dms = useMemo(() => determineDmsType(bodyshop), [bodyshop]); - // RR advisors state + // ---------------- RR Advisors (unchanged behavior) ---------------- const [advisors, setAdvisors] = useState([]); const [advLoading, setAdvLoading] = useState(false); // Normalize advisor fields coming from various shapes const getAdvisorNumber = (a) => a?.advisorId; - const getAdvisorLabel = (a) => `${a?.firstName} ${a?.lastName}`?.trim(); + const getAdvisorLabel = (a) => `${a?.firstName || ""} ${a?.lastName || ""}`.trim(); const fetchRrAdvisors = (refresh = false) => { if (!wsssocket) return; @@ -84,6 +85,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) { const list = ack.result ?? []; setAdvisors(Array.isArray(list) ? list : []); } else if (ack) { + // Preserve original logging semantics console.error("Something went wrong fetching DMS Advisors"); } setAdvLoading(false); @@ -95,13 +97,13 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) { if (dms === "rr") fetchRrAdvisors(false); }, [dms, bodyshop?.id]); + // ---------------- Payers helpers (non-RR) ---------------- const handlePayerSelect = (value, index) => { form.setFieldsValue({ - payers: form.getFieldValue("payers").map((payer, mapIndex) => { + payers: (form.getFieldValue("payers") || []).map((payer, mapIndex) => { if (index !== mapIndex) return payer; const cdkPayer = bodyshop.cdk_configuration.payers && bodyshop.cdk_configuration.payers.find((i) => i.name === value); - if (!cdkPayer) return payer; return { @@ -113,38 +115,30 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) { }); }; + // ---------------- Submit (RR precedence preserved) ---------------- const handleFinish = (values) => { - // 1) RR takes precedence regardless of Fortellis split + // RR takes precedence regardless of Fortellis split if (dms === "rr") { - // values will now include advisorNo from the RR dropdown + // values will include advisorNo (and makeOverride if provided) wsssocket.emit("rr-export-job", { bodyshopId: bodyshop?.id, jobId: job.id, job, txEnvelope: values }); + } else if (Fortellis.treatment === "on") { + // Fallback to existing Fortellis behavior + wsssocket.emit("fortellis-export-job", { + jobid: job.id, + txEnvelope: { ...values, SubscriptionID: bodyshop.cdk_dealerid } + }); } else { - // 2) Fallback to existing behavior - if (Fortellis.treatment === "on") { - wsssocket.emit("fortellis-export-job", { - jobid: job.id, - txEnvelope: { - ...values, - SubscriptionID: bodyshop.cdk_dealerid - } - }); - } else { - // cdk/pbs/etc. - socket.emit(`${dms}-export-job`, { - jobid: job.id, - txEnvelope: values - }); - } + // CDK/PBS/etc. + socket.emit(`${dms}-export-job`, { jobid: job.id, txEnvelope: values }); } - if (logsRef?.current) { - logsRef.current.scrollIntoView({ behavior: "smooth" }); - } + // Scroll logs into view (original behavior) + logsRef?.current?.scrollIntoView({ behavior: "smooth" }); }; return ( @@ -153,6 +147,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) { form={form} layout="vertical" onFinish={handleFinish} + style={{ width: "100%" }} initialValues={{ story: `${t("jobs.labels.dms.defaultstory", { ro_number: job.ro_number, @@ -160,7 +155,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) { ins_co_nm: job.ins_co_nm || "N/A", clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${job.po_number || ""}` }).trim()}.${ - job.area_of_damage && job.area_of_damage.impact1 + job.area_of_damage?.impact1 ? " " + t("jobs.labels.dms.damageto", { area_of_damage: (job.area_of_damage && job.area_of_damage.impact1.padStart(2, "0")) || "UNKNOWN" @@ -168,94 +163,151 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) { : "" }`.slice(0, 239), inservicedate: dayjs( - `${(job.v_model_yr && (job.v_model_yr < 100 ? (job.v_model_yr >= (dayjs().year() + 1) % 100 ? 1900 + parseInt(job.v_model_yr) : 2000 + parseInt(job.v_model_yr)) : job.v_model_yr)) || 2019}-01-01` + `${ + (job.v_model_yr && + (job.v_model_yr < 100 + ? job.v_model_yr >= (dayjs().year() + 1) % 100 + ? 1900 + parseInt(job.v_model_yr, 10) + : 2000 + parseInt(job.v_model_yr, 10) + : job.v_model_yr)) || + 2019 + }-01-01` ) }} > - + {/* TOP ROW — bottom-aligned so the Refresh button sits flush */} + {dms !== "rr" && ( - - - + + + + + )} - {/* RR Advisor Number dropdown */} - {dms === "rr" && ( - - { + const value = getAdvisorNumber(a); + if (value == null) return null; + return { value: String(value), label: getAdvisorLabel(a) || String(value) }; + }) + .filter(Boolean)} + notFoundContent={advLoading ? t("general.loading") : t("general.none")} + /> + + - ))} - - - - - )} - + + )} + + )} + {/* Validation gates & summary (unchanged logic) */} {() => { // 1) Sum allocated payers @@ -393,31 +471,31 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) { }); // 2) Subtotal from socket.allocationsSummary (existing behavior) - const totals = socket - ? socket.allocationsSummary && - socket.allocationsSummary.reduce( - (acc, val) => ({ - totalSale: acc.totalSale.add(Dinero(val.sale)), - totalCost: acc.totalCost.add(Dinero(val.cost)) - }), - { totalSale: Dinero(), totalCost: Dinero() } - ) - : { totalSale: Dinero(), totalCost: Dinero() }; + const totals = + socket && socket.allocationsSummary + ? socket.allocationsSummary.reduce( + (acc, val) => ({ + totalSale: acc.totalSale.add(Dinero(val.sale)), + totalCost: acc.totalCost.add(Dinero(val.cost)) + }), + { totalSale: Dinero(), totalCost: Dinero() } + ) + : { totalSale: Dinero(), totalCost: Dinero() }; const discrep = totals ? totals.totalSale.subtract(totalAllocated) : Dinero(); - // 3) New: validation gates + // 3) Validation gates const advisorOk = dms !== "rr" || !!form.getFieldValue("advisorNo"); - // Require at least one complete payer row + // Require at least one complete payer row for non-RR const payersOk = dms === "rr" || (payers.length > 0 && payers.every((p) => p?.name && p.dms_acctnumber && (p.amount ?? "") !== "" && p.controlnumber)); // 4) Disable rules: - // - For non-RR: keep the original discrepancy rule (must have summary and zero discrepancy) - // - For RR: ignore discrepancy rule, but require advisor + payer rows + // - For non-RR: must have summary and zero discrepancy + // - For RR: ignore discrepancy rule, but require advisor const nonRrDiscrepancyGate = dms !== "rr" && (socket.allocationsSummary ? discrep.getAmount() !== 0 : true); const disablePost = !advisorOk || !payersOk || nonRrDiscrepancyGate; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 193aba22a..336ec5126 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -1212,6 +1212,7 @@ }, "general": { "actions": { + "optional": "Optional", "add": "Add", "autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.", "calculate": "Calculate", @@ -1777,6 +1778,8 @@ "id": "DMS ID", "inservicedate": "In Service Date", "journal": "Journal #", + "make_override": "Make Override", + "advisor": "Advisor #", "lines": "Posting Lines", "name1": "Customer Name", "payer": { @@ -1784,7 +1787,8 @@ "control_type": "Control Type", "controlnumber": "Control Number", "dms_acctnumber": "DMS Account #", - "name": "Payer Name" + "name": "Payer Name", + "payer_type": "Payer" }, "sale": "Sale", "sale_dms_acctnumber": "Sale DMS Acct #",