diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index f2c3b671a..0ab4e1f61 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -11149,6 +11149,27 @@ + + dateinpast + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + dlexpirebeforereturn false @@ -12370,6 +12391,27 @@ + + leasereturn + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + out false @@ -14735,6 +14777,27 @@ + + vacationadded + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -27343,6 +27406,27 @@ + + estimator + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + existing_jobs false @@ -32543,6 +32627,48 @@ + + systemnotes + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + usernotes + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -32699,6 +32825,27 @@ + + saving + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + selectexistingornew false @@ -33103,6 +33250,27 @@ + + tax_number + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + @@ -41346,6 +41514,27 @@ + + efficiencyoverperiod + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobs false diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index e7243044d..e8389290f 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -142,3 +142,9 @@ } } } + + +//Update row highlighting on production board. +.ant-table-tbody > tr.ant-table-row:hover > td { + background: #eaeaea !important; +} \ No newline at end of file diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx new file mode 100644 index 000000000..e92bb175b --- /dev/null +++ b/client/src/components/bill-detail-edit/bill-detail-edit-component.jsx @@ -0,0 +1,251 @@ +import { useMutation, useQuery } from "@apollo/client"; +import { Button, Form, PageHeader, Popconfirm, Space } from "antd"; +import moment from "moment"; +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 { createStructuredSelector } from "reselect"; +import { + DELETE_BILL_LINE, + INSERT_NEW_BILL_LINES, + UPDATE_BILL_LINE +} from "../../graphql/bill-lines.queries"; +import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import AuditTrailMapping from "../../utils/AuditTrailMappings"; +import AlertComponent from "../alert/alert.component"; +import BillFormContainer from "../bill-form/bill-form.container"; +import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component"; +import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component"; +import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container"; +import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container"; +import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; +import BillDetailEditReturn from "./bill-detail-edit-return.component"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + setPartsOrderContext: (context) => + dispatch(setModalContext({ context: context, modal: "partsOrder" })), + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(BillDetailEditcontainer); + +export function BillDetailEditcontainer({ + setPartsOrderContext, + insertAuditTrail, + bodyshop, +}) { + const search = queryString.parse(useLocation().search); + + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [visible, setVisible] = useState(false); + const [updateLoading, setUpdateLoading] = useState(false); + const [update_bill] = useMutation(UPDATE_BILL); + const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES); + const [updateBillLine] = useMutation(UPDATE_BILL_LINE); + const [deleteBillLine] = useMutation(DELETE_BILL_LINE); + + const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, { + variables: { billid: search.billid }, + skip: !!!search.billid, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only", + }); + + const handleSave = () => { + //It's got a previously deducted bill line! + if ( + data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 || + form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > + 0 + ) + setVisible(true); + else { + form.submit(); + } + }; + + const handleFinish = async (values) => { + setUpdateLoading(true); + //let adjustmentsToInsert = {}; + + const { billlines, upload, ...bill } = values; + const updates = []; + updates.push( + update_bill({ + variables: { billId: search.billid, bill: bill }, + }) + ); + + billlines.forEach((l) => { + delete l.selected; + }); + + //Find bill lines that were deleted. + const deletedJobLines = []; + + data.bills_by_pk.billlines.forEach((a) => { + const matchingRecord = billlines.find((b) => b.id === a.id); + if (!matchingRecord) { + deletedJobLines.push(a); + } + }); + + deletedJobLines.forEach((d) => { + updates.push(deleteBillLine({ variables: { id: d.id } })); + }); + + billlines.forEach((billline) => { + const { deductedfromlbr, inventories, jobline, ...il } = billline; + delete il.__typename; + + if (il.id) { + updates.push( + updateBillLine({ + variables: { + billLineId: il.id, + billLine: { + ...il, + deductedfromlbr: deductedfromlbr, + joblineid: il.joblineid === "noline" ? null : il.joblineid, + }, + }, + }) + ); + } else { + //It's a new line, have to insert it. + updates.push( + insertBillLine({ + variables: { + billLines: [ + { + ...il, + deductedfromlbr: deductedfromlbr, + billid: search.billid, + joblineid: il.joblineid === "noline" ? null : il.joblineid, + }, + ], + }, + }) + ); + } + }); + + await Promise.all(updates); + + insertAuditTrail({ + jobid: bill.jobid, + billid: search.billid, + operation: AuditTrailMapping.billupdated(bill.invoice_number), + }); + + await refetch(); + form.setFieldsValue(transformData(data)); + form.resetFields(); + setVisible(false); + setUpdateLoading(false); + }; + + if (error) return ; + if (!search.billid) return <>; //
{t("bills.labels.noneselected")}
; + + const exported = data && data.bills_by_pk && data.bills_by_pk.exported; + + return ( + <> + {loading && } + {data && ( + <> + + + + form.submit()} + onCancel={() => setVisible(false)} + okButtonProps={{ loading: updateLoading }} + title={t("bills.labels.editadjwarning")} + > + + + + + + } + /> +
+ + + {bodyshop.uselocalmediaserver ? ( + + ) : ( + + )} + + + )} + + ); +} + +const transformData = (data) => { + return data + ? { + ...data.bills_by_pk, + + billlines: data.bills_by_pk.billlines.map((i) => { + return { + ...i, + joblineid: !!i.joblineid ? i.joblineid : "noline", + applicable_taxes: { + federal: + (i.applicable_taxes && i.applicable_taxes.federal) || false, + state: (i.applicable_taxes && i.applicable_taxes.state) || false, + local: (i.applicable_taxes && i.applicable_taxes.local) || false, + }, + }; + }), + date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null, + } + : {}; +}; diff --git a/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx b/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx new file mode 100644 index 000000000..81571f8b0 --- /dev/null +++ b/client/src/components/bill-detail-edit/bill-detail-edit-return.component.jsx @@ -0,0 +1,185 @@ +import { Button, Checkbox, Form, Modal } from "antd"; +import queryString from "query-string"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { useHistory, useLocation } from "react-router-dom"; +import { createStructuredSelector } from "reselect"; +import { insertAuditTrail } from "../../redux/application/application.actions"; +import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import ReadOnlyFormItemComponent from "../form-items-formatted/read-only-form-item.component"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, +}); +const mapDispatchToProps = (dispatch) => ({ + setPartsOrderContext: (context) => + dispatch(setModalContext({ context: context, modal: "partsOrder" })), + insertAuditTrail: ({ jobid, operation }) => + dispatch(insertAuditTrail({ jobid, operation })), +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(BillDetailEditReturn); + +export function BillDetailEditReturn({ + setPartsOrderContext, + insertAuditTrail, + bodyshop, + data, + disabled, +}) { + const search = queryString.parse(useLocation().search); + const history = useHistory(); + const { t } = useTranslation(); + const [form] = Form.useForm(); + const [visible, setVisible] = useState(false); + + const handleFinish = ({ billlines }) => { + const selectedLines = billlines.filter((l) => l.selected).map((l) => l.id); + + setPartsOrderContext({ + actions: {}, + context: { + jobId: data.bills_by_pk.jobid, + vendorId: data.bills_by_pk.vendorid, + returnFromBill: data.bills_by_pk.id, + invoiceNumber: data.bills_by_pk.invoice_number, + linesToOrder: data.bills_by_pk.billlines + .filter((l) => selectedLines.includes(l.id)) + .map((i) => { + return { + line_desc: i.line_desc, + // db_price: i.actual_price, + act_price: i.actual_price, + cost: i.actual_cost, + quantity: i.quantity, + joblineid: i.joblineid, + oem_partno: i.jobline && i.jobline.oem_partno, + part_type: i.jobline && i.jobline.part_type, + }; + }), + isReturn: true, + }, + }); + delete search.billid; + + history.push({ search: queryString.stringify(search) }); + setVisible(false); + }; + useEffect(() => { + if (visible === false) form.resetFields(); + }, [visible, form]); + + return ( + <> + setVisible(false)} + destroyOnClose + title={t("bills.actions.return")} + onOk={() => form.submit()} + > +
+ + {(fields, { add, remove, move }) => { + return ( + + + + + + + + + + + + {fields.map((field, index) => ( + + + + + + + + ))} + +
+ { + form.setFieldsValue({ + billlines: form + .getFieldsValue() + .billlines.map((b) => ({ + ...b, + selected: e.target.checked, + })), + }); + }} + /> + {t("billlines.fields.line_desc")}{t("billlines.fields.quantity")}{t("billlines.fields.actual_price")}{t("billlines.fields.actual_cost")}
+ + + + + + + + + + + + + + + + + + + +
+ ); + }} +
+
+
+ + + ); +} diff --git a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx index b9bbb791b..4df0959c7 100644 --- a/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx +++ b/client/src/components/bill-detail-edit/bill-detail-edit.container.jsx @@ -1,68 +1,12 @@ -import { useMutation, useQuery } from "@apollo/client"; -import { - Button, - Drawer, - Form, - Grid, - PageHeader, - Popconfirm, - Space, -} from "antd"; -import moment from "moment"; +import { Drawer, Grid } from "antd"; import queryString from "query-string"; -import React, { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { connect } from "react-redux"; +import React from "react"; import { useHistory, useLocation } from "react-router-dom"; -import { createStructuredSelector } from "reselect"; -import { - DELETE_BILL_LINE, - INSERT_NEW_BILL_LINES, - UPDATE_BILL_LINE, -} from "../../graphql/bill-lines.queries"; -import { QUERY_BILL_BY_PK, UPDATE_BILL } from "../../graphql/bills.queries"; -import { insertAuditTrail } from "../../redux/application/application.actions"; -import { setModalContext } from "../../redux/modals/modals.actions"; -import { selectBodyshop } from "../../redux/user/user.selectors"; -import AuditTrailMapping from "../../utils/AuditTrailMappings"; -import AlertComponent from "../alert/alert.component"; -import BillFormContainer from "../bill-form/bill-form.container"; -import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component"; -import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-button.component"; -import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-gallery.container"; -import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container"; -import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; +import BillDetailEditComponent from "./bill-detail-edit-component"; -const mapStateToProps = createStructuredSelector({ - bodyshop: selectBodyshop, -}); -const mapDispatchToProps = (dispatch) => ({ - setPartsOrderContext: (context) => - dispatch(setModalContext({ context: context, modal: "partsOrder" })), - insertAuditTrail: ({ jobid, operation }) => - dispatch(insertAuditTrail({ jobid, operation })), -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(BillDetailEditcontainer); - -export function BillDetailEditcontainer({ - setPartsOrderContext, - insertAuditTrail, - bodyshop, -}) { +export default function BillDetailEditcontainer() { const search = queryString.parse(useLocation().search); const history = useHistory(); - const { t } = useTranslation(); - const [form] = Form.useForm(); - const [visible, setVisible] = useState(false); - const [updateLoading, setUpdateLoading] = useState(false); - const [update_bill] = useMutation(UPDATE_BILL); - const [insertBillLine] = useMutation(INSERT_NEW_BILL_LINES); - const [updateBillLine] = useMutation(UPDATE_BILL_LINE); - const [deleteBillLine] = useMutation(DELETE_BILL_LINE); const selectedBreakpoint = Object.entries(Grid.useBreakpoint()) .filter((screen) => !!screen[1]) @@ -80,114 +24,6 @@ export function BillDetailEditcontainer({ ? bpoints[selectedBreakpoint[0]] : "100%"; - const { loading, error, data, refetch } = useQuery(QUERY_BILL_BY_PK, { - variables: { billid: search.billid }, - skip: !!!search.billid, - fetchPolicy: "network-only", - nextFetchPolicy: "network-only", - }); - - const handleSave = () => { - //It's got a previously deducted bill line! - if ( - data.bills_by_pk.billlines.filter((b) => b.deductedfromlbr).length > 0 || - form.getFieldValue("billlines").filter((b) => b.deductedfromlbr).length > - 0 - ) - setVisible(true); - else { - form.submit(); - } - }; - - const handleFinish = async (values) => { - setUpdateLoading(true); - //let adjustmentsToInsert = {}; - - const { billlines, upload, ...bill } = values; - const updates = []; - updates.push( - update_bill({ - variables: { billId: search.billid, bill: bill }, - }) - ); - - //Find bill lines that were deleted. - const deletedJobLines = []; - - data.bills_by_pk.billlines.forEach((a) => { - const matchingRecord = billlines.find((b) => b.id === a.id); - if (!matchingRecord) { - deletedJobLines.push(a); - } - }); - - deletedJobLines.forEach((d) => { - updates.push(deleteBillLine({ variables: { id: d.id } })); - }); - - billlines.forEach((billline) => { - const { deductedfromlbr, inventories, jobline, ...il } = billline; - delete il.__typename; - - if (il.id) { - updates.push( - updateBillLine({ - variables: { - billLineId: il.id, - billLine: { - ...il, - deductedfromlbr: deductedfromlbr, - joblineid: il.joblineid === "noline" ? null : il.joblineid, - }, - }, - }) - ); - } else { - //It's a new line, have to insert it. - updates.push( - insertBillLine({ - variables: { - billLines: [ - { - ...il, - deductedfromlbr: deductedfromlbr, - billid: search.billid, - joblineid: il.joblineid === "noline" ? null : il.joblineid, - }, - ], - }, - }) - ); - } - }); - - await Promise.all(updates); - - insertAuditTrail({ - jobid: bill.jobid, - billid: search.billid, - operation: AuditTrailMapping.billupdated(bill.invoice_number), - }); - - await refetch(); - form.setFieldsValue(transformData(data)); - form.resetFields(); - setVisible(false); - setUpdateLoading(false); - }; - - useEffect(() => { - if (search.billid && data) { - form.resetFields(); - } - }, [form, search.billid, data]); - - if (error) return ; - if (!search.billid) return <>; //
{t("bills.labels.noneselected")}
; - - const exported = data && data.bills_by_pk && data.bills_by_pk.exported; - return ( - {loading && } - {!loading && ( - <> - - - - form.submit()} - onCancel={() => setVisible(false)} - okButtonProps={{ loading: updateLoading }} - title={t("bills.labels.editadjwarning")} - > - - - - - - } - /> -
- - - {bodyshop.uselocalmediaserver ? ( - - ) : ( - - )} - - - )} +
); } - -const transformData = (data) => { - return data - ? { - ...data.bills_by_pk, - - billlines: data.bills_by_pk.billlines.map((i) => { - return { - ...i, - joblineid: !!i.joblineid ? i.joblineid : "noline", - applicable_taxes: { - federal: - (i.applicable_taxes && i.applicable_taxes.federal) || false, - state: (i.applicable_taxes && i.applicable_taxes.state) || false, - local: (i.applicable_taxes && i.applicable_taxes.local) || false, - }, - }; - }), - date: data.bills_by_pk ? moment(data.bills_by_pk.date) : null, - } - : {}; -}; diff --git a/client/src/components/bill-form/bill-form.lines.component.jsx b/client/src/components/bill-form/bill-form.lines.component.jsx index d884b61d8..c41e60369 100644 --- a/client/src/components/bill-form/bill-form.lines.component.jsx +++ b/client/src/components/bill-form/bill-form.lines.component.jsx @@ -1,14 +1,14 @@ import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; +import { useTreatments } from "@splitsoftware/splitio-react"; import { - Button, - Form, + Button, Form, Input, InputNumber, Select, Space, Switch, Table, - Tooltip, + Tooltip } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; @@ -17,9 +17,8 @@ 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 CurrencyInput from "../form-items-formatted/currency-form-item.component"; import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component"; -import { useTreatments } from "@splitsoftware/splitio-react"; +import CurrencyInput from "../form-items-formatted/currency-form-item.component"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser 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 5c7553fd5..9dea48f71 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 @@ -12,6 +12,7 @@ import { DateFormatter } from "../../utils/DateFormatter"; import { alphaSort, dateSort } from "../../utils/sorters"; import { TemplateList } from "../../utils/TemplateConstants"; import BillDeleteButton from "../bill-delete-button/bill-delete-button.component"; +import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; const mapStateToProps = createStructuredSelector({ @@ -58,39 +59,15 @@ export function BillsListTableComponent({ )} - + /> + {record.isinhouse && ( +
+ + + + + p.registrationexpires !== c.registrationexpires + } + > + {() => { + const expires = form.getFieldValue("registrationexpires"); - - - - - - + const dateover = expires && moment(expires).isBefore(moment()); + + if (dateover) + return ( + + + + {t("contracts.labels.dateinpast")} + + + ); + + return <>; + }} + +
+
+ + + + + p.insuranceexpires !== c.insuranceexpires + } + > + {() => { + const expires = form.getFieldValue("insuranceexpires"); + + const dateover = expires && moment(expires).isBefore(moment()); + + if (dateover) + return ( + + + + {t("contracts.labels.dateinpast")} + + + ); + + return <>; + }} + +
diff --git a/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx b/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx index 9516013d3..771a321d7 100644 --- a/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx +++ b/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx @@ -34,6 +34,9 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => { + ); }; diff --git a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx index 62f6d1119..562a6d4c6 100644 --- a/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx +++ b/client/src/components/courtesy-cars-list/courtesy-cars-list.component.jsx @@ -52,6 +52,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) { text: t("courtesycars.status.sold"), value: "courtesycars.status.sold", }, + { + text: t("courtesycars.status.leasereturn"), + value: "courtesycars.status.leasereturn", + }, ], onFilter: (value, record) => value.includes(record.status), sortOrder: diff --git a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx index fda3aab74..a1d0eb7af 100644 --- a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx +++ b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx @@ -5,7 +5,7 @@ import AlertComponent from "../alert/alert.component"; import { Prompt, useLocation } from "react-router-dom"; import "./form-fields-changed.styles.scss"; -export default function FormsFieldChanged({ form }) { +export default function FormsFieldChanged({ form, skipPrompt }) { const { t } = useTranslation(); const handleReset = () => { @@ -25,7 +25,7 @@ export default function FormsFieldChanged({ form }) { return ( { if (loc.pathname === location.pathname) return false; return t("general.messages.unsavedchangespopup"); diff --git a/client/src/components/job-detail-lines/job-lines.component.jsx b/client/src/components/job-detail-lines/job-lines.component.jsx index de3aec81d..2b1ddf2b9 100644 --- a/client/src/components/job-detail-lines/job-lines.component.jsx +++ b/client/src/components/job-detail-lines/job-lines.component.jsx @@ -115,7 +115,10 @@ export function JobLinesComponent({ sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order, ellipsis: true, - render: (text, record) => record.oem_partno, + render: (text, record) => + `${record.oem_partno || ""} ${ + record.alt_partno ? `(${record.alt_partno})` : "" + }`.trim(), }, { title: t("joblines.fields.op_code_desc"), @@ -461,7 +464,12 @@ export function JobLinesComponent({ context: { jobId: job.id, job: job, - linesToOrder: selectedLines, + linesToOrder: selectedLines.map((l) => ({ + ...l, + oem_partno: `${l.oem_partno || ""} ${ + l.alt_partno ? `(${l.alt_partno})` : "" + }`.trim(), + })), }, }); @@ -477,7 +485,18 @@ export function JobLinesComponent({ setState({ ...state, filteredInfo: { - part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL,PAG"], + part_type: [ + "PAN", + "PAC", + "PAR", + "PAL", + "PAA", + "PAM", + "PAP", + "PAS", + "PASL", + "PAG", + ], }, }); }} diff --git a/client/src/components/job-line-convert-to-labor/job-line-convert-to-labor.component.jsx b/client/src/components/job-line-convert-to-labor/job-line-convert-to-labor.component.jsx index 972d5cfd9..dea8e5dcc 100644 --- a/client/src/components/job-line-convert-to-labor/job-line-convert-to-labor.component.jsx +++ b/client/src/components/job-line-convert-to-labor/job-line-convert-to-labor.component.jsx @@ -66,10 +66,8 @@ export function JobLineConvertToLabor({ const newAdjustments = _.cloneDeep( existingAdjustments.data.jobs_by_pk.lbr_adjustments ); - - newAdjustments[mod_lbr_ty] = - (newAdjustments[mod_lbr_ty] || 0) + - calculateAdjustment({ mod_lbr_ty, job, jobline }); + const adjustment = calculateAdjustment({ mod_lbr_ty, job, jobline }); + newAdjustments[mod_lbr_ty] = (newAdjustments[mod_lbr_ty] || 0) + adjustment; const jobUpdate = client.mutate({ mutation: UPDATE_JOB, @@ -83,7 +81,13 @@ export function JobLineConvertToLabor({ mutation: UPDATE_JOB_LINE, variables: { lineId: jobline.id, - line: { convertedtolbr: true }, + line: { + convertedtolbr: true, + convertedtolbr_data: { + mod_lbr_ty: mod_lbr_ty, + mod_lb_hrs: adjustment, + }, + }, }, }); diff --git a/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx b/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx index 309c433cf..b0fc2f05c 100644 --- a/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx +++ b/client/src/components/jobs-admin-dates/jobs-admin-dates.component.jsx @@ -1,3 +1,4 @@ +import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import { useMutation } from "@apollo/client"; import { Button, Form, notification } from "antd"; import React, { useEffect, useState } from "react"; @@ -37,6 +38,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) { setLoading(true); const result = await updateJob({ variables: { jobId: job.id, job: values }, + refetchQueries: ['GET_JOB_BY_PK'], + awaitRefetchQueries:true }); const changedAuditFields = form.getFieldsValue( @@ -65,6 +68,8 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) { }), }); } + form.resetFields(); + form.resetFields(); setLoading(false); //Get the owner details, populate it all back into the job. }; @@ -87,6 +92,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) { : null, }} > + {job.ownr_ea || ""} + {job.owner?.tax_number && ( + + {job.owner?.tax_number || ""} + + )} diff --git a/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx index 92ba94f17..f58c8025c 100644 --- a/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx +++ b/client/src/components/jobs-documents-local-gallery/jobs-documents-local-gallery.container.jsx @@ -14,6 +14,7 @@ import { selectAllMedia } from "../../redux/media/media.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { CreateExplorerLinkForJob } from "../../utils/localmedia"; import DocumentsLocalUploadComponent from "../documents-local-upload/documents-local-upload.component"; +import JobsLocalGalleryDownloadButton from "./jobs-documents-local-gallery.download"; import JobsDocumentsLocalGalleryReassign from "./jobs-documents-local-gallery.reassign.component"; import JobsDocumentsLocalGallerySelectAllComponent from "./jobs-documents-local-gallery.selectall.component"; @@ -78,6 +79,7 @@ export function JobsDocumentsLocalGallery({ + ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect( + mapStateToProps, + mapDispatchToProps +)(JobsLocalGalleryDownloadButton); + +export function JobsLocalGalleryDownloadButton({ + bodyshop, + galleryImages, + allMedia, + job, +}) { + const { t } = useTranslation(); + const [download, setDownload] = useState(null); + + function downloadProgress(progressEvent) { + setDownload((currentDownloadState) => { + return { + downloaded: progressEvent.loaded || 0, + speed: + (progressEvent.loaded || 0) - + ((currentDownloadState && currentDownloadState.downloaded) || 0), + }; + }); + } + + const handleDownload = async () => { + const theDownloadedZip = await cleanAxios.post( + `${bodyshop.localmediaserverhttp}/jobs/download`, + { + jobid: job.id, + files: ((allMedia && allMedia[job.id]) || []) + .filter((i) => i.isSelected) + .map((i) => i.filename), + }, + { + headers: { ims_token: bodyshop.localmediatoken }, + responseType: "arraybuffer", + onDownloadProgress: downloadProgress, + } + ); + setDownload(null); + standardMediaDownload(theDownloadedZip.data, job.ro_number); + }; + + return ( + + ); +} + +function standardMediaDownload(bufferData, filename) { + const a = document.createElement("a"); + const url = window.URL.createObjectURL(new Blob([bufferData])); + a.href = url; + a.download = `${filename}.zip`; + a.click(); +} diff --git a/client/src/components/jobs-list/jobs-list.component.jsx b/client/src/components/jobs-list/jobs-list.component.jsx index 3c2553462..9848e7ee4 100644 --- a/client/src/components/jobs-list/jobs-list.component.jsx +++ b/client/src/components/jobs-list/jobs-list.component.jsx @@ -77,6 +77,12 @@ export function JobsList({ bodyshop }) { (j.v_model_desc || "") .toLowerCase() .includes(searchText.toLowerCase()) || + (j.est_ct_fn || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.est_ct_ln || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || (j.v_make_desc || "") .toLowerCase() .includes(searchText.toLowerCase()) @@ -264,6 +270,32 @@ export function JobsList({ bodyshop }) { {record.clm_total} ), }, + { + title: t("jobs.labels.estimator"), + dataIndex: "jobs.labels.estimator", + key: "jobs.labels.estimator", + ellipsis: true, + responsive: ["xl"], + filterSearch: true, + filters: + (jobs && + jobs + .map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim()) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "N/A", + value: [s], + }; + })) || + [], + onFilter: (value, record) => + value.includes( + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() + ), + render: (text, record) => + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(), + }, { title: t("jobs.fields.comment"), dataIndex: "comment", diff --git a/client/src/components/jobs-notes/jobs.notes.component.jsx b/client/src/components/jobs-notes/jobs.notes.component.jsx index 8f60e879d..4aaf95caf 100644 --- a/client/src/components/jobs-notes/jobs.notes.component.jsx +++ b/client/src/components/jobs-notes/jobs.notes.component.jsx @@ -14,6 +14,7 @@ import { selectJobReadOnly } from "../../redux/application/application.selectors import { setModalContext } from "../../redux/modals/modals.actions"; import { DateTimeFormatter } from "../../utils/DateFormatter"; import { TemplateList } from "../../utils/TemplateConstants"; +import useLocalStorage from "../../utils/useLocalStorage"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import NoteUpsertModal from "../note-upsert-modal/note-upsert-modal.container"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; @@ -40,6 +41,8 @@ export function JobNotesComponent({ relatedRos, }) { const { t } = useTranslation(); + const [filter, setFilter] = useLocalStorage("filter_job_notes_icons", null); + const Templates = TemplateList("job_special", { ro_number, }); @@ -50,6 +53,18 @@ export function JobNotesComponent({ dataIndex: "icons", key: "icons", width: 80, + filteredValue: filter?.icons || null, + filters: [ + { + text: t("notes.labels.usernotes"), + value: false, + }, + { + text: t("notes.labels.systemnotes"), + value: true, + }, + ], + onFilter: (value, record) => record.audit === value, render: (text, record) => ( {record.critical ? ( @@ -131,6 +146,10 @@ export function JobNotesComponent({ }, ]; + const handleTableChange = (pagination, filters, sorter) => { + setFilter(filters); + }; + return (
@@ -166,6 +185,7 @@ export function JobNotesComponent({ columns={columns} rowKey="id" dataSource={data} + onChange={handleTableChange} />
diff --git a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx index e1a598562..84e4912e0 100644 --- a/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx +++ b/client/src/components/jobs-ready-list/jobs-ready-list.component.jsx @@ -89,6 +89,12 @@ export function JobsReadyList({ bodyshop }) { (j.v_model_desc || "") .toLowerCase() .includes(searchText.toLowerCase()) || + (j.est_ct_fn || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || + (j.est_ct_ln || "") + .toLowerCase() + .includes(searchText.toLowerCase()) || (j.v_make_desc || "") .toLowerCase() .includes(searchText.toLowerCase()) @@ -276,6 +282,32 @@ export function JobsReadyList({ bodyshop }) { {record.clm_total} ), }, + { + title: t("jobs.labels.estimator"), + dataIndex: "jobs.labels.estimator", + key: "jobs.labels.estimator", + ellipsis: true, + responsive: ["xl"], + filterSearch: true, + filters: + (jobs && + jobs + .map((j) => `${j.est_ct_fn || ""} ${j.est_ct_ln || ""}`.trim()) + .filter(onlyUnique) + .map((s) => { + return { + text: s || "N/A", + value: [s], + }; + })) || + [], + onFilter: (value, record) => + value.includes( + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim() + ), + render: (text, record) => + `${record.est_ct_fn || ""} ${record.est_ct_ln || ""}`.trim(), + }, { title: t("jobs.fields.comment"), dataIndex: "comment", diff --git a/client/src/components/labor-allocations-table/labor-allocations-table.component.jsx b/client/src/components/labor-allocations-table/labor-allocations-table.component.jsx index bbb09178c..420bcb7c6 100644 --- a/client/src/components/labor-allocations-table/labor-allocations-table.component.jsx +++ b/client/src/components/labor-allocations-table/labor-allocations-table.component.jsx @@ -1,12 +1,13 @@ import { EditFilled } from "@ant-design/icons"; import { Card, Col, Row, Space, Table } from "antd"; import _ from "lodash"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; +import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { alphaSort } from "../../utils/sorters"; import LaborAllocationsAdjustmentEdit from "../labor-allocations-adjustment-edit/labor-allocations-adjustment-edit.component"; import "./labor-allocations-table.styles.scss"; @@ -44,10 +45,10 @@ export function LaborAllocationsTable({ if (!jobId) setTotals([]); }, [joblines, timetickets, bodyshop, adjustments, jobId]); - // const convertedLines = useMemo( - // () => joblines && joblines.filter((j) => j.convertedtolbr), - // [joblines] - // ); + const convertedLines = useMemo( + () => joblines && joblines.filter((j) => j.convertedtolbr), + [joblines] + ); const columns = [ { @@ -120,52 +121,68 @@ export function LaborAllocationsTable({ ), }, ]; - // const convertedTableCols = [ - // { - // title: t("joblines.fields.line_desc"), - // dataIndex: "line_desc", - // key: "line_desc", - // ellipsis: true, - // }, - // { - // title: t("joblines.fields.op_code_desc"), - // dataIndex: "op_code_desc", - // key: "op_code_desc", - // ellipsis: true, - // render: (text, record) => - // `${record.op_code_desc || ""}${ - // record.alt_partm ? ` ${record.alt_partm}` : "" - // }`, - // }, + const convertedTableCols = [ + { + title: t("joblines.fields.line_desc"), + dataIndex: "line_desc", + key: "line_desc", + ellipsis: true, + }, + { + title: t("joblines.fields.op_code_desc"), + dataIndex: "op_code_desc", + key: "op_code_desc", + ellipsis: true, + render: (text, record) => + `${record.op_code_desc || ""}${ + record.alt_partm ? ` ${record.alt_partm}` : "" + }`, + }, - // { - // title: t("joblines.fields.act_price"), - // dataIndex: "act_price", - // key: "act_price", - // ellipsis: true, - // render: (text, record) => ( - // <> - // - // {record.db_ref === "900510" || record.db_ref === "900511" - // ? record.prt_dsmk_m - // : record.act_price} - // - // {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? ( - // {`(${record.prt_dsmk_p}%)`} - // ) : ( - // <> - // )} - // - // ), - // }, - // { - // title: t("joblines.fields.part_qty"), - // dataIndex: "part_qty", - // key: "part_qty", - // }, - // ]; + { + title: t("joblines.fields.act_price"), + dataIndex: "act_price", + key: "act_price", + ellipsis: true, + render: (text, record) => ( + <> + + {record.db_ref === "900510" || record.db_ref === "900511" + ? record.prt_dsmk_m + : record.act_price} + + {record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? ( + {`(${record.prt_dsmk_p}%)`} + ) : ( + <> + )} + + ), + }, + { + title: t("joblines.fields.part_qty"), + dataIndex: "part_qty", + key: "part_qty", + }, + { + title: t("joblines.fields.mod_lbr_ty"), + dataIndex: "conv_mod_lbr_ty", + key: "conv_mod_lbr_ty", + render: (text, record) => + record.convertedtolbr_data && record.convertedtolbr_data.mod_lbr_ty, + }, + { + title: t("joblines.fields.mod_lb_hrs"), + dataIndex: "conv_mod_lb_hrs", + key: "conv_mod_lb_hrs", + render: (text, record) => + record.convertedtolbr_data && + record.convertedtolbr_data.mod_lb_hrs && + record.convertedtolbr_data.mod_lb_hrs.toFixed(1), + }, + ]; const handleTableChange = (pagination, filters, sorter) => { setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); @@ -187,23 +204,21 @@ export function LaborAllocationsTable({ />
- { - // convertedLines && convertedLines.length > 0 && ( - // - // - // - // - // - // ) - } + {convertedLines && convertedLines.length > 0 && ( + + +
+ + + )} ); } diff --git a/client/src/components/owner-detail-form/owner-detail-form.component.jsx b/client/src/components/owner-detail-form/owner-detail-form.component.jsx index f5fe04d5d..7178aa322 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.component.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.component.jsx @@ -94,6 +94,12 @@ export default function OwnerDetailFormComponent({ form, loading }) { > + + + diff --git a/client/src/components/owner-detail-form/owner-detail-form.container.jsx b/client/src/components/owner-detail-form/owner-detail-form.container.jsx index 464893ebb..95f14ec00 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.container.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.container.jsx @@ -20,9 +20,10 @@ function OwnerDetailFormContainer({ owner, refetch }) { if (!!result.errors) { notification["error"]({ message: t("owners.errors.saving", { - message: JSON.stringify(result.errors), + error: JSON.stringify(result.errors), }), }); + setLoading(false); return; } diff --git a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.js b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.js index be54fac39..9f41906db 100644 --- a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.js +++ b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.js @@ -39,7 +39,9 @@ export function ScheduleCalendarHeaderComponent({ const ATSToday = useMemo(() => { if (!events) return []; return _.groupBy( - events.filter((e) => moment(date).isSame(moment(e.start), "day")), + events.filter( + (e) => !e.vacation && moment(date).isSame(moment(e.start), "day") + ), "job.alt_transport" ); }, [events, date]); diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx index c681ee4ef..29f2bb59f 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.component.jsx @@ -80,6 +80,7 @@ export default function ScoreboardTimeTickets() { totalThisMonth: 0, totalLastMonth: 0, totalOverPeriod: 0, + actualTotalOverPeriod: 0, employees: {}, }; data.fixedperiod.forEach((ticket) => { @@ -92,6 +93,7 @@ export default function ScoreboardTimeTickets() { totalThisMonth: 0, totalLastMonth: 0, totalOverPeriod: 0, + actualTotalOverPeriod: 0, }; } @@ -182,6 +184,9 @@ export default function ScoreboardTimeTickets() { ret.employees[ticket.employee.employee_number].totalOverPeriod = ret.employees[ticket.employee.employee_number].totalOverPeriod + ticket.productivehrs; + ret.employees[ticket.employee.employee_number].actualTotalOverPeriod = + ret.employees[ticket.employee.employee_number].actualTotalOverPeriod + + (ticket.actualhrs || 0); if (!totals.employees[ticket.employee.employee_number]) totals.employees[ticket.employee.employee_number] = { @@ -219,7 +224,7 @@ export default function ScoreboardTimeTickets() { roundObject(ret); roundObject(totals); roundObject(ret2); - console.log(ret); + return { fixed: ret, timeperiod: { @@ -231,6 +236,8 @@ export default function ScoreboardTimeTickets() { }; }, [fixedPeriods, data, startDate, endDate]); + console.log(calculatedData); + if (error) return ; if (loading) return ; return ( diff --git a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx index 72f1637b9..0b1b9b6bf 100644 --- a/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx +++ b/client/src/components/scoreboard-timetickets/scoreboard-timetickets.stats.component.jsx @@ -19,7 +19,6 @@ export default connect( export function ScoreboardTicketsStats({ data, bodyshop }) { const { t } = useTranslation(); - console.log(data); const columns = [ { title: t("employees.fields.employee_number"), @@ -57,6 +56,16 @@ export function ScoreboardTicketsStats({ data, bodyshop }) { key: "totalOverPeriod", sorter: (a, b) => a.totalOverPeriod - b.totalOverPeriod, }, + { + title: t("scoreboard.labels.efficiencyoverperiod"), + dataIndex: "efficiencyoverperiod", + key: "efficiencyoverperiod", + render: (text, record) => + `${( + (record.totalOverPeriod / (record.actualTotalOverPeriod || .1)) * + 100 + ).toFixed(1)} %`, + }, ]; const tableData = data @@ -112,6 +121,7 @@ export function ScoreboardTicketsStats({ data, bodyshop }) {
+