768 lines
26 KiB
JavaScript
768 lines
26 KiB
JavaScript
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 (
|
|
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
<ConfidenceDisplay rowValue={rowValue} />
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
]
|
|
: []),
|
|
{
|
|
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) => (
|
|
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.is_credit_memo !== cur.is_credit_memo}>
|
|
{() => props.children}
|
|
</Form.Item>
|
|
),
|
|
formInput: (record, index) => (
|
|
<BillLineSearchSelect
|
|
ref={(el) => {
|
|
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: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
|
|
},
|
|
|
|
{
|
|
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: () => <InputNumber precision={0} min={1} disabled={disabled} tabIndex={0} />
|
|
},
|
|
{
|
|
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) => (
|
|
<CurrencyInput
|
|
min={0}
|
|
disabled={disabled}
|
|
tabIndex={0}
|
|
// NOTE: Autofill should only happen on forward Tab out of Retail
|
|
onKeyDown={(e) => {
|
|
if (e.key === "Tab" && !e.shiftKey) autofillActualCost(index);
|
|
}}
|
|
/>
|
|
),
|
|
additional: (record, index) =>
|
|
InstanceRenderManager({
|
|
rome: (
|
|
<Form.Item dependencies={["billlines", record.name, "actual_price"]} noStyle>
|
|
{() => {
|
|
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 (
|
|
<Space size="small">
|
|
<Form.Item
|
|
noStyle
|
|
label={t("joblines.fields.create_ppc")}
|
|
key={`${index}ppc`}
|
|
valuePropName="checked"
|
|
name={[record.name, "create_ppc"]}
|
|
>
|
|
<Checkbox />
|
|
</Form.Item>
|
|
{t("joblines.fields.create_ppc")}
|
|
</Space>
|
|
);
|
|
}
|
|
return null;
|
|
}}
|
|
</Form.Item>
|
|
)
|
|
})
|
|
},
|
|
{
|
|
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 (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
width: "100%",
|
|
alignItems: "center",
|
|
height: CONTROL_HEIGHT
|
|
}}
|
|
>
|
|
<div style={{ flex: "1 1 auto", minWidth: 0 }}>
|
|
<Form.Item noStyle {...bindProps}>
|
|
<CurrencyInput
|
|
min={0}
|
|
disabled={disabled}
|
|
controls={false}
|
|
tabIndex={0}
|
|
style={{ width: "100%", height: CONTROL_HEIGHT }}
|
|
onFocus={() => autofillActualCost(index)}
|
|
/>
|
|
</Form.Item>
|
|
</div>
|
|
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
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 (
|
|
<Tooltip title={`${(lineDiscount * 100).toFixed(2) || 0}%`}>
|
|
<div
|
|
style={{
|
|
height: CONTROL_HEIGHT,
|
|
minWidth: CONTROL_HEIGHT,
|
|
padding: "0 10px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
boxSizing: "border-box",
|
|
borderStyle: "solid",
|
|
borderWidth: 1,
|
|
borderLeftWidth: 0,
|
|
...shell,
|
|
borderTopRightRadius: 6,
|
|
borderBottomRightRadius: 6
|
|
}}
|
|
>
|
|
<DollarCircleFilled style={{ color: statusColor, lineHeight: 1 }} />
|
|
</div>
|
|
</Tooltip>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
</div>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
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: () => (
|
|
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled} tabIndex={0}>
|
|
{bodyshopHasDmsKey(bodyshop)
|
|
? CiecaSelect(true, false)
|
|
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
|
|
</Select>
|
|
)
|
|
},
|
|
...(billEdit
|
|
? []
|
|
: [
|
|
{
|
|
title: t("billlines.fields.location"),
|
|
dataIndex: "location",
|
|
editable: true,
|
|
label: t("billlines.fields.location"),
|
|
formItemProps: (field) => ({
|
|
key: `${field.name}location`,
|
|
name: [field.name, "location"]
|
|
}),
|
|
formInput: () => (
|
|
<Select disabled={disabled} tabIndex={0}>
|
|
{bodyshop.md_parts_locations.map((loc, idx) => (
|
|
<Select.Option key={idx} value={loc}>
|
|
{loc}
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
)
|
|
}
|
|
]),
|
|
{
|
|
title: t("billlines.labels.deductedfromlbr"),
|
|
dataIndex: "deductedfromlbr",
|
|
editable: true,
|
|
width: "40px",
|
|
formItemProps: (field) => ({
|
|
valuePropName: "checked",
|
|
key: `${field.name}deductedfromlbr`,
|
|
name: [field.name, "deductedfromlbr"]
|
|
}),
|
|
formInput: () => <Switch disabled={disabled} tabIndex={0} />,
|
|
additional: (record, index) => (
|
|
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
|
|
{() => {
|
|
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 (
|
|
<div>
|
|
{Enhanced_Payroll.treatment === "on" ? (
|
|
<Space>
|
|
{t("joblines.fields.assigned_team", { name: employeeTeamName?.name })}
|
|
{`${jobline.mod_lb_hrs} units/${t(`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`)}`}
|
|
</Space>
|
|
) : null}
|
|
|
|
<Form.Item
|
|
label={t("joblines.fields.mod_lbr_ty")}
|
|
key={`${index}modlbrty`}
|
|
initialValue={jobline ? jobline.mod_lbr_ty : null}
|
|
rules={[{ required: true }]}
|
|
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
|
|
>
|
|
<Select allowClear>
|
|
<Select.Option value="LAA">{t("joblines.fields.lbr_types.LAA")}</Select.Option>
|
|
<Select.Option value="LAB">{t("joblines.fields.lbr_types.LAB")}</Select.Option>
|
|
<Select.Option value="LAD">{t("joblines.fields.lbr_types.LAD")}</Select.Option>
|
|
<Select.Option value="LAE">{t("joblines.fields.lbr_types.LAE")}</Select.Option>
|
|
<Select.Option value="LAF">{t("joblines.fields.lbr_types.LAF")}</Select.Option>
|
|
<Select.Option value="LAG">{t("joblines.fields.lbr_types.LAG")}</Select.Option>
|
|
<Select.Option value="LAM">{t("joblines.fields.lbr_types.LAM")}</Select.Option>
|
|
<Select.Option value="LAR">{t("joblines.fields.lbr_types.LAR")}</Select.Option>
|
|
<Select.Option value="LAS">{t("joblines.fields.lbr_types.LAS")}</Select.Option>
|
|
<Select.Option value="LAU">{t("joblines.fields.lbr_types.LAU")}</Select.Option>
|
|
<Select.Option value="LA1">{t("joblines.fields.lbr_types.LA1")}</Select.Option>
|
|
<Select.Option value="LA2">{t("joblines.fields.lbr_types.LA2")}</Select.Option>
|
|
<Select.Option value="LA3">{t("joblines.fields.lbr_types.LA3")}</Select.Option>
|
|
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
|
|
{Enhanced_Payroll.treatment === "on" ? (
|
|
<Form.Item
|
|
label={t("billlines.labels.mod_lbr_adjustment")}
|
|
name={[record.name, "lbr_adjustment", "mod_lb_hrs"]}
|
|
rules={[{ required: true }]}
|
|
>
|
|
<InputNumber precision={5} min={0.01} max={jobline ? jobline.mod_lb_hrs : 0} />
|
|
</Form.Item>
|
|
) : (
|
|
<Form.Item
|
|
label={t("jobs.labels.adjustmentrate")}
|
|
name={[record.name, "lbr_adjustment", "rate"]}
|
|
initialValue={bodyshop.default_adjustment_rate}
|
|
rules={[{ required: true }]}
|
|
>
|
|
<InputNumber precision={2} min={0.01} />
|
|
</Form.Item>
|
|
)}
|
|
|
|
<Space>{price && adjustmentRate && `${(price / adjustmentRate).toFixed(1)} hrs`}</Space>
|
|
</div>
|
|
);
|
|
|
|
return <></>;
|
|
}}
|
|
</Form.Item>
|
|
)
|
|
},
|
|
|
|
...InstanceRenderManager({
|
|
rome: [],
|
|
imex: [
|
|
{
|
|
title: t("billlines.fields.federal_tax_applicable"),
|
|
dataIndex: "applicable_taxes.federal",
|
|
editable: true,
|
|
width: "40px",
|
|
formItemProps: (field) => ({
|
|
key: `${field.name}fedtax`,
|
|
valuePropName: "checked",
|
|
name: [field.name, "applicable_taxes", "federal"],
|
|
initialValue: InstanceRenderManager({
|
|
imex: true,
|
|
rome: false
|
|
})
|
|
}),
|
|
formInput: () => <Switch disabled={disabled} tabIndex={0} />
|
|
}
|
|
]
|
|
}),
|
|
|
|
{
|
|
title: t("billlines.fields.state_tax_applicable"),
|
|
dataIndex: "applicable_taxes.state",
|
|
editable: true,
|
|
width: "40px",
|
|
formItemProps: (field) => ({
|
|
key: `${field.name}statetax`,
|
|
valuePropName: "checked",
|
|
name: [field.name, "applicable_taxes", "state"]
|
|
}),
|
|
formInput: () => <Switch disabled={disabled} tabIndex={0} />
|
|
},
|
|
|
|
...InstanceRenderManager({
|
|
rome: [],
|
|
imex: [
|
|
{
|
|
title: t("billlines.fields.local_tax_applicable"),
|
|
dataIndex: "applicable_taxes.local",
|
|
editable: true,
|
|
width: "40px",
|
|
formItemProps: (field) => ({
|
|
key: `${field.name}localtax`,
|
|
valuePropName: "checked",
|
|
name: [field.name, "applicable_taxes", "local"]
|
|
}),
|
|
formInput: () => <Switch disabled={disabled} tabIndex={0} />
|
|
}
|
|
]
|
|
}),
|
|
|
|
{
|
|
title: t("general.labels.actions"),
|
|
dataIndex: "actions",
|
|
render: (text, record) => (
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => {
|
|
const currentLine = getFieldValue(["billlines", record.name]);
|
|
const invLen = currentLine?.inventories?.length ?? 0;
|
|
|
|
return (
|
|
<Space wrap>
|
|
<Button
|
|
icon={<DeleteFilled />}
|
|
disabled={disabled || invLen > 0}
|
|
onClick={() => remove(record.name)}
|
|
tabIndex={0}
|
|
/>
|
|
|
|
{Simple_Inventory.treatment === "on" && (
|
|
<BilllineAddInventory
|
|
disabled={!billEdit || form.isFieldsTouched() || form.getFieldValue("is_credit_memo")}
|
|
billline={currentLine}
|
|
jobid={getFieldValue("jobid")}
|
|
/>
|
|
)}
|
|
</Space>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
)
|
|
}
|
|
];
|
|
};
|
|
|
|
const mergedColumns = (remove) =>
|
|
columns(remove).map((col) => {
|
|
if (!col.editable) return col;
|
|
|
|
return {
|
|
...col,
|
|
onCell: (record) => ({
|
|
record,
|
|
formItemProps: col.formItemProps,
|
|
formInput: col.formInput,
|
|
additional: col.additional,
|
|
wrapper: col.wrapper,
|
|
skipFormItem: col.skipFormItem
|
|
})
|
|
};
|
|
});
|
|
|
|
return (
|
|
<Form.List
|
|
name="billlines"
|
|
rules={[
|
|
{
|
|
validator: async (_, billlines) => {
|
|
if (!billlines || billlines.length < 1) {
|
|
return Promise.reject(new Error(t("billlines.validation.atleastone")));
|
|
}
|
|
}
|
|
}
|
|
]}
|
|
>
|
|
{(fields, { add, remove }) => {
|
|
const hasRows = fields.length > 0;
|
|
|
|
return (
|
|
<>
|
|
<Table
|
|
className="bill-lines-table"
|
|
components={{ body: { cell: EditableCell } }}
|
|
size="small"
|
|
bordered
|
|
dataSource={fields}
|
|
rowKey="key"
|
|
columns={mergedColumns(remove)}
|
|
scroll={hasRows ? { x: "max-content" } : undefined}
|
|
pagination={false}
|
|
rowClassName="editable-row"
|
|
/>
|
|
|
|
<div style={{ marginTop: 12 }}>
|
|
<Form.Item style={{ marginBottom: 0 }}>
|
|
<Button
|
|
disabled={disabled}
|
|
onClick={() => {
|
|
const newIndex = fields.length;
|
|
add(
|
|
InstanceRenderManager({
|
|
imex: { applicable_taxes: { federal: true } },
|
|
rome: { applicable_taxes: { federal: false } }
|
|
})
|
|
);
|
|
setTimeout(() => {
|
|
const firstField = firstFieldRefs.current[newIndex];
|
|
if (firstField?.focus) {
|
|
firstField.focus();
|
|
}
|
|
}, 100);
|
|
}}
|
|
style={{ width: "100%" }}
|
|
>
|
|
{t("billlines.actions.newline")}
|
|
</Button>
|
|
</Form.Item>
|
|
</div>
|
|
</>
|
|
);
|
|
}}
|
|
</Form.List>
|
|
);
|
|
}
|
|
|
|
export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalLinesComponent);
|
|
|
|
const EditableCell = ({
|
|
record,
|
|
children,
|
|
formInput,
|
|
formItemProps,
|
|
additional,
|
|
wrapper: Wrapper,
|
|
skipFormItem,
|
|
...restProps
|
|
}) => {
|
|
const rawProps = formItemProps?.(record);
|
|
|
|
const propsFinal = rawProps
|
|
? (() => {
|
|
// eslint-disable-next-line no-unused-vars
|
|
const { key, ...rest } = rawProps;
|
|
return rest;
|
|
})()
|
|
: undefined;
|
|
|
|
const control = skipFormItem ? (
|
|
(formInput && formInput(record, record.name, propsFinal)) || children
|
|
) : (
|
|
<Form.Item labelCol={{ span: 0 }} {...propsFinal} style={{ marginBottom: 0 }}>
|
|
{(formInput && formInput(record, record.name, propsFinal)) || children}
|
|
</Form.Item>
|
|
);
|
|
|
|
const cellInner = additional ? (
|
|
<div>
|
|
{control}
|
|
{additional(record, record.name)}
|
|
</div>
|
|
) : (
|
|
control
|
|
);
|
|
|
|
const { style: tdStyle, ...tdRest } = restProps;
|
|
|
|
const td = (
|
|
<td {...tdRest} style={{ ...tdStyle, verticalAlign: "middle" }}>
|
|
{cellInner}
|
|
</td>
|
|
);
|
|
|
|
if (Wrapper) return <Wrapper>{td}</Wrapper>;
|
|
return td;
|
|
};
|