import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd"; import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectDarkMode } from "../../redux/application/application.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CiecaSelect from "../../utils/Ciecaselect"; import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component"; import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import ConfidenceDisplay from "./bill-form.lines.confidence.component.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, isDarkMode: selectDarkMode }); const mapDispatchToProps = () => ({}); export function BillEnterModalLinesComponent({ bodyshop, isDarkMode, disabled, lineData, discount, form, responsibilityCenters, billEdit, isAiScan }) { const { t } = useTranslation(); const { setFieldsValue, getFieldsValue, getFieldValue } = form; const firstFieldRefs = useRef({}); const CONTROL_HEIGHT = 32; const normalizeDiscount = (d) => { const n = Number(d); if (!Number.isFinite(n) || n <= 0) return 0; return n > 1 ? n / 100 : n; }; const round2 = (v) => Math.round((v + Number.EPSILON) * 100) / 100; const isBlank = (v) => v === null || v === undefined || v === "" || Number.isNaN(v); const toNumber = (raw) => { if (raw === null || raw === undefined) return NaN; if (typeof raw === "number") return raw; if (typeof raw === "string") { const cleaned = raw .trim() .replace(/[^\d.,-]/g, "") .replace(/,/g, ""); return Number.parseFloat(cleaned); } if (typeof raw === "object") { try { if (typeof raw.toNumber === "function") return raw.toNumber(); const v = raw.valueOf?.(); if (typeof v === "number") return v; if (typeof v === "string") { const cleaned = v .trim() .replace(/[^\d.,-]/g, "") .replace(/,/g, ""); return Number.parseFloat(cleaned); } } catch { // ignore } } return NaN; }; const setLineField = (index, field, value) => { if (typeof form.setFieldValue === "function") { form.setFieldValue(["billlines", index, field], value); return; } const lines = form.getFieldValue("billlines") || []; form.setFieldsValue({ billlines: lines.map((l, i) => (i === index ? { ...l, [field]: value } : l)) }); }; // Only fill actual_cost when the user forward-tabs out of Retail (actual_price) const autofillActualCost = (index) => { Promise.resolve().then(() => { const retailRaw = form.getFieldValue(["billlines", index, "actual_price"]); const actualRaw = form.getFieldValue(["billlines", index, "actual_cost"]); const d = normalizeDiscount(discount); if (!isBlank(actualRaw)) return; const retail = toNumber(retailRaw); if (!Number.isFinite(retail)) return; const next = round2(retail * (1 - d)); setLineField(index, "actual_cost", next); }); }; const getIndicatorColor = (lineDiscount) => { const d = normalizeDiscount(discount); if (Math.abs(lineDiscount - d) > 0.005) return lineDiscount > d ? "orange" : "red"; return "green"; }; const getIndicatorShellStyles = (statusColor) => { if (isDarkMode) { if (statusColor === "green") return { borderColor: "rgba(82, 196, 26, 0.75)", background: "rgba(82, 196, 26, 0.10)" }; if (statusColor === "orange") return { borderColor: "rgba(250, 173, 20, 0.75)", background: "rgba(250, 173, 20, 0.10)" }; return { borderColor: "rgba(255, 77, 79, 0.75)", background: "rgba(255, 77, 79, 0.10)" }; } if (statusColor === "green") return { borderColor: "#b7eb8f", background: "#f6ffed" }; if (statusColor === "orange") return { borderColor: "#ffe58f", background: "#fffbe6" }; return { borderColor: "#ffccc7", background: "#fff2f0" }; }; const { treatments: { Simple_Inventory, Enhanced_Payroll } } = useTreatmentsWithConfig({ attributes: {}, names: ["Simple_Inventory", "Enhanced_Payroll"], splitKey: bodyshop && bodyshop.imexshopid }); const columns = (remove) => { return [ ...(isAiScan ? [ { title: t("billlines.fields.confidence"), dataIndex: "confidence", editable: true, width: "5rem", formItemProps: (field) => ({ key: `${field.index}confidence`, name: [field.name, "confidence"], label: t("billlines.fields.confidence") }), formInput: (record) => { const rowValue = getFieldValue(["billlines", record.name]); return (
); } } ] : []), { title: t("billlines.fields.jobline"), dataIndex: "joblineid", editable: true, minWidth: "10rem", formItemProps: (field) => ({ key: `${field.name}joblinename`, name: [field.name, "joblineid"], label: t("billlines.fields.jobline"), rules: [{ required: true }] }), wrapper: (props) => ( prev.is_credit_memo !== cur.is_credit_memo}> {() => props.children} ), formInput: (record, index) => ( { firstFieldRefs.current[index] = el; }} disabled={disabled} options={lineData} style={{ minWidth: "20rem", whiteSpace: "normal", height: "auto", minHeight: `${CONTROL_HEIGHT}px` }} allowRemoved={form.getFieldValue("is_credit_memo") || false} onSelect={(value, opt) => { // IMPORTANT: // Do NOT autofill actual_cost here. It should only fill when the user forward-tabs // from Retail (actual_price) -> Actual Cost (actual_cost). setFieldsValue({ billlines: (getFieldValue("billlines") || []).map((item, idx) => { if (idx !== index) return item; return { ...item, line_desc: opt.line_desc, quantity: opt.part_qty || 1, actual_price: opt.cost, original_actual_price: opt.cost, // actual_cost intentionally untouched here cost_center: opt.part_type ? bodyshopHasDmsKey(bodyshop) ? opt.part_type !== "PAE" ? opt.part_type : null : responsibilityCenters.defaults && (responsibilityCenters.defaults.costs[opt.part_type] || null) : null }; }) }); }} /> ) }, { title: t("billlines.fields.line_desc"), dataIndex: "line_desc", editable: true, minWidth: "10rem", formItemProps: (field) => ({ key: `${field.name}line_desc`, name: [field.name, "line_desc"], label: t("billlines.fields.line_desc"), rules: [{ required: true }] }), formInput: () => }, { title: t("billlines.fields.quantity"), dataIndex: "quantity", editable: true, width: "4rem", formItemProps: (field) => ({ key: `${field.name}quantity`, name: [field.name, "quantity"], label: t("billlines.fields.quantity"), rules: [ { required: true }, ({ getFieldValue: gf }) => ({ validator(_, value) { const invLen = gf(["billlines", field.name, "inventories"])?.length ?? 0; if (value && invLen > value) { return Promise.reject( t("bills.validation.inventoryquantity", { number: invLen }) ); } return Promise.resolve(); } }) ] }), formInput: () => }, { title: t("billlines.fields.actual_price"), dataIndex: "actual_price", width: "8rem", editable: true, formItemProps: (field) => ({ key: `${field.name}actual_price`, name: [field.name, "actual_price"], label: t("billlines.fields.actual_price"), rules: [ { required: true }, { validator: (_, value) => { return Math.abs(parseFloat(value)) < 0.01 ? Promise.reject() : Promise.resolve(); }, warningOnly: true } ], hasFeedback: true }), formInput: (record, index) => ( { if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index); }} /> ), additional: (record, index) => InstanceRenderManager({ rome: ( {() => { const billLine = getFieldValue(["billlines", record.name]); const jobLine = lineData.find((line) => line.id === billLine?.joblineid); if (!billEdit && billLine && jobLine && billLine?.actual_price !== jobLine?.act_price) { return ( {t("joblines.fields.create_ppc")} ); } return null; }} ) }) }, { title: t("billlines.fields.actual_cost"), dataIndex: "actual_cost", editable: true, width: "10rem", skipFormItem: true, formItemProps: (field) => ({ key: `${field.name}actual_cost`, name: [field.name, "actual_cost"], label: t("billlines.fields.actual_cost"), rules: [{ required: true }] }), formInput: (record, index, fieldProps) => { const { name, rules, valuePropName, getValueFromEvent, normalize, validateTrigger, initialValue } = fieldProps || {}; const bindProps = { name, rules, valuePropName, getValueFromEvent, normalize, validateTrigger, initialValue }; return (
autofillActualCost(index)} />
{() => { const all = getFieldsValue(["billlines"]); const line = all?.billlines?.[index]; if (!line) return null; const ap = toNumber(line.actual_price); const ac = toNumber(line.actual_cost); let lineDiscount = 0; if (Number.isFinite(ap) && ap !== 0 && Number.isFinite(ac)) { lineDiscount = 1 - ac / ap; } const statusColor = getIndicatorColor(lineDiscount); const shell = getIndicatorShellStyles(statusColor); return (
); }}
); } }, { title: t("billlines.fields.cost_center"), dataIndex: "cost_center", editable: true, formItemProps: (field) => ({ key: `${field.name}cost_center`, name: [field.name, "cost_center"], label: t("billlines.fields.cost_center"), valuePropName: "value", rules: [{ required: true }] }), formInput: () => ( ({ value: loc, label: loc }))} /> ) } ]), { title: t("billlines.labels.deductedfromlbr"), dataIndex: "deductedfromlbr", editable: true, width: "40px", formItemProps: (field) => ({ valuePropName: "checked", key: `${field.name}deductedfromlbr`, name: [field.name, "deductedfromlbr"] }), formInput: () => , additional: (record, index) => ( {() => { const price = getFieldValue(["billlines", record.name, "actual_price"]); const adjustmentRate = getFieldValue(["billlines", record.name, "lbr_adjustment", "rate"]); const billline = getFieldValue(["billlines", record.name]); const jobline = lineData.find((line) => line.id === billline?.joblineid); const employeeTeamName = bodyshop.employee_teams.find((team) => team.id === jobline?.assigned_team); if (getFieldValue(["billlines", record.name, "deductedfromlbr"])) return (
{Enhanced_Payroll.treatment === "on" ? ( {t("joblines.fields.assigned_team", { name: employeeTeamName?.name })} {`${jobline.mod_lb_hrs} units/${t(`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`)}`} ) : null}