666 lines
21 KiB
JavaScript
666 lines
21 KiB
JavaScript
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
|
|
import { useTreatments } from "@splitsoftware/splitio-react";
|
|
import {
|
|
Button,
|
|
Form,
|
|
Input,
|
|
InputNumber,
|
|
Select,
|
|
Space,
|
|
Switch,
|
|
Table,
|
|
Tooltip,
|
|
} from "antd";
|
|
import React from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { connect } from "react-redux";
|
|
import { createStructuredSelector } from "reselect";
|
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
import CiecaSelect from "../../utils/Ciecaselect";
|
|
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";
|
|
|
|
const mapStateToProps = createStructuredSelector({
|
|
//currentUser: selectCurrentUser
|
|
bodyshop: selectBodyshop,
|
|
});
|
|
const mapDispatchToProps = (dispatch) => ({
|
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
|
});
|
|
|
|
export function BillEnterModalLinesComponent({
|
|
bodyshop,
|
|
disabled,
|
|
lineData,
|
|
discount,
|
|
form,
|
|
responsibilityCenters,
|
|
billEdit,
|
|
billid,
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const { setFieldsValue, getFieldsValue, getFieldValue } = form;
|
|
const { Simple_Inventory } = useTreatments(
|
|
["Simple_Inventory"],
|
|
{},
|
|
bodyshop && bodyshop.imexshopid
|
|
);
|
|
const columns = (remove) => {
|
|
return [
|
|
{
|
|
title: t("billlines.fields.jobline"),
|
|
dataIndex: "joblineid",
|
|
editable: true,
|
|
width: "20rem",
|
|
formItemProps: (field) => {
|
|
return {
|
|
key: `${field.index}joblinename`,
|
|
name: [field.name, "joblineid"],
|
|
label: t("billlines.fields.jobline"),
|
|
rules: [
|
|
{
|
|
required: true,
|
|
//message: t("general.validation.required"),
|
|
},
|
|
],
|
|
};
|
|
},
|
|
wrapper: (props) => (
|
|
<Form.Item
|
|
noStyle
|
|
shouldUpdate={(prev, cur) =>
|
|
prev.is_credit_memo !== cur.is_credit_memo
|
|
}
|
|
>
|
|
{() => {
|
|
return props.children;
|
|
}}
|
|
</Form.Item>
|
|
),
|
|
formInput: (record, index) => (
|
|
<BillLineSearchSelect
|
|
disabled={disabled}
|
|
options={lineData}
|
|
style={{ width: "100%", minWidth: "10rem" }}
|
|
allowRemoved={form.getFieldValue("is_credit_memo") || false}
|
|
onSelect={(value, opt) => {
|
|
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,
|
|
cost_center: opt.part_type
|
|
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
|
? opt.part_type !== "PAE"
|
|
? opt.part_type
|
|
: null
|
|
: responsibilityCenters.defaults &&
|
|
(responsibilityCenters.defaults.costs[
|
|
opt.part_type
|
|
] ||
|
|
null)
|
|
: null,
|
|
};
|
|
}
|
|
return item;
|
|
}
|
|
),
|
|
});
|
|
}}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: t("billlines.fields.line_desc"),
|
|
dataIndex: "line_desc",
|
|
editable: true,
|
|
|
|
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"),
|
|
},
|
|
],
|
|
};
|
|
},
|
|
formInput: (record, index) => <Input disabled={disabled} />,
|
|
},
|
|
{
|
|
title: t("billlines.fields.quantity"),
|
|
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();
|
|
},
|
|
}),
|
|
],
|
|
};
|
|
},
|
|
formInput: (record, index) => (
|
|
<InputNumber precision={0} min={1} disabled={disabled} />
|
|
),
|
|
},
|
|
{
|
|
title: t("billlines.fields.actual_price"),
|
|
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"),
|
|
},
|
|
],
|
|
};
|
|
},
|
|
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;
|
|
}
|
|
),
|
|
});
|
|
}}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
title: t("billlines.fields.actual_cost"),
|
|
dataIndex: "actual_cost",
|
|
editable: true,
|
|
width: "8rem",
|
|
|
|
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"),
|
|
},
|
|
],
|
|
};
|
|
},
|
|
formInput: (record, index) => (
|
|
<CurrencyInput
|
|
min={0}
|
|
disabled={disabled}
|
|
controls={false}
|
|
addonAfter={
|
|
<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}%`}>
|
|
<DollarCircleFilled
|
|
style={{
|
|
color:
|
|
Math.abs(lineDiscount - discount) > 0.005
|
|
? lineDiscount > discount
|
|
? "orange"
|
|
: "red"
|
|
: "green",
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
);
|
|
}}
|
|
</Form.Item>
|
|
}
|
|
/>
|
|
),
|
|
// 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>
|
|
// ),
|
|
},
|
|
{
|
|
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"),
|
|
},
|
|
],
|
|
};
|
|
},
|
|
formInput: (record, index) => (
|
|
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
|
|
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
|
? 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) => {
|
|
return {
|
|
key: `${field.index}location`,
|
|
name: [field.name, "location"],
|
|
};
|
|
},
|
|
formInput: (record, index) => (
|
|
<Select disabled={disabled}>
|
|
{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,
|
|
formItemProps: (field) => {
|
|
return {
|
|
valuePropName: "checked",
|
|
key: `${field.index}deductedfromlbr`,
|
|
name: [field.name, "deductedfromlbr"],
|
|
};
|
|
},
|
|
formInput: (record, index) => <Switch disabled={disabled} />,
|
|
additional: (record, index) => (
|
|
<Form.Item shouldUpdate style={{ display: "inline-block" }}>
|
|
{() => {
|
|
const price = getFieldValue([
|
|
"billlines",
|
|
record.name,
|
|
"actual_price",
|
|
]);
|
|
|
|
const adjustmentRate = getFieldValue([
|
|
"billlines",
|
|
record.name,
|
|
"lbr_adjustment",
|
|
"rate",
|
|
]);
|
|
|
|
if (getFieldValue(["billlines", record.name, "deductedfromlbr"]))
|
|
return (
|
|
<div>
|
|
<Form.Item
|
|
label={t("joblines.fields.mod_lbr_ty")}
|
|
key={`${index}modlbrty`}
|
|
rules={[
|
|
{
|
|
required: true,
|
|
//message: t("general.validation.required"),
|
|
},
|
|
]}
|
|
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>
|
|
<Form.Item
|
|
label={t("jobs.labels.adjustmentrate")}
|
|
name={[record.name, "lbr_adjustment", "rate"]}
|
|
initialValue={bodyshop.default_adjustment_rate}
|
|
rules={[
|
|
{
|
|
required: true,
|
|
//message: t("general.validation.required"),
|
|
},
|
|
]}
|
|
>
|
|
<InputNumber precision={2} min={0.01} />
|
|
</Form.Item>
|
|
{price &&
|
|
adjustmentRate &&
|
|
`${(price / adjustmentRate).toFixed(1)} hrs`}
|
|
</div>
|
|
);
|
|
return <></>;
|
|
}}
|
|
</Form.Item>
|
|
),
|
|
},
|
|
// {
|
|
// title: t("billlines.fields.federal_tax_applicable"),
|
|
// dataIndex: "applicable_taxes.federal",
|
|
// editable: true,
|
|
|
|
// formItemProps: (field) => {
|
|
// return {
|
|
// key: `${field.index}fedtax`,
|
|
// valuePropName: "checked",
|
|
// // initialValue: true,
|
|
// name: [field.name, "applicable_taxes", "federal"],
|
|
// };
|
|
// },
|
|
// formInput: (record, index) => <Switch disabled={disabled} />,
|
|
// },
|
|
{
|
|
title: t("billlines.fields.state_tax_applicable"),
|
|
dataIndex: "applicable_taxes.state",
|
|
editable: true,
|
|
|
|
formItemProps: (field) => {
|
|
return {
|
|
key: `${field.index}statetax`,
|
|
valuePropName: "checked",
|
|
name: [field.name, "applicable_taxes", "state"],
|
|
};
|
|
},
|
|
formInput: (record, index) => <Switch disabled={disabled} />,
|
|
},
|
|
// {
|
|
// title: t("billlines.fields.local_tax_applicable"),
|
|
// dataIndex: "applicable_taxes.local",
|
|
// editable: true,
|
|
|
|
// formItemProps: (field) => {
|
|
// return {
|
|
// key: `${field.index}localtax`,
|
|
// valuePropName: "checked",
|
|
// name: [field.name, "applicable_taxes", "local"],
|
|
// };
|
|
// },
|
|
// formInput: (record, index) => <Switch disabled={disabled} />,
|
|
// },
|
|
{
|
|
title: t("general.labels.actions"),
|
|
|
|
dataIndex: "actions",
|
|
render: (text, record) => (
|
|
<Form.Item shouldUpdate noStyle>
|
|
{() => (
|
|
<Space wrap>
|
|
<Button
|
|
disabled={
|
|
disabled ||
|
|
getFieldValue("billlines")[record.fieldKey]?.inventories
|
|
?.length > 0
|
|
}
|
|
onClick={() => remove(record.name)}
|
|
>
|
|
<DeleteFilled />
|
|
</Button>
|
|
{Simple_Inventory.treatment === "on" && (
|
|
<BilllineAddInventory
|
|
disabled={
|
|
!billEdit ||
|
|
form.isFieldsTouched() ||
|
|
form.getFieldValue("is_credit_memo")
|
|
}
|
|
billline={getFieldValue("billlines")[record.fieldKey]}
|
|
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,
|
|
dataIndex: col.dataIndex,
|
|
title: col.title,
|
|
}),
|
|
};
|
|
});
|
|
|
|
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, move }) => {
|
|
return (
|
|
<>
|
|
<Table
|
|
components={{
|
|
body: {
|
|
cell: EditableCell,
|
|
},
|
|
}}
|
|
size="small"
|
|
bordered
|
|
dataSource={fields}
|
|
columns={mergedColumns(remove)}
|
|
scroll={{ x: true }}
|
|
pagination={false}
|
|
rowClassName="editable-row"
|
|
/>
|
|
<Form.Item>
|
|
<Button
|
|
disabled={disabled}
|
|
onClick={() => {
|
|
add();
|
|
}}
|
|
style={{ width: "100%" }}
|
|
>
|
|
{t("billlines.actions.newline")}
|
|
</Button>
|
|
</Form.Item>
|
|
</>
|
|
);
|
|
}}
|
|
</Form.List>
|
|
);
|
|
}
|
|
|
|
export default connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps
|
|
)(BillEnterModalLinesComponent);
|
|
|
|
const EditableCell = ({
|
|
dataIndex,
|
|
title,
|
|
inputType,
|
|
record,
|
|
index,
|
|
children,
|
|
formInput,
|
|
formItemProps,
|
|
additional,
|
|
wrapper,
|
|
...restProps
|
|
}) => {
|
|
if (additional)
|
|
return (
|
|
<td {...restProps}>
|
|
<Space size="small">
|
|
<Form.Item
|
|
name={dataIndex}
|
|
labelCol={{ span: 0 }}
|
|
{...(formItemProps && formItemProps(record))}
|
|
>
|
|
{(formInput && formInput(record, record.name)) || children}
|
|
</Form.Item>
|
|
{additional && additional(record, record.name)}
|
|
</Space>
|
|
</td>
|
|
);
|
|
if (wrapper)
|
|
return (
|
|
<wrapper>
|
|
<td {...restProps}>
|
|
<Form.Item
|
|
labelCol={{ span: 0 }}
|
|
name={dataIndex}
|
|
{...(formItemProps && formItemProps(record))}
|
|
>
|
|
{(formInput && formInput(record, record.name)) || children}
|
|
</Form.Item>
|
|
</td>
|
|
</wrapper>
|
|
);
|
|
return (
|
|
<td {...restProps}>
|
|
<Form.Item
|
|
labelCol={{ span: 0 }}
|
|
name={dataIndex}
|
|
{...(formItemProps && formItemProps(record))}
|
|
>
|
|
{(formInput && formInput(record, record.name)) || children}
|
|
</Form.Item>
|
|
</td>
|
|
);
|
|
};
|