From faf9fb75c5eff7c23aa61f877f1ff37a8c77e52c Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Thu, 5 Mar 2026 18:01:53 -0800 Subject: [PATCH 01/16] IO-3601 Additional QBO Logging Signed-off-by: Allan Carr --- server/accounting/qbo/qbo-payables.js | 45 ++++++++++---- server/accounting/qbo/qbo-payments.js | 27 +++++---- server/accounting/qbo/qbo-receivables.js | 75 ++++++++++++++++++------ 3 files changed, 107 insertions(+), 40 deletions(-) diff --git a/server/accounting/qbo/qbo-payables.js b/server/accounting/qbo/qbo-payables.js index 85c989b21..23f538a48 100644 --- a/server/accounting/qbo/qbo-payables.js +++ b/server/accounting/qbo/qbo-payables.js @@ -130,12 +130,13 @@ exports.default = async (req, res) => { async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) { try { + const url = urlBuilder( + qbo_realmId, + "query", + `select * From vendor where DisplayName = '${StandardizeName(bill.vendor.name)}'` + ); const result = await oauthClient.makeApiCall({ - url: urlBuilder( - qbo_realmId, - "query", - `select * From vendor where DisplayName = '${StandardizeName(bill.vendor.name)}'` - ), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -150,6 +151,11 @@ async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) { bodyshopid: bill.job.shopid, email: req.user.email }); + logger.log("qbo-payables-query", "DEBUG", req.user.email, null, { + method: "QueryVendorRecord", + call: url, + result: result.json + }); return result.json?.QueryResponse?.Vendor?.[0]; } catch (error) { @@ -167,8 +173,9 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) { DisplayName: StandardizeName(bill.vendor.name) }; try { + const url = urlBuilder(qbo_realmId, "vendor"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "vendor"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -184,6 +191,12 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) { bodyshopid: bill.job.shopid, email: req.user.email }); + logger.log("qbo-payments-insert", "DEBUG", req.user.email, null, { + method: "InsertVendorRecord", + call: url, + Vendor: Vendor, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); @@ -274,11 +287,12 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) VendorRef: { value: vendor.Id }, - ...(vendor.TermRef && !bill.is_credit_memo && { - SalesTermRef: { - value: vendor.TermRef.value - } - }), + ...(vendor.TermRef && + !bill.is_credit_memo && { + SalesTermRef: { + value: vendor.TermRef.value + } + }), TxnDate: moment(bill.date) //.tz(bill.job.bodyshop.timezone) .format("YYYY-MM-DD"), @@ -318,8 +332,9 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) [logKey]: logValue }); try { + const url = urlBuilder(qbo_realmId, bill.is_credit_memo ? "vendorcredit" : "bill"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, bill.is_credit_memo ? "vendorcredit" : "bill"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -335,6 +350,12 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) bodyshopid: bill.job.shopid, email: req.user.email }); + logger.log("qbo-payables-insert", "DEBUG", req.user.email, null, { + method: "InsertBill", + call: url, + postingObj: bill.is_credit_memo ? VendorCredit : billQbo, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); diff --git a/server/accounting/qbo/qbo-payments.js b/server/accounting/qbo/qbo-payments.js index 47cdc9c97..dc17908b2 100644 --- a/server/accounting/qbo/qbo-payments.js +++ b/server/accounting/qbo/qbo-payments.js @@ -82,14 +82,7 @@ exports.default = async (req, res) => { if (isThreeTier || (!isThreeTier && twoTierPref === "name")) { //Insert the name/owner and account for whether the source should be the ins co in 3 tier.. - ownerCustomerTier = await QueryOwner( - oauthClient, - qbo_realmId, - req, - payment.job, - isThreeTier, - insCoCustomerTier - ); + ownerCustomerTier = await QueryOwner(oauthClient, qbo_realmId, req, payment.job, insCoCustomerTier); //Query for the owner itself. if (!ownerCustomerTier) { ownerCustomerTier = await InsertOwner( @@ -229,8 +222,9 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef) paymentQbo }); try { + const url = urlBuilder(qbo_realmId, "payment"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "payment"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -246,6 +240,12 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef) bodyshopid: payment.job.shopid, email: req.user.email }); + logger.log("qbo-payments-insert", "DEBUG", req.user.email, null, { + method: "InsertPayment", + call: url, + paymentQbo: paymentQbo, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); @@ -428,8 +428,9 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe paymentQbo }); try { + const url = urlBuilder(qbo_realmId, "creditmemo"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "creditmemo"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -445,6 +446,12 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe bodyshopid: req.user.bodyshopid, email: req.user.email }); + logger.log("qbo-metadata-query", "DEBUG", req.user.email, null, { + method: "InsertCreditMemo", + call: url, + paymentQbo: paymentQbo, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); diff --git a/server/accounting/qbo/qbo-receivables.js b/server/accounting/qbo/qbo-receivables.js index ef64fcdd8..71c935a5f 100644 --- a/server/accounting/qbo/qbo-receivables.js +++ b/server/accounting/qbo/qbo-receivables.js @@ -213,12 +213,13 @@ exports.default = async (req, res) => { async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) { try { + const url = urlBuilder( + qbo_realmId, + "query", + `select * From Customer where DisplayName = '${StandardizeName(job.ins_co_nm.trim())}' and Active = true` + ); const result = await oauthClient.makeApiCall({ - url: urlBuilder( - qbo_realmId, - "query", - `select * From Customer where DisplayName = '${StandardizeName(job.ins_co_nm.trim())}' and Active = true` - ), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -233,6 +234,11 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-query", "DEBUG", req.user.email, job.id, { + method: "QueryInsuranceCo", + call: url, + result: result.json + }); return result.json?.QueryResponse?.Customer?.[0]; } catch (error) { @@ -266,8 +272,9 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) { } }; try { + const url = urlBuilder(qbo_realmId, "customer"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "customer"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -283,6 +290,12 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-insert", "DEBUG", req.user.email, job.id, { + method: "InsertInsuranceCo", + call: url, + customerObj: Customer, + result: result.json + }); return result.json?.Customer; } catch (error) { @@ -298,12 +311,13 @@ exports.InsertInsuranceCo = InsertInsuranceCo; async function QueryOwner(oauthClient, qbo_realmId, req, job, parentTierRef) { const ownerName = generateOwnerTier(job, true, null); + const url = urlBuilder( + qbo_realmId, + "query", + `select * From Customer where DisplayName = '${StandardizeName(ownerName)}' and Active = true` + ); const result = await oauthClient.makeApiCall({ - url: urlBuilder( - qbo_realmId, - "query", - `select * From Customer where DisplayName = '${StandardizeName(ownerName)}' and Active = true` - ), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -318,6 +332,11 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job, parentTierRef) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-query", "DEBUG", req.user.email, job.id, { + method: "QueryOwner", + call: url, + result: result.json + }); return result.json?.QueryResponse?.Customer?.find((x) => x.ParentRef?.value === parentTierRef?.Id); } @@ -347,8 +366,9 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare : {}) }; try { + const url = urlBuilder(qbo_realmId, "customer"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "customer"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -364,6 +384,12 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-insert", "DEBUG", req.user.email, job.id, { + method: "InsertOwner", + call: url, + customerObj: Customer, + result: result.json + }); return result.json?.Customer; } catch (error) { @@ -378,12 +404,13 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare exports.InsertOwner = InsertOwner; async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) { + const url = urlBuilder( + qbo_realmId, + "query", + `select * From Customer where DisplayName = '${job.ro_number}' and Active = true` + ); const result = await oauthClient.makeApiCall({ - url: urlBuilder( - qbo_realmId, - "query", - `select * From Customer where DisplayName = '${job.ro_number}' and Active = true` - ), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -398,6 +425,11 @@ async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-query", "DEBUG", req.user.email, job.id, { + method: "QueryJob", + call: url, + result: result.json + }); const customers = result.json?.QueryResponse?.Customer; return customers && (parentTierRef ? customers.find((x) => x.ParentRef.value === parentTierRef.Id) : customers[0]); @@ -423,8 +455,9 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { } }; try { + const url = urlBuilder(qbo_realmId, "customer"); const result = await oauthClient.makeApiCall({ - url: urlBuilder(qbo_realmId, "customer"), + url: url, method: "POST", headers: { "Content-Type": "application/json" @@ -440,6 +473,12 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) { jobid: job.id, email: req.user.email }); + logger.log("qbo-receivables-insert", "DEBUG", req.user.email, job.id, { + method: "InsertJob", + call: url, + customerObj: Customer, + result: result.json + }); if (result.status >= 400) { throw new Error(JSON.stringify(result.json.Fault)); From 8577929bd45aee78d8fe458025a302f176db0ee1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 6 Mar 2026 11:17:27 -0800 Subject: [PATCH 02/16] IO-3600 Job Line Close Select Box Filter Signed-off-by: Allan Carr --- .../jobs-close-lines/jobs-close-lines.component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/jobs-close-lines/jobs-close-lines.component.jsx b/client/src/components/jobs-close-lines/jobs-close-lines.component.jsx index e737173e6..6b52cbeee 100644 --- a/client/src/components/jobs-close-lines/jobs-close-lines.component.jsx +++ b/client/src/components/jobs-close-lines/jobs-close-lines.component.jsx @@ -138,7 +138,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) { showSearch={{ optionFilterProp: "children", filterOption: (input, option) => - option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 + option?.value?.toLowerCase().indexOf(input?.toLowerCase()) >= 0 }} disabled={jobRO} options={bodyshop.md_responsibility_centers.profits.map((p) => ({ @@ -166,7 +166,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) { showSearch={{ optionFilterProp: "children", filterOption: (input, option) => - option.children.toLowerCase().indexOf(input.toLowerCase()) >= 0 + option?.value?.toLowerCase().indexOf(input?.toLowerCase()) >= 0 }} disabled={jobRO} options={bodyshop.md_responsibility_centers.profits.map((p) => ({ From 818aedf04fb53f04a1506d036cb25da5a82688b4 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 6 Mar 2026 12:43:12 -0800 Subject: [PATCH 03/16] IO-3604 Tech Job Drawer Signed-off-by: Allan Carr --- .../tech-lookup-jobs-drawer.component.jsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/client/src/components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component.jsx b/client/src/components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component.jsx index 9610728a3..7a6ba1cc6 100644 --- a/client/src/components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component.jsx +++ b/client/src/components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component.jsx @@ -25,10 +25,7 @@ const mapDispatchToProps = (dispatch) => ({ }); export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) { - const breakpoints = Grid.useBreakpoint(); - const selectedBreakpoint = Object.entries(breakpoints) - .filter(([, isOn]) => !!isOn) - .slice(-1)[0]; + const screens = Grid.useBreakpoint(); const bpoints = { xs: "100%", @@ -36,10 +33,16 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) { md: "100%", lg: "100%", xl: "90%", - xxl: "85%" + xxl: "90%" }; - const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%"; + let drawerPercentage = "100%"; + if (screens.xxl) drawerPercentage = bpoints.xxl; + else if (screens.xl) drawerPercentage = bpoints.xl; + else if (screens.lg) drawerPercentage = bpoints.lg; + else if (screens.md) drawerPercentage = bpoints.md; + else if (screens.sm) drawerPercentage = bpoints.sm; + else if (screens.xs) drawerPercentage = bpoints.xs; const location = useLocation(); const history = useNavigate(); From da26954c3bfe769a136905d9a37c45b17daa91b1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 6 Mar 2026 14:21:00 -0800 Subject: [PATCH 04/16] IO-3596 Manual Line Lock Down Signed-off-by: Allan Carr --- .../job-detail-lines/job-lines.component.jsx | 82 ++++++++++--------- .../components/rbac-wrapper/rbac-defaults.js | 1 + .../shop-info/shop-info.rbac.component.jsx | 13 +++ client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 6 files changed, 61 insertions(+), 38 deletions(-) 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 d5df5480e..16a3d6c87 100644 --- a/client/src/components/job-detail-lines/job-lines.component.jsx +++ b/client/src/components/job-detail-lines/job-lines.component.jsx @@ -33,7 +33,7 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import _ from "lodash"; import { FaTasks } from "react-icons/fa"; -import { selectBodyshop } from "../../redux/user/user.selectors"; +import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors"; import dayjs from "../../utils/day"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; @@ -49,6 +49,7 @@ import JobLinesPartPriceChange from "./job-lines-part-price-change.component"; import JobLinesExpanderSimple from "./jobs-lines-expander-simple.component"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component.jsx"; const UPDATE_JOB_LINES_LOCATION_BULK = gql` mutation UPDATE_JOB_LINES_LOCATION_BULK($ids: [uuid!]!, $location: String!) { @@ -66,7 +67,8 @@ const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, jobRO: selectJobReadOnly, technician: selectTechnician, - isPartsEntry: selectIsPartsEntry + isPartsEntry: selectIsPartsEntry, + authLevel: selectAuthLevel }); const mapDispatchToProps = (dispatch) => ({ @@ -94,7 +96,8 @@ export function JobLinesComponent({ setTaskUpsertContext, billsQuery, handlePartsOrderOnRowClick, - isPartsEntry + isPartsEntry, + authLevel }) { const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK); const [bulkUpdateLocations] = useMutation(UPDATE_JOB_LINES_LOCATION_BULK); @@ -386,18 +389,20 @@ export function JobLinesComponent({ key: "actions", render: (text, record) => ( - {(record.manual_line || jobIsPrivate) && !technician && ( - - {!isPartsEntry && ( + {!isPartsEntry && HasRbacAccess({ bodyshop, authLevel, action: "jobs:manual-line" }) && ( + ); 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 d6cd0f5ef..6f2be05e6 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 @@ -157,7 +157,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) { - diff --git a/client/src/components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component.jsx b/client/src/components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component.jsx index 3487ad672..0b4cde1e0 100644 --- a/client/src/components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component.jsx +++ b/client/src/components/jobs-admin-owner-reassociate/jobs-admin-owner-reassociate.component.jsx @@ -54,7 +54,7 @@ export default function JobAdminOwnerReassociate({ job }) {
{t("jobs.labels.associationwarning")}
- diff --git a/client/src/components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component.jsx b/client/src/components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component.jsx index 79ebe9aa0..565bd403c 100644 --- a/client/src/components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component.jsx +++ b/client/src/components/jobs-admin-vehicle-reassociate/jobs-admin-vehicle-reassociate.component.jsx @@ -54,7 +54,7 @@ export default function JobAdminOwnerReassociate({ job }) {
{t("jobs.labels.associationwarning")}
- From 0d502d4dd4be2dd02c45450a713b1fbfbe2cf0cf Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Fri, 6 Mar 2026 18:31:22 -0800 Subject: [PATCH 08/16] IO-3571 Create Job Done Button Loading Signed-off-by: Allan Carr --- .../jobs-create/jobs-create.component.jsx | 21 +++++++++---------- client/src/translations/en_us/common.json | 1 + client/src/translations/es/common.json | 1 + client/src/translations/fr/common.json | 1 + 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/client/src/pages/jobs-create/jobs-create.component.jsx b/client/src/pages/jobs-create/jobs-create.component.jsx index d760aa0cc..5e822b1aa 100644 --- a/client/src/pages/jobs-create/jobs-create.component.jsx +++ b/client/src/pages/jobs-create/jobs-create.component.jsx @@ -12,12 +12,11 @@ import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; export default function JobsCreateComponent({ form }) { const [pageIndex, setPageIndex] = useState(0); - const [errorMessage, setErrorMessage] = useState(null); - + const [isSubmitting, setIsSubmitting] = useState(false); const [state] = useContext(JobCreateContext); - const { t } = useTranslation(); + const steps = [ { title: t("jobs.labels.create.vehicleinfo"), @@ -42,11 +41,9 @@ export default function JobsCreateComponent({ form }) { const next = () => { setPageIndex(pageIndex + 1); - console.log("Next"); }; const prev = () => { setPageIndex(pageIndex - 1); - console.log("Previous"); }; const ProgressButtons = ({ top }) => { @@ -79,17 +76,21 @@ export default function JobsCreateComponent({ form }) { {pageIndex === steps.length - 1 && ( )}
@@ -146,13 +147,11 @@ export default function JobsCreateComponent({ form }) { ) : (
- {errorMessage ? (
) : null} - {steps.map((item, idx) => (
Date: Mon, 9 Mar 2026 12:53:59 -0400 Subject: [PATCH 09/16] feature/IO-3571-Create-Job-Done-Loading - Fix set is submitting --- client/src/pages/jobs-create/jobs-create.component.jsx | 5 +---- client/src/pages/jobs-create/jobs-create.container.jsx | 6 +++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/client/src/pages/jobs-create/jobs-create.component.jsx b/client/src/pages/jobs-create/jobs-create.component.jsx index 5e822b1aa..7d48d035a 100644 --- a/client/src/pages/jobs-create/jobs-create.component.jsx +++ b/client/src/pages/jobs-create/jobs-create.component.jsx @@ -10,10 +10,9 @@ import JobsCreateOwnerInfoContainer from "../../components/jobs-create-owner-inf import JobsCreateVehicleInfoContainer from "../../components/jobs-create-vehicle-info/jobs-create-vehicle-info.container"; import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; -export default function JobsCreateComponent({ form }) { +export default function JobsCreateComponent({ form, isSubmitting }) { const [pageIndex, setPageIndex] = useState(0); const [errorMessage, setErrorMessage] = useState(null); - const [isSubmitting, setIsSubmitting] = useState(false); const [state] = useContext(JobCreateContext); const { t } = useTranslation(); @@ -78,7 +77,6 @@ export default function JobsCreateComponent({ form }) { type="primary" loading={isSubmitting} onClick={() => { - setIsSubmitting(true); form .validateFields() .then(() => { @@ -86,7 +84,6 @@ export default function JobsCreateComponent({ form }) { }) .catch((error) => { console.log("error", error); - setIsSubmitting(false); }); }} > diff --git a/client/src/pages/jobs-create/jobs-create.container.jsx b/client/src/pages/jobs-create/jobs-create.container.jsx index 97aaa3437..5b5627ef1 100644 --- a/client/src/pages/jobs-create/jobs-create.container.jsx +++ b/client/src/pages/jobs-create/jobs-create.container.jsx @@ -46,6 +46,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr }); const [form] = Form.useForm(); const [state, setState] = contextState; + const [isSubmitting, setIsSubmitting] = useState(false); const [insertJob] = useMutation(INSERT_NEW_JOB); const [loadOwner, remoteOwnerData] = useLazyQuery(QUERY_OWNER_FOR_JOB_CREATION); @@ -83,16 +84,19 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr newJobId: resp.data.insert_jobs.returning[0].id }); logImEXEvent("manual_job_create_completed", {}); + setIsSubmitting(false); }) .catch((error) => { notification.error({ title: t("jobs.errors.creating", { error: error }) }); setState({ ...state, error: error }); + setIsSubmitting(false); }); }; const handleFinish = (values) => { + setIsSubmitting(true); let job = Object.assign( {}, values, @@ -297,7 +301,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, curr }) }} > - + From 8d00fc29d10eda670a1532915813741a91c3fb4d Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 9 Mar 2026 12:59:00 -0400 Subject: [PATCH 10/16] feature/IO-3603-Production-Board-Note-Autofocus - Fix --- ...oduction-list-columns.comment.component.jsx | 18 +++++++++++++----- ...n-list-columns.productionnote.component.jsx | 18 +++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/client/src/components/production-list-columns/production-list-columns.comment.component.jsx b/client/src/components/production-list-columns/production-list-columns.comment.component.jsx index 43d27c40a..10d084cb7 100644 --- a/client/src/components/production-list-columns/production-list-columns.comment.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.comment.component.jsx @@ -12,6 +12,7 @@ export default function ProductionListColumnComment({ record, usePortal = false const [note, setNote] = useState(record.comment || ""); const [open, setOpen] = useState(false); const textAreaRef = useRef(null); + const rafIdRef = useRef(null); const [updateAlert] = useMutation(UPDATE_JOB); @@ -37,14 +38,21 @@ export default function ProductionListColumnComment({ record, usePortal = false }; const handleOpenChange = (flag) => { + if (rafIdRef.current) { + cancelAnimationFrame(rafIdRef.current); + rafIdRef.current = null; + } setOpen(flag); if (flag) { setNote(record.comment || ""); - requestAnimationFrame(() => { - try { - textAreaRef.current?.focus?.({ preventScroll: true }); - } catch { - textAreaRef.current?.focus?.(); + rafIdRef.current = requestAnimationFrame(() => { + rafIdRef.current = null; + if (textAreaRef.current?.focus) { + try { + textAreaRef.current.focus({ preventScroll: true }); + } catch { + textAreaRef.current.focus(); + } } }); } diff --git a/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx b/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx index ea85f3dcc..21d129adc 100644 --- a/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.productionnote.component.jsx @@ -21,6 +21,7 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext, useP const [note, setNote] = useState(record.production_vars?.note || ""); const [open, setOpen] = useState(false); const textAreaRef = useRef(null); + const rafIdRef = useRef(null); const [updateAlert] = useMutation(UPDATE_JOB); @@ -53,14 +54,21 @@ function ProductionListColumnProductionNote({ record, setNoteUpsertContext, useP const handleOpenChange = useCallback( (flag) => { + if (rafIdRef.current) { + cancelAnimationFrame(rafIdRef.current); + rafIdRef.current = null; + } setOpen(flag); if (flag) { setNote(record.production_vars?.note || ""); - requestAnimationFrame(() => { - try { - textAreaRef.current?.focus?.({ preventScroll: true }); - } catch { - textAreaRef.current?.focus?.(); + rafIdRef.current = requestAnimationFrame(() => { + rafIdRef.current = null; + if (textAreaRef.current?.focus) { + try { + textAreaRef.current.focus({ preventScroll: true }); + } catch { + textAreaRef.current.focus(); + } } }); } From 49816d5d439958f67a8cd926a1a64eb58b78feb1 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 9 Mar 2026 16:28:10 -0700 Subject: [PATCH 11/16] IO-3607 Employee Drop Down Inactive filter Signed-off-by: Allan Carr --- .../src/components/email-overlay/email-overlay.component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/email-overlay/email-overlay.component.jsx b/client/src/components/email-overlay/email-overlay.component.jsx index a5b439da0..3037322e8 100644 --- a/client/src/components/email-overlay/email-overlay.component.jsx +++ b/client/src/components/email-overlay/email-overlay.component.jsx @@ -41,7 +41,7 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b const emailsToMenu = { items: [ ...bodyshop.employees - .filter((e) => e.user_email) + .filter((e) => e.user_email && e.active === true) .map((e, idx) => ({ key: idx, label: `${e.first_name} ${e.last_name}`, @@ -59,7 +59,7 @@ export function EmailOverlayComponent({ emailConfig, form, selectedMediaState, b const menuCC = { items: [ ...bodyshop.employees - .filter((e) => e.user_email) + .filter((e) => e.user_email && e.active === true) .map((e, idx) => ({ key: idx, label: `${e.first_name} ${e.last_name}`, From 26fc76a767adfd74473959b950bc71f1726c51e0 Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 9 Mar 2026 17:42:13 -0700 Subject: [PATCH 12/16] IO-3606 Tech Console Job Clock In Ticket Date Signed-off-by: Allan Carr --- .../tech-job-clock-in-form.container.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx b/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx index 4bceddeb2..c18382d6d 100644 --- a/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx +++ b/client/src/components/tech-job-clock-in-form/tech-job-clock-in-form.container.jsx @@ -66,10 +66,9 @@ export function TechClockInContainer({ setTimeTicketContext, technician, bodysho employeeid: technician.id, date: typeof bodyshop.timezone === "string" - ? // TODO: Client Update - This may be broken - dayjs.tz(theTime, bodyshop.timezone).format("YYYY-MM-DD") + ? dayjs(theTime).tz(bodyshop.timezone).format("YYYY-MM-DD") : typeof bodyshop.timezone === "number" - ? dayjs(theTime).format("YYYY-MM-DD").utcOffset(bodyshop.timezone) + ? dayjs(theTime).utcOffset(bodyshop.timezone).format("YYYY-MM-DD") : dayjs(theTime).format("YYYY-MM-DD"), clockon: dayjs(theTime), jobid: values.jobid, From 05cd60c2a1664cffd243539250025f80ea70fdbe Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Mon, 9 Mar 2026 17:57:55 -0700 Subject: [PATCH 13/16] IO-3592 WIP Summary Reports Signed-off-by: Allan Carr --- client/src/translations/en_us/common.json | 6 +++-- client/src/translations/es/common.json | 4 +++- client/src/translations/fr/common.json | 4 +++- client/src/utils/TemplateConstants.js | 28 +++++++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/client/src/translations/en_us/common.json b/client/src/translations/en_us/common.json index 02e952d61..20c8d5acc 100644 --- a/client/src/translations/en_us/common.json +++ b/client/src/translations/en_us/common.json @@ -3372,8 +3372,10 @@ "void_ros": "Void ROs", "work_in_progress_committed_labour": "Work in Progress - Committed Labor", "work_in_progress_jobs": "Work in Progress - Jobs", - "work_in_progress_labour": "Work in Progress - Labor", - "work_in_progress_payables": "Work in Progress - Payables" + "work_in_progress_labour": "Work in Progress - Labor (Detail)", + "work_in_progress_labour_summary": "Work in Progress - Labor (Summary)", + "work_in_progress_payables": "Work in Progress - Payables (Detail)", + "work_in_progress_payables_summary": "Work in Progress - Payables (Summary)" } }, "schedule": { diff --git a/client/src/translations/es/common.json b/client/src/translations/es/common.json index f10585f6f..8eaae2da5 100644 --- a/client/src/translations/es/common.json +++ b/client/src/translations/es/common.json @@ -3373,7 +3373,9 @@ "work_in_progress_committed_labour": "", "work_in_progress_jobs": "", "work_in_progress_labour": "", - "work_in_progress_payables": "" + "work_in_progress_labour_summary": "", + "work_in_progress_payables": "", + "work_in_progress_payables_summary": "" } }, "schedule": { diff --git a/client/src/translations/fr/common.json b/client/src/translations/fr/common.json index 4f7edc58d..f18d53108 100644 --- a/client/src/translations/fr/common.json +++ b/client/src/translations/fr/common.json @@ -3373,7 +3373,9 @@ "work_in_progress_committed_labour": "", "work_in_progress_jobs": "", "work_in_progress_labour": "", - "work_in_progress_payables": "" + "work_in_progress_labour_summary": "", + "work_in_progress_payables": "", + "work_in_progress_payables_summary": "" } }, "schedule": { diff --git a/client/src/utils/TemplateConstants.js b/client/src/utils/TemplateConstants.js index 83a6eca4a..4e27d4d6b 100644 --- a/client/src/utils/TemplateConstants.js +++ b/client/src/utils/TemplateConstants.js @@ -1717,6 +1717,20 @@ export const TemplateList = (type, context) => { group: "jobs", featureNameRestricted: "timetickets" }, + work_in_progress_labour_summary: { + title: i18n.t("reportcenter.templates.work_in_progress_labour_summary"), + description: "", + subject: i18n.t("reportcenter.templates.work_in_progress_labour_summary"), + key: "work_in_progress_labour_summary", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open") + }, + group: "jobs", + featureNameRestricted: "timetickets" + }, work_in_progress_committed_labour: { title: i18n.t("reportcenter.templates.work_in_progress_committed_labour"), description: "", @@ -1746,6 +1760,20 @@ export const TemplateList = (type, context) => { group: "jobs", featureNameRestricted: "bills" }, + work_in_progress_payables_summary: { + title: i18n.t("reportcenter.templates.work_in_progress_payables_summary"), + description: "", + subject: i18n.t("reportcenter.templates.work_in_progress_payables_summary"), + key: "work_in_progress_payables_summary", + //idtype: "vendor", + disabled: false, + rangeFilter: { + object: i18n.t("reportcenter.labels.objects.jobs"), + field: i18n.t("jobs.fields.date_open") + }, + group: "jobs", + featureNameRestricted: "bills" + }, lag_time: { title: i18n.t("reportcenter.templates.lag_time"), description: "", From f1f705903a77109399ef709937842556475b9e4a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Tue, 10 Mar 2026 20:02:32 -0700 Subject: [PATCH 14/16] IO-3582 Add Return From Inv to Parts Return Table Signed-off-by: Allan Carr --- .../parts-order-list-table.component.jsx | 19 +++++++++++++++---- client/src/graphql/bills.queries.js | 4 ++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx index 050fb2ec0..9c06ce4e2 100644 --- a/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx +++ b/client/src/components/parts-order-list-table/parts-order-list-table.component.jsx @@ -70,6 +70,12 @@ export function PartsOrderListTableComponent({ const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER); const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : []; + + const enrichedPartsOrders = parts_orders.map((order) => ({ + ...order, + invoice_number: order.bill?.invoice_number + })); + const { refetch } = billsQuery; const recordActions = (record, showView = false) => ( @@ -222,7 +228,12 @@ export function PartsOrderListTableComponent({ dataIndex: "order_number", key: "order_number", sorter: (a, b) => alphaSort(a.invoice_number, b.invoice_number), - sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order + sortOrder: state.sortedInfo.columnKey === "invoice_number" && state.sortedInfo.order, + render: (text, record) => ( + + {record.order_number} {record.invoice_number && `(${record.invoice_number})`} + + ) }, { title: t("parts_orders.fields.order_date"), @@ -272,10 +283,10 @@ export function PartsOrderListTableComponent({ setState({ ...state, filteredInfo: filters, sortedInfo: sorter }); }; - const filteredPartsOrders = parts_orders + const filteredPartsOrders = enrichedPartsOrders ? searchText === "" - ? parts_orders - : parts_orders.filter( + ? enrichedPartsOrders + : enrichedPartsOrders.filter( (b) => (b.order_number || "").toString().toLowerCase().includes(searchText.toLowerCase()) || (b.vendor.name || "").toLowerCase().includes(searchText.toLowerCase()) diff --git a/client/src/graphql/bills.queries.js b/client/src/graphql/bills.queries.js index f7cad4209..36f3b2f73 100644 --- a/client/src/graphql/bills.queries.js +++ b/client/src/graphql/bills.queries.js @@ -91,6 +91,10 @@ export const QUERY_PARTS_BILLS_BY_JOBID = gql` order_number comments user_email + bill { + id + invoice_number + } } parts_dispatch(where: { jobid: { _eq: $jobid } }) { id From e669c19b9894a9abf21c6315e922d9aa88bf658a Mon Sep 17 00:00:00 2001 From: Allan Carr Date: Wed, 11 Mar 2026 18:48:23 -0700 Subject: [PATCH 15/16] IO-3584 Duplicate Job with Full Rates Signed-off-by: Allan Carr --- client/src/graphql/jobs.queries.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 21d9522b1..35a3a1f26 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -1375,6 +1375,9 @@ export const QUERY_JOB_FOR_DUPE = gql` agt_ph2x area_of_damage cat_no + cieca_pfl + cieca_pfo + cieca_pft cieca_stl cieca_ttl clm_addr1 @@ -1452,6 +1455,7 @@ export const QUERY_JOB_FOR_DUPE = gql` labor_rate_desc labor_rate_id local_tax_rate + materials other_amount_payable owner_owing ownerid From 7ec8a73c30a8d9b6a7b0c7313d55695f1f77e9b0 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 12 Mar 2026 12:33:54 -0400 Subject: [PATCH 16/16] hotfix/2026-03-12 - Be more specific on CDK error passing, resolve circular dependency --- server/cdk/cdk-get-makes.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/server/cdk/cdk-get-makes.js b/server/cdk/cdk-get-makes.js index 319ecd131..9067f869e 100644 --- a/server/cdk/cdk-get-makes.js +++ b/server/cdk/cdk-get-makes.js @@ -66,7 +66,12 @@ exports.default = async function ReloadCdkMakes(req, res) { } catch (error) { logger.log("cdk-replace-makes-models-error", "ERROR", req.user.email, null, { cdk_dealerid, - error + error: { + message: error?.message, + stack: error?.stack, + name: error?.name, + code: error?.code + } }); res.status(500).json(error); } @@ -105,7 +110,12 @@ async function GetCdkMakes(req, cdk_dealerid) { } catch (error) { logger.log("cdk-replace-makes-models-error", "ERROR", req.user.email, null, { cdk_dealerid, - error + error: { + message: error?.message, + stack: error?.stack, + name: error?.name, + code: error?.code + } }); throw new Error(error); @@ -141,7 +151,12 @@ async function GetFortellisMakes(req, cdk_dealerid) { } catch (error) { logger.log("fortellis-replace-makes-models-error", "ERROR", req.user.email, null, { cdk_dealerid, - error + error: { + message: error?.message, + stack: error?.stack, + name: error?.name, + code: error?.code + } }); throw new Error(error);