diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index d37bdbacf..4a3d405fa 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -32515,6 +32515,27 @@ + + saving + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -32583,6 +32604,27 @@ + + cm_received + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + comments false @@ -33008,6 +33050,27 @@ + + mark_as_received + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + newpartsorder false @@ -33202,6 +33265,27 @@ + + line_updated + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + received false diff --git a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx new file mode 100644 index 000000000..555903813 --- /dev/null +++ b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.component.jsx @@ -0,0 +1,136 @@ +import { Checkbox, Form, Skeleton, Typography } from "antd"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; +import "./bill-cm-returns-table.styles.scss"; +export default function BillCmdReturnsTableComponent({ + form, + loadOutstandingReturns, + returnLoading, + returnData, +}) { + const { t } = useTranslation(); + + useEffect(() => { + if (returnData) { + form.setFieldsValue({ + outstanding_returns: returnData.parts_order_lines, + }); + } + }, [returnData, form]); + + return ( + + prev.jobid !== cur.jobid || + prev.is_credit_memo !== cur.is_credit_memo || + prev.vendorid !== cur.vendorid + } + noStyle + > + {() => { + const isReturn = form.getFieldValue("is_credit_memo"); + + if (!isReturn) { + return null; + } + + if (returnLoading) return ; + + return ( + + {(fields, { add, remove, move }) => { + return ( + <> + + {t("bills.labels.creditsnotreceived")} + + + + + + + + + + + + + + {fields.map((field, index) => ( + + + + + + + + + + + ))} + +
{t("parts_orders.fields.line_desc")}{t("parts_orders.fields.part_type")}{t("parts_orders.fields.quantity")}{t("parts_orders.fields.act_price")}{t("parts_orders.fields.cost")}{t("parts_orders.labels.mark_as_received")}
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + ); + }} +
+ ); + }} +
+ ); +} diff --git a/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss new file mode 100644 index 000000000..442c4f35d --- /dev/null +++ b/client/src/components/bill-cm-returns-table/bill-cm-returns-table.styles.scss @@ -0,0 +1,19 @@ +.bill-cm-returns-table { + table-layout: fixed; + width: 100%; + + th, + td { + padding: 8px; + text-align: left; + border-bottom: 1px solid #ddd; + + .ant-form-item { + margin-bottom: 0px !important; + } + } + + tr:hover { + background-color: #f5f5f5; + } +} \ No newline at end of file diff --git a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx index 98bb7ac50..b9a13e06b 100644 --- a/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx +++ b/client/src/components/bill-enter-modal/bill-enter-modal.container.jsx @@ -11,6 +11,7 @@ import { QUERY_JOB_LBR_ADJUSTMENTS, UPDATE_JOB, } from "../../graphql/jobs.queries"; +import { MUTATION_MARK_RETURN_RECEIVED } from "../../graphql/parts-orders.queries"; import { insertAuditTrail } from "../../redux/application/application.actions"; import { toggleModalVisible } from "../../redux/modals/modals.actions"; import { selectBillEnterModal } from "../../redux/modals/modals.selectors"; @@ -47,6 +48,7 @@ function BillEnterModalContainer({ const [enterAgain, setEnterAgain] = useState(false); const [insertBill] = useMutation(INSERT_NEW_BILL); const [updateJobLines] = useMutation(UPDATE_JOB_LINE); + const [updatePartsOrderLines] = useMutation(MUTATION_MARK_RETURN_RECEIVED); const [loading, setLoading] = useState(false); const client = useApolloClient(); @@ -76,7 +78,8 @@ function BillEnterModalContainer({ } setLoading(true); - const { upload, location, ...remainingValues } = values; + const { upload, location, outstanding_returns, ...remainingValues } = + values; let adjustmentsToInsert = {}; @@ -156,6 +159,25 @@ function BillEnterModalContainer({ }); } + const markPolReceived = outstanding_returns.filter( + (o) => o.cm_received === true + ); + + if (markPolReceived.length > 0) { + const r2 = await updatePartsOrderLines({ + variables: { partsLineIds: markPolReceived.map((p) => p.id) }, + }); + if (!!r2.errors) { + setLoading(false); + setEnterAgain(false); + notification["error"]({ + message: t("parts_orders.errors.updating", { + message: JSON.stringify(r2.errors), + }), + }); + } + } + if (!!r1.errors) { setLoading(false); setEnterAgain(false); diff --git a/client/src/components/bill-form/bill-form.component.jsx b/client/src/components/bill-form/bill-form.component.jsx index 0c48a61df..f57c56819 100644 --- a/client/src/components/bill-form/bill-form.component.jsx +++ b/client/src/components/bill-form/bill-form.component.jsx @@ -47,6 +47,7 @@ export function BillFormComponent({ billEdit, disableInvNumber, job, + loadOutstandingReturns, }) { const { t } = useTranslation(); const client = useApolloClient(); @@ -58,6 +59,14 @@ export function BillFormComponent({ ); const handleVendorSelect = (props, opt) => { setDiscount(opt.discount); + + opt && + loadOutstandingReturns({ + variables: { + jobId: form.getFieldValue("jobid"), + vendorId: opt.value, + }, + }); }; useEffect(() => { @@ -65,8 +74,8 @@ export function BillFormComponent({ }, [job, form]); useEffect(() => { - if (form.getFieldValue("vendorid") && vendorAutoCompleteOptions) { - const vendorId = form.getFieldValue("vendorid"); + const vendorId = form.getFieldValue("vendorid"); + if (vendorId && vendorAutoCompleteOptions) { const matchingVendors = vendorAutoCompleteOptions.filter( (v) => v.id === vendorId ); @@ -74,10 +83,25 @@ export function BillFormComponent({ setDiscount(matchingVendors[0].discount); } } - if (form.getFieldValue("jobid")) { - loadLines({ variables: { id: form.getFieldValue("jobid") } }); + const jobId = form.getFieldValue("jobid"); + if (jobId) { + loadLines({ variables: { id: jobId } }); + if (form.getFieldValue("is_credit_memo") && vendorId) { + loadOutstandingReturns({ + variables: { + jobId: jobId, + vendorId: vendorId, + }, + }); + } } - }, [form, setDiscount, vendorAutoCompleteOptions, loadLines]); + }, [ + form, + loadOutstandingReturns, + setDiscount, + vendorAutoCompleteOptions, + loadLines, + ]); return (
@@ -107,6 +131,13 @@ export function BillFormComponent({ onBlur={() => { if (form.getFieldValue("jobid") !== null) { loadLines({ variables: { id: form.getFieldValue("jobid") } }); + if (form.getFieldValue("vendorid") !== null) + loadOutstandingReturns({ + variables: { + jobId: form.getFieldValue("jobid"), + vendorId: form.getFieldValue("vendorid"), + }, + }); } }} /> @@ -228,8 +259,22 @@ export function BillFormComponent({ rules={[ ({ getFieldValue }) => ({ validator(rule, value) { + if ( + value === true && + getFieldValue("jobid") && + getFieldValue("vendorid") + ) { + loadOutstandingReturns({ + variables: { + jobId: form.getFieldValue("jobid"), + vendorId: form.getFieldValue("vendorid"), + }, + }); + } + if ( !bodyshop.bill_allow_post_to_closed && + job && (job.status === bodyshop.md_ro_statuses.default_invoiced || job.status === bodyshop.md_ro_statuses.default_exported || job.status === bodyshop.md_ro_statuses.default_void) && diff --git a/client/src/components/bill-form/bill-form.container.jsx b/client/src/components/bill-form/bill-form.container.jsx index 9c4b578d0..4fe056667 100644 --- a/client/src/components/bill-form/bill-form.container.jsx +++ b/client/src/components/bill-form/bill-form.container.jsx @@ -6,6 +6,8 @@ import { GET_JOB_LINES_TO_ENTER_BILL } from "../../graphql/jobs-lines.queries"; import { SEARCH_VENDOR_AUTOCOMPLETE } from "../../graphql/vendors.queries"; import { selectBodyshop } from "../../redux/user/user.selectors"; import BillFormComponent from "./bill-form.component"; +import BillCmdReturnsTableComponent from "../bill-cm-returns-table/bill-cm-returns-table.component"; +import { QUERY_UNRECEIVED_LINES } from "../../graphql/parts-orders.queries"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -27,20 +29,34 @@ export function BillFormContainer({ GET_JOB_LINES_TO_ENTER_BILL ); + const [loadOutstandingReturns, { loading: returnLoading, data: returnData }] = + useLazyQuery(QUERY_UNRECEIVED_LINES); + return ( - + <> + + {!billEdit && ( + + )} + ); } export default connect(mapStateToProps, null)(BillFormContainer); diff --git a/client/src/components/bills-list-table/bills-list-table.component.jsx b/client/src/components/bills-list-table/bills-list-table.component.jsx index d15b29a20..ed4fb70e6 100644 --- a/client/src/components/bills-list-table/bills-list-table.component.jsx +++ b/client/src/components/bills-list-table/bills-list-table.component.jsx @@ -59,7 +59,6 @@ export function BillsListTableComponent({ record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid } onClick={() => { - console.log(record); setPartsOrderContext({ actions: {}, context: { diff --git a/client/src/components/parts-order-cm-received/parts-order-cm-received.component.jsx b/client/src/components/parts-order-cm-received/parts-order-cm-received.component.jsx new file mode 100644 index 000000000..ed08ea3ea --- /dev/null +++ b/client/src/components/parts-order-cm-received/parts-order-cm-received.component.jsx @@ -0,0 +1,66 @@ +import { useMutation } from "@apollo/client"; +import { Checkbox, notification, Space, Spin } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { MUTATION_UPDATE_PO_CM_REECEIVED } from "../../graphql/parts-orders.queries"; + +export default function PartsOrderCmReceived({ + checked, + orderLineId, + partsOrderId, +}) { + const [updateLine] = useMutation(MUTATION_UPDATE_PO_CM_REECEIVED); + const { t } = useTranslation(); + + const [loading, setLoading] = useState(false); + + const handleChange = async (e) => { + setLoading(true); + const result = await updateLine({ + variables: { + partsLineId: orderLineId, + partsOrder: { cm_received: e.target.checked }, + }, + update(cache) { + cache.modify({ + id: cache.identify({ + id: partsOrderId, + __typename: "parts_orders", + }), + + fields: { + parts_order_lines(ex, { readField }) { + console.log(ex); + return ex.map((lineref) => { + if (orderLineId.id !== readField("id", lineref)) { + lineref.cm_received = e.target.checked; + } + return lineref; + }); + }, + }, + }); + }, + }); + + if (!!!result.errors) { + notification["success"]({ + message: t("parts_orders.successes.line_updated"), + }); + } else { + notification["error"]({ + message: t("parts_orders.errors.saving", { + error: JSON.stringify(result.errors), + }), + }); + } + setLoading(false); + }; + + return ( + + + {loading && } + + ); +} diff --git a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx index 4c3ad5b64..a554b7f0c 100644 --- a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx +++ b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx @@ -29,6 +29,7 @@ import { alphaSort } from "../../utils/sorters"; import { TemplateList } from "../../utils/TemplateConstants"; import DataLabel from "../data-label/data-label.component"; import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component"; +import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component"; import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component"; import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container"; import PrintWrapper from "../print-wrapper/print-wrapper.component"; @@ -346,6 +347,23 @@ export function PartsOrderListTableComponent({ dataIndex: "status", key: "status", }, + + ...(selectedPartsOrderRecord && selectedPartsOrderRecord.return + ? [ + { + title: t("parts_orders.fields.cm_received"), + dataIndex: "cm_received", + key: "cm_received", + render: (text, record) => ( + + ), + }, + ] + : []), { title: t("parts_orders.fields.backordered_on"), dataIndex: "backordered_on", diff --git a/client/src/components/parts-order-modal/parts-order-modal.container.jsx b/client/src/components/parts-order-modal/parts-order-modal.container.jsx index 3b6003b65..b449b3881 100644 --- a/client/src/components/parts-order-modal/parts-order-modal.container.jsx +++ b/client/src/components/parts-order-modal/parts-order-modal.container.jsx @@ -305,6 +305,7 @@ export function PartsOrderModalContainer({ quantity: value.part_qty, job_line_id: isReturn ? value.joblineid : value.id, part_type: value.part_type, + ...(isReturn && { cm_received: false }), }); return acc; }, []) diff --git a/client/src/components/production-list-detail/production-list-detail.component.jsx b/client/src/components/production-list-detail/production-list-detail.component.jsx index ac0ae9dc8..e5495b181 100644 --- a/client/src/components/production-list-detail/production-list-detail.component.jsx +++ b/client/src/components/production-list-detail/production-list-detail.component.jsx @@ -21,6 +21,7 @@ import OwnerNameDisplay from "../owner-name-display/owner-name-display.component import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { setModalContext } from "../../redux/modals/modals.actions"; +import ScoreboardAddButton from "../job-scoreboard-add-button/job-scoreboard-add-button.component"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -59,7 +60,7 @@ export function ProductionListDetail({ jobs, setPrintCenterContext }) { + {" "} + } /> diff --git a/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx b/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx index 82cd14ab3..0a2407a55 100644 --- a/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx +++ b/client/src/components/scoreboard-jobs-list/scoreboard-jobs-list.component.jsx @@ -1,13 +1,42 @@ -import React from "react"; -import { Dropdown, Button, Table, Space } from "antd"; +import React, { useState } from "react"; +import { Dropdown, Button, Table, Space, Card, Input } from "antd"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import ScoreboardRemoveButton from "../scoreboard-remove-button/scorebard-remove-button.component"; import { DateFormatter } from "../../utils/DateFormatter"; import ScoreboardEntryEdit from "../scoreboard-entry-edit/scoreboard-entry-edit.component"; +import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; export default function ScoreboardJobsList({ scoreBoardlist }) { const { t } = useTranslation(); + const [searchText, setSearchText] = useState(""); + + const jobs = scoreBoardlist + ? searchText === "" + ? scoreBoardlist + : scoreBoardlist.filter( + (sb) => + (sb.job.ro_number || "") + .toString() + .toLowerCase() + .includes(searchText.toLowerCase()) || + (sb.job.ownr_co_nm || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (sb.job.ownr_fn || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (sb.job.ownr_ln || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (sb.job.v_model_desc || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (sb.job.v_make_desc || "") + .toLowerCase() + .includes(searchText.toLowerCase()) + ) + : []; const columns = [ { @@ -20,7 +49,25 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { ), }, + { + title: t("jobs.fields.owner"), + dataIndex: "owner", + key: "owner", + ellipsis: true, + render: (text, record) => , + }, + { + title: t("jobs.fields.vehicle"), + dataIndex: "vehicle", + key: "vehicle", + ellipsis: true, + render: (text, record) => ( + {`${record.job.v_model_yr || ""} ${ + record.job.v_make_desc || "" + } ${record.job.v_model_desc || ""}`} + ), + }, { title: t("scoreboard.fields.date"), dataIndex: "date", @@ -51,17 +98,29 @@ export default function ScoreboardJobsList({ scoreBoardlist }) { ]; const overlay = ( -
+ e.stopPropagation()} + extra={ + { + setSearchText(e.target.value); + }} + value={searchText} + enterButton + onClick={(e) => e.stopPropagation()} + /> + } + > e.stopPropagation()} /> - + ); return ( diff --git a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx index 6c0673ca3..85592044c 100644 --- a/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx +++ b/client/src/components/scoreboard-targets-table/scoreboard-targets-table.component.jsx @@ -25,10 +25,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { const values = useMemo(() => { const dateHash = _.groupBy(scoreBoardlist, "date"); - console.log( - "🚀 ~ file: scoreboard-targets-table.component.jsx ~ line 31 ~ values ~ dateHash", - dateHash - ); let ret = { todayBody: 0, @@ -71,10 +67,6 @@ export function ScoreboardTargetsTable({ bodyshop, scoreBoardlist }) { return ret; }, [scoreBoardlist]); - console.log( - "🚀 ~ file: scoreboard-targets-table.component.jsx ~ line 51 ~ values ~ values", - values - ); return (