From afeaeca1a17c915bc8f8cfb9031bacaa3889fe5c Mon Sep 17 00:00:00 2001 From: Patrick Fic Date: Wed, 26 Feb 2020 17:28:45 -0800 Subject: [PATCH] WIP on invoice enter modal. --- bodyshop_translations.babel | 110 +++++++++++ .../invoice-enter-modal.component.jsx | 184 +++++++++--------- .../invoice-enter-modal.container.jsx | 49 +++-- .../invoice-enter-modal.table.component.jsx | 131 +++++++++++++ client/src/graphql/jobs-lines.queries.js | 26 +++ client/src/translations/en_us/common.json | 9 +- client/src/translations/es/common.json | 9 +- client/src/translations/fr/common.json | 9 +- 8 files changed, 418 insertions(+), 109 deletions(-) create mode 100644 client/src/components/invoice-enter-modal/invoice-enter-modal.table.component.jsx diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index b854e8eff..e73a047bc 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1750,6 +1750,32 @@ invoices + + actions + + + receive + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + errors @@ -1795,6 +1821,27 @@ + + validation + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -1884,6 +1931,27 @@ + + total + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + vendor false @@ -1910,6 +1978,27 @@ labels + + actions + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + new false @@ -2182,6 +2271,27 @@ + + part_qty + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + part_type false diff --git a/client/src/components/invoice-enter-modal/invoice-enter-modal.component.jsx b/client/src/components/invoice-enter-modal/invoice-enter-modal.component.jsx index bbccfd0d5..16d425581 100644 --- a/client/src/components/invoice-enter-modal/invoice-enter-modal.component.jsx +++ b/client/src/components/invoice-enter-modal/invoice-enter-modal.component.jsx @@ -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() ? : null}
- - {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") - } - ] - })( - { - console.log("props", props); - }} - /> - )} - - - {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") - } - ] - })( - - )} - +
+ + {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") + } + ] + })( + + )} + + + {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") + } + ] + })( + + )} + + +
- {getFieldDecorator( - "invoice_number", - {} - )()} + {getFieldDecorator("invoice_number", { + rules: [ + { required: true, message: t("general.validation.required") } + ] + })()} - {getFieldDecorator("date", {})()} + {getFieldDecorator("date", { + rules: [ + { required: true, message: t("general.validation.required") } + ] + })()} {getFieldDecorator("is_credit_memo", { @@ -95,45 +118,24 @@ export default function InvoiceEnterModalComponent({ valuePropName: "checked" })()} + + {getFieldDecorator("total", { + rules: [ + { required: true, message: t("general.validation.required") } + ] + })()} +
- { - // - // {getFieldDecorator("line_desc", { - // initialValue: jobLine.line_desc - // })()} - // - // - // {getFieldDecorator("oem_partno", { - // initialValue: jobLine.oem_partno - // })()} - // - // - // {getFieldDecorator("part_type", { - // initialValue: jobLine.part_type - // })()} - // - // - // {getFieldDecorator("mod_lbr_ty", { - // initialValue: jobLine.mod_lbr_ty - // })()} - // - // - // {getFieldDecorator("op_code_desc", { - // initialValue: jobLine.op_code_desc - // })()} - // - // - // {getFieldDecorator("mod_lb_hrs", { - // initialValue: jobLine.mod_lb_hrs - // })()} - // - // - // {getFieldDecorator("act_price", { - // initialValue: jobLine.act_price - // })()} - // - } + + + + + Table of added items. +
); diff --git a/client/src/components/invoice-enter-modal/invoice-enter-modal.container.jsx b/client/src/components/invoice-enter-modal/invoice-enter-modal.container.jsx index 32bcd0b0c..c790514ea 100644 --- a/client/src/components/invoice-enter-modal/invoice-enter-modal.container.jsx +++ b/client/src/components/invoice-enter-modal/invoice-enter-modal.container.jsx @@ -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} /> ); } diff --git a/client/src/components/invoice-enter-modal/invoice-enter-modal.table.component.jsx b/client/src/components/invoice-enter-modal/invoice-enter-modal.table.component.jsx new file mode 100644 index 000000000..156a0680f --- /dev/null +++ b/client/src/components/invoice-enter-modal/invoice-enter-modal.table.component.jsx @@ -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) => ( + + {record.oem_partno ? record.oem_partno : record.op_code_desc} + + ) + }, + { + 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) => ( + {record.db_price} + ) + }, + { + 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) => ( + {record.act_price} + ) + }, + { + 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) => ( +
+ +
+ ) + } + ]; + + 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 ( + ({ ...item }))} + rowKey="id" + dataSource={lineData} + onChange={handleTableChange} + /> + ); +} diff --git a/client/src/graphql/jobs-lines.queries.js b/client/src/graphql/jobs-lines.queries.js index 197e4f6e6..4f7bb7be1 100644 --- a/client/src/graphql/jobs-lines.queries.js +++ b/client/src/graphql/jobs-lines.queries.js @@ -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 + } + } +`; diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 8bc222578..a9ba6c91d 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -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 #" diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index accf712d2..ded57f8a9 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -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 #" diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index ca3ce145a..1e1ca27c2 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -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 #"