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: () => (