diff --git a/.prettierrc.js b/.prettierrc.js index f2c6fc849..079bb8106 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -9,10 +9,10 @@ const config = { arrowParens: "always", jsxSingleQuote: false, bracketSameLine: false, - endOfLine: "lf", - importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], - importOrderSeparation: true, - importOrderSortSpecifiers: true + endOfLine: "lf" + // importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"], + // importOrderSeparation: true, + // importOrderSortSpecifiers: true }; module.exports = config; diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index aeabb8f20..9887c892f 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -1789,6 +1789,27 @@ + + jobclosedwithbypass + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + jobconverted false @@ -6135,6 +6156,221 @@ + + md_ro_guard + + + enabled + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_ar + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_bills + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_cm + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_labor + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_ppd + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_profit + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_sublet + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + masterbypass + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + totalgppercent_minimum + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + md_tasks_presets @@ -10663,6 +10899,27 @@ + + md_ro_guard + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + md_tasks_presets false @@ -11067,6 +11324,32 @@ + + roguard + + + title + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + scheduling false @@ -21835,6 +22118,27 @@ + + act_price_before_ppc + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + ah_detail_line false @@ -30796,6 +31100,27 @@ labels + + accountsreceivable + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + act_price_ppc false @@ -33110,6 +33435,27 @@ + + masterbypass + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + materials @@ -33241,6 +33587,132 @@ + + outstanding_ar + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + outstanding_credit_memos + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + outstanding_ppd + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + outstanding_reconciliation_discrep + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + outstanding_sublets + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + outstandinghours + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + override_header false @@ -33430,6 +33902,27 @@ + + performance + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + pimraryamountpayable false @@ -33687,6 +34180,27 @@ + + ppdnotexported + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + profileadjustments false @@ -33708,6 +34222,48 @@ + + profitbypassrequired + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + profits + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + prt_dsmk_total false @@ -34070,6 +34626,242 @@ + + ro_guard + + + enforce_ar + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_bills + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_cm + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_labor + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_ppd + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_profit + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_sublet + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforce_validation + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + enforced + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + + + roguard + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + + + roguardwarnings + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + rosaletotal false @@ -34280,6 +35072,27 @@ + + subletsnotcompleted + false + + + + + + en-US + false + + + es-MX + false + + + fr-CA + false + + + subletstotal false diff --git a/client/.eslintrc b/client/.eslintrc new file mode 100644 index 000000000..cc0f939b4 --- /dev/null +++ b/client/.eslintrc @@ -0,0 +1,5 @@ +{ + "extends": [ + "react-app" + ] +} diff --git a/client/package-lock.json b/client/package-lock.json index 13a72ca36..a4f887a06 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -96,6 +96,8 @@ "browserslist-to-esbuild": "^2.1.1", "cross-env": "^7.0.3", "cypress": "^13.6.6", + "eslint": "^8.57.0", + "eslint-config-react-app": "^7.0.1", "eslint-plugin-cypress": "^2.15.1", "memfs": "^4.6.0", "os-browserify": "^0.3.0", @@ -104,6 +106,7 @@ "source-map-explorer": "^2.5.3", "vite": "^5.0.11", "vite-plugin-babel": "^1.2.0", + "vite-plugin-eslint": "^1.8.1", "vite-plugin-legacy": "^2.1.0", "vite-plugin-node-polyfills": "^0.19.0", "vite-plugin-pwa": "^0.19.0", @@ -11934,7 +11937,8 @@ }, "node_modules/eslint": { "version": "8.57.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -11987,7 +11991,8 @@ }, "node_modules/eslint-config-react-app": { "version": "7.0.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz", + "integrity": "sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA==", "dependencies": { "@babel/core": "^7.16.0", "@babel/eslint-parser": "^7.16.3", @@ -26439,6 +26444,36 @@ "vite": ">=5.0.0" } }, + "node_modules/vite-plugin-eslint": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/vite-plugin-eslint/-/vite-plugin-eslint-1.8.1.tgz", + "integrity": "sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^4.2.1", + "@types/eslint": "^8.4.5", + "rollup": "^2.77.2" + }, + "peerDependencies": { + "eslint": ">=7", + "vite": ">=2" + } + }, + "node_modules/vite-plugin-eslint/node_modules/rollup": { + "version": "2.79.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", + "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/vite-plugin-legacy": { "version": "2.1.0", "dev": true, diff --git a/client/package.json b/client/package.json index 686a6c6e4..9af0befa4 100644 --- a/client/package.json +++ b/client/package.json @@ -140,6 +140,8 @@ "browserslist-to-esbuild": "^2.1.1", "cross-env": "^7.0.3", "cypress": "^13.6.6", + "eslint": "^8.57.0", + "eslint-config-react-app": "^7.0.1", "eslint-plugin-cypress": "^2.15.1", "memfs": "^4.6.0", "os-browserify": "^0.3.0", @@ -148,6 +150,7 @@ "source-map-explorer": "^2.5.3", "vite": "^5.0.11", "vite-plugin-babel": "^1.2.0", + "vite-plugin-eslint": "^1.8.1", "vite-plugin-legacy": "^2.1.0", "vite-plugin-node-polyfills": "^0.19.0", "vite-plugin-pwa": "^0.19.0", 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 index 44833c1b5..7a8e20cd3 100644 --- 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 @@ -34,6 +34,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b actions: {}, context: { jobId: data.bills_by_pk.jobid, + job: data.bills_by_pk.job, vendorId: data.bills_by_pk.vendorid, returnFromBill: data.bills_by_pk.id, invoiceNumber: data.bills_by_pk.invoice_number, 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 5db868406..03541a5ca 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 @@ -57,7 +57,7 @@ export function BillsListTableComponent({ )} diff --git a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx index cbdd26770..6d296edd1 100644 --- a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx +++ b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx @@ -1,9 +1,8 @@ -import React, { useState } from "react"; -import { Button, Form, InputNumber, Popover } from "antd"; -import { logImEXEvent } from "../../firebase/firebase.utils"; -import { useTranslation } from "react-i18next"; import { CalculatorFilled } from "@ant-design/icons"; - +import { Button, Form, InputNumber, Popover, Space } from "antd"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { logImEXEvent } from "../../firebase/firebase.utils"; export default function CABCpvrtCalculator({ disabled, form }) { const [visibility, setVisibility] = useState(false); @@ -27,10 +26,14 @@ export default function CABCpvrtCalculator({ disabled, form }) { - - +
+ + + + +
); diff --git a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx index b08e53db6..eac3846aa 100644 --- a/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx +++ b/client/src/components/courtesy-car-form/courtesy-car-form.component.jsx @@ -11,11 +11,15 @@ import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component"; import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component"; import FormDatePicker from "../form-date-picker/form-date-picker.component"; -//import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; +import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; -export default function CourtesyCarCreateFormComponent({ form, saveLoading }) { +export default function CourtesyCarCreateFormComponent({ + form, + saveLoading, + newCC, +}) { const { t } = useTranslation(); const client = useApolloClient(); @@ -30,7 +34,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading }) { } /> - {/* */} + {newCC ? null : } ; - if (!!!jobTotals) return ; + if (!!!jobTotals) { + if (showWarning && warningCallback && typeof warningCallback === "function") { + warningCallback({ key: "bills", warning: t("jobs.errors.nofinancial") }); + } + return ; + } const totals = jobTotals; @@ -97,9 +109,21 @@ export default function JobBillsTotalComponent({ loading, bills, partsOrders, jo const discrepWithCms = discrepWithLbrAdj.add(totalReturns); const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number. + if (showWarning && warningCallback && typeof warningCallback === "function") { + if (discrepWithCms.getAmount() !== 0) { + warningCallback({ + key: "bills", + warning: t("jobs.labels.outstanding_reconciliation_discrep") + }); + } + if (calculatedCreditsNotReceived.getAmount() > 0) { + warningCallback({ key: "cm", warning: t("jobs.labels.outstanding_credit_memos") }); + } + } + return ( - - + + + + {showWarning && + (discrepWithCms.getAmount() !== 0 || + discrepWithLbrAdj.getAmount() !== 0 || + discrepancy.getAmount() !== 0) && ( + + )} - + + {showWarning && calculatedCreditsNotReceived.getAmount() > 0 && ( + + )} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-gaurd.labor.jsx b/client/src/components/job-close-ro-guard/job-close-ro-gaurd.labor.jsx new file mode 100644 index 000000000..82721ccbc --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-gaurd.labor.jsx @@ -0,0 +1,62 @@ +import React from "react"; + +import { useQuery } from "@apollo/client"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import AlertComponent from "../alert/alert.component"; +import LaborAllocationsTableComponent from "../labor-allocations-table/labor-allocations-table.component"; +import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component"; +import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardLabor); + +export function JobCloseRoGuardLabor({ job, jobRO, bodyshop, form, warningCallback }) { + const { loading, error, data, refetch } = useQuery(GET_LINE_TICKET_BY_PK, { + variables: { id: job.id }, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); + const { + treatments: { Enhanced_Payroll } + } = useSplitTreatments({ + attributes: {}, + names: ["Enhanced_Payroll"], + splitKey: bodyshop.imexshopid + }); + + if (loading) return ; + if (error) return ; + + return Enhanced_Payroll.treatment === "on" ? ( + + ) : ( + + ); +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.ar.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.ar.jsx new file mode 100644 index 000000000..a5ca9a74d --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.ar.jsx @@ -0,0 +1,62 @@ +import React, { useEffect } from "react"; + +import { Alert, Card } from "antd"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import { useMemo } from "react"; +import Dinero from "dinero.js"; +import DataLabel from "../data-label/data-label.component"; + +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardProfit); + +export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallback }) { + const { t } = useTranslation(); + + const total = useMemo(() => { + return ( + job.payments && + job.payments.reduce((acc, val) => { + acc = acc.add(Dinero({ amount: Math.round(val.amount * 100) })); + return acc; + }, Dinero()) + ); + }, [job.payments]); + + const balance = useMemo(() => { + if (job && job.job_totals && job.job_totals.totals.total_repairs) + return Dinero(job.job_totals.totals.total_repairs).subtract(total); + return Dinero({ amount: 0 }).subtract(total); + }, [job, total]); + + useEffect(() => { + if (balance.getAmount() !== 0) { + warningCallback({ key: "ar", warning: t("jobs.labels.outstanding_ar") }); + } + }, [balance, t, warningCallback]); + + return ( + + {total.toFormat()} + + {balance.toFormat()} + + {balance.getAmount() !== 0 && ( + + )} + + ); +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.bills.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.bills.jsx new file mode 100644 index 000000000..4bf03ea46 --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.bills.jsx @@ -0,0 +1,40 @@ +import React from "react"; + +import { useQuery } from "@apollo/client"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import AlertComponent from "../alert/alert.component"; +import JobBillsTotalComponent from "../job-bills-total/job-bills-total.component"; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardBills); + +export function JobCloseRoGuardBills({ job, jobRO, bodyshop, form, warningCallback }) { + const { loading, error, data } = useQuery(QUERY_BILLS_BY_JOBID, { + variables: { jobid: job.id }, + fetchPolicy: "network-only", + nextFetchPolicy: "network-only" + }); + + if (error) return ; + + return ( + + ); +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx new file mode 100644 index 000000000..16451593f --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx @@ -0,0 +1,234 @@ +import React, { useCallback, useState } from "react"; +import { LockOutlined } from "@ant-design/icons"; +import { Badge, Card, Col, Collapse, Form, Input, Row, Space, Tooltip } from "antd"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import JobCloseRoGuardLabor from "./job-close-ro-gaurd.labor"; +import JobCloseRoGuardAr from "./job-close-ro-guard.ar"; +import JobCloseRoGuardBills from "./job-close-ro-guard.bills"; +import JobCloseRoGuardPpd from "./job-close-ro-guard.ppd"; +import JobCloseRoGuardProfit from "./job-close-ro-guard.profit"; +import "./job-close-ro-guard.styles.scss"; +import JobCloseRoGuardSublet from "./job-close-ro-guard.sublet"; +import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle"; +import InstanceRenderManager from "../../utils/instanceRenderMgr"; + +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardContainer); + +export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) { + const { t } = useTranslation(); + const [warnings, setWarnings] = useState([]); + + const warningCallback = useCallback( + ({ key, warning }) => + setWarnings((state) => { + if (state.find((s) => s.key === key)) return state; + return [...state, { key, warning }]; + }), + [] + ); + + if (!bodyshop?.md_ro_guard?.enabled) return null; + + return ( + <> + {warnings.length > 0 && ( + + + {t("jobs.labels.roguardwarnings")} + + } + > +
    + {warnings.map((w, index) => ( +
  • + {bodyshop.md_ro_guard[`enforce_${w.key}`] && ( + + + + )} + {w.warning} +
  • + ))} +
+
+ )} + + + + + + + + + + + + + + + + + + {InstanceRenderManager({ + rome: ( + + {/* */} + + + ) + })} + + + + + ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_bills && + warnings.find((w) => w.key === "bills") + ) { + return Promise.reject( + t("jobs.labels.ro_guard.enforce_validation", { + message: t("jobs.labels.ro_guard.enforce_bills") + }) + ); + } + return Promise.resolve(); + } + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_cm && + warnings.find((w) => w.key === "cm") + ) { + return Promise.reject( + t("jobs.labels.ro_guard.enforce_validation", { + message: t("jobs.labels.ro_guard.enforce_cm") + }) + ); + } + return Promise.resolve(); + } + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_profit && + warnings.find((w) => w.key === "profit") + ) { + return Promise.reject( + t("jobs.labels.ro_guard.enforce_validation", { + message: t("jobs.labels.ro_guard.enforce_profit") + }) + ); + } + return Promise.resolve(); + } + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_ar && + warnings.find((w) => w.key === "ar") + ) { + return Promise.reject( + t("jobs.labels.ro_guard.enforce_validation", { + message: t("jobs.labels.ro_guard.enforce_ar") + }) + ); + } + return Promise.resolve(); + } + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_sublet && + warnings.find((w) => w.key === "sublet") + ) { + return Promise.reject( + t("jobs.labels.ro_guard.enforce_validation", { + message: t("jobs.labels.ro_guard.enforce_sublet") + }) + ); + } + return Promise.resolve(); + } + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_ppd && + warnings.find((w) => w.key === "ppd") + ) { + return Promise.reject( + t("jobs.labels.ro_guard.enforce_validation", { + message: t("jobs.labels.ro_guard.enforce_ppd") + }) + ); + } + return Promise.resolve(); + } + }), + ({ getFieldValue }) => ({ + validator(_, value) { + if ( + !PasswordCheck({ bodyshop, value }) && + bodyshop.md_ro_guard.enforce_labor && + warnings.find((w) => w.key === "labor") + ) { + return Promise.reject( + t("jobs.labels.ro_guard.enforce_validation", { + message: t("jobs.labels.ro_guard.enforce_labor") + }) + ); + } + return Promise.resolve(); + } + }) + ]} + > + } type="password" placeholder="Password" disabled={jobRO} /> + + + + + + + + + + + + + ); +} + +function PasswordCheck({ bodyshop, value }) { + return value === bodyshop?.md_ro_guard?.masterbypass; +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.ppd.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.ppd.jsx new file mode 100644 index 000000000..21d106b9f --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.ppd.jsx @@ -0,0 +1,83 @@ +import React, { useEffect } from "react"; + +import { Alert, Card, Table } from "antd"; +import { t } from "i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import { alphaSort } from "../../utils/sorters"; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRGuardPpd); + +export function JobCloseRGuardPpd({ job, jobRO, bodyshop, form, warningCallback }) { + const linesWithPPD = job?.joblines.filter((j) => j.act_price_before_ppc !== 0 && j.act_price_before_ppc !== null); + + useEffect(() => { + if (linesWithPPD.length > 0) { + warningCallback({ key: "ppd", warning: t("jobs.labels.outstanding_sublets") }); + } + }, [linesWithPPD.length, warningCallback]); + + const columns = [ + { + title: t("joblines.fields.line_desc"), + dataIndex: "line_desc", + fixed: "left", + key: "line_desc", + sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), + onCell: (record) => ({ + className: record.manual_line && "job-line-manual", + style: { + ...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}) + } + }), + ellipsis: true + }, + { + title: t("joblines.fields.act_price"), + dataIndex: "act_price", + key: "act_price", + sorter: (a, b) => a.act_price - b.act_price, + + ellipsis: true, + render: (text, record) => {record.act_price} + }, + { + title: t("joblines.fields.act_price_before_ppc"), + dataIndex: "act_price_before_ppc", + key: "act_price_before_ppc", + sorter: (a, b) => a.act_price_before_ppc - b.act_price_before_ppc, + + ellipsis: true, + render: (text, record) => {record.act_price_before_ppc} + }, + { + title: t("joblines.fields.part_qty"), + dataIndex: "part_qty", + key: "part_qty" + }, + { + title: t("joblines.fields.notes"), + dataIndex: "notes", + key: "notes" + } + ]; + + return ( + + + {linesWithPPD.length > 0 && ( + + )} + + ); +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.profit.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.profit.jsx new file mode 100644 index 000000000..2add7848d --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.profit.jsx @@ -0,0 +1,62 @@ +import React, { useEffect, useState } from "react"; + +import { Alert, Card } from "antd"; +import axios from "axios"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component"; +import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; + +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardProfit); + +export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallback }) { + const [costingData, setCostingData] = useState(null); + const [loading, setLoading] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + async function getData() { + try { + if (job.id) { + setLoading(true); + const { data } = await axios.post("/job/costing", { jobid: job.id }); + setCostingData(data); + } + } catch (error) {} + setLoading(false); + } + + getData(); + }, [job.id]); + + const enforceProfitPassword = + parseFloat(costingData?.summaryData.gppercent) < bodyshop?.md_ro_guard?.totalgppercent_minimum; + + useEffect(() => { + if (enforceProfitPassword && typeof warningCallback === "function") { + warningCallback({ key: "profit", warning: t("jobs.labels.profitbypassrequired") }); + } + }, [enforceProfitPassword, t, warningCallback]); + + if (loading || !costingData) return ; + + return ( + + + {enforceProfitPassword && ( + + )} + + ); +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.styles.scss b/client/src/components/job-close-ro-guard/job-close-ro-guard.styles.scss new file mode 100644 index 000000000..51f790b77 --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.styles.scss @@ -0,0 +1,10 @@ +.ro-guard-col { + .ant-card { + height: 100%; + } +} +.ro-guard-col-50 { + .ant-card { + height: 50%; + } +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.sublet.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.sublet.jsx new file mode 100644 index 000000000..c6468c012 --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.sublet.jsx @@ -0,0 +1,76 @@ +import React, { useEffect } from "react"; + +import { Alert, Card, Table } from "antd"; +import { t } from "i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import CurrencyFormatter from "../../utils/CurrencyFormatter"; +import { alphaSort } from "../../utils/sorters"; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRGuardSublet); + +export function JobCloseRGuardSublet({ job, jobRO, bodyshop, form, warningCallback }) { + const subletsNotDone = job?.joblines.filter( + (j) => (j.part_type === "PAS" || j.part_type === "PASL") && (!j.sublet_completed || !j.sublet_ignored) + ); + + const columns = [ + { + title: t("joblines.fields.line_desc"), + dataIndex: "line_desc", + fixed: "left", + key: "line_desc", + sorter: (a, b) => alphaSort(a.line_desc, b.line_desc), + onCell: (record) => ({ + className: record.manual_line && "job-line-manual", + style: { + ...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {}) + } + }), + ellipsis: true + }, + { + title: t("joblines.fields.act_price"), + dataIndex: "act_price", + key: "act_price", + sorter: (a, b) => a.act_price - b.act_price, + + ellipsis: true, + render: (text, record) => {record.act_price} + }, + { + title: t("joblines.fields.part_qty"), + dataIndex: "part_qty", + key: "part_qty" + }, + { + title: t("joblines.fields.notes"), + dataIndex: "notes", + key: "notes" + } + ]; + + useEffect(() => { + if (subletsNotDone.length > 0) { + warningCallback({ key: "sublet", warning: t("jobs.labels.outstanding_sublets") }); + } + }, [subletsNotDone.length, warningCallback]); + + return ( + +
+ {subletsNotDone.length > 0 && ( + + )} + + ); +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.totals.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.totals.jsx new file mode 100644 index 000000000..e765c941c --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.totals.jsx @@ -0,0 +1,26 @@ +import React from 'react'; + +import { connect } from 'react-redux'; +import { createStructuredSelector } from 'reselect'; +import { selectJobReadOnly } from '../../redux/application/application.selectors'; +import { selectBodyshop } from '../../redux/user/user.selectors'; +import JobTotalsTableTotals from '../job-totals-table/job-totals.table.totals.component'; +import { Card } from 'antd'; +import { t } from 'i18next'; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly, +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRGuardTotals); + +export function JobCloseRGuardTotals({ job, jobRO, bodyshop, form }) { + return ( + + + + ); +} diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.tt-lifecycle.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.tt-lifecycle.jsx new file mode 100644 index 000000000..2fe16e07d --- /dev/null +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.tt-lifecycle.jsx @@ -0,0 +1,24 @@ +import React from "react"; + +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectJobReadOnly } from "../../redux/application/application.selectors"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import JobLifecycleComponent from "../job-lifecycle/job-lifecycle.component"; +const mapStateToProps = createStructuredSelector({ + //currentUser: selectCurrentUser + bodyshop: selectBodyshop, + jobRO: selectJobReadOnly +}); +const mapDispatchToProps = (dispatch) => ({ + //setUserLanguage: language => dispatch(setUserLanguage(language)) +}); +export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardTTLifeCycle); + +export function JobCloseRoGuardTTLifeCycle({ job, jobRO, bodyshop, form }) { + return ( +
+ +
+ ); +} diff --git a/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx b/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx index 38a832d91..6d778bb7e 100644 --- a/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx +++ b/client/src/components/job-costing-statistics/job-costing-statistics.component.jsx @@ -1,11 +1,35 @@ -import { Statistic } from "antd"; -import React from "react"; -import { useTranslation } from "react-i18next"; -import Dinero from "dinero.js"; +import { Space, Statistic } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Dinero from 'dinero.js'; -export default function JobCostingStatistics({ summaryData }) { +export default function JobCostingStatistics({ summaryData, onlyGP }) { const { t } = useTranslation(); + const gpTotals = ( + <> + + + + {' '} + + + + + + ); + + if (onlyGP) return gpTotals; + return (
diff --git a/client/src/components/job-detail-lines/job-lines-expander.component.jsx b/client/src/components/job-detail-lines/job-lines-expander.component.jsx index 1152dc546..16d2d6fa5 100644 --- a/client/src/components/job-detail-lines/job-lines-expander.component.jsx +++ b/client/src/components/job-detail-lines/job-lines-expander.component.jsx @@ -1,5 +1,5 @@ import { useQuery } from "@apollo/client"; -import { Col, Divider, Row, Skeleton, Space, Timeline, Typography } from "antd"; +import { Col, Row, Skeleton, Space, Timeline, Typography } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; @@ -43,13 +43,25 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) { ? data.parts_order_lines.map((line) => ({ key: line.id, children: ( - } wrap> - - {line.parts_order.order_number} - - {line.parts_order.order_date} - {line.parts_order.vendor.name} - + +
+ + {line.parts_order.order_number} + + + + {line.parts_order.order_date} + + {line.parts_order.vendor.name} + {line.backordered_eta ? ( + + + {`${t("parts_orders.fields.backordered_eta")}: `} + {line.backordered_eta} + + + ) : null} + ) })) : [ @@ -61,6 +73,37 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) { } />{" "} + + {t("parts_dispatch.labels.parts_dispatch")} + 0 + ? data.parts_dispatch_lines.map((line) => ({ + key: line.id, + children: ( + + + {line.parts_dispatch.number} + + + {bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name} + + + + {t("parts_dispatch_lines.fields.accepted_at")} + {line.accepted_at} + + + + ) + })) + : { + key: "dispatch-lines", + children: t("parts_orders.labels.notyetordered") + } + } + /> + {t("bills.labels.bills")} - - {t("parts_dispatch.labels.parts_dispatch")} - 0 - ? data.parts_dispatch_lines.map((line) => ({ - key: line.id, - children: ( - } wrap> - {line.parts_dispatch.number} - {bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name} - - {t("parts_dispatch_lines.fields.accepted_at")} - {line.accepted_at} - - - ) - })) - : { - key: "dispatch-lines", - children: t("parts_orders.labels.notyetordered") - } - } - /> - ); } diff --git a/client/src/components/job-payments/job-payments.component.jsx b/client/src/components/job-payments/job-payments.component.jsx index bfb74d8fc..979b23d44 100644 --- a/client/src/components/job-payments/job-payments.component.jsx +++ b/client/src/components/job-payments/job-payments.component.jsx @@ -133,6 +133,7 @@ export function JobPayments({ } ]; + //Same as in RO guard. If changed, update in both. const total = useMemo(() => { return ( job.payments && diff --git a/client/src/components/job-search-select/job-search-select.component.jsx b/client/src/components/job-search-select/job-search-select.component.jsx index 117d0f407..90ced9d1c 100644 --- a/client/src/components/job-search-select/job-search-select.component.jsx +++ b/client/src/components/job-search-select/job-search-select.component.jsx @@ -1,3 +1,4 @@ +import { LoadingOutlined } from "@ant-design/icons"; import { useLazyQuery } from "@apollo/client"; import { Select, Space, Spin, Tag } from "antd"; import _ from "lodash"; @@ -6,8 +7,6 @@ import { useTranslation } from "react-i18next"; import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries"; import AlertComponent from "../alert/alert.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; -import { SearchOutlined } from "@ant-design/icons"; -import { LoadingOutlined } from "@ant-design/icons"; const { Option } = Select; @@ -19,9 +18,14 @@ const JobSearchSelect = ( const [theOptions, setTheOptions] = useState([]); const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_JOBS_FOR_AUTOCOMPLETE, {}); - const [callIdSearch, { loading: idLoading, error: idError, data: idData }] = useLazyQuery( - SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE - ); + const [ + callIdSearch, + { + //loading: idLoading, + error: idError, + data: idData + } + ] = useLazyQuery(SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE); const executeSearch = (v) => { if (v && v.variables?.search !== "" && v.variables.search.length >= 2) callSearch(v); @@ -86,9 +90,9 @@ const JobSearchSelect = ( {`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${ o.ro_number || t("general.labels.na") - } | ${OwnerNameDisplayFunction(o)} | ${ - o.v_model_yr || "" - } ${o.v_make_desc || ""} ${o.v_model_desc || ""}`} + } | ${OwnerNameDisplayFunction(o)} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${ + o.v_model_desc || "" + }`} {o.status} diff --git a/client/src/components/jobs-available-table/jobs-available-table.container.jsx b/client/src/components/jobs-available-table/jobs-available-table.container.jsx index a8784fda8..5e7681853 100644 --- a/client/src/components/jobs-available-table/jobs-available-table.container.jsx +++ b/client/src/components/jobs-available-table/jobs-available-table.container.jsx @@ -592,32 +592,34 @@ function ResolveCCCLineIssues(estData, bodyshop) { //Group by line no // For everything but the first one, strip out the price number in - // InstanceRenderManager({executeFunction:true, args:[], promanager: () => { - // const groupedByLineRef = _.groupBy(estData.joblines.data, "line_ref"); - // Object.keys(groupedByLineRef).forEach((lineRef) => { - // let index0ActPrice; - // groupedByLineRef[lineRef].forEach((line, index) => { - // //Let the first one keep it - // if (index === 0){ - // index0ActPrice = line.act_price; - // return;} - // //Web Est seems to have additional costs with UNQ_SEQ 0. Keep them all? - // if (line.unq_seq === 0) return; - // if(index0ActPrice !== line.act_price){ - // line.notes += ` | Price override.`; - // return; - // } - // const indexInEstData = estData.joblines.data.findIndex( - // (l) => l.unq_seq === line.unq_seq - // ); - // estData.joblines.data[ - // indexInEstData - // ].notes += ` | Scrubbed due to the line_ref issue. (prev act price = ${estData.joblines.data[indexInEstData].act_price})`; - // estData.joblines.data[indexInEstData].act_price = 0; - // estData.joblines.data[indexInEstData].db_price = 0; - // }); - // }) - // }}) + InstanceRenderManager({ + executeFunction: true, + args: [], + promanager: () => { + const groupedByLineRef = _.groupBy(estData.joblines.data, "line_ref"); + Object.keys(groupedByLineRef).forEach((lineRef) => { + let index0ActPrice; + groupedByLineRef[lineRef].forEach((line, index) => { + //Let the first one keep it + if (index === 0) { + index0ActPrice = line.act_price; + return; + } + //Web Est seems to have additional costs with UNQ_SEQ 0. Keep them all? + if (line.unq_seq === 0) return; + if (index0ActPrice !== line.act_price) { + // line.notes += ` | Price override.`; + return; + } + const indexInEstData = estData.joblines.data.findIndex((l) => l.unq_seq === line.unq_seq); + //estData.joblines.data[indexInEstData].notes += + // ` | Act Price delete. (prev act price = ${estData.joblines.data[indexInEstData].act_price})`; + estData.joblines.data[indexInEstData].act_price = 0; + estData.joblines.data[indexInEstData].db_price = 0; + }); + }); + } + }); InstanceRenderManager({ executeFunction: true, 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 649f49584..e9e842d6a 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,5 +1,5 @@ import { EditFilled } from "@ant-design/icons"; -import { Card, Col, Row, Space, Table, Typography } from "antd"; +import { Alert, Card, Col, Row, Space, Table, Typography } from "antd"; import _ from "lodash"; import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -18,7 +18,17 @@ const mapStateToProps = createStructuredSelector({ technician: selectTechnician }); -export function LaborAllocationsTable({ jobId, joblines, timetickets, bodyshop, adjustments, technician }) { +export function LaborAllocationsTable({ + jobId, + joblines, + timetickets, + bodyshop, + adjustments, + technician, + showWarning, + warningCallback, + disabled +}) { const { t } = useTranslation(); const [totals, setTotals] = useState([]); const [state, setState] = useState({ @@ -73,7 +83,7 @@ export function LaborAllocationsTable({ jobId, joblines, timetickets, bodyshop, render: (text, record) => ( {record.adjustments.toFixed(1)} - {!technician && ( + {!technician && !disabled && ( @@ -227,6 +241,9 @@ export function LaborAllocationsTable({ jobId, joblines, timetickets, bodyshop, )} + {showWarning && summary.difference !== 0 && ( + + )} ); } diff --git a/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx b/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx index 164c75fb2..3e8adc17b 100644 --- a/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx +++ b/client/src/components/labor-allocations-table/labor-allocations-table.payroll.component.jsx @@ -1,4 +1,4 @@ -import { Button, Card, Col, notification, Row, Space, Table, Typography } from "antd"; +import { Alert, Button, Card, Col, notification, Row, Space, Table, Typography } from "antd"; import { SyncOutlined } from "@ant-design/icons"; import axios from "axios"; import _ from "lodash"; @@ -23,7 +23,9 @@ export function PayrollLaborAllocationsTable({ bodyshop, adjustments, technician, - refetch + refetch, + showWarning, + warningCallback }) { const { t } = useTranslation(); const [totals, setTotals] = useState([]); @@ -191,6 +193,10 @@ export function PayrollLaborAllocationsTable({ { hrs_total: 0, hrs_claimed: 0, adjustments: 0, difference: 0 } ); + if (summary.difference !== 0 && typeof warningCallback === "function") { + warningCallback({ key: "labor", warning: t("jobs.labels.outstandinghours") }); + } + return ( @@ -285,6 +291,9 @@ export function PayrollLaborAllocationsTable({ )} + {showWarning && summary.difference !== 0 && ( + + )} ); } diff --git a/client/src/components/parts-order-modal/parts-order-modal.component.jsx b/client/src/components/parts-order-modal/parts-order-modal.component.jsx index 946b29bad..86ab18aac 100644 --- a/client/src/components/parts-order-modal/parts-order-modal.component.jsx +++ b/client/src/components/parts-order-modal/parts-order-modal.component.jsx @@ -182,7 +182,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState, } ]} > - + ({ setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })) }); -const PaymentMarkForExportButton = ({ bodyshop, payment, refetch, setPaymentContext, currentUser }) => { +const PaymentMarkForExportButton = ({ bodyshop, payment, refetch, setPaymentContext, currentUser, paymentModal }) => { const { t } = useTranslation(); const [insertExportLog, { loading: exportLogLoading }] = useMutation(INSERT_EXPORT_LOG); const [updatePayment, { loading: updatePaymentLoading }] = useMutation(UPDATE_PAYMENT); @@ -56,16 +58,21 @@ const PaymentMarkForExportButton = ({ bodyshop, payment, refetch, setPaymentCont refetch }, context: { + ...paymentModal.context, + ...paymentModal.context, ...payment, + smartRefetch: true, exportedat: today } }); - if (refetch) - refetch( - paymentUpdateResponse && - paymentUpdateResponse.data.update_payments.returning[0] - ); + if (refetch) { + if (paymentModal.context.refetchRequiresContext) { + refetch(paymentUpdateResponse && paymentUpdateResponse.data.update_payments.returning[0]); + } else { + refetch(); + } + } } else { notification["error"]({ message: t("payments.errors.exporting", { diff --git a/client/src/components/payment-modal/payment-modal.container.jsx b/client/src/components/payment-modal/payment-modal.container.jsx index 1502f1e48..6535005d2 100644 --- a/client/src/components/payment-modal/payment-modal.container.jsx +++ b/client/src/components/payment-modal/payment-modal.container.jsx @@ -1,33 +1,30 @@ -import { useMutation } from "@apollo/client"; - -import { Button, Form, Modal, notification, Space } from "antd"; -import React, { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { INSERT_NEW_PAYMENT, UPDATE_PAYMENT } from "../../graphql/payments.queries"; -import { setEmailOptions } from "../../redux/email/email.actions"; -import { toggleModalVisible } from "../../redux/modals/modals.actions"; -import { selectPayment } from "../../redux/modals/modals.selectors"; -import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; -import { GenerateDocument } from "../../utils/RenderTemplate"; -import { TemplateList } from "../../utils/TemplateConstants"; +import {useMutation} from "@apollo/client"; +import {Button, Form, Modal, notification, Space} from "antd"; +import React, {useEffect, useState} from "react"; +import {useTranslation} from "react-i18next"; +import {connect} from "react-redux"; +import {createStructuredSelector} from "reselect"; +import {INSERT_NEW_PAYMENT, UPDATE_PAYMENT} from "../../graphql/payments.queries"; +import {toggleModalVisible} from "../../redux/modals/modals.actions"; +import {selectPayment} from "../../redux/modals/modals.selectors"; +import {selectBodyshop} from "../../redux/user/user.selectors"; +import {GenerateDocument} from "../../utils/RenderTemplate"; +import {TemplateList} from "../../utils/TemplateConstants"; import PaymentForm from "../payment-form/payment-form.component"; -import PaymentMarkForExportButton from "../payment-mark-export-button/payment-mark-export-button-component"; +import PaymentMarkForExportButton + from "../payment-mark-export-button/payment-mark-export-button-component"; import PaymentReexportButton from "../payment-reexport-button/payment-reexport-button.component"; const mapStateToProps = createStructuredSelector({ paymentModal: selectPayment, bodyshop: selectBodyshop, - currentUser: selectCurrentUser }); const mapDispatchToProps = (dispatch) => ({ - setEmailOptions: (e) => dispatch(setEmailOptions(e)), toggleModalVisible: () => dispatch(toggleModalVisible("payment")) }); -function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, currentUser, setEmailOptions }) { +function PaymentModalContainer({paymentModal, toggleModalVisible, bodyshop }) { const [form] = Form.useForm(); const [enterAgain, setEnterAgain] = useState(false); const [insertPayment] = useMutation(INSERT_NEW_PAYMENT); @@ -35,7 +32,6 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, cur const { t } = useTranslation(); const { context, actions, open } = paymentModal; - const [loading, setLoading] = useState(false); const handleFinish = async (values) => { @@ -85,13 +81,19 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, cur }); if (!!!updatedPayment.errors) { - notification["success"]({ message: t("payments.successes.payment") }); + notification["success"]({ message: t("payments.successes.paymentupdate") }); } else { - notification["error"]({ message: t("payments.errors.payment") }); + notification["error"]({ message: t("payments.errors.paymentupdate") }); } } - if (actions.refetch) actions.refetch(updatedPayment && updatedPayment.data.update_payments.returning[0]); + if (actions.refetch) { + if (context.refetchRequiresContext) { + actions.refetch(updatedPayment && updatedPayment.data.update_payments.returning[0]); + } else { + actions.refetch(); + } + } if (enterAgain) { const prev = form.getFieldsValue(["date"]); @@ -166,13 +168,7 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, cur )} -
+ diff --git a/client/src/components/payment-reexport-button/payment-reexport-button.component.jsx b/client/src/components/payment-reexport-button/payment-reexport-button.component.jsx index 5325231e4..3abd5bfd5 100644 --- a/client/src/components/payment-reexport-button/payment-reexport-button.component.jsx +++ b/client/src/components/payment-reexport-button/payment-reexport-button.component.jsx @@ -3,14 +3,20 @@ import { Button, notification } from "antd"; import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; import { UPDATE_PAYMENT } from "../../graphql/payments.queries"; import { setModalContext } from "../../redux/modals/modals.actions"; +import { selectPayment } from "../../redux/modals/modals.selectors"; + +const mapStateToProps = createStructuredSelector({ + paymentModal: selectPayment +}); const mapDispatchToProps = (dispatch) => ({ setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })) }); -const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => { +const PaymentReexportButton = ({ paymentModal, payment, refetch, setPaymentContext }) => { const { t } = useTranslation(); const [updatePayment, { loading }] = useMutation(UPDATE_PAYMENT); @@ -34,15 +40,20 @@ const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => { refetch }, context: { + ...paymentModal.context, + ...paymentModal.context, ...payment, exportedat: null } }); - if (refetch) - refetch( - paymentUpdateResponse && - paymentUpdateResponse.data.update_payments.returning[0] - ); + + if (refetch) { + if (paymentModal.context.refetchRequiresContext) { + refetch(paymentUpdateResponse && paymentUpdateResponse.data.update_payments.returning[0]); + } else { + refetch(); + } + } } else { notification["error"]({ message: t("payments.errors.exporting", { @@ -59,4 +70,4 @@ const PaymentReexportButton = ({ payment, refetch, setPaymentContext }) => { ); }; -export default connect(null, mapDispatchToProps)(PaymentReexportButton); +export default connect(mapStateToProps, mapDispatchToProps)(PaymentReexportButton); diff --git a/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx b/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx index 1a5dd6774..c325c0f7a 100644 --- a/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx +++ b/client/src/components/payments-list-paginated/payment-list-paginated.component.jsx @@ -14,11 +14,11 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter"; import { TemplateList } from "../../utils/TemplateConstants"; +import { pageLimit } from "../../utils/config"; import { alphaSort } from "../../utils/sorters"; import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; -import { pageLimit } from "../../utils/config"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -171,7 +171,7 @@ export function PaymentsListPaginated({ } : refetch }, - context: apolloResults ? apolloResults : record + context: { ...(apolloResults ? apolloResults : record), refetchRequiresContext: true } }); }} > diff --git a/client/src/components/print-center-jobs-labels/print-center-jobs-labels.component.jsx b/client/src/components/print-center-jobs-labels/print-center-jobs-labels.component.jsx index fd53d25ab..6639f14f3 100644 --- a/client/src/components/print-center-jobs-labels/print-center-jobs-labels.component.jsx +++ b/client/src/components/print-center-jobs-labels/print-center-jobs-labels.component.jsx @@ -1,4 +1,13 @@ -import { Button, Card, Form, InputNumber, notification, Popover, Radio } from "antd"; +import { + Button, + Card, + Form, + InputNumber, + notification, + Popover, + Radio, + Space, +} from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -95,10 +104,16 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) { >
- - +
+ + + + +
); diff --git a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx index 7acbd6668..fd0d6cb2f 100644 --- a/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx +++ b/client/src/components/schedule-calendar-wrapper/schedule-calendar-header.component.jsx @@ -59,25 +59,33 @@ export function ScheduleCalendarHeaderComponent({ {loadData && loadData.allJobsOut ? ( loadData.allJobsOut.map((j) => (
- - - - )) ) : ( - + )} @@ -92,26 +100,30 @@ export function ScheduleCalendarHeaderComponent({ {loadData && loadData.allJobsIn ? ( loadData.allJobsIn.map((j) => ( - - - - )) ) : ( - + )} @@ -121,27 +133,33 @@ export function ScheduleCalendarHeaderComponent({ const LoadComponent = loadData ? (
- - - - {(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(2)} - - - - {(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(2)} - - - + + + + {(loadData.allHoursInBody || 0) && + loadData.allHoursInBody.toFixed(1)} + / + {(loadData.allHoursInRefinish || 0) && + loadData.allHoursInRefinish.toFixed(1)} + /{(loadData.allHoursIn || 0) && loadData.allHoursIn.toFixed(1)} + + + + {(loadData.allHoursOut || 0) && loadData.allHoursOut.toFixed(1)} + + + +
    {Object.keys(ATSToday).map((key, idx) => ( diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx index 90f4a5034..fd9ba96a8 100644 --- a/client/src/components/shop-info/shop-info.component.jsx +++ b/client/src/components/shop-info/shop-info.component.jsx @@ -1,24 +1,25 @@ -import { useSplitTreatments } from "@splitsoftware/splitio-react"; -import { Button, Card, Tabs } from "antd"; -import React from "react"; -import { useTranslation } from "react-i18next"; -import { connect } from "react-redux"; -import { createStructuredSelector } from "reselect"; -import { selectBodyshop } from "../../redux/user/user.selectors"; -import ShopInfoGeneral from "./shop-info.general.component"; -import ShopInfoIntakeChecklistComponent from "./shop-info.intake.component"; -import ShopInfoLaborRates from "./shop-info.laborrates.component"; -import ShopInfoOrderStatusComponent from "./shop-info.orderstatus.component"; -import ShopInfoPartsScan from "./shop-info.parts-scan"; -import ShopInfoRbacComponent from "./shop-info.rbac.component"; -import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycenters.component"; -import ShopInfoROStatusComponent from "./shop-info.rostatus.component"; -import ShopInfoSchedulingComponent from "./shop-info.scheduling.component"; -import ShopInfoSpeedPrint from "./shop-info.speedprint.component"; -import { useLocation, useNavigate } from "react-router-dom"; -import ShopInfoTaskPresets from "./shop-info.task-presets.component"; -import queryString from "query-string"; -import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import { useSplitTreatments } from '@splitsoftware/splitio-react'; +import { Button, Card, Tabs } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; +import { createStructuredSelector } from 'reselect'; +import { selectBodyshop } from '../../redux/user/user.selectors'; +import ShopInfoGeneral from './shop-info.general.component'; +import ShopInfoIntakeChecklistComponent from './shop-info.intake.component'; +import ShopInfoLaborRates from './shop-info.laborrates.component'; +import ShopInfoOrderStatusComponent from './shop-info.orderstatus.component'; +import ShopInfoPartsScan from './shop-info.parts-scan'; +import ShopInfoRbacComponent from './shop-info.rbac.component'; +import ShopInfoResponsibilityCenterComponent from './shop-info.responsibilitycenters.component'; +import ShopInfoROStatusComponent from './shop-info.rostatus.component'; +import ShopInfoSchedulingComponent from './shop-info.scheduling.component'; +import ShopInfoSpeedPrint from './shop-info.speedprint.component'; +import { useLocation, useNavigate } from 'react-router-dom'; +import ShopInfoTaskPresets from './shop-info.task-presets.component'; +import queryString from 'query-string'; +import InstanceRenderManager from '../../utils/instanceRenderMgr'; +import ShopInfoRoGuard from './shop-info.roguard.component'; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop @@ -111,7 +112,18 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) { children: } ] - : []) + : []), + ...InstanceRenderManager({ + imex: [ + { + key: 'roguard', + label: t('bodyshop.labels.roguard.title'), + children: , + }, + ], + rome: 'USE_IMEX', + promanager: [], + }), ]; return ( + + + + + + + {() => { + const disabled = !form.getFieldValue(["md_ro_guard", "enabled"]); + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {InstanceRenderManager({ + rome: ( + + + + ) + })} + + + + + + ); + }} + +
+ ); +} diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index e1d5b93e1..6bbafe6e3 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -138,6 +138,7 @@ export const QUERY_BODYSHOP = gql` tt_enforce_hours_for_tech_console md_tasks_presets use_paint_scale_data + md_ro_guard employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) { id name @@ -265,6 +266,7 @@ export const UPDATE_SHOP = gql` enforce_conversion_category tt_enforce_hours_for_tech_console md_tasks_presets + md_ro_guard employee_teams(order_by: { name: asc }, where: { active: { _eq: true } }) { id name diff --git a/client/src/graphql/courtesy-car.queries.js b/client/src/graphql/courtesy-car.queries.js index 173cb4e19..a7433e940 100644 --- a/client/src/graphql/courtesy-car.queries.js +++ b/client/src/graphql/courtesy-car.queries.js @@ -41,7 +41,7 @@ export const QUERY_AVAILABLE_CC = gql` `; export const CHECK_CC_FLEET_NUMBER = gql` - query CHECK_VENDOR_NAME($name: String!) { + query CHECK_CC_FLEET_NUMBER($name: String!) { courtesycars_aggregate(where: { fleetnumber: { _ilike: $name } }) { aggregate { count diff --git a/client/src/graphql/jobs-lines.queries.js b/client/src/graphql/jobs-lines.queries.js index bb7b7cf20..b755ee705 100644 --- a/client/src/graphql/jobs-lines.queries.js +++ b/client/src/graphql/jobs-lines.queries.js @@ -219,6 +219,12 @@ export const UPDATE_JOB_LINE = gql` id notes mod_lbr_ty + mod_lb_hrs + part_type + op_code_desc + prt_dsmk_m + prt_dsmk_p + tax_part part_qty db_price act_price diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 1dd4ea175..ed0415b0a 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -1969,6 +1969,19 @@ export const QUERY_JOB_CLOSE_DETAILS = gql` kmout qb_multiple_payers lbr_adjustments + payments { + amount + created_at + date + exportedat + id + jobid + memo + payer + paymentnum + transactionid + type + } joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { id removed @@ -1981,6 +1994,7 @@ export const QUERY_JOB_CLOSE_DETAILS = gql` db_price act_price part_qty + notes mod_lbr_ty db_hrs mod_lb_hrs @@ -1992,6 +2006,9 @@ export const QUERY_JOB_CLOSE_DETAILS = gql` prt_dsmk_p convertedtolbr convertedtolbr_data + act_price_before_ppc + sublet_ignored + sublet_completed } } } @@ -2221,6 +2238,8 @@ export const GET_JOB_LINE_ORDERS = gql` parts_order_lines(where: { job_line_id: { _eq: $joblineid } }) { id act_price + backordered_eta + backordered_on parts_order { id order_date diff --git a/client/src/pages/bills/bills.page.component.jsx b/client/src/pages/bills/bills.page.component.jsx index eba2096d6..175f6a5c0 100644 --- a/client/src/pages/bills/bills.page.component.jsx +++ b/client/src/pages/bills/bills.page.component.jsx @@ -216,7 +216,7 @@ export function BillsListPage({ loading, data, refetch, total, setPartsOrderCont extra={ {search.search && ( - <> + {t("general.labels.searchresults", { search: search.search })} @@ -229,7 +229,7 @@ export function BillsListPage({ loading, data, refetch, total, setPartsOrderCont > {t("general.actions.clear")} - + )}
- {j.ro_number} + + {j.ro_number} ( + {j.status}) + - {`(${(j.labhrs.aggregate.sum.mod_lb_hrs + j.larhrs.aggregate.sum.mod_lb_hrs).toFixed( + + {`(${j.labhrs.aggregate.sum.mod_lb_hrs.toFixed( 1 - )} ${t("general.labels.hours")})`} + )}/${j.larhrs.aggregate.sum.mod_lb_hrs.toFixed(1)}/${( + j.labhrs.aggregate.sum.mod_lb_hrs + + j.larhrs.aggregate.sum.mod_lb_hrs + ).toFixed(1)} ${t("general.labels.hours")})`} - {j.scheduled_completion} + + + {j.scheduled_completion} +
{t("appointments.labels.nocompletingjobs")} + {t("appointments.labels.nocompletingjobs")} +
+ {j.ro_number} - {j.status} + - {`(${(j.labhrs.aggregate.sum.mod_lb_hrs + j.larhrs.aggregate.sum.mod_lb_hrs).toFixed( + + {`(${j.labhrs.aggregate.sum.mod_lb_hrs.toFixed( 1 - )} ${t("general.labels.hours")})`} + )}/${j.larhrs.aggregate.sum.mod_lb_hrs.toFixed(1)}/${( + j.labhrs.aggregate.sum.mod_lb_hrs + + j.larhrs.aggregate.sum.mod_lb_hrs + ).toFixed(1)} ${t("general.labels.hours")})`} + {j.scheduled_in}
{t("appointments.labels.noarrivingjobs")} + {t("appointments.labels.noarrivingjobs")} +