315 lines
11 KiB
JavaScript
315 lines
11 KiB
JavaScript
import { Alert, Button, Checkbox, message, Modal, Space } from "antd";
|
|
import ResponsiveTable from "../responsive-table/responsive-table.component";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { alphaSort } from "../../utils/sorters";
|
|
|
|
const normalizeRrList = (list) => {
|
|
if (!Array.isArray(list)) return [];
|
|
return list
|
|
.map((row) => {
|
|
const custNo = row.custNo || row.CustomerId || row.customerId || null;
|
|
const name =
|
|
row.name ||
|
|
[row.CustomerName?.FirstName, row.CustomerName?.LastName].filter(Boolean).join(" ").trim() ||
|
|
(custNo ? String(custNo) : "");
|
|
if (!custNo) return null;
|
|
const vinOwner = !!(row.vinOwner ?? row.isVehicleOwner);
|
|
|
|
const address =
|
|
row.address && typeof row.address === "object"
|
|
? {
|
|
line1: row.address.line1 ?? row.address.addr1 ?? row.address.Address1 ?? undefined,
|
|
line2: row.address.line2 ?? row.address.addr2 ?? row.address.Address2 ?? undefined,
|
|
city: row.address.city ?? undefined,
|
|
state: row.address.state ?? row.address.stateOrProvince ?? undefined,
|
|
postalCode: row.address.postalCode ?? row.address.zip ?? undefined,
|
|
country: row.address.country ?? row.address.countryCode ?? undefined
|
|
}
|
|
: undefined;
|
|
|
|
return { custNo: String(custNo), name, vinOwner, address };
|
|
})
|
|
.filter(Boolean);
|
|
};
|
|
|
|
const rrAddressToString = (addr) => {
|
|
if (!addr) return "";
|
|
const parts = [
|
|
addr.line1,
|
|
addr.line2,
|
|
[addr.city, addr.state].filter(Boolean).join(" "),
|
|
addr.postalCode,
|
|
addr.country
|
|
].filter(Boolean);
|
|
return parts.join(", ");
|
|
};
|
|
|
|
export default function RRCustomerSelector({
|
|
jobid,
|
|
socket,
|
|
job,
|
|
rrOpenRoLimit = false,
|
|
onRrOpenRoFinished,
|
|
rrValidationPending = false,
|
|
onValidationFinished
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [open, setOpen] = useState(false);
|
|
const [customerList, setCustomerList] = useState([]);
|
|
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
// Show dialog automatically when validation is pending
|
|
// BUT: skip this for early RO flow (job already has dms_id)
|
|
useEffect(() => {
|
|
if (rrValidationPending && !job?.dms_id) {
|
|
setOpen(true);
|
|
}
|
|
}, [rrValidationPending, job?.dms_id]);
|
|
|
|
// Listen for RR customer selection list
|
|
useEffect(() => {
|
|
if (!socket) return;
|
|
const handleRrSelectCustomer = (list) => {
|
|
const normalized = normalizeRrList(list);
|
|
|
|
// If list is empty, it means early RO exists and customer selection should be skipped
|
|
// Don't open the modal in this case
|
|
if (normalized.length === 0) {
|
|
setRefreshing(false);
|
|
return;
|
|
}
|
|
|
|
setOpen(true);
|
|
setCustomerList(normalized);
|
|
const firstOwner = normalized.find((r) => r.vinOwner)?.custNo;
|
|
setSelectedCustomer(firstOwner ? String(firstOwner) : null);
|
|
setRefreshing(false);
|
|
};
|
|
socket.on("rr-select-customer", handleRrSelectCustomer);
|
|
return () => {
|
|
socket.off("rr-select-customer", handleRrSelectCustomer);
|
|
};
|
|
}, [socket]);
|
|
|
|
// VIN owner set
|
|
const rrOwnerSet = useMemo(() => {
|
|
return new Set(customerList.filter((c) => c?.vinOwner || c?.isVehicleOwner).map((c) => String(c.custNo)));
|
|
}, [customerList]);
|
|
const rrHasVinOwner = rrOwnerSet.size > 0;
|
|
|
|
// Enforce VIN owner stays selected if present
|
|
useEffect(() => {
|
|
if (!rrHasVinOwner) return;
|
|
const firstOwner = (customerList.find((c) => c.vinOwner) || {}).custNo;
|
|
if (firstOwner && String(selectedCustomer) !== String(firstOwner)) {
|
|
setSelectedCustomer(String(firstOwner));
|
|
}
|
|
}, [rrHasVinOwner, customerList, selectedCustomer]);
|
|
|
|
const onUseSelected = () => {
|
|
if (!selectedCustomer) {
|
|
message.warning(t("general.actions.select"));
|
|
return;
|
|
}
|
|
if (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;
|
|
}
|
|
socket.emit("rr-selected-customer", { jobId: jobid, custNo: String(selectedCustomer) }, (ack) => {
|
|
if (ack?.ok) {
|
|
message.success(t("dms.messages.customerSelected"));
|
|
} else if (ack?.error) {
|
|
message.error(ack.error);
|
|
}
|
|
});
|
|
};
|
|
|
|
const onCreateNew = () => {
|
|
if (rrHasVinOwner) return;
|
|
socket.emit("rr-selected-customer", { jobId: jobid, create: true }, (ack) => {
|
|
if (ack?.ok) {
|
|
if (ack.custNo) setSelectedCustomer(String(ack.custNo));
|
|
message.success(t("dms.messages.customerCreated"));
|
|
} else if (ack?.error) {
|
|
message.error(ack.error);
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setOpen(false);
|
|
};
|
|
|
|
const refreshRrSearch = () => {
|
|
setRefreshing(true);
|
|
const to = setTimeout(() => setRefreshing(false), 12000);
|
|
const stop = () => {
|
|
clearTimeout(to);
|
|
setRefreshing(false);
|
|
socket.off("export-failed", stop);
|
|
socket.off("rr-select-customer", stop);
|
|
};
|
|
socket.once("rr-select-customer", stop);
|
|
socket.once("export-failed", stop);
|
|
socket.emit("rr-export-job", { jobId: jobid });
|
|
};
|
|
|
|
const columns = [
|
|
{ title: t("jobs.fields.dms.id"), dataIndex: "custNo", key: "custNo" },
|
|
{
|
|
title: t("jobs.fields.dms.vinowner"),
|
|
dataIndex: "vinOwner",
|
|
key: "vinOwner",
|
|
render: (_t, r) => <Checkbox disabled checked={!!(r.vinOwner ?? r.isVehicleOwner)} />
|
|
},
|
|
{
|
|
title: t("jobs.fields.dms.name1"),
|
|
dataIndex: "name",
|
|
key: "name",
|
|
sorter: (a, b) => alphaSort(a?.name, b?.name)
|
|
},
|
|
{
|
|
title: t("jobs.fields.dms.address"),
|
|
key: "address",
|
|
render: (record) => rrAddressToString(record.address)
|
|
}
|
|
];
|
|
|
|
const rrDisableRow = (record) => {
|
|
if (!rrHasVinOwner) return false;
|
|
return !rrOwnerSet.has(String(record.custNo));
|
|
};
|
|
|
|
// For early RO flow: show validation banner even when modal is closed
|
|
if (!open) {
|
|
if (rrValidationPending && job?.dms_id) {
|
|
return (
|
|
<div style={{ marginBottom: 16 }}>
|
|
<Alert
|
|
type="info"
|
|
showIcon
|
|
title="Complete Validation in Reynolds"
|
|
description={
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
<div>
|
|
We created the Repair Order. Please validate the totals and taxes in the DMS system. When done, click{" "}
|
|
<strong>Finished</strong> to finalize and mark this export as complete.
|
|
</div>
|
|
<div>
|
|
<Space>
|
|
<Button type="primary" onClick={onValidationFinished}>
|
|
Finished
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Modal open={open} onCancel={handleClose} footer={null} width={800} title={t("dms.selectCustomer")}>
|
|
<ResponsiveTable
|
|
title={() => (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
{/* Open RO limit banner */}
|
|
{rrOpenRoLimit && (
|
|
<Alert
|
|
type="error"
|
|
showIcon
|
|
title="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>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
{/* Validation step banner - only show for NON-early RO flow (legacy) */}
|
|
{rrValidationPending && !job?.dms_id && (
|
|
<Alert
|
|
type="info"
|
|
showIcon
|
|
title="Complete Validation in Reynolds"
|
|
description={
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
<div>
|
|
We created the Repair Order. Please validate the totals and taxes in the DMS system. When done,
|
|
click <strong>Finished</strong> to finalize and mark this export as complete.
|
|
</div>
|
|
<div>
|
|
<Space>
|
|
<Button type="primary" onClick={onValidationFinished}>
|
|
Finished
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
</div>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
<Button onClick={onUseSelected} disabled={!selectedCustomer || rrOpenRoLimit}>
|
|
{t("jobs.actions.dms.useselected")}
|
|
</Button>
|
|
{/* No generic in RR */}
|
|
<Button onClick={onCreateNew} disabled={rrHasVinOwner}>
|
|
{t("jobs.actions.dms.createnewcustomer")}
|
|
</Button>
|
|
</div>
|
|
|
|
{rrHasVinOwner && (
|
|
<Alert
|
|
type="warning"
|
|
showIcon
|
|
title="VIN ownership enforced"
|
|
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>
|
|
)}
|
|
pagination={{ placement: "top" }}
|
|
columns={columns}
|
|
mobileColumnKeys={["custNo", "vinOwner", "name", "address"]}
|
|
rowKey={(r) => r.custNo}
|
|
dataSource={customerList}
|
|
rowSelection={{
|
|
onSelect: (record) => setSelectedCustomer(record?.custNo ? String(record.custNo) : null),
|
|
type: "radio",
|
|
selectedRowKeys: selectedCustomer ? [selectedCustomer] : [],
|
|
getCheckboxProps: (record) => ({ disabled: rrDisableRow(record) })
|
|
}}
|
|
/>
|
|
</Modal>
|
|
);
|
|
}
|