feature/IO-3499-React-19 checkpoint

This commit is contained in:
Dave
2026-01-20 15:01:38 -05:00
parent bc78bbd5fa
commit a91bfea581

View File

@@ -14,13 +14,11 @@ import CurrencyInput from "../form-items-formatted/currency-form-item.component"
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js"; import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
isDarkMode: selectDarkMode isDarkMode: selectDarkMode
}); });
const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) const mapDispatchToProps = () => ({});
});
export function BillEnterModalLinesComponent({ export function BillEnterModalLinesComponent({
bodyshop, bodyshop,
@@ -35,6 +33,102 @@ export function BillEnterModalLinesComponent({
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
// Keep input row heights consistent with the rest of the table controls.
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; // supports 15 or 0.15
};
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;
};
// safe per-field setter (supports AntD 6+ setFieldValue, falls back to setFieldsValue)
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))
});
};
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) => {
// bring back the “colored shell” feel around the $ indicator while keeping row height stable
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 { const {
treatments: { Simple_Inventory, Enhanced_Payroll } treatments: { Simple_Inventory, Enhanced_Payroll }
} = useTreatmentsWithConfig({ } = useTreatmentsWithConfig({
@@ -50,24 +144,15 @@ export function BillEnterModalLinesComponent({
dataIndex: "joblineid", dataIndex: "joblineid",
editable: true, editable: true,
minWidth: "10rem", minWidth: "10rem",
formItemProps: (field) => { formItemProps: (field) => ({
return { key: `${field.index}joblinename`,
key: `${field.index}joblinename`, name: [field.name, "joblineid"],
name: [field.name, "joblineid"], label: t("billlines.fields.jobline"),
label: t("billlines.fields.jobline"), rules: [{ required: true }]
rules: [ }),
{
required: true
//message: t("general.validation.required"),
}
]
};
},
wrapper: (props) => ( wrapper: (props) => (
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.is_credit_memo !== cur.is_credit_memo}> <Form.Item noStyle shouldUpdate={(prev, cur) => prev.is_credit_memo !== cur.is_credit_memo}>
{() => { {() => props.children}
return props.children;
}}
</Form.Item> </Form.Item>
), ),
formInput: (record, index) => ( formInput: (record, index) => (
@@ -75,35 +160,37 @@ export function BillEnterModalLinesComponent({
disabled={disabled} disabled={disabled}
options={lineData} options={lineData}
style={{ style={{
//width: "10rem",
// maxWidth: "20rem",
minWidth: "20rem", minWidth: "20rem",
whiteSpace: "normal", whiteSpace: "normal",
height: "auto", height: "auto",
minHeight: "32px" // default height of Ant Design inputs minHeight: `${CONTROL_HEIGHT}px`
}} }}
allowRemoved={form.getFieldValue("is_credit_memo") || false} allowRemoved={form.getFieldValue("is_credit_memo") || false}
onSelect={(value, opt) => { onSelect={(value, opt) => {
const d = normalizeDiscount(discount);
const retail = Number(opt.cost);
const computedActual = Number.isFinite(retail) ? round2(retail * (1 - d)) : null;
setFieldsValue({ setFieldsValue({
billlines: getFieldsValue(["billlines"]).billlines.map((item, idx) => { billlines: (getFieldValue("billlines") || []).map((item, idx) => {
if (idx === index) { if (idx !== index) return item;
return {
...item, return {
line_desc: opt.line_desc, ...item,
quantity: opt.part_qty || 1, line_desc: opt.line_desc,
actual_price: opt.cost, quantity: opt.part_qty || 1,
original_actual_price: opt.cost, actual_price: opt.cost,
cost_center: opt.part_type original_actual_price: opt.cost,
? bodyshopHasDmsKey(bodyshop) actual_cost: isBlank(item.actual_cost) ? computedActual : item.actual_cost,
? opt.part_type !== "PAE" cost_center: opt.part_type
? opt.part_type ? bodyshopHasDmsKey(bodyshop)
: null ? opt.part_type !== "PAE"
: responsibilityCenters.defaults && ? opt.part_type
(responsibilityCenters.defaults.costs[opt.part_type] || null) : null
: null : responsibilityCenters.defaults &&
}; (responsibilityCenters.defaults.costs[opt.part_type] || null)
} : null
return item; };
}) })
}); });
}} }}
@@ -115,19 +202,12 @@ export function BillEnterModalLinesComponent({
dataIndex: "line_desc", dataIndex: "line_desc",
editable: true, editable: true,
minWidth: "10rem", minWidth: "10rem",
formItemProps: (field) => { formItemProps: (field) => ({
return { key: `${field.index}line_desc`,
key: `${field.index}line_desc`, name: [field.name, "line_desc"],
name: [field.name, "line_desc"], label: t("billlines.fields.line_desc"),
label: t("billlines.fields.line_desc"), rules: [{ required: true }]
rules: [ }),
{
required: true
//message: t("general.validation.required"),
}
]
};
},
formInput: () => <Input.TextArea disabled={disabled} autoSize /> formInput: () => <Input.TextArea disabled={disabled} autoSize />
}, },
{ {
@@ -135,31 +215,26 @@ export function BillEnterModalLinesComponent({
dataIndex: "quantity", dataIndex: "quantity",
editable: true, editable: true,
width: "4rem", width: "4rem",
formItemProps: (field) => { formItemProps: (field) => ({
return { key: `${field.index}quantity`,
key: `${field.index}quantity`, name: [field.name, "quantity"],
name: [field.name, "quantity"], label: t("billlines.fields.quantity"),
label: t("billlines.fields.quantity"), rules: [
rules: [ { required: true },
{ ({ getFieldValue: gf }) => ({
required: true validator(rule, value) {
//message: t("general.validation.required"), if (value && gf("billlines")[field.fieldKey]?.inventories?.length > value) {
}, return Promise.reject(
({ getFieldValue }) => ({ t("bills.validation.inventoryquantity", {
validator(rule, value) { number: gf("billlines")[field.fieldKey]?.inventories?.length
if (value && getFieldValue("billlines")[field.fieldKey]?.inventories?.length > value) { })
return Promise.reject( );
t("bills.validation.inventoryquantity", {
number: getFieldValue("billlines")[field.fieldKey]?.inventories?.length
})
);
}
return Promise.resolve();
} }
}) return Promise.resolve();
] }
}; })
}, ]
}),
formInput: () => <InputNumber precision={0} min={1} disabled={disabled} /> formInput: () => <InputNumber precision={0} min={1} disabled={disabled} />
}, },
{ {
@@ -167,37 +242,19 @@ export function BillEnterModalLinesComponent({
dataIndex: "actual_price", dataIndex: "actual_price",
width: "8rem", width: "8rem",
editable: true, editable: true,
formItemProps: (field) => { formItemProps: (field) => ({
return { key: `${field.index}actual_price`,
key: `${field.index}actual_price`, name: [field.name, "actual_price"],
name: [field.name, "actual_price"], label: t("billlines.fields.actual_price"),
label: t("billlines.fields.actual_price"), rules: [{ required: true }]
rules: [ }),
{
required: true
//message: t("general.validation.required"),
}
]
};
},
formInput: (record, index) => ( formInput: (record, index) => (
<CurrencyInput <CurrencyInput
min={0} min={0}
disabled={disabled} disabled={disabled}
onBlur={(e) => { onBlur={() => autofillActualCost(index)}
setFieldsValue({ onKeyDown={(e) => {
billlines: getFieldsValue("billlines").billlines.map((item, idx) => { if (e.key === "Tab") autofillActualCost(index);
if (idx === index) {
return {
...item,
actual_cost: item.actual_cost
? item.actual_cost
: Math.round((parseFloat(e.target.value) * (1 - discount) + Number.EPSILON) * 100) / 100
};
}
return item;
})
});
}} }}
/> />
), ),
@@ -224,9 +281,8 @@ export function BillEnterModalLinesComponent({
{t("joblines.fields.create_ppc")} {t("joblines.fields.create_ppc")}
</Space> </Space>
); );
} else {
return null;
} }
return null;
}} }}
</Form.Item> </Form.Item>
) )
@@ -237,100 +293,105 @@ export function BillEnterModalLinesComponent({
dataIndex: "actual_cost", dataIndex: "actual_cost",
editable: true, editable: true,
width: "10rem", width: "10rem",
skipFormItem: true,
formItemProps: (field) => ({
key: `${field.index}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 || {};
formItemProps: (field) => { const bindProps = {
return { name,
key: `${field.index}actual_cost`, rules,
name: [field.name, "actual_cost"], valuePropName,
label: t("billlines.fields.actual_cost"), getValueFromEvent,
rules: [ normalize,
{ validateTrigger,
required: true initialValue
//message: t("general.validation.required"),
}
]
}; };
},
formInput: (record, index) => (
<Space.Compact style={{ width: "100%" }}>
<CurrencyInput min={0} disabled={disabled} controls={false} style={{ width: "100%" }} />
<Form.Item shouldUpdate noStyle>
{() => {
const line = getFieldsValue(["billlines"]).billlines[index];
if (!line) return null;
let lineDiscount = 1 - line.actual_cost / line.actual_price;
if (isNaN(lineDiscount)) lineDiscount = 0;
return (
<Tooltip title={`${(lineDiscount * 100).toFixed(2) || 0}%`}>
<div
style={{
padding: "4px 11px",
display: "flex",
alignItems: "center",
background: isDarkMode ? "#141414" : "#fafafa",
border: isDarkMode ? "1px solid #424242" : "1px solid #d9d9d9",
borderLeft: 0
}}
>
<DollarCircleFilled
style={{
color:
Math.abs(lineDiscount - discount) > 0.005
? lineDiscount > discount
? "orange"
: "red"
: "green"
}}
/>
</div>
</Tooltip>
);
}}
</Form.Item>
</Space.Compact>
)
// additional: (record, index) => (
// <Form.Item shouldUpdate>
// {() => {
// const line = getFieldsValue(["billlines"]).billlines[index];
// if (!!!line) return null;
// const lineDiscount = (
// 1 -
// Math.round((line.actual_cost / line.actual_price) * 100) / 100
// ).toPrecision(2);
// return ( return (
// <Tooltip title={`${(lineDiscount * 100).toFixed(0) || 0}%`}> <div
// <DollarCircleFilled style={{
// style={{ display: "flex",
// color: lineDiscount - discount !== 0 ? "red" : "green", width: "100%",
// }} alignItems: "center",
// /> height: CONTROL_HEIGHT
// </Tooltip> }}
// ); >
// }} <div style={{ flex: "1 1 auto", minWidth: 0 }}>
// </Form.Item> <Form.Item noStyle {...bindProps}>
// ), <CurrencyInput
min={0}
disabled={disabled}
controls={false}
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"), title: t("billlines.fields.cost_center"),
dataIndex: "cost_center", dataIndex: "cost_center",
editable: true, editable: true,
formItemProps: (field) => ({
formItemProps: (field) => { key: `${field.index}cost_center`,
return { name: [field.name, "cost_center"],
key: `${field.index}cost_center`, label: t("billlines.fields.cost_center"),
name: [field.name, "cost_center"], valuePropName: "value",
label: t("billlines.fields.cost_center"), rules: [{ required: true }]
valuePropName: "value", }),
rules: [
{
required: true
//message: t("general.validation.required"),
}
]
};
},
formInput: () => ( formInput: () => (
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}> <Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
{bodyshopHasDmsKey(bodyshop) {bodyshopHasDmsKey(bodyshop)
@@ -347,12 +408,10 @@ export function BillEnterModalLinesComponent({
dataIndex: "location", dataIndex: "location",
editable: true, editable: true,
label: t("billlines.fields.location"), label: t("billlines.fields.location"),
formItemProps: (field) => { formItemProps: (field) => ({
return { key: `${field.index}location`,
key: `${field.index}location`, name: [field.name, "location"]
name: [field.name, "location"] }),
};
},
formInput: () => ( formInput: () => (
<Select disabled={disabled}> <Select disabled={disabled}>
{bodyshop.md_parts_locations.map((loc, idx) => ( {bodyshop.md_parts_locations.map((loc, idx) => (
@@ -369,25 +428,19 @@ export function BillEnterModalLinesComponent({
dataIndex: "deductedfromlbr", dataIndex: "deductedfromlbr",
editable: true, editable: true,
width: "40px", width: "40px",
formItemProps: (field) => { formItemProps: (field) => ({
return { valuePropName: "checked",
valuePropName: "checked", key: `${field.index}deductedfromlbr`,
key: `${field.index}deductedfromlbr`, name: [field.name, "deductedfromlbr"]
name: [field.name, "deductedfromlbr"] }),
};
},
formInput: () => <Switch disabled={disabled} />, formInput: () => <Switch disabled={disabled} />,
additional: (record, index) => ( additional: (record, index) => (
<Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}> <Form.Item shouldUpdate noStyle style={{ display: "inline-block" }}>
{() => { {() => {
const price = getFieldValue(["billlines", record.name, "actual_price"]); const price = getFieldValue(["billlines", record.name, "actual_price"]);
const adjustmentRate = getFieldValue(["billlines", record.name, "lbr_adjustment", "rate"]); const adjustmentRate = getFieldValue(["billlines", record.name, "lbr_adjustment", "rate"]);
const billline = getFieldValue(["billlines", record.name]); const billline = getFieldValue(["billlines", record.name]);
const jobline = lineData.find((line) => line.id === billline?.joblineid); const jobline = lineData.find((line) => line.id === billline?.joblineid);
const employeeTeamName = bodyshop.employee_teams.find((team) => team.id === jobline?.assigned_team); const employeeTeamName = bodyshop.employee_teams.find((team) => team.id === jobline?.assigned_team);
if (getFieldValue(["billlines", record.name, "deductedfromlbr"])) if (getFieldValue(["billlines", record.name, "deductedfromlbr"]))
@@ -395,9 +448,7 @@ export function BillEnterModalLinesComponent({
<div> <div>
{Enhanced_Payroll.treatment === "on" ? ( {Enhanced_Payroll.treatment === "on" ? (
<Space> <Space>
{t("joblines.fields.assigned_team", { {t("joblines.fields.assigned_team", { name: employeeTeamName?.name })}
name: employeeTeamName?.name
})}
{`${jobline.mod_lb_hrs} units/${t(`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`)}`} {`${jobline.mod_lb_hrs} units/${t(`joblines.fields.lbr_types.${jobline.mod_lbr_ty}`)}`}
</Space> </Space>
) : null} ) : null}
@@ -406,12 +457,7 @@ export function BillEnterModalLinesComponent({
label={t("joblines.fields.mod_lbr_ty")} label={t("joblines.fields.mod_lbr_ty")}
key={`${index}modlbrty`} key={`${index}modlbrty`}
initialValue={jobline ? jobline.mod_lbr_ty : null} initialValue={jobline ? jobline.mod_lbr_ty : null}
rules={[ rules={[{ required: true }]}
{
required: true
//message: t("general.validation.required"),
}
]}
name={[record.name, "lbr_adjustment", "mod_lbr_ty"]} name={[record.name, "lbr_adjustment", "mod_lbr_ty"]}
> >
<Select allowClear> <Select allowClear>
@@ -431,16 +477,12 @@ export function BillEnterModalLinesComponent({
<Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option> <Select.Option value="LA4">{t("joblines.fields.lbr_types.LA4")}</Select.Option>
</Select> </Select>
</Form.Item> </Form.Item>
{Enhanced_Payroll.treatment === "on" ? ( {Enhanced_Payroll.treatment === "on" ? (
<Form.Item <Form.Item
label={t("billlines.labels.mod_lbr_adjustment")} label={t("billlines.labels.mod_lbr_adjustment")}
name={[record.name, "lbr_adjustment", "mod_lb_hrs"]} name={[record.name, "lbr_adjustment", "mod_lb_hrs"]}
rules={[ rules={[{ required: true }]}
{
required: true
//message: t("general.validation.required"),
}
]}
> >
<InputNumber precision={5} min={0.01} max={jobline ? jobline.mod_lb_hrs : 0} /> <InputNumber precision={5} min={0.01} max={jobline ? jobline.mod_lb_hrs : 0} />
</Form.Item> </Form.Item>
@@ -449,12 +491,7 @@ export function BillEnterModalLinesComponent({
label={t("jobs.labels.adjustmentrate")} label={t("jobs.labels.adjustmentrate")}
name={[record.name, "lbr_adjustment", "rate"]} name={[record.name, "lbr_adjustment", "rate"]}
initialValue={bodyshop.default_adjustment_rate} initialValue={bodyshop.default_adjustment_rate}
rules={[ rules={[{ required: true }]}
{
required: true
//message: t("general.validation.required"),
}
]}
> >
<InputNumber precision={2} min={0.01} /> <InputNumber precision={2} min={0.01} />
</Form.Item> </Form.Item>
@@ -463,6 +500,7 @@ export function BillEnterModalLinesComponent({
<Space>{price && adjustmentRate && `${(price / adjustmentRate).toFixed(1)} hrs`}</Space> <Space>{price && adjustmentRate && `${(price / adjustmentRate).toFixed(1)} hrs`}</Space>
</div> </div>
); );
return <></>; return <></>;
}} }}
</Form.Item> </Form.Item>
@@ -477,17 +515,11 @@ export function BillEnterModalLinesComponent({
dataIndex: "applicable_taxes.federal", dataIndex: "applicable_taxes.federal",
editable: true, editable: true,
width: "40px", width: "40px",
formItemProps: (field) => { formItemProps: (field) => ({
return { key: `${field.index}fedtax`,
key: `${field.index}fedtax`, valuePropName: "checked",
valuePropName: "checked", name: [field.name, "applicable_taxes", "federal"]
initialValue: InstanceRenderManager({ }),
imex: true,
rome: false
}),
name: [field.name, "applicable_taxes", "federal"]
};
},
formInput: () => <Switch disabled={disabled} /> formInput: () => <Switch disabled={disabled} />
} }
] ]
@@ -498,13 +530,11 @@ export function BillEnterModalLinesComponent({
dataIndex: "applicable_taxes.state", dataIndex: "applicable_taxes.state",
editable: true, editable: true,
width: "40px", width: "40px",
formItemProps: (field) => { formItemProps: (field) => ({
return { key: `${field.index}statetax`,
key: `${field.index}statetax`, valuePropName: "checked",
valuePropName: "checked", name: [field.name, "applicable_taxes", "state"]
name: [field.name, "applicable_taxes", "state"] }),
};
},
formInput: () => <Switch disabled={disabled} /> formInput: () => <Switch disabled={disabled} />
}, },
@@ -516,20 +546,18 @@ export function BillEnterModalLinesComponent({
dataIndex: "applicable_taxes.local", dataIndex: "applicable_taxes.local",
editable: true, editable: true,
width: "40px", width: "40px",
formItemProps: (field) => { formItemProps: (field) => ({
return { key: `${field.index}localtax`,
key: `${field.index}localtax`, valuePropName: "checked",
valuePropName: "checked", name: [field.name, "applicable_taxes", "local"]
name: [field.name, "applicable_taxes", "local"] }),
};
},
formInput: () => <Switch disabled={disabled} /> formInput: () => <Switch disabled={disabled} />
} }
] ]
}), }),
{ {
title: t("general.labels.actions"), title: t("general.labels.actions"),
dataIndex: "actions", dataIndex: "actions",
render: (text, record) => ( render: (text, record) => (
<Form.Item shouldUpdate noStyle> <Form.Item shouldUpdate noStyle>
@@ -541,6 +569,7 @@ export function BillEnterModalLinesComponent({
> >
<DeleteFilled /> <DeleteFilled />
</Button> </Button>
{Simple_Inventory.treatment === "on" && ( {Simple_Inventory.treatment === "on" && (
<BilllineAddInventory <BilllineAddInventory
disabled={!billEdit || form.isFieldsTouched() || form.getFieldValue("is_credit_memo")} disabled={!billEdit || form.isFieldsTouched() || form.getFieldValue("is_credit_memo")}
@@ -559,6 +588,7 @@ export function BillEnterModalLinesComponent({
const mergedColumns = (remove) => const mergedColumns = (remove) =>
columns(remove).map((col) => { columns(remove).map((col) => {
if (!col.editable) return col; if (!col.editable) return col;
return { return {
...col, ...col,
onCell: (record) => ({ onCell: (record) => ({
@@ -566,8 +596,8 @@ export function BillEnterModalLinesComponent({
formItemProps: col.formItemProps, formItemProps: col.formItemProps,
formInput: col.formInput, formInput: col.formInput,
additional: col.additional, additional: col.additional,
dataIndex: col.dataIndex, wrapper: col.wrapper,
title: col.title skipFormItem: col.skipFormItem
}) })
}; };
}); });
@@ -586,33 +616,40 @@ export function BillEnterModalLinesComponent({
]} ]}
> >
{(fields, { add, remove }) => { {(fields, { add, remove }) => {
const hasRows = fields.length > 0;
return ( return (
<> <>
<Table <Table
components={{ className="bill-lines-table"
body: { components={{ body: { cell: EditableCell } }}
cell: EditableCell
}
}}
size="small" size="small"
bordered bordered
dataSource={fields} dataSource={fields}
columns={mergedColumns(remove)} columns={mergedColumns(remove)}
scroll={{ x: true }} scroll={hasRows ? { x: "max-content" } : undefined} // <-- no scrollbar when empty
pagination={false} pagination={false}
rowClassName="editable-row" rowClassName="editable-row"
/> />
<Form.Item>
<Button <div style={{ marginTop: 12 }}>
disabled={disabled} <Form.Item style={{ marginBottom: 0 }}>
onClick={() => { <Button
add(); disabled={disabled}
}} onClick={() => {
style={{ width: "100%" }} add(
> InstanceRenderManager({
{t("billlines.actions.newline")} imex: { applicable_taxes: { federal: true } },
</Button> rome: { applicable_taxes: { federal: false } }
</Form.Item> })
);
}}
style={{ width: "100%" }}
>
{t("billlines.actions.newline")}
</Button>
</Form.Item>
</div>
</> </>
); );
}} }}
@@ -623,18 +660,17 @@ export function BillEnterModalLinesComponent({
export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalLinesComponent); export default connect(mapStateToProps, mapDispatchToProps)(BillEnterModalLinesComponent);
const EditableCell = ({ const EditableCell = ({
dataIndex,
record, record,
children, children,
formInput, formInput,
formItemProps, formItemProps,
additional, additional,
wrapper: Wrapper, wrapper: Wrapper,
skipFormItem,
...restProps ...restProps
}) => { }) => {
const rawProps = formItemProps?.(record); const rawProps = formItemProps?.(record);
// DO NOT mutate rawProps; omit `key` immutably
const propsFinal = rawProps const propsFinal = rawProps
? (() => { ? (() => {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
@@ -643,36 +679,38 @@ const EditableCell = ({
})() })()
: undefined; : undefined;
if (additional) { const control = skipFormItem ? (
return ( (formInput && formInput(record, record.name, propsFinal)) || children
<td {...restProps}> ) : (
<div> <Form.Item
<Form.Item name={dataIndex} labelCol={{ span: 0 }} {...propsFinal}> labelCol={{ span: 0 }}
{(formInput && formInput(record, record.name)) || children} {...propsFinal}
</Form.Item> style={{ marginBottom: 0 }} // <-- important: remove default Form.Item margin
{additional(record, record.name)} >
</div> {(formInput && formInput(record, record.name, propsFinal)) || children}
</td> </Form.Item>
); );
}
if (Wrapper) { const cellInner = additional ? (
return ( <div>
<Wrapper> {control}
<td {...restProps}> {additional(record, record.name)}
<Form.Item labelCol={{ span: 0 }} name={dataIndex} {...propsFinal}> </div>
{(formInput && formInput(record, record.name)) || children} ) : (
</Form.Item> control
</td> );
</Wrapper>
);
}
return ( const { style: tdStyle, ...tdRest } = restProps;
<td {...restProps}>
<Form.Item labelCol={{ span: 0 }} name={dataIndex} {...propsFinal}> const td = (
{(formInput && formInput(record, record.name)) || children} <td
</Form.Item> {...tdRest}
style={{ ...tdStyle, verticalAlign: "middle" }} // optional but helps consistency
>
{cellInner}
</td> </td>
); );
if (Wrapper) return <Wrapper>{td}</Wrapper>;
return td;
}; };