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