Merge remote-tracking branch 'origin/release/2026-01-23' into feature/IO-3499-React-19

This commit is contained in:
Dave
2026-01-26 14:04:10 -05:00
26 changed files with 122 additions and 71 deletions

View File

@@ -169,7 +169,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
refetch={refetch} refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />} {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear /> <Input
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
enterButton
/>
</Space> </Space>
} }
> >

View File

@@ -182,7 +182,13 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
refetch={refetch} refetch={refetch}
/> />
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />} {bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
<Input value={state.search} onChange={handleSearch} placeholder={t("general.labels.search")} allowClear /> <Input
value={state.search}
onChange={handleSearch}
placeholder={t("general.labels.search")}
allowClear
enterButton
/>
</Space> </Space>
} }
> >

View File

@@ -204,6 +204,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
onChange={handleSearch} onChange={handleSearch}
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
allowClear allowClear
enterButton
/> />
</Space> </Space>
} }

View File

@@ -232,6 +232,7 @@ export function BillsListTableComponent({
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -99,6 +99,7 @@ export default function ContractsCarsComponent({ loading, data, selectedCarId, h
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
value={state.search} value={state.search}
onChange={(e) => setState({ ...state, search: e.target.value })} onChange={(e) => setState({ ...state, search: e.target.value })}
enterButton
/> />
} }
> >

View File

@@ -123,6 +123,7 @@ export default function ContractsJobsComponent({ loading, data, selectedJob, han
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
value={state.search} value={state.search}
onChange={(e) => setState({ ...state, search: e.target.value })} onChange={(e) => setState({ ...state, search: e.target.value })}
enterButton
/> />
} }
> >

View File

@@ -164,6 +164,7 @@ export function ContractsList({ bodyshop, loading, contracts, refetch, total, se
const updatedSearch = { ...search, search: value }; const updatedSearch = { ...search, search: value };
history({ search: queryString.stringify(updatedSearch) }); history({ search: queryString.stringify(updatedSearch) });
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -50,7 +50,8 @@ export function JobCreateIOU({ bodyshop, currentUser, job, selectedJobLines, tec
config: { config: {
status: bodyshop.md_ro_statuses.default_open, status: bodyshop.md_ro_statuses.default_open,
bodyshopid: bodyshop.id, bodyshopid: bodyshop.id,
useremail: currentUser.email useremail: currentUser.email,
timezone: bodyshop.timezone
}, },
currentUser currentUser
}); });

View File

@@ -682,6 +682,7 @@ export function JobLinesComponent({
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -136,6 +136,7 @@ export function JobsAvailableScan({ partnerVersion, refetch }) {
onChange={(e) => { onChange={(e) => {
setSearchText(e.currentTarget.value); setSearchText(e.currentTarget.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -196,13 +196,16 @@ export function JobsAvailableComponent({ bodyshop, loading, data, refetch, addJo
> >
{t("general.actions.deleteall")} {t("general.actions.deleteall")}
</Button> </Button>
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
onChange={(e) => { onChange={(e) => {
setSearchText(e.currentTarget.value); setSearchText(e.currentTarget.value);
}} }}
enterButton
/> />
<Link to="/manage/jobs/new">
<Button>{t("jobs.actions.manualnew")}</Button>
</Link>
</Space> </Space>
} }
> >

View File

@@ -264,7 +264,7 @@ export function JobsDetailHeaderActions({
DuplicateJob({ DuplicateJob({
apolloClient: client, apolloClient: client,
jobId: job.id, jobId: job.id,
config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported, timezone: bodyshop.timezone },
completionCallback: (newJobId) => { completionCallback: (newJobId) => {
history(`/manage/jobs/${newJobId}`); history(`/manage/jobs/${newJobId}`);
notification.success({ notification.success({
@@ -279,7 +279,7 @@ export function JobsDetailHeaderActions({
DuplicateJob({ DuplicateJob({
apolloClient: client, apolloClient: client,
jobId: job.id, jobId: job.id,
config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported }, config: { defaultOpenStatus: bodyshop.md_ro_statuses.default_imported, timezone: bodyshop.timezone },
completionCallback: (newJobId) => { completionCallback: (newJobId) => {
history(`/manage/jobs/${newJobId}`); history(`/manage/jobs/${newJobId}`);
notification.success({ notification.success({

View File

@@ -15,7 +15,7 @@ export default async function DuplicateJob({
}) { }) {
logImEXEvent("job_duplicate"); logImEXEvent("job_duplicate");
const { defaultOpenStatus } = config; const { defaultOpenStatus, timezone } = config;
//get a list of all fields on the job //get a list of all fields on the job
const res = await apolloClient.query({ const res = await apolloClient.query({
query: QUERY_JOB_FOR_DUPE, query: QUERY_JOB_FOR_DUPE,
@@ -31,9 +31,12 @@ export default async function DuplicateJob({
delete existingJob.updatedat; delete existingJob.updatedat;
delete existingJob.cieca_stl; delete existingJob.cieca_stl;
delete existingJob.cieca_ttl; delete existingJob.cieca_ttl;
!keepJobLines && delete existingJob.clm_total;
const newJob = { const newJob = {
...existingJob, ...existingJob,
date_estimated: dayjs().tz(timezone, false).format("YYYY-MM-DD"),
date_open: dayjs(),
status: defaultOpenStatus status: defaultOpenStatus
}; };
@@ -70,7 +73,7 @@ export default async function DuplicateJob({
export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToKeep, currentUser }) { export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToKeep, currentUser }) {
logImEXEvent("job_create_iou"); logImEXEvent("job_create_iou");
const { status } = config; const { status, timezone } = config;
//get a list of all fields on the job //get a list of all fields on the job
const res = await apolloClient.query({ const res = await apolloClient.query({
query: QUERY_JOB_FOR_DUPE, query: QUERY_JOB_FOR_DUPE,
@@ -88,10 +91,10 @@ export async function CreateIouForJob({ apolloClient, jobId, config, jobLinesToK
const newJob = { const newJob = {
...existingJob, ...existingJob,
converted: true, converted: true,
status: status, status: status,
iouparent: jobId, iouparent: jobId,
date_estimated: dayjs().tz(timezone, false).format("YYYY-MM-DD"),
date_open: dayjs(), date_open: dayjs(),
audit_trails: { audit_trails: {
data: [ data: [

View File

@@ -101,6 +101,7 @@ export function PartDispatchTableComponent({ bodyshop, job, billsQuery }) {
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -295,6 +295,7 @@ export function PartsOrderListTableComponent({
e.preventDefault(); e.preventDefault();
setSearchText(e.target.value); setSearchText(e.target.value);
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -269,6 +269,7 @@ export function PartsQueueListComponent({ bodyshop }) {
return ( return (
<Card <Card
title={t("titles.bc.parts-queue")}
extra={ extra={
<Space wrap> <Space wrap>
<Button onClick={() => refetch()}> <Button onClick={() => refetch()}>

View File

@@ -45,6 +45,7 @@ export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading })
setFilter({ ...filter, search: e.target.value }); setFilter({ ...filter, search: e.target.value });
logImEXEvent("visual_board_filter_search", { search: e.target.value }); logImEXEvent("visual_board_filter_search", { search: e.target.value });
}} }}
enterButton
/> />
<EmployeeSearchSelectComponent <EmployeeSearchSelectComponent
style={{ minWidth: "20rem" }} style={{ minWidth: "20rem" }}

View File

@@ -44,6 +44,7 @@ export default function ProfileShopsComponent({ loading, data, updateActiveShop
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
allowClear allowClear
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
enterButton
/> />
} }
> >

View File

@@ -3,8 +3,7 @@ import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors"; import FeatureWrapper from "../feature-wrapper/feature-wrapper.component";
import FeatureWrapper, { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component";
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -12,15 +11,13 @@ import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.c
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
const timeZonesList = Intl.supportedValuesOf("timeZone"); const timeZonesList = Intl.supportedValuesOf("timeZone");
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({});
bodyshop: selectBodyshop
});
const mapDispatchToProps = () => ({ const mapDispatchToProps = () => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) //setUserLanguage: language => dispatch(setUserLanguage(language))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral); export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral);
export function ShopInfoGeneral({ form, bodyshop }) { export function ShopInfoGeneral({ form }) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -378,34 +375,6 @@ export function ShopInfoGeneral({ form, bodyshop }) {
> >
<Select mode="tags" /> <Select mode="tags" />
</Form.Item>, </Form.Item>,
...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
? [
<Form.Item
key="tt_allow_post_to_invoiced"
name={["tt_allow_post_to_invoiced"]}
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
valuePropName="checked"
>
<Switch />
</Form.Item>,
<Form.Item
key="tt_enforce_hours_for_tech_console"
name={["tt_enforce_hours_for_tech_console"]}
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
valuePropName="checked"
>
<Switch />
</Form.Item>,
<Form.Item
key="bill_allow_post_to_closed"
name={["bill_allow_post_to_closed"]}
label={t("bodyshop.fields.bill_allow_post_to_closed")}
valuePropName="checked"
>
<Switch />
</Form.Item>
]
: []),
<Form.Item <Form.Item
key="md_ded_notes" key="md_ded_notes"
name={["md_ded_notes"]} name={["md_ded_notes"]}

View File

@@ -313,6 +313,38 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) {
> >
<Select mode="tags" /> <Select mode="tags" />
</Form.Item>, </Form.Item>,
...(HasFeatureAccess({ featureName: "timetickets", bodyshop })
? [
<Form.Item
key="tt_allow_post_to_invoiced"
name={["tt_allow_post_to_invoiced"]}
label={t("bodyshop.fields.tt_allow_post_to_invoiced")}
valuePropName="checked"
>
<Switch />
</Form.Item>,
<Form.Item
key="tt_enforce_hours_for_tech_console"
name={["tt_enforce_hours_for_tech_console"]}
label={t("bodyshop.fields.tt_enforce_hours_for_tech_console")}
valuePropName="checked"
>
<Switch />
</Form.Item>
]
: []),
...(HasFeatureAccess({ featureName: "bills", bodyshop })
? [
<Form.Item
key="bill_allow_post_to_closed"
name={["bill_allow_post_to_closed"]}
label={t("bodyshop.fields.bill_allow_post_to_closed")}
valuePropName="checked"
>
<Switch />
</Form.Item>
]
: []),
...(HasFeatureAccess({ featureName: "export", bodyshop }) ...(HasFeatureAccess({ featureName: "export", bodyshop })
? [ ? [
...(ClosingPeriod.treatment === "on" ...(ClosingPeriod.treatment === "on"

View File

@@ -181,6 +181,7 @@ export function ExportLogsPageComponent() {
searchParams.search = value; searchParams.search = value;
history({ search: queryString.stringify(searchParams) }); history({ search: queryString.stringify(searchParams) });
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -4,7 +4,6 @@ import { PageHeader } from "@ant-design/pro-layout";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import AlertComponent from "../../components/alert/alert.component"; import AlertComponent from "../../components/alert/alert.component";
import JobsAvailableTableContainer from "../../components/jobs-available-table/jobs-available-table.container"; import JobsAvailableTableContainer from "../../components/jobs-available-table/jobs-available-table.container";
@@ -25,6 +24,26 @@ const mapDispatchToProps = (dispatch) => ({
export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, setSelectedHeader }) { export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation(); const { t } = useTranslation();
const getOS = () => {
const userAgent = navigator.userAgent;
if (userAgent.indexOf("Win") !== -1) return "windows";
if (userAgent.indexOf("Mac") !== -1) return "mac";
if (userAgent.indexOf("Linux") !== -1) return "linux";
return "unknown";
};
const os = getOS();
const downloadUrl = InstanceRenderManager({
imex:
os === "windows"
? "https://imex-partner.s3.ca-central-1.amazonaws.com/imex-partner-x64.exe"
: "https://imex-partner.s3.ca-central-1.amazonaws.com/imex-partner-arm64.dmg",
rome:
os === "windows"
? "https://rome-partner.s3.us-east-2.amazonaws.com/rome-partner-x64.exe"
: "https://rome-partner.s3.us-east-2.amazonaws.com/rome-partner-arm64.dmg"
});
useEffect(() => { useEffect(() => {
document.title = t("titles.jobsavailable", { document.title = t("titles.jobsavailable", {
app: InstanceRenderManager({ app: InstanceRenderManager({
@@ -39,24 +58,12 @@ export function JobsAvailablePageContainer({ partnerVersion, setBreadcrumbs, set
return ( return (
<RbacWrapper action="jobs:available-list"> <RbacWrapper action="jobs:available-list">
<div> <div>
<PageHeader <PageHeader />
title={t("titles.bc.availablejobs")}
extra={
<Link to="/manage/jobs/new">
<Button>{t("jobs.actions.manualnew")}</Button>
</Link>
}
/>
{!partnerVersion && ( {!partnerVersion && (
<AlertComponent <AlertComponent
type="warning" type="warning"
action={ action={
<a <a href={downloadUrl}>
href={InstanceRenderManager({
imex: "https://partner.imex.online/Setup.exe",
rome: "https://partner.romeonline.io/Setup.exe"
})}
>
<Button size="small">{t("general.actions.download")}</Button> <Button size="small">{t("general.actions.download")}</Button>
</a> </a>
} }

View File

@@ -167,6 +167,7 @@ export function PhonebookPageComponent({ bodyshop, authLevel }) {
searchParams.page = 1; searchParams.page = 1;
history({ search: queryString.stringify(searchParams) }); history({ search: queryString.stringify(searchParams) });
}} }}
enterButton
/> />
</Space> </Space>
} }

View File

@@ -267,7 +267,6 @@ function GenerateCostingData(job) {
const materialsHours = { mapaHrs: 0, mashHrs: 0 }; const materialsHours = { mapaHrs: 0, mashHrs: 0 };
let mashOpCodes = InstanceManager({ let mashOpCodes = InstanceManager({
imex: [],
rome: ParseCalopCode(job.materials["MASH"]?.cal_opcode) rome: ParseCalopCode(job.materials["MASH"]?.cal_opcode)
}); });
let hasMapaLine = false; let hasMapaLine = false;
@@ -356,8 +355,14 @@ function GenerateCostingData(job) {
if (val.mod_lbr_ty === "LAR") { if (val.mod_lbr_ty === "LAR") {
materialsHours.mapaHrs += val.mod_lb_hrs || 0; materialsHours.mapaHrs += val.mod_lb_hrs || 0;
} }
if (val.mod_lbr_ty !== "LAR" && mashOpCodes.includes(val.lbr_op)) { if (InstanceManager({ imex: true, rome: false })) {
materialsHours.mashHrs += val.mod_lb_hrs || 0; if (val.mod_lbr_ty !== "LAR") {
materialsHours.mashHrs += val.mod_lb_hrs || 0;
}
} else {
if (val.mod_lbr_ty !== "LAR" && mashOpCodes.includes(val.lbr_op)) {
materialsHours.mashHrs += val.mod_lb_hrs || 0;
}
} }
} }
@@ -517,10 +522,7 @@ function GenerateCostingData(job) {
} }
} }
if (InstanceManager({ imex: true, rome: false })) { if (InstanceManager({ rome: true })) {
//this might need to be removed for ImEX
jobLineTotalsByProfitCenter.parts[key] = jobLineTotalsByProfitCenter.parts[key].add(disc).add(markup);
} else {
const correspondingCiecaStlTotalLine = job.cieca_stl?.data.find( const correspondingCiecaStlTotalLine = job.cieca_stl?.data.find(
(c) => c.ttl_typecd === convertedKey.toUpperCase() (c) => c.ttl_typecd === convertedKey.toUpperCase()
); );
@@ -535,7 +537,11 @@ function GenerateCostingData(job) {
if (!hasMapaLine) { if (!hasMapaLine) {
let threshold; let threshold;
if (job.materials["MAPA"].cal_maxdlr !== undefined && job.materials["MAPA"].cal_maxdlr >= 0) { if (
job.materials["MAPA"] &&
job.materials["MAPA"].cal_maxdlr !== undefined &&
job.materials["MAPA"].cal_maxdlr >= 0
) {
//It has an upper threshhold. //It has an upper threshhold.
threshold = Dinero({ threshold = Dinero({
amount: Math.round(job.materials["MAPA"].cal_maxdlr * 100) amount: Math.round(job.materials["MAPA"].cal_maxdlr * 100)
@@ -581,7 +587,11 @@ function GenerateCostingData(job) {
} }
if (!hasMashLine) { if (!hasMashLine) {
let threshold; let threshold;
if (job.materials["MASH"].cal_maxdlr !== undefined && job.materials["MASH"].cal_maxdlr >= 0) { if (
job.materials["MASH"] &&
job.materials["MASH"].cal_maxdlr !== undefined &&
job.materials["MASH"].cal_maxdlr >= 0
) {
//It has an upper threshhold. //It has an upper threshhold.
threshold = Dinero({ threshold = Dinero({
amount: Math.round(job.materials["MASH"].cal_maxdlr * 100) amount: Math.round(job.materials["MASH"].cal_maxdlr * 100)

View File

@@ -47,14 +47,14 @@ exports.totalsSsu = async function (req, res) {
throw new Error("Failed to update job totals"); throw new Error("Failed to update job totals");
} }
res.status(200).send(); res.status(200).json({ success: true });
} catch (error) { } catch (error) {
logger.log("job-totals-ssu-USA-error", "error", req?.user?.email, id, { logger.log("job-totals-ssu-USA-error", "error", req?.user?.email, id, {
jobid: id, jobid: id,
error: error.message, error: error.message,
stack: error.stack stack: error.stack
}); });
res.status(503).send(); res.status(503).json({ error: "Failed to calculate totals" });
} }
}; };

View File

@@ -47,14 +47,14 @@ exports.totalsSsu = async function (req, res) {
throw new Error("Failed to update job totals"); throw new Error("Failed to update job totals");
} }
res.status(200).send(); res.status(200).json({ success: true });
} catch (error) { } catch (error) {
logger.log("job-totals-ssu-error", "error", req.user.email, id, { logger.log("job-totals-ssu-error", "error", req.user.email, id, {
jobid: id, jobid: id,
error: error.message, error: error.message,
stack: error.stack stack: error.stack
}); });
res.status(503).send(); res.status(503).json({ error: "Failed to calculate totals" });
} }
}; };