WIP on invoice enter modal.

This commit is contained in:
Patrick Fic
2020-02-26 17:28:45 -08:00
parent 7d79bb2689
commit afeaeca1a1
8 changed files with 418 additions and 109 deletions

View File

@@ -1750,6 +1750,32 @@
<folder_node>
<name>invoices</name>
<children>
<folder_node>
<name>actions</name>
<children>
<concept_node>
<name>receive</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
<name>errors</name>
<children>
@@ -1795,6 +1821,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>validation</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
</children>
</folder_node>
<folder_node>
@@ -1884,6 +1931,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>total</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>vendor</name>
<definition_loaded>false</definition_loaded>
@@ -1910,6 +1978,27 @@
<folder_node>
<name>labels</name>
<children>
<concept_node>
<name>actions</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>new</name>
<definition_loaded>false</definition_loaded>
@@ -2182,6 +2271,27 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>part_qty</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>part_type</name>
<definition_loaded>false</definition_loaded>

View File

@@ -1,7 +1,18 @@
import { Modal, Form, Input, Switch, DatePicker, AutoComplete } from "antd";
import {
AutoComplete,
Button,
DatePicker,
Form,
Input,
InputNumber,
Modal,
Switch,
Row,
Col
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import ResetForm from "../form-items-formatted/reset-form-item.component";
import InvoiceEnterModalTableComponent from "./invoice-enter-modal.table.component";
export default function InvoiceEnterModalComponent({
visible,
@@ -10,12 +21,15 @@ export default function InvoiceEnterModalComponent({
handleSubmit,
form,
handleRoAutoComplete,
handleRoSelect,
roAutoCompleteOptions,
handleVendorAutoComplete,
vendorAutoCompleteOptions
vendorAutoCompleteOptions,
lineData,
linesState
}) {
const { t } = useTranslation();
const { getFieldDecorator, isFieldsTouched, resetFields } = form;
const { getFieldDecorator, resetFields } = form;
console.log("invoice", invoice);
return (
@@ -33,61 +47,70 @@ export default function InvoiceEnterModalComponent({
onCancel={handleCancel}
destroyOnClose
>
{isFieldsTouched() ? <ResetForm resetFields={resetFields} /> : null}
<Form onSubmit={handleSubmit} autoComplete={"off"}>
<Form.Item label={t("invoices.fields.ro_number")}>
{getFieldDecorator("jobid", {
rules: [
{
required: true,
pattern: new RegExp(
"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
),
message: t("invoices.errors.invalidro")
}
]
})(
<AutoComplete
name="ro_number"
dataSource={roAutoCompleteOptions}
onSearch={handleRoAutoComplete}
autoFocus
backfill={true}
onBlur={props => {
console.log("props", props);
}}
/>
)}
</Form.Item>
<Form.Item label={t("invoices.fields.vendor")}>
{getFieldDecorator("vendorid", {
rules: [
{
required: true,
pattern: new RegExp(
"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
),
message: t("invoices.errors.invalidvendor")
}
]
})(
<AutoComplete
name="vendor_id"
dataSource={vendorAutoCompleteOptions}
onSearch={handleVendorAutoComplete}
backfill
/>
)}
</Form.Item>
<div style={{ display: "flex" }}>
<Form.Item label={t("invoices.fields.ro_number")}>
{getFieldDecorator("jobid", {
rules: [
{
required: true,
pattern: new RegExp(
"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
),
message: t("invoices.errors.invalidro")
}
]
})(
<AutoComplete
name="ro_number"
dataSource={roAutoCompleteOptions}
onSearch={handleRoAutoComplete}
autoFocus
style={{ width: "300px" }}
onSelect={handleRoSelect}
backfill
/>
)}
</Form.Item>
<Form.Item label={t("invoices.fields.vendor")}>
{getFieldDecorator("vendorid", {
rules: [
{
required: true,
pattern: new RegExp(
"[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
),
message: t("invoices.errors.invalidvendor")
}
]
})(
<AutoComplete
name="vendor_id"
dataSource={vendorAutoCompleteOptions}
style={{ width: "300px" }}
onSearch={handleVendorAutoComplete}
backfill
/>
)}
</Form.Item>
<Button onClick={() => resetFields()}>
{t("general.actions.reset")}
</Button>
</div>
<div style={{ display: "flex" }}>
<Form.Item label={t("invoices.fields.invoice_number")}>
{getFieldDecorator(
"invoice_number",
{}
)(<Input name="invoice_number" />)}
{getFieldDecorator("invoice_number", {
rules: [
{ required: true, message: t("general.validation.required") }
]
})(<Input name="invoice_number" />)}
</Form.Item>
<Form.Item label={t("invoices.fields.date")}>
{getFieldDecorator("date", {})(<DatePicker name="date" />)}
{getFieldDecorator("date", {
rules: [
{ required: true, message: t("general.validation.required") }
]
})(<DatePicker name="date" />)}
</Form.Item>
<Form.Item label={t("invoices.fields.is_credit_memo")}>
{getFieldDecorator("is_credit_memo", {
@@ -95,45 +118,24 @@ export default function InvoiceEnterModalComponent({
valuePropName: "checked"
})(<Switch name="is_credit_memo" />)}
</Form.Item>
<Form.Item label={t("invoices.fields.total")}>
{getFieldDecorator("total", {
rules: [
{ required: true, message: t("general.validation.required") }
]
})(<InputNumber precision={2} min={0} name="total" />)}
</Form.Item>
</div>
{
// <Form.Item label={t("joblines.fields.line_desc")}>
// {getFieldDecorator("line_desc", {
// initialValue: jobLine.line_desc
// })(<Input name="line_desc" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.oem_partno")}>
// {getFieldDecorator("oem_partno", {
// initialValue: jobLine.oem_partno
// })(<Input name="oem_partno" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.part_type")}>
// {getFieldDecorator("part_type", {
// initialValue: jobLine.part_type
// })(<Input name="part_type" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.mod_lbr_ty")}>
// {getFieldDecorator("mod_lbr_ty", {
// initialValue: jobLine.mod_lbr_ty
// })(<Input name="mod_lbr_ty" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.op_code_desc")}>
// {getFieldDecorator("op_code_desc", {
// initialValue: jobLine.op_code_desc
// })(<Input name="op_code_desc" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.mod_lb_hrs")}>
// {getFieldDecorator("mod_lb_hrs", {
// initialValue: jobLine.mod_lb_hrs
// })(<InputNumber name="mod_lb_hrs" />)}
// </Form.Item>
// <Form.Item label={t("joblines.fields.act_price")}>
// {getFieldDecorator("act_price", {
// initialValue: jobLine.act_price
// })(<InputNumber name="act_price" />)}
// </Form.Item>
}
<Row>
<Col span={12}>
<InvoiceEnterModalTableComponent
lineData={lineData}
linesState={linesState}
/>
</Col>
<Col span={12}>Table of added items.</Col>
</Row>
</Form>
</Modal>
);

View File

@@ -1,21 +1,20 @@
import { Form, notification } from "antd";
import React, { useState } from "react";
import { useQuery } from "react-apollo";
import { useQuery, useLazyQuery } from "react-apollo";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import {
INSERT_NEW_JOB_LINE,
UPDATE_JOB_LINE
} from "../../graphql/jobs-lines.queries";
import { SEARCH_RO_AUTOCOMPLETE } from "../../graphql/jobs.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { toggleModalVisible } from "../../redux/modals/modals.actions";
import { selectInvoiceEnterModal } from "../../redux/modals/modals.selectors";
import InvoiceEnterModalComponent from "./invoice-enter-modal.component";
import { SEARCH_RO_AUTOCOMPLETE } from "../../graphql/jobs.queries";
import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { GET_JOB_LINES_TO_ENTER_INVOICE } from "../../graphql/jobs-lines.queries";
const mapStateToProps = createStructuredSelector({
invoiceEnterModal: selectInvoiceEnterModal
invoiceEnterModal: selectInvoiceEnterModal,
bodyshop: selectBodyshop
});
const mapDispatchToProps = dispatch => ({
toggleModalVisible: () => dispatch(toggleModalVisible("invoiceEnter"))
@@ -24,19 +23,20 @@ const mapDispatchToProps = dispatch => ({
function InvoiceEnterModalContainer({
invoiceEnterModal,
toggleModalVisible,
form
form,
bodyshop
}) {
const { t } = useTranslation();
const roSearchState = useState("");
const linesState = useState([]);
const roSearchState = useState({ text: "", selectedId: null });
const [roSearch, setRoSearch] = roSearchState;
const handleRoAutoComplete = e => {
setRoSearch(e);
setRoSearch({ ...roSearch, text: e });
};
const { data: RoAutoCompleteData } = useQuery(SEARCH_RO_AUTOCOMPLETE, {
fetchPolicy: "network-only",
variables: { search: `%${roSearch}%` },
skip: !roSearch
variables: { search: `%${roSearch.text}%` },
skip: !roSearch.text || roSearch.text.length < 3
});
const vendorSearchState = useState("");
@@ -49,10 +49,26 @@ function InvoiceEnterModalContainer({
{
fetchPolicy: "network-only",
variables: { search: `%${vendorSearch}%` },
skip: !vendorSearch
skip: !vendorSearch || vendorSearch.length < 3
}
);
const [
loadLines,
{ called, loading: lineLoading, data: lineData }
] = useLazyQuery(GET_JOB_LINES_TO_ENTER_INVOICE, {
fetchPolicy: "network-only",
variables: { id: roSearch.selectedId }
});
if (roSearch.selectedId) {
if (!called) loadLines();
console.log("lineData", lineData);
}
const handleRoSelect = v => {
setRoSearch({ ...roSearch, selectedId: v });
};
const handleSubmit = e => {
e.preventDefault();
form.validateFieldsAndScroll((err, values) => {
@@ -126,6 +142,7 @@ function InvoiceEnterModalContainer({
handleCancel={handleCancel}
form={form}
handleRoAutoComplete={handleRoAutoComplete}
handleRoSelect={handleRoSelect}
roAutoCompleteOptions={
RoAutoCompleteData
? RoAutoCompleteData.jobs.reduce((acc, value) => {
@@ -152,6 +169,8 @@ function InvoiceEnterModalContainer({
}, [])
: null
}
linesState={linesState}
lineData={lineData ? lineData.joblines : null}
/>
);
}

View File

@@ -0,0 +1,131 @@
import React, { useState } from "react";
import { Table, Button, Icon } from "antd";
import { useTranslation } from "react-i18next";
import { alphaSort } from "../../utils/sorters";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
export default function InvoiceEnterModalTableComponent({
lineData,
linesState
}) {
const [selectedLines, setSelectedLines] = linesState;
const [state, setState] = useState({
sortedInfo: {}
});
const { t } = useTranslation();
const columns = [
{
title: t("joblines.fields.line_desc"),
dataIndex: "line_desc",
key: "line_desc",
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
sortOrder:
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
ellipsis: true
},
{
title: t("joblines.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) =>
alphaSort(
a.oem_partno ? a.oem_partno : a.op_code_desc,
b.oem_partno ? b.oem_partno : b.op_code_desc
),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
ellipsis: true,
render: (text, record) => (
<span>
{record.oem_partno ? record.oem_partno : record.op_code_desc}
</span>
)
},
{
title: t("joblines.fields.part_type"),
dataIndex: "part_type",
key: "part_type",
sorter: (a, b) => alphaSort(a.part_type, b.part_type),
sortOrder:
state.sortedInfo.columnKey === "part_type" && state.sortedInfo.order
},
{
title: t("joblines.fields.db_price"),
dataIndex: "db_price",
key: "db_price",
sorter: (a, b) => a.db_price - b.db_price,
sortOrder:
state.sortedInfo.columnKey === "db_price" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.db_price}</CurrencyFormatter>
)
},
{
title: t("joblines.fields.act_price"),
dataIndex: "act_price",
key: "act_price",
sorter: (a, b) => a.act_price - b.act_price,
sortOrder:
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
render: (text, record) => (
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
)
},
{
title: t("joblines.fields.part_qty"),
dataIndex: "part_qty",
key: "part_qty",
sorter: (a, b) => a.part_qty - b.part_qty,
sortOrder:
state.sortedInfo.columnKey === "part_qty" && state.sortedInfo.order
},
{
title: t("joblines.fields.mod_lb_hrs"),
dataIndex: "mod_lb_hrs",
key: "mod_lb_hrs",
sorter: (a, b) => a.mod_lb_hrs - b.mod_lb_hrs,
sortOrder:
state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order
},
{
title: t("invoices.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<div>
<Button>
<Icon type="select" />
</Button>
</div>
)
}
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const formItemLayout = {
labelCol: {
xs: { span: 12 },
sm: { span: 5 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 12 }
}
};
return (
<Table
{...formItemLayout}
size="small"
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns.map(item => ({ ...item }))}
rowKey="id"
dataSource={lineData}
onChange={handleTableChange}
/>
);
}

View File

@@ -71,3 +71,29 @@ export const UPDATE_JOB_LINE = gql`
}
}
`;
export const GET_JOB_LINES_TO_ENTER_INVOICE = gql`
query GET_JOB_LINES_TO_ENTER_INVOICE($id: uuid!) {
joblines(
where: {
jobid: { _eq: $id }
oem_partno: { _neq: "" }
act_price: { _gt: "0" }
}
) {
id
line_desc
part_type
oem_partno
db_price
act_price
part_qty
mod_lbr_ty
db_hrs
mod_lb_hrs
lbr_op
lbr_amt
op_code_desc
}
}
`;

View File

@@ -145,18 +145,24 @@
}
},
"invoices": {
"actions": {
"receive": "Receive Part"
},
"errors": {
"invalidro": "Not a valid RO.",
"invalidvendor": "Not a valid vendor."
"invalidvendor": "Not a valid vendor.",
"validation": "Please ensure all fields are entered correctly. "
},
"fields": {
"date": "Invoice Date",
"invoice_number": "Invoice Number",
"is_credit_memo": "Credit Memo?",
"ro_number": "RO Number",
"total": "Invoice Total",
"vendor": "Vendor"
},
"labels": {
"actions": "Actions",
"new": "New Invoice"
}
},
@@ -177,6 +183,7 @@
"mod_lbr_ty": "Labor Type",
"oem_partno": "OEM Part #",
"op_code_desc": "Operation Code Description",
"part_qty": "Quantity",
"part_type": "Part Type",
"status": "Status",
"unq_seq": "Seq #"

View File

@@ -145,18 +145,24 @@
}
},
"invoices": {
"actions": {
"receive": ""
},
"errors": {
"invalidro": "",
"invalidvendor": ""
"invalidvendor": "",
"validation": ""
},
"fields": {
"date": "",
"invoice_number": "",
"is_credit_memo": "",
"ro_number": "",
"total": "",
"vendor": ""
},
"labels": {
"actions": "",
"new": ""
}
},
@@ -177,6 +183,7 @@
"mod_lbr_ty": "Tipo de trabajo",
"oem_partno": "OEM parte #",
"op_code_desc": "",
"part_qty": "",
"part_type": "Tipo de parte",
"status": "Estado",
"unq_seq": "Seq #"

View File

@@ -145,18 +145,24 @@
}
},
"invoices": {
"actions": {
"receive": ""
},
"errors": {
"invalidro": "",
"invalidvendor": ""
"invalidvendor": "",
"validation": ""
},
"fields": {
"date": "",
"invoice_number": "",
"is_credit_memo": "",
"ro_number": "",
"total": "",
"vendor": ""
},
"labels": {
"actions": "",
"new": ""
}
},
@@ -177,6 +183,7 @@
"mod_lbr_ty": "Type de travail",
"oem_partno": "Pièce OEM #",
"op_code_desc": "",
"part_qty": "",
"part_type": "Type de pièce",
"status": "Statut",
"unq_seq": "Seq #"