Added parts backorder + receiving for orders BOD-159

This commit is contained in:
Patrick Fic
2020-06-25 10:03:46 -07:00
parent db08be58ac
commit 2e30a9078e
21 changed files with 852 additions and 76 deletions

View File

@@ -8,6 +8,8 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import queryString from "query-string";
import { useLocation } from "react-router-dom";
const mapDispatchToProps = (dispatch) => ({
setPartsOrderContext: (context) =>
@@ -22,7 +24,6 @@ export function InvoicesListTableComponent({
job,
loading,
invoicesQuery,
selectedInvoice,
handleOnRowClick,
setPartsOrderContext,
setInvoiceEnterContext,
@@ -32,6 +33,8 @@ export function InvoicesListTableComponent({
const [state, setState] = useState({
sortedInfo: {},
});
const search = queryString.parse(useLocation().search);
const selectedInvoice = search.invoiceid;
const invoices = invoicesQuery.data ? invoicesQuery.data.invoices : [];
const { refetch } = invoicesQuery;
@@ -91,7 +94,8 @@ export function InvoicesListTableComponent({
render: (text, record) => (
<div>
<Link
to={`/manage/invoices?invoiceid=${record.id}&vendorid=${record.vendorid}`}>
to={`/manage/invoices?invoiceid=${record.id}&vendorid=${record.vendorid}`}
>
<Button>{t("invoices.actions.edit")}</Button>
</Link>
<Button
@@ -116,7 +120,8 @@ export function InvoicesListTableComponent({
isReturn: true,
},
})
}>
}
>
{t("invoices.actions.return")}
</Button>
</div>
@@ -232,11 +237,11 @@ export function InvoicesListTableComponent({
</Descriptions.Item>
</Descriptions>
<Table
size='small'
size="small"
scroll={{ x: "50%", y: "40rem" }}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey='id'
rowKey="id"
dataSource={record.invoicelines}
/>
</div>
@@ -246,9 +251,9 @@ export function InvoicesListTableComponent({
return (
<Table
loading={loading}
size='small'
size="small"
title={() => (
<div className='imex-table-header'>
<div className="imex-table-header">
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
@@ -260,7 +265,8 @@ export function InvoicesListTableComponent({
job,
},
});
}}>
}}
>
{t("jobs.actions.postInvoices")}
</Button>
<Button
@@ -273,10 +279,11 @@ export function InvoicesListTableComponent({
(invoicesQuery.data && invoicesQuery.data.invoices) || [],
},
});
}}>
}}
>
{t("jobs.actions.reconcile")}
</Button>{" "}
<div className='imex-table-header__search'>
<div className="imex-table-header__search">
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
@@ -290,7 +297,7 @@ export function InvoicesListTableComponent({
expandedRowRender={rowExpander}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey='id'
rowKey="id"
dataSource={invoices}
onChange={handleTableChange}
expandable={{

View File

@@ -1,3 +1,4 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Dropdown, Input, Menu, Table } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -11,6 +12,8 @@ import AllocationsAssignmentContainer from "../allocations-assignment/allocation
import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
import queryString from "query-string";
import { useHistory, useLocation } from "react-router-dom";
const mapDispatchToProps = (dispatch) => ({
setJobLineEditContext: (context) =>
@@ -35,6 +38,9 @@ export function JobLinesComponent({
});
const { t } = useTranslation();
const search = queryString.parse(useLocation().search);
const history = useHistory();
const columns = [
{
title: "#",
@@ -213,7 +219,8 @@ export function JobLinesComponent({
actions: { refetch: refetch },
context: record,
});
}}>
}}
>
{t("general.actions.edit")}
</Button>
<AllocationsAssignmentContainer
@@ -237,9 +244,9 @@ export function JobLinesComponent({
const markMenu = (
<Menu onClick={handleMark}>
<Menu.Item key='PAA'>PAA</Menu.Item>
<Menu.Item key='PAN'>PAN</Menu.Item>
<Menu.Item key='PAL'>PAL</Menu.Item>
<Menu.Item key="PAA">PAA</Menu.Item>
<Menu.Item key="PAN">PAN</Menu.Item>
<Menu.Item key="PAL">PAL</Menu.Item>
</Menu>
);
@@ -248,16 +255,19 @@ export function JobLinesComponent({
<PartsOrderModalContainer />
<Table
columns={columns}
rowKey='id'
rowKey="id"
loading={loading}
size='small'
size="small"
pagination={{ position: "top", defaultPageSize: 50 }}
dataSource={jobLines}
onChange={handleTableChange}
scroll={{ x: true, y: "40rem" }}
title={() => {
return (
<div className='imex-table-header'>
<div className="imex-table-header">
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<Button
disabled={selectedLines.length > 0 ? false : true}
onClick={() => {
@@ -268,7 +278,8 @@ export function JobLinesComponent({
linesToOrder: selectedLines,
},
});
}}>
}}
>
{t("parts.actions.order")}
</Button>
<Dropdown overlay={markMenu} trigger={["click"]}>
@@ -284,10 +295,11 @@ export function JobLinesComponent({
actions: { refetch: refetch },
context: { jobid: jobId },
});
}}>
}}
>
{t("joblines.actions.new")}
</Button>
<div className='imex-table-header__search'>
<div className="imex-table-header__search">
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
@@ -300,11 +312,16 @@ export function JobLinesComponent({
);
}}
expandedRowRender={(record) => (
<div style={{ margin: 0 }}>
<div>
<strong>{t("parts_orders.labels.orderhistory")}</strong>
{record.parts_order_lines.map((item) => (
<div key={item.id}>
{`${item.parts_order.order_number || ""} from `}
<Link
to={`/manage/jobs/${jobId}?tab=partssublet&partsorderid=${item.parts_order.id}`}
>
{item.parts_order.order_number || ""}
</Link>
-
<Link to={`/manage/shop/vendors/${item.parts_order.vendor.id}`}>
{item.parts_order.vendor.name || ""}
</Link>

View File

@@ -44,7 +44,8 @@ export function JobsCloseExportButton({ bodyshop, jobId, disabled }) {
let PartnerResponse;
try {
PartnerResponse = await axios.post(
"http://localhost:1337/qb/",
// "http://localhost:1337/qb/",
"http://b47e67f9cbe3.ngrok.io/qb/",
QbXmlResponse.data,
{
headers: {

View File

@@ -21,6 +21,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(setModalContext({ context: context, modal: "schedule" })),
setInvoiceEnterContext: (context) =>
dispatch(setModalContext({ context: context, modal: "invoiceEnter" })),
setPaymentContext: (context) =>
dispatch(setModalContext({ context: context, modal: "payment" })),
});
export function JobsDetailHeaderActions({
@@ -29,12 +31,13 @@ export function JobsDetailHeaderActions({
refetch,
setScheduleContext,
setInvoiceEnterContext,
setPaymentContext,
}) {
const { t } = useTranslation();
const client = useApolloClient();
const history = useHistory();
const statusmenu = (
<Menu key='popovermenu'>
<Menu key="popovermenu">
<Menu.Item
onClick={() => {
setScheduleContext({
@@ -44,30 +47,43 @@ export function JobsDetailHeaderActions({
job: job,
},
});
}}>
}}
>
{t("jobs.actions.schedule")}
</Menu.Item>
<Menu.Item key='cccontract'>
<Menu.Item
key="enterpayments"
onClick={() => {
setPaymentContext({
actions: {},
context: { jobId: job.id },
});
}}
>
{t("menus.header.enterpayment")}
</Menu.Item>
<Menu.Item key="cccontract">
<Link
to={{
pathname: "/manage/courtesycars/contracts/new",
state: { jobId: job.id },
}}>
}}
>
{t("menus.jobsactions.newcccontract")}
</Link>
</Menu.Item>
<Menu.Item
key='addtoproduction'
key="addtoproduction"
disabled={!!!job.converted || !!job.inproduction}
onClick={() => AddToProduction(client, job.id, refetch)}>
onClick={() => AddToProduction(client, job.id, refetch)}
>
{t("jobs.actions.addtoproduction")}
</Menu.Item>
<Menu.Item key='duplicatejob'>
<Menu.Item key="duplicatejob">
<Popconfirm
title={t("jobs.labels.duplicateconfirm")}
okText='Yes'
cancelText='No'
okText="Yes"
cancelText="No"
onClick={(e) => e.stopPropagation()}
onConfirm={() =>
DuplicateJob(
@@ -79,12 +95,13 @@ export function JobsDetailHeaderActions({
}
)
}
getPopupContainer={(trigger) => trigger.parentNode}>
getPopupContainer={(trigger) => trigger.parentNode}
>
{t("menus.jobsactions.duplicate")}
</Popconfirm>
</Menu.Item>
<Menu.Item
key='postinvoices'
key="postinvoices"
onClick={() => {
setInvoiceEnterContext({
actions: { refetch: refetch },
@@ -92,14 +109,16 @@ export function JobsDetailHeaderActions({
job: job,
},
});
}}>
}}
>
{t("jobs.actions.postInvoices")}
</Menu.Item>
<Menu.Item key='closejob'>
<Menu.Item key="closejob">
<Link
to={{
pathname: `/manage/jobs/${job.id}/close`,
}}>
}}
>
{t("menus.jobsactions.closejob")}
</Link>
</Menu.Item>
@@ -108,10 +127,11 @@ export function JobsDetailHeaderActions({
);
return (
<Dropdown
className='imex-flex-row__margin'
className="imex-flex-row__margin"
overlay={statusmenu}
trigger={["click"]}
key='changestatus'>
key="changestatus"
>
<Button>
{t("general.labels.actions")} <DownCircleFilled />
</Button>

View File

@@ -4,6 +4,7 @@ import AlertComponent from "../alert/alert.component";
import InvoicesListTableComponent from "../invoices-list-table/invoices-list-table.component";
import JobInvoicesTotalsComponent from "../job-invoices-total/job-invoices-total.component";
import PartsOrderModal from "../parts-order-modal/parts-order-modal.container";
import { PartsOrderListTableComponent } from "../parts-order-list-table/parts-order-list-table.component";
const tableCol = {
xs: {
span: 24,
@@ -25,22 +26,29 @@ const totalsCol = {
export default function JobsDetailPliComponent({
job,
invoicesQuery,
handleOnRowClick,
handleInvoiceOnRowClick,
handlePartsOrderOnRowClick,
selectedInvoice,
}) {
return (
<div>
<PartsOrderModal />
{invoicesQuery.error ? (
<AlertComponent message={invoicesQuery.error.message} type='error' />
<AlertComponent message={invoicesQuery.error.message} type="error" />
) : null}
<Row>
<Col {...tableCol}>
<PartsOrderListTableComponent
job={job}
loading={invoicesQuery.loading}
handleOnRowClick={handlePartsOrderOnRowClick}
selectedInvoice={selectedInvoice}
invoicesQuery={invoicesQuery}
/>
<InvoicesListTableComponent
job={job}
loading={invoicesQuery.loading}
handleOnRowClick={handleOnRowClick}
selectedInvoice={selectedInvoice}
handleOnRowClick={handleInvoiceOnRowClick}
invoicesQuery={invoicesQuery}
/>
</Col>

View File

@@ -13,7 +13,7 @@ export default function JobsDetailPliContainer({ job }) {
const search = queryString.parse(useLocation().search);
const history = useHistory();
const handleOnRowClick = (record) => {
const handleInvoiceOnRowClick = (record) => {
if (record) {
if (record.id) {
search.invoiceid = record.id;
@@ -25,12 +25,24 @@ export default function JobsDetailPliContainer({ job }) {
}
};
const handlePartsOrderOnRowClick = (record) => {
if (record) {
if (record.id) {
search.partsorderid = record.id;
history.push({ search: queryString.stringify(search) });
}
} else {
delete search.partsorderid;
history.push({ search: queryString.stringify(search) });
}
};
return (
<JobsDetailPliComponent
job={job}
invoicesQuery={invoicesQuery}
handleOnRowClick={handleOnRowClick}
selectedInvoice={search.invoiceid}
handleInvoiceOnRowClick={handleInvoiceOnRowClick}
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
/>
);
}

View File

@@ -0,0 +1,52 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "antd";
import { useMutation } from "@apollo/react-hooks";
import { MUTATION_BACKORDER_PART_LINE } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
});
export function PartsOrderLineBackorderButton({
partsOrderStatus,
partsLineId,
jobLineId,
bodyshop,
}) {
const [loading, setLoading] = useState(false);
const [backorderLine] = useMutation(MUTATION_BACKORDER_PART_LINE);
const { t } = useTranslation();
const isAlreadyBackordered =
bodyshop.md_order_statuses.default_bo === partsOrderStatus;
const handleOnClick = async () => {
setLoading(true);
const result = await backorderLine({
variables: {
jobLineId,
partsLineId,
status: isAlreadyBackordered
? bodyshop.md_order_statuses.default_received || "Received*"
: bodyshop.md_order_statuses.default_bo || "Backordered*",
},
});
setLoading(false);
};
return (
<Button loading={loading} onClick={handleOnClick}>
{isAlreadyBackordered
? t("parts_orders.actions.receive")
: t("parts_orders.actions.backordered")}
</Button>
);
}
export default connect(mapStateToProps, null)(PartsOrderLineBackorderButton);

View File

@@ -0,0 +1,215 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Input, Table } from "antd";
import queryString from "query-string";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation } from "react-router-dom";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateFormatter } from "../../utils/DateFormatter";
import { alphaSort } from "../../utils/sorters";
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
const mapDispatchToProps = (dispatch) => ({});
export function PartsOrderListTableComponent({
job,
loading,
invoicesQuery,
handleOnRowClick,
}) {
const { t } = useTranslation();
const [state, setState] = useState({
sortedInfo: {},
});
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const parts_orders = invoicesQuery.data
? invoicesQuery.data.parts_orders
: [];
const { refetch } = invoicesQuery;
const columns = [
{
title: t("vendors.fields.name"),
dataIndex: "vendorname",
key: "vendorname",
sorter: (a, b) => alphaSort(a.vendor.name, b.vendor.name),
sortOrder:
state.sortedInfo.columnKey === "vendorname" && state.sortedInfo.order,
render: (text, record) => <span>{record.vendor.name}</span>,
},
{
title: t("parts_orders.fields.order_number"),
dataIndex: "order_number",
key: "order_number",
sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number),
sortOrder:
state.sortedInfo.columnKey === "invoice_number" &&
state.sortedInfo.order,
},
{
title: t("parts_orders.fields.order_date"),
dataIndex: "order_date",
key: "order_date",
sorter: (a, b) => a.order_date - b.order_date,
sortOrder:
state.sortedInfo.columnKey === "order_date" && state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.order_date}</DateFormatter>
),
},
{
title: t("parts_orders.fields.deliver_by"),
dataIndex: "deliver_by",
key: "deliver_by",
sorter: (a, b) => a.deliver_by - b.deliver_by,
sortOrder:
state.sortedInfo.columnKey === "deliver_by" && state.sortedInfo.order,
render: (text, record) => (
<DateFormatter>{record.deliver_by}</DateFormatter>
),
},
];
const handleTableChange = (pagination, filters, sorter) => {
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const rowExpander = (record) => {
const columns = [
{
title: t("parts_orders.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,
},
{
title: t("parts_orders.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("parts_orders.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("parts_orders.fields.oem_partno"),
dataIndex: "oem_partno",
key: "oem_partno",
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
sortOrder:
state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order,
},
{
title: t("parts_orders.fields.line_remarks"),
dataIndex: "line_remarks",
key: "line_remarks",
},
{
title: t("parts_orders.fields.status"),
dataIndex: "status",
key: "status",
},
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<div>
<PartsOrderLineBackorderButton
partsOrderStatus={record.status}
partsLineId={record.id}
jobLineId={record.job_line_id}
/>
</div>
),
},
];
return (
<div>
<Table
size="small"
scroll={{ x: "50%", y: "40rem" }}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={record.parts_order_lines}
/>
</div>
);
};
return (
<Table
loading={loading}
size="small"
title={() => (
<div className="imex-table-header">
<Button onClick={() => refetch()}>
<SyncOutlined />
</Button>
<div className="imex-table-header__search">
<Input.Search
placeholder={t("general.labels.search")}
onChange={(e) => {
e.preventDefault();
}}
/>
</div>
</div>
)}
scroll={{ x: "50%", y: "40rem" }}
expandedRowRender={rowExpander}
pagination={{ position: "top", defaultPageSize: 25 }}
columns={columns}
rowKey="id"
dataSource={parts_orders}
onChange={handleTableChange}
expandable={{
expandedRowKeys: [selectedpartsorder],
onExpand: (expanded, record) => {
handleOnRowClick(expanded ? record : null);
},
}}
rowSelection={{
onSelect: (record) => {
handleOnRowClick(record);
},
selectedRowKeys: [selectedpartsorder],
type: "radio",
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleOnRowClick(record);
}, // click row
onDoubleClick: (event) => {}, // double click row
onContextMenu: (event) => {}, // right button click row
onMouseEnter: (event) => {}, // mouse enter row
onMouseLeave: (event) => {}, // mouse leave row
};
}}
/>
);
}
export default connect(null, mapDispatchToProps)(PartsOrderListTableComponent);

View File

@@ -1,6 +1,6 @@
import { useElements, useStripe, CardElement } from "@stripe/react-stripe-js";
import { Form, Modal, notification } from "antd";
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -37,6 +37,7 @@ function InvoiceEnterModalContainer({
const elements = useElements();
const { t } = useTranslation();
const { context, actions, visible } = paymentModal;
const [loading, setLoading] = useState(false);
const stripeStateArr = useState({
error: null,
@@ -118,6 +119,10 @@ function InvoiceEnterModalContainer({
toggleModalVisible();
};
useEffect(() => {
if (visible) form.resetFields();
}, [visible, form]);
return (
<Modal
title={t("payments.labels.new")}

View File

@@ -36,11 +36,6 @@ export function ProductionBoardKanbanComponent({ data, bodyshop }) {
bodyshop.md_ro_statuses.production_statuses,
]);
const findById = (id) => {
return id;
//return (data && data.find((x) => x.id === id).ro_number) || null;
};
const client = useApolloClient();
const handleDragEnd = async (card, source, destination) => {

View File

@@ -1,6 +1,6 @@
import { DeleteFilled } from "@ant-design/icons";
import { Button, Form, Input, InputNumber, Select, Row, Col } from "antd";
import React, { useState } from "react";
import { Button, Col, Form, Input, InputNumber, Row } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
//TODO Fix up styles.
export default function ShopInfoSchedulingComponent({ form }) {