From a91bfea581822e69a2fe7facf5be4d5543a62de5 Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 20 Jan 2026 15:01:38 -0500 Subject: [PATCH] feature/IO-3499-React-19 checkpoint --- .../bill-form/bill-form.lines.component.jsx | 652 +++++++++--------- 1 file changed, 345 insertions(+), 307 deletions(-) diff --git a/client/src/components/bill-form/bill-form.lines.component.jsx b/client/src/components/bill-form/bill-form.lines.component.jsx index 396516903..5b4a5ca8c 100644 --- a/client/src/components/bill-form/bill-form.lines.component.jsx +++ b/client/src/components/bill-form/bill-form.lines.component.jsx @@ -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) => ( prev.is_credit_memo !== cur.is_credit_memo}> - {() => { - return props.children; - }} + {() => props.children} ), 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: () => }, { @@ -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: () => }, { @@ -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) => ( { - 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")} ); - } else { - return null; } + return null; }} ) @@ -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) => ( - - - - {() => { - 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 ( - -
- 0.005 - ? lineDiscount > discount - ? "orange" - : "red" - : "green" - }} - /> -
-
- ); - }} -
-
- ) - // additional: (record, index) => ( - // - // {() => { - // 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 ( +
+
+ + 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) => { - 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: () => ( {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: () => , 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"])) @@ -395,9 +448,7 @@ export function BillEnterModalLinesComponent({
{Enhanced_Payroll.treatment === "on" ? ( - {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}`)}`} ) : 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"]} > + {Enhanced_Payroll.treatment === "on" ? ( @@ -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 }]} > @@ -463,6 +500,7 @@ export function BillEnterModalLinesComponent({ {price && adjustmentRate && `${(price / adjustmentRate).toFixed(1)} hrs`}
); + return <>; }}
@@ -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: () => } ] @@ -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: () => }, @@ -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: () => } ] }), + { title: t("general.labels.actions"), - dataIndex: "actions", render: (text, record) => ( @@ -541,6 +569,7 @@ export function BillEnterModalLinesComponent({ > + {Simple_Inventory.treatment === "on" && ( 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 ( <> - - - + +
+ + + +
); }} @@ -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 ( - - ); - } + const control = skipFormItem ? ( + (formInput && formInput(record, record.name, propsFinal)) || children + ) : ( + + {(formInput && formInput(record, record.name, propsFinal)) || children} + + ); - if (Wrapper) { - return ( - - - - ); - } + const cellInner = additional ? ( +
+ {control} + {additional(record, record.name)} +
+ ) : ( + control + ); - return ( - ); + + if (Wrapper) return {td}; + return td; };
-
- - {(formInput && formInput(record, record.name)) || children} - - {additional(record, record.name)} -
-
- - {(formInput && formInput(record, record.name)) || children} - - - - {(formInput && formInput(record, record.name)) || children} - + const { style: tdStyle, ...tdRest } = restProps; + + const td = ( + + {cellInner}