Merged in release/2025-08-29 (pull request #2518)

Release/2025 08 29
This commit is contained in:
Dave Richer
2025-08-27 19:27:22 +00:00
47 changed files with 6046 additions and 2150 deletions

View File

@@ -1,20 +1,20 @@
import { ApolloProvider } from "@apollo/client";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react";
import { CookiesProvider } from "react-cookie";
import { useTranslation } from "react-i18next";
import { connect, useSelector } from "react-redux";
import { createStructuredSelector } from "reselect";
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
import { setDarkMode } from "../redux/application/application.actions";
import { selectDarkMode } from "../redux/application/application.selectors";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import client from "../utils/GraphQLClient";
import App from "./App";
import * as Sentry from "@sentry/react";
import getTheme from "./themeProvider";
import { CookiesProvider } from "react-cookie";
import { createStructuredSelector } from "reselect";
import { selectCurrentUser } from "../redux/user/user.selectors.js";
import { selectDarkMode } from "../redux/application/application.selectors";
import { setDarkMode } from "../redux/application/application.actions";
// Base Split configuration
const config = {
@@ -85,7 +85,7 @@ function AppContainer({ currentUser, setDarkMode }) {
theme={theme}
form={{
validateMessages: {
required: t("general.validation.required", { label: "{{label}}" })
required: t("general.validation.required", { label: "${label}" })
}
}}
>

View File

@@ -22,9 +22,9 @@ export function BreadCrumbs({ breadcrumbs, bodyshop, isPartsEntry }) {
} = useSplitTreatments({
attributes: {},
names: ["OpenSearch"],
splitKey: bodyshop && bodyshop.imexshopid
splitKey: bodyshop?.imexshopid
});
// TODO - Client Update - Technically key is not doing anything here
return (
<Row className="breadcrumb-container">
<Col xs={24} sm={24} md={16}>
@@ -35,7 +35,7 @@ export function BreadCrumbs({ breadcrumbs, bodyshop, isPartsEntry }) {
key: "home",
title: (
<Link to={isPartsEntry ? `/parts/` : `/manage/`}>
<HomeFilled /> {(bodyshop && bodyshop.shopname && `(${bodyshop.shopname})`) || ""}
<HomeFilled /> {(bodyshop?.shopname && `(${bodyshop.shopname})`) || ""}
</Link>
)
},

View File

@@ -4,13 +4,8 @@ import parsePhoneNumber from "libphonenumber-js";
import { forwardRef } from "react";
import "./phone-form-item.styles.scss";
function FormItemPhone(props) {
return (
<Input
// country="ca" ref={ref} className="ant-input"
{...props}
/>
);
function FormItemPhone(props, ref) {
return <Input ref={ref} {...props} />;
}
export default forwardRef(FormItemPhone);

View File

@@ -1,6 +1,5 @@
import { AlertOutlined, BulbOutlined } from "@ant-design/icons";
import { Button, Layout, Space } from "antd";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
@@ -17,18 +16,7 @@ const mapStateToProps = createStructuredSelector({
export function GlobalFooter({ isPartsEntry }) {
const { t } = useTranslation();
useEffect(() => {
// Canny Not Required on Parts Entry
if (isPartsEntry) return;
window.Canny("initChangelog", {
appID: "680bd2c7ee501290377f6686",
position: "top",
align: "left",
theme: "light" // options: light [default], dark, auto
});
}, [isPartsEntry]);
if (isPartsEntry) {
return (
<Footer>

View File

@@ -8,19 +8,20 @@ import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectIsPartsEntry, selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
jobRO: selectJobReadOnly
jobRO: selectJobReadOnly,
isPartsEntry: selectIsPartsEntry
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail, isPartsEntry }) {
const { t } = useTranslation();
const [availableStatuses, setAvailableStatuses] = useState([]);
@@ -45,25 +46,43 @@ export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
});
};
// Updates available statuses based on job and bodyshop context
useEffect(() => {
//Figure out what scenario were in, populate accodingly
if (job && bodyshop) {
if (bodyshop.md_ro_statuses.pre_production_statuses.includes(job.status)) {
setAvailableStatuses(bodyshop.md_ro_statuses.pre_production_statuses);
} else if (bodyshop.md_ro_statuses.production_statuses.includes(job.status)) {
setAvailableStatuses(bodyshop.md_ro_statuses.production_statuses);
} else if (bodyshop.md_ro_statuses.post_production_statuses.includes(job.status)) {
setAvailableStatuses(
bodyshop.md_ro_statuses.post_production_statuses.filter(
(s) => s !== bodyshop.md_ro_statuses.default_invoiced && s !== bodyshop.md_ro_statuses.default_exported
)
);
} else {
console.log("Status didn't match any restrictions. Allowing all status changes.");
setAvailableStatuses(bodyshop.md_ro_statuses.statuses);
}
if (!job || !bodyshop) return;
const { md_ro_statuses } = bodyshop;
const {
parts_statuses,
pre_production_statuses,
production_statuses,
post_production_statuses,
statuses,
default_invoiced,
default_exported
} = md_ro_statuses;
if (isPartsEntry) {
// Set parts-specific statuses for parts entry scenario
setAvailableStatuses(parts_statuses);
return;
}
}, [job, setAvailableStatuses, bodyshop]);
// Handle non-parts entry scenarios based on job status
if (pre_production_statuses.includes(job.status)) {
setAvailableStatuses(pre_production_statuses);
} else if (production_statuses.includes(job.status)) {
setAvailableStatuses(production_statuses);
} else if (post_production_statuses.includes(job.status)) {
// Filter out invoiced and exported statuses for post-production
setAvailableStatuses(
post_production_statuses.filter((status) => status !== default_invoiced && status !== default_exported)
);
} else {
// Default to all statuses if no specific restrictions apply
console.log("Status didn't match any restrictions. Allowing all status changes.");
setAvailableStatuses(statuses);
}
}, [job, bodyshop, isPartsEntry, setAvailableStatuses]);
const statusMenu = {
items: [

View File

@@ -37,7 +37,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
.filter(
(temp) =>
(!temp.regions ||
(temp.regions && temp.regions[bodyshop.region_config]) ||
temp.regions?.[bodyshop.region_config] ||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)) &&
(!temp.dms || temp.dms === false)
)
@@ -46,7 +46,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
.filter(
(temp) =>
!temp.regions ||
(temp.regions && temp.regions[bodyshop.region_config]) ||
temp.regions?.[bodyshop.region_config] ||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
);
@@ -82,7 +82,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
variables: { id: jobId }
},
{
to: job && job.ownr_ea,
to: job?.ownr_ea,
subject: cards.find((c) => c.key === key)?.subject
},
"e",
@@ -129,7 +129,7 @@ export function PrintCenterJobsPartsComponent({ printCenterModal, bodyshop, tech
const columns = `repeat(${actions.length}, 1fr)`;
return (
<Col key={item.key} xs={24} sm={12}>
<Col key={item.key} xs={24} sm={24} md={24} lg={24} xl={24}>
<Card hoverable style={{ minHeight: 100 }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ flex: "1 1 70%", minWidth: 0 }}>

View File

@@ -64,7 +64,7 @@ export default function ShopInfoContainer() {
onFinish={handleFinish}
initialValues={
data
? data.bodyshops[0].accountingconfig.ClosingPeriod
? data?.bodyshops?.[0]?.accountingconfig?.ClosingPeriod
? {
...data.bodyshops[0],
accountingconfig: {

View File

@@ -96,13 +96,15 @@ export function SimplifiedPartsJobsListComponent({
key: "status",
ellipsis: true,
sorter: search?.search ? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.active_statuses) : true,
sorter: search?.search
? (a, b) => statusSort(a.status, b.status, bodyshop.md_ro_statuses.parts_active_statuses)
: true,
sortOrder: sortcolumn === "status" && sortorder,
render: (text, record) => {
return record.status || t("general.labels.na");
},
filteredValue: filter?.status || null,
filters: bodyshop.md_ro_statuses.statuses.map((s) => {
filters: bodyshop.md_ro_statuses.parts_statuses.map((s) => {
return { text: s, value: [s] };
}),
onFilter: (value, record) => value.includes(record.status)

View File

@@ -167,7 +167,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
socketRef.current = socketInstance;
const handleBodyshopMessage = (message) => {
if (!message || !message.type) return;
if (!message?.type) return;
switch (message.type) {
case "alert-update":
store.dispatch(addAlerts(message.payload));
@@ -512,21 +512,20 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
};
const unsubscribe = auth.onIdTokenChanged(async (user) => {
if (user) {
const token = await user.getIdToken();
if (socketRef.current) {
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id });
} else {
initializeSocket(token).catch((err) =>
console.error(`Something went wrong Initializing Sockets: ${err?.message || ""}`)
);
}
if (!user) {
socketRef.current?.disconnect();
socketRef.current = null;
setIsConnected(false);
return;
}
const token = await user.getIdToken();
if (socketRef.current) {
socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id });
} else {
if (socketRef.current) {
socketRef.current.disconnect();
socketRef.current = null;
setIsConnected(false);
}
initializeSocket(token).catch((err) =>
console.error("Something went wrong Initializing Sockets:", err?.message || "")
);
}
});

View File

@@ -4,6 +4,8 @@ import { getAuth, updatePassword, updateProfile } from "@firebase/auth";
import { getFirestore } from "@firebase/firestore";
import { getMessaging, getToken, onMessage } from "@firebase/messaging";
import { store } from "../redux/store";
import * as amplitude from '@amplitude/analytics-browser';
import posthog from 'posthog-js'
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
initializeApp(config);
@@ -71,25 +73,33 @@ onMessage(messaging, (payload) => {
});
export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
const state = stateProp || store.getState();
const eventParams = {
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
...additionalParams
};
// axios.post("/ioevent", {
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
// operationName: eventName,
// variables: additionalParams,
// dbevent: false,
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
// });
// console.log(
// "%c[Analytics]",
// "background-color: green ;font-weight:bold;",
// eventName,
// eventParams
// );
logEvent(analytics, eventName, eventParams);
try {
const state = stateProp || store.getState();
const eventParams = {
shop: (state.user && state.user.bodyshop && state.user.bodyshop.shopname) || null,
user: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
...additionalParams
};
// axios.post("/ioevent", {
// useremail: (state.user && state.user.currentUser && state.user.currentUser.email) || null,
// bodyshopid: (state.user && state.user.bodyshop && state.user.bodyshop.id) || null,
// operationName: eventName,
// variables: additionalParams,
// dbevent: false,
// env: `master-AIO|${import.meta.env.VITE_APP_GIT_SHA_DATE}`
// });
// console.log(
// "%c[Analytics]",
// "background-color: green ;font-weight:bold;",
// eventName,
// eventParams
// );
logEvent(analytics, eventName, eventParams);
amplitude.track(eventName, eventParams);
posthog.capture(eventName, eventParams);
} finally {
//If it fails, just keep going.
}
};

View File

@@ -14,6 +14,8 @@ import { persistor, store } from "./redux/store";
import reportWebVitals from "./reportWebVitals";
import "./translations/i18n";
import "./utils/CleanAxios";
import * as amplitude from "@amplitude/analytics-browser";
import { PostHogProvider } from "posthog-js/react";
window.global ||= window;
@@ -23,10 +25,10 @@ registerSW({ immediate: true });
// Dinero.globalLocale = "en-CA";
Dinero.globalRoundingMode = "HALF_EVEN";
amplitude.init("6228a598e57cd66875cfd41604f1f891", {});
const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouterV6(createBrowserRouter);
const router = sentryCreateBrowserRouter(createRoutesFromElements(<Route path="*" element={<AppContainer />} />));
if (import.meta.env.DEV) {
let styles =
"font-weight: bold; font-size: 50px;color: red; 6px 6px 0 rgb(226,91,14) , 9px 9px 0 rgb(245,221,8) , 12px 12px 0 rgb(5,148,68) ";
@@ -37,7 +39,12 @@ function App() {
return (
<PersistGate loading={<LoadingSpinner message="Restoring your settings..." />} persistor={persistor}>
<Provider store={store}>
<RouterProvider router={router} />
<PostHogProvider
apiKey={import.meta.env.VITE_PUBLIC_POSTHOG_KEY}
options={{ autocapture: false, capture_exceptions: true }}
>
<RouterProvider router={router} />
</PostHogProvider>
</Provider>
</PersistGate>
);

View File

@@ -2,7 +2,7 @@ import { FloatButton, Layout, Spin } from "antd";
import { Route, Routes } from "react-router-dom";
// import preval from "preval.macro";
import { lazy, Suspense, useEffect, useState } from "react";
import { lazy, Suspense, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -23,6 +23,7 @@ import UpdateAlert from "../../components/update-alert/update-alert.component";
import { selectBodyshop, selectInstanceConflict, selectPartsManagementOnly } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import useAlertsNotifications from "../../hooks/useAlertsNotifications.jsx";
import { selectDarkMode } from "../../redux/application/application.selectors.js";
const PrintCenterModalContainer = lazy(
() => import("../../components/print-center-modal/print-center-modal.container")
@@ -107,22 +108,26 @@ const { Content } = Layout;
const mapStateToProps = createStructuredSelector({
conflict: selectInstanceConflict,
bodyshop: selectBodyshop,
partsManagementOnly: selectPartsManagementOnly
partsManagementOnly: selectPartsManagementOnly,
isDarkMode: selectDarkMode
});
export function Manage({ conflict, bodyshop, partsManagementOnly }) {
export function Manage({ conflict, bodyshop, partsManagementOnly, isDarkMode }) {
const { t } = useTranslation();
const [chatVisible] = useState(false);
const didMount = useRef(false);
// Centralized alerts handling (fetch + dedupe + notifications)
useAlertsNotifications();
useEffect(() => {
if (didMount.current) return; // prevents dev StrictMode double-run
didMount.current = true;
window.Canny("initChangelog", {
appID: "680bd2c7ee501290377f6686",
position: "top",
align: "left",
theme: "light" // options: light [default], dark, auto
theme: !isDarkMode ? "light" : "dark"
});
}, []);

View File

@@ -60,19 +60,19 @@ function SimplifiedPartsJobsDetailContainer({ setBreadcrumbs, addRecentItem, set
imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)"
}),
ro_number: (data.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na")
ro_number: data.jobs_by_pk?.ro_number || t("general.labels.na")
});
setBreadcrumbs([
{ link: "/parts/", label: t("titles.bc.jobs") },
{ link: "/parts", label: t("titles.bc.parts") },
{
link: `/parts/jobs/${jobId}`,
label: t("titles.bc.jobs-detail", {
number: (data && data.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na")
number: (data?.jobs_by_pk && data.jobs_by_pk.ro_number) || t("general.labels.na")
})
}
]);
if (data && data.jobs_by_pk) {
if (data?.jobs_by_pk) {
setJobReadOnly(IsJobReadOnly(data.jobs_by_pk));
addRecentItem(

View File

@@ -22,7 +22,7 @@ export function SimplifiedPartsJobsPage({ setBreadcrumbs, setSelectedHeader }) {
})
});
setSelectedHeader("parts-queue");
setBreadcrumbs([{ link: "/parts", label: t("titles.bc.simplified-parts-jobs") }]);
setBreadcrumbs([{ link: "/parts", label: t("titles.bc.parts") }]);
}, [setBreadcrumbs, t, setSelectedHeader]);
return (

View File

@@ -3,7 +3,7 @@ import { FloatButton, Layout, Spin } from "antd";
import { lazy, Suspense, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Route, Routes } from "react-router-dom";
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import BreadCrumbs from "../../components/breadcrumbs/breadcrumbs.component.jsx";
import ConflictComponent from "../../components/conflict/conflict.component.jsx";
@@ -39,6 +39,15 @@ const mapStateToProps = createStructuredSelector({
export function SimplifiedPartsPage({ conflict, bodyshop }) {
const { t } = useTranslation();
// Redirector to strip '/parts/jobs' from path for non-detail routes
function JobsStripRedirect() {
// lazy import to avoid top-level import churn
const location = useLocation();
const { pathname, search, hash } = location;
const target = pathname.replace("/parts/jobs", "/parts") + (search || "") + (hash || "");
return <Navigate to={target} replace />;
}
// Centralized alerts handling (fetch + dedupe + notifications)
useAlertsNotifications();
@@ -67,6 +76,10 @@ export function SimplifiedPartsPage({ conflict, bodyshop }) {
<EmailOverlayContainer />
<PrintCenterModalContainer />
<Routes>
{/* Redirect legacy or relative routes that include '/jobs' segment */}
<Route path="jobs" element={<JobsStripRedirect />} />
<Route path="jobs/*" element={<JobsStripRedirect />} />
<Route
path="/"
element={

View File

@@ -42,33 +42,31 @@ export function VehicleDetailContainer({ setBreadcrumbs, addRecentItem, setSelec
imex: "$t(titles.imexonline)",
rome: "$t(titles.romeonline)"
}),
vehicle:
data && data.vehicles_by_pk
? `${(data.vehicles_by_pk && data.vehicles_by_pk.v_model_yr) || ""} ${
(data.vehicles_by_pk && data.vehicles_by_pk.v_make_desc) || ""
} ${(data.vehicles_by_pk && data.vehicles_by_pk.v_model_desc) || ""}`
: ""
vehicle: data?.vehicles_by_pk
? `${data.vehicles_by_pk?.v_model_yr || ""} ${
data.vehicles_by_pk?.v_make_desc || ""
} ${data.vehicles_by_pk?.v_model_desc || ""}`
: ""
});
setSelectedHeader("vehicles");
const crumbs = [];
if (isPartsEntry) crumbs.push({ link: "/parts/", label: t("titles.bc.jobs") });
if (isPartsEntry) crumbs.push({ link: "/parts", label: t("titles.bc.parts") });
crumbs.push({ link: `${basePath}/vehicles`, label: t("titles.bc.vehicles") });
crumbs.push({
link: `${basePath}/vehicles/${vehId}`,
label: t("titles.bc.vehicle-details", {
vehicle:
data && data.vehicles_by_pk
? `${(data.vehicles_by_pk && data.vehicles_by_pk.v_model_yr) || ""} ${
(data.vehicles_by_pk && data.vehicles_by_pk.v_make_desc) || ""
} ${(data.vehicles_by_pk && data.vehicles_by_pk.v_model_desc) || ""}`
: ""
vehicle: data?.vehicles_by_pk
? `${data.vehicles_by_pk?.v_model_yr || ""} ${
data.vehicles_by_pk?.v_make_desc || ""
} ${data.vehicles_by_pk?.v_model_desc || ""}`
: ""
})
});
setBreadcrumbs(crumbs);
if (data && data.vehicles_by_pk)
if (data?.vehicles_by_pk)
addRecentItem(
CreateRecentItem(
vehId,

View File

@@ -33,7 +33,7 @@ export function VehiclesPageContainer({ setBreadcrumbs, setSelectedHeader, isPar
if (isPartsEntry) {
setBreadcrumbs([
{ link: "/parts/", label: t("titles.bc.jobs") },
{ link: "/parts", label: t("titles.bc.parts") },
{ link: `${basePath}/vehicles`, label: t("titles.bc.vehicles") }
]);
} else {

View File

@@ -49,6 +49,8 @@ import {
validatePasswordResetSuccess
} from "./user.actions";
import UserActionTypes from "./user.types";
import * as amplitude from '@amplitude/analytics-browser';
import posthog from 'posthog-js';
const fpPromise = FingerprintJS.load();
@@ -83,8 +85,6 @@ export function* onCheckUserSession() {
export function* isUserAuthenticated() {
try {
logImEXEvent("redux_auth_check");
const user = yield getCurrentUser();
if (!user) {
yield put(unauthorizedUser());
@@ -92,6 +92,8 @@ export function* isUserAuthenticated() {
}
LogRocket.identify(user.email);
amplitude.setUserId(user.email);
posthog.identify(user.email);
const eulaQuery = yield client.query({
query: QUERY_EULA,
@@ -137,6 +139,7 @@ export function* signOutStart() {
imexshopid: state.user.bodyshop.imexshopid,
type: "messaging"
});
amplitude.reset();
} catch {
console.log("No FCM token. Skipping unsubscribe.");
}
@@ -266,11 +269,11 @@ export function* signInSuccessSaga({ payload }) {
instanceSeg,
...(isParts
? [
InstanceRenderManager({
imex: "ImexPartsManagement",
rome: "RomePartsManagement"
})
]
InstanceRenderManager({
imex: "ImexPartsManagement",
rome: "RomePartsManagement"
})
]
: [])
];
window.$crisp.push(["set", "session:segments", [segs]]);
@@ -295,7 +298,6 @@ export function* signInSuccessSaga({ payload }) {
setUserId(analytics, payload.email);
setUserProperties(analytics, payload);
yield logImEXEvent("redux_sign_in_success");
}
export function* onSendPasswordResetStart() {
@@ -362,6 +364,7 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
}
try {
amplitude.setGroup('Shop', payload.shopname);
window.$crisp.push(["set", "user:company", [payload.shopname]]);
if (authRecord[0] && authRecord[0].user.validemail) {
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);

View File

@@ -3523,6 +3523,8 @@
}
},
"titles": {
"parts_settings": "Parts Management Settings | {{app}}",
"simplified-parts-jobs": "Parts Management | {{app}}",
"accounting-payables": "Payables | {{app}}",
"accounting-payments": "Payments | {{app}}",
"accounting-receivables": "Receivables | {{app}}",
@@ -3530,7 +3532,7 @@
"app": "",
"bc": {
"simplified-parts-jobs": "Jobs",
"parts": "Jobs",
"parts": "Parts",
"parts_settings": "Settings",
"accounting-payables": "Payables",
"accounting-payments": "Payments",

View File

@@ -3523,6 +3523,9 @@
}
},
"titles": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "",
"accounting-payments": "",
"accounting-receivables": "",

View File

@@ -3523,6 +3523,9 @@
}
},
"titles": {
"simplified-parts-jobs": "",
"parts": "",
"parts_settings": "",
"accounting-payables": "",
"accounting-payments": "",
"accounting-receivables": "",