Files
bodyshop/client/src/components/dms-customer-selector/rr-customer-selector.jsx

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