Compare commits

..

6 Commits

Author SHA1 Message Date
Patrick Fic
4043bd3d33 Merged in feature/IO-3722-disable-contact-fortellis (pull request #3286)
IO-3722 Remove delivery date for bypass vehicles.

Approved-by: Dave Richer
2026-05-28 18:33:50 +00:00
Patrick Fic
4fd2f034a3 Merged in feature/IO-3722-disable-contact-fortellis (pull request #3283)
IO-3722 Remove customer lookup by Vehicle Owner.

Approved-by: Dave Richer
2026-05-28 16:55:21 +00:00
Patrick Fic
bd25245290 Merged in feature/IO-3722-disable-contact-fortellis (pull request #3281)
IO-3722 Fix undefined customer ref.
2026-05-27 21:19:02 +00:00
Patrick Fic
6472b053ed Merged in feature/IO-3722-disable-contact-fortellis (pull request #3280)
Resolve inversed if statement.
2026-05-27 19:54:30 +00:00
Patrick Fic
169070594c Merged in feature/IO-3722-disable-contact-fortellis (pull request #3279)
IO-3722 Add additional await.
2026-05-27 19:42:38 +00:00
Dave Richer
0974e69a50 Merged in feature/IO-3722-disable-contact-fortellis (pull request #3277)
IO-3722 Disable contact API calls for Fortellis.
2026-05-27 18:36:51 +00:00
117 changed files with 4353 additions and 7665 deletions

View File

@@ -7,7 +7,6 @@ _reference
client client
redis/dockerdata redis/dockerdata
hasura hasura
harness-feature-flags-export
node_modules node_modules
# Files to exclude # Files to exclude
.ebignore .ebignore

View File

@@ -7,7 +7,6 @@
/client /client
/firebase /firebase
/hasura /hasura
/harness-feature-flags-export
/jsreport /jsreport
/node_modules /node_modules
.env.local .env.local

6
.gitignore vendored
View File

@@ -17,9 +17,6 @@ jsreport/auth-server/node_modules
client/coverage client/coverage
admin/coverage admin/coverage
# Generated Harness/Split feature flag export artifacts
/harness-feature-flags-export/
# production # production
/build /build
client/build client/build
@@ -156,5 +153,4 @@ docker_data
.terraform .terraform
terraform.tfvars terraform.tfvars
terraform.exe

File diff suppressed because it is too large Load Diff

3193
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,61 +8,62 @@
"private": true, "private": true,
"proxy": "http://localhost:4000", "proxy": "http://localhost:4000",
"dependencies": { "dependencies": {
"@amplitude/analytics-browser": "^2.42.4", "@amplitude/analytics-browser": "^2.38.0",
"@ant-design/pro-layout": "^7.22.6", "@ant-design/pro-layout": "^7.22.6",
"@apollo/client": "^4.2.0", "@apollo/client": "^4.1.6",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@documenso/embed-react": "^0.6.1", "@documenso/embed-react": "^0.5.1",
"@emotion/is-prop-valid": "^1.4.0", "@emotion/is-prop-valid": "^1.4.0",
"@fingerprintjs/fingerprintjs": "^5.2.0", "@fingerprintjs/fingerprintjs": "^5.1.0",
"@firebase/analytics": "^0.10.22", "@firebase/analytics": "^0.10.21",
"@firebase/app": "^0.14.12", "@firebase/app": "^0.14.10",
"@firebase/auth": "^1.13.1", "@firebase/auth": "^1.12.2",
"@firebase/firestore": "^4.14.1", "@firebase/firestore": "^4.13.0",
"@firebase/messaging": "^0.12.26", "@firebase/messaging": "^0.12.25",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.12.0", "@reduxjs/toolkit": "^2.11.2",
"@sentry/cli": "^3.4.3", "@sentry/cli": "^3.3.5",
"@sentry/react": "^10.53.1", "@sentry/react": "^10.47.0",
"@sentry/vite-plugin": "^4.9.1", "@sentry/vite-plugin": "^4.9.1",
"@splitsoftware/splitio-react": "^2.6.1",
"@tanem/react-nprogress": "^5.0.63", "@tanem/react-nprogress": "^5.0.63",
"antd": "^6.4.3", "antd": "^6.3.5",
"apollo-link-logger": "^3.0.0", "apollo-link-logger": "^3.0.0",
"autosize": "^6.0.1", "autosize": "^6.0.1",
"axios": "^1.16.1", "axios": "^1.14.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"css-box-model": "^1.2.1", "css-box-model": "^1.2.1",
"dayjs": "^1.11.20", "dayjs": "^1.11.20",
"dayjs-business-days2": "^1.3.3", "dayjs-business-days2": "^1.3.3",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^17.4.2", "dotenv": "^17.3.1",
"env-cmd": "^11.0.0", "env-cmd": "^11.0.0",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"graphql": "^16.14.0", "graphql": "^16.13.2",
"graphql-ws": "^6.0.8", "graphql-ws": "^6.0.8",
"i18next": "^25.10.10", "i18next": "^25.10.10",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"immutability-helper": "^3.1.1", "immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.13.3", "libphonenumber-js": "^1.12.41",
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"logrocket": "^12.1.1", "logrocket": "^12.1.0",
"markerjs2": "^2.32.7", "markerjs2": "^2.32.7",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"normalize-url": "^8.1.1", "normalize-url": "^8.1.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"phone": "^3.1.71", "phone": "^3.1.71",
"posthog-js": "^1.376.0", "posthog-js": "^1.364.4",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^9.3.1", "query-string": "^9.3.1",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",
"react": "^19.2.6", "react": "^19.2.4",
"react-big-calendar": "^1.19.4", "react-big-calendar": "^1.19.4",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^8.1.2", "react-cookie": "^8.1.0",
"react-dom": "^19.2.6", "react-dom": "^19.2.4",
"react-grid-gallery": "^1.0.1", "react-grid-gallery": "^1.0.1",
"react-grid-layout": "^2.2.3", "react-grid-layout": "^2.2.3",
"react-i18next": "^16.6.6", "react-i18next": "^16.6.6",
@@ -72,22 +73,22 @@
"react-number-format": "^5.4.5", "react-number-format": "^5.4.5",
"react-popopo": "^2.1.9", "react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.62", "react-product-fruits": "^2.2.62",
"react-redux": "^9.3.0", "react-redux": "^9.2.0",
"react-resizable": "^3.1.3", "react-resizable": "^3.1.3",
"react-router-dom": "^7.15.1", "react-router-dom": "^7.13.2",
"react-sticky": "^6.0.3", "react-sticky": "^6.0.3",
"react-virtuoso": "^4.18.7", "react-virtuoso": "^4.18.3",
"recharts": "^3.8.1", "recharts": "^3.8.1",
"redux": "^5.0.1", "redux": "^5.0.1",
"redux-actions": "^3.0.3", "redux-actions": "^3.0.3",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"redux-saga": "^1.5.0", "redux-saga": "^1.4.2",
"redux-state-sync": "^3.1.4", "redux-state-sync": "^3.1.4",
"reselect": "^5.2.0", "reselect": "^5.1.1",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"sass": "^1.100.0", "sass": "^1.98.0",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"styled-components": "^6.4.2", "styled-components": "^6.3.12",
"vite-plugin-ejs": "^1.7.0", "vite-plugin-ejs": "^1.7.0",
"web-vitals": "^5.2.0" "web-vitals": "^5.2.0"
}, },
@@ -137,14 +138,14 @@
"@rollup/rollup-linux-x64-gnu": "4.6.1" "@rollup/rollup-linux-x64-gnu": "4.6.1"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^6.2.3", "@ant-design/icons": "^6.1.1",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.29.7", "@babel/preset-react": "^7.28.5",
"@dotenvx/dotenvx": "^1.68.1", "@dotenvx/dotenvx": "^1.59.1",
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@playwright/test": "^1.60.0", "@playwright/test": "^1.58.2",
"@testing-library/dom": "^10.4.1", "@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
@@ -156,21 +157,21 @@
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-compiler": "^19.1.0-rc.2", "eslint-plugin-react-compiler": "^19.1.0-rc.2",
"globals": "^17.6.0", "globals": "^17.4.0",
"jsdom": "^28.1.0", "jsdom": "^28.1.0",
"memfs": "^4.57.2", "memfs": "^4.57.1",
"os-browserify": "^0.3.0", "os-browserify": "^0.3.0",
"playwright": "^1.60.0", "playwright": "^1.58.2",
"react-error-overlay": "^6.1.0", "react-error-overlay": "^6.1.0",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3", "source-map-explorer": "^2.5.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-babel": "^1.7.3", "vite-plugin-babel": "^1.6.0",
"vite-plugin-eslint": "^1.8.1", "vite-plugin-eslint": "^1.8.1",
"vite-plugin-node-polyfills": "^0.28.0", "vite-plugin-node-polyfills": "^0.26.0",
"vite-plugin-pwa": "^1.3.0", "vite-plugin-pwa": "^1.2.0",
"vite-plugin-style-import": "^2.0.0", "vite-plugin-style-import": "^2.0.0",
"vitest": "^4.1.7", "vitest": "^4.1.2",
"workbox-window": "^7.4.1" "workbox-window": "^7.4.0"
} }
} }

View File

@@ -5593,6 +5593,29 @@ Demo: https://rawgit.com/Sphinxxxx/color-conversion/master/demo/index.html
----------- -----------
The following NPM packages may be included in this product:
- @splitsoftware/splitio-commons@1.6.1
- @splitsoftware/splitio-react@1.7.1
These packages each contain the following license and notice below:
Copyright © 2022 Split Software, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-----------
The following NPM packages may be included in this product: The following NPM packages may be included in this product:
- @stripe/react-stripe-js@1.9.0 - @stripe/react-stripe-js@1.9.0

View File

@@ -0,0 +1,184 @@
import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider, Grid } 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 { useDispatch, useSelector } from "react-redux";
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 { signOutStart } from "../redux/user/user.actions";
import client from "../utils/GraphQLClient";
import App from "./App";
import getTheme from "./themeProvider";
// Base Split configuration
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon"
}
};
function SplitClientProvider({ children }) {
const imexshopid = useSelector((state) => state.user.imexshopid);
const splitClient = useSplitClient({ key: imexshopid || "anon" });
useEffect(() => {
if (import.meta.env.DEV && splitClient && imexshopid) {
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
}
}, [splitClient, imexshopid]);
return children;
}
function AppContainer() {
const { t } = useTranslation();
const dispatch = useDispatch();
const currentUser = useSelector(selectCurrentUser);
const isDarkMode = useSelector(selectDarkMode);
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isUltraWide = Boolean(screens.xxxl);
const theme = useMemo(() => {
const baseTheme = getTheme(isDarkMode);
return {
...baseTheme,
token: {
...(baseTheme.token || {}),
screenXXXL: 2160
},
components: {
...(baseTheme.components || {}),
Table: {
...(baseTheme.components?.Table || {}),
cellFontSizeSM: isPhone ? 12 : 13,
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
cellFontSize: isUltraWide ? 15 : 14,
cellPaddingInlineSM: isPhone ? 8 : 10,
cellPaddingInlineMD: isPhone ? 10 : 14,
cellPaddingInline: isUltraWide ? 20 : 16,
cellPaddingBlockSM: isPhone ? 8 : 10,
cellPaddingBlockMD: isPhone ? 10 : 12,
cellPaddingBlock: isUltraWide ? 14 : 12,
selectionColumnWidth: isPhone ? 44 : 52
}
}
};
}, [isDarkMode, isPhone, isUltraWide]);
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
const antdPagination = useMemo(
() => ({
showSizeChanger: !isPhone,
totalBoundaryShowSizeChanger: 100
}),
[isPhone]
);
const antdForm = useMemo(
() => ({
validateMessages: {
required: t("general.validation.required", { label: "${label}" })
}
}),
[t]
);
// Global seamless logout listener with redirect to /signin
useEffect(() => {
const handleSeamlessLogout = (event) => {
if (event.data?.type !== "seamlessLogoutRequest") return;
// Only accept messages from the parent window
if (event.source !== window.parent) return;
const targetOrigin = event.origin || "*";
if (currentUser?.authorized !== true) {
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
return;
}
dispatch(signOutStart());
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
};
window.addEventListener("message", handleSeamlessLogout);
return () => {
window.removeEventListener("message", handleSeamlessLogout);
};
}, [dispatch, currentUser?.authorized]);
// Update data-theme attribute (no cleanup to avoid transient style churn)
useEffect(() => {
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
}, [isDarkMode]);
// Sync darkMode with localStorage
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) {
dispatch(setDarkMode(false));
return;
}
const key = `dark-mode-${uid}`;
const raw = localStorage.getItem(key);
if (raw == null) {
dispatch(setDarkMode(false));
return;
}
try {
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
} catch {
dispatch(setDarkMode(false));
}
}, [currentUser?.uid, dispatch]);
// Persist darkMode
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) return;
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
}, [isDarkMode, currentUser?.uid]);
return (
<CookiesProvider>
<ApolloProvider client={client}>
<ConfigProvider
input={antdInput}
locale={enLocale}
theme={theme}
form={antdForm}
table={antdTable}
pagination={antdPagination}
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
popupOverflow="viewport"
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
</CookiesProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -1,6 +1,6 @@
import { ApolloProvider } from "@apollo/client/react"; import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react"; import * as Sentry from "@sentry/react";
import { FeatureFlagProvider, useFeatureFlagClient } from "../feature-flags/splitio-react-replacement"; import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider } from "antd"; import { ConfigProvider } from "antd";
import enLocale from "antd/es/locale/en_US"; import enLocale from "antd/es/locale/en_US";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
@@ -16,21 +16,23 @@ import client from "../utils/GraphQLClient";
import App from "./App"; import App from "./App";
import getTheme from "./themeProvider"; import getTheme from "./themeProvider";
// Base Split configuration
const config = { const config = {
core: { core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon" key: "anon"
} }
}; };
function FeatureFlagClientProvider({ children }) { function SplitClientProvider({ children }) {
const imexshopid = useSelector((state) => state.user.imexshopid); const imexshopid = useSelector((state) => state.user.imexshopid);
const featureFlagClient = useFeatureFlagClient({ key: imexshopid || "anon" }); const splitClient = useSplitClient({ key: imexshopid || "anon" });
useEffect(() => { useEffect(() => {
if (import.meta.env.DEV && featureFlagClient && imexshopid) { if (import.meta.env.DEV && splitClient && imexshopid) {
console.log(`Feature flag client initialized with key: ${imexshopid}, isReady: ${featureFlagClient.isReady}`); console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
} }
}, [featureFlagClient, imexshopid]); }, [splitClient, imexshopid]);
return children; return children;
} }
@@ -122,11 +124,11 @@ function AppContainer() {
<ApolloProvider client={client}> <ApolloProvider client={client}>
<ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}> <ConfigProvider input={antdInput} locale={enLocale} theme={theme} form={antdForm}>
<GlobalLoadingBar /> <GlobalLoadingBar />
<FeatureFlagProvider config={config}> <SplitFactoryProvider config={config}>
<FeatureFlagClientProvider> <SplitClientProvider>
<App /> <App />
</FeatureFlagClientProvider> </SplitClientProvider>
</FeatureFlagProvider> </SplitFactoryProvider>
</ConfigProvider> </ConfigProvider>
</ApolloProvider> </ApolloProvider>
</CookiesProvider> </CookiesProvider>

View File

@@ -0,0 +1,184 @@
import { ApolloProvider } from "@apollo/client/react";
import * as Sentry from "@sentry/react";
import { SplitFactoryProvider, useSplitClient } from "@splitsoftware/splitio-react";
import { ConfigProvider, Grid } 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 { useDispatch, useSelector } from "react-redux";
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 { signOutStart } from "../redux/user/user.actions";
import client from "../utils/GraphQLClient";
import App from "./App";
import getTheme from "./themeProvider";
// Base Split configuration
const config = {
core: {
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
key: "anon"
}
};
function SplitClientProvider({ children }) {
const imexshopid = useSelector((state) => state.user.imexshopid);
const splitClient = useSplitClient({ key: imexshopid || "anon" });
useEffect(() => {
if (import.meta.env.DEV && splitClient && imexshopid) {
console.log(`Split client initialized with key: ${imexshopid}, isReady: ${splitClient.isReady}`);
}
}, [splitClient, imexshopid]);
return children;
}
function AppContainer() {
const { t } = useTranslation();
const dispatch = useDispatch();
const currentUser = useSelector(selectCurrentUser);
const isDarkMode = useSelector(selectDarkMode);
const screens = Grid.useBreakpoint();
const isPhone = !screens.md;
const isUltraWide = Boolean(screens.xxxl);
const theme = useMemo(() => {
const baseTheme = getTheme(isDarkMode);
return {
...baseTheme,
token: {
...(baseTheme.token || {}),
screenXXXL: 2160
},
components: {
...(baseTheme.components || {}),
Table: {
...(baseTheme.components?.Table || {}),
cellFontSizeSM: isPhone ? 12 : 13,
cellFontSizeMD: isPhone ? 13 : isUltraWide ? 15 : 14,
cellFontSize: isUltraWide ? 15 : 14,
cellPaddingInlineSM: isPhone ? 8 : 10,
cellPaddingInlineMD: isPhone ? 10 : 14,
cellPaddingInline: isUltraWide ? 20 : 16,
cellPaddingBlockSM: isPhone ? 8 : 10,
cellPaddingBlockMD: isPhone ? 10 : 12,
cellPaddingBlock: isUltraWide ? 14 : 12,
selectionColumnWidth: isPhone ? 44 : 52
}
}
};
}, [isDarkMode, isPhone, isUltraWide]);
const antdInput = useMemo(() => ({ autoComplete: "new-password" }), []);
const antdTable = useMemo(() => ({ scroll: { x: "max-content" } }), []);
const antdPagination = useMemo(
() => ({
showSizeChanger: !isPhone,
totalBoundaryShowSizeChanger: 100
}),
[isPhone]
);
const antdForm = useMemo(
() => ({
validateMessages: {
required: t("general.validation.required", { label: "${label}" })
}
}),
[t]
);
// Global seamless logout listener with redirect to /signin
useEffect(() => {
const handleSeamlessLogout = (event) => {
if (event.data?.type !== "seamlessLogoutRequest") return;
// Only accept messages from the parent window
if (event.source !== window.parent) return;
const targetOrigin = event.origin || "*";
if (currentUser?.authorized !== true) {
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "already_logged_out" }, targetOrigin);
return;
}
dispatch(signOutStart());
window.parent?.postMessage({ type: "seamlessLogoutResponse", status: "logged_out" }, targetOrigin);
};
window.addEventListener("message", handleSeamlessLogout);
return () => {
window.removeEventListener("message", handleSeamlessLogout);
};
}, [dispatch, currentUser?.authorized]);
// Update data-theme attribute (no cleanup to avoid transient style churn)
useEffect(() => {
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
}, [isDarkMode]);
// Sync darkMode with localStorage
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) {
dispatch(setDarkMode(false));
return;
}
const key = `dark-mode-${uid}`;
const raw = localStorage.getItem(key);
if (raw == null) {
dispatch(setDarkMode(false));
return;
}
try {
dispatch(setDarkMode(Boolean(JSON.parse(raw))));
} catch {
dispatch(setDarkMode(false));
}
}, [currentUser?.uid, dispatch]);
// Persist darkMode
useEffect(() => {
const uid = currentUser?.uid;
if (!uid) return;
localStorage.setItem(`dark-mode-${uid}`, JSON.stringify(isDarkMode));
}, [isDarkMode, currentUser?.uid]);
return (
<CookiesProvider>
<ApolloProvider client={client}>
<ConfigProvider
input={antdInput}
locale={enLocale}
theme={theme}
form={antdForm}
table={antdTable}
pagination={antdPagination}
componentSize={isPhone ? "small" : isUltraWide ? "large" : "middle"}
popupOverflow="viewport"
>
<GlobalLoadingBar />
<SplitFactoryProvider config={config}>
<SplitClientProvider>
<App />
</SplitClientProvider>
</SplitFactoryProvider>
</ConfigProvider>
</ApolloProvider>
</CookiesProvider>
);
}
export default Sentry.withProfiler(AppContainer);

View File

@@ -1,4 +1,4 @@
import { useSplitClient } from "../feature-flags/splitio-react-replacement"; import { useSplitClient } from "@splitsoftware/splitio-react";
import { Button, Result } from "antd"; import { Button, Result } from "antd";
//import LogRocket from "logrocket"; //import LogRocket from "logrocket";
import { lazy, Suspense, useEffect, useState } from "react"; import { lazy, Suspense, useEffect, useState } from "react";
@@ -225,22 +225,13 @@ export function App({
path="/parts/*" path="/parts/*"
element={ element={
<ErrorBoundary> <ErrorBoundary>
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}> <PrivateRoute isAuthorized={currentUser.authorized} />
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
</ErrorBoundary> </ErrorBoundary>
} }
> >
<Route path="*" element={<SimplifiedPartsPageContainer />} /> <Route path="*" element={<SimplifiedPartsPageContainer />} />
</Route> </Route>
<Route <Route path="/edit/*" element={<PrivateRoute isAuthorized={currentUser.authorized} />}>
path="/edit/*"
element={
<SocketProvider bodyshop={bodyshop} navigate={navigate} currentUser={currentUser}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
}
>
<Route path="*" element={<DocumentEditorContainer />} /> <Route path="*" element={<DocumentEditorContainer />} />
</Route> </Route>
</Routes> </Routes>

View File

@@ -512,52 +512,7 @@
.esignature-embed { .esignature-embed {
display: block; width: 100%;
width: 100%; height: 100%;
height: 100%; border-width: 0;
border-width: 0; }
}
.esignature-modal {
.ant-modal {
top: 16px;
max-width: calc(100vw - 32px);
padding-bottom: 0;
}
.ant-modal-content {
display: flex;
flex-direction: column;
max-height: calc(100vh - 32px);
}
.ant-modal-body {
flex: 1 1 auto;
min-height: 0;
}
}
.esignature-modal-frame {
width: 100%;
height: calc(100vh - 150px);
min-height: 320px;
overflow: hidden;
}
@media (max-width: 768px), (max-height: 560px) {
.esignature-modal {
.ant-modal {
top: 8px;
max-width: calc(100vw - 16px);
}
.ant-modal-content {
max-height: calc(100vh - 16px);
}
}
.esignature-modal-frame {
height: calc(100vh - 132px);
min-height: 0;
}
}

View File

@@ -37,7 +37,6 @@ const defaultTheme = (isDarkMode) => ({
isDarkMode isDarkMode
), ),
colorError: isDarkMode ? "#ff4d4f" : "#f5222d", colorError: isDarkMode ? "#ff4d4f" : "#f5222d",
colorShadow: isDarkMode ? "rgba(0, 0, 0, 0.7)" : "#000000",
colorBgBase: isDarkMode ? "#1f1f1f" : "#ffffff" // Align with Ant Design dark mode colorBgBase: isDarkMode ? "#1f1f1f" : "#ffffff" // Align with Ant Design dark mode
} }
}); });

View File

@@ -1,5 +1,5 @@
import { useApolloClient, useMutation } from "@apollo/client/react"; import { useApolloClient, useMutation } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Divider, Form, Modal, Space } from "antd"; import { Button, Checkbox, Divider, Form, Modal, Space } from "antd";
import _ from "lodash"; import _ from "lodash";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";

View File

@@ -1,6 +1,6 @@
import Icon, { UploadOutlined } from "@ant-design/icons"; import Icon, { UploadOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client/react"; import { useApolloClient } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd"; import { Alert, Divider, Form, Input, Select, Space, Statistic, Switch, Upload } from "antd";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -1,5 +1,5 @@
import { useLazyQuery, useQuery } from "@apollo/client/react"; import { useLazyQuery, useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries"; import { QUERY_OUTSTANDING_INVENTORY } from "../../graphql/inventory.queries";

View File

@@ -1,5 +1,5 @@
import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons"; import { DeleteFilled, DollarCircleFilled } from "@ant-design/icons";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd"; import { Button, Checkbox, Form, Input, InputNumber, Select, Space, Switch, Table, Tooltip } from "antd";
import { useRef } from "react"; import { useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -36,7 +36,6 @@ export function BillEnterModalLinesComponent({
const { t } = useTranslation(); const { t } = useTranslation();
const { setFieldsValue, getFieldsValue, getFieldValue } = form; const { setFieldsValue, getFieldsValue, getFieldValue } = form;
const firstFieldRefs = useRef({}); const firstFieldRefs = useRef({});
const lineDescriptionRefs = useRef({});
const CONTROL_HEIGHT = 32; const CONTROL_HEIGHT = 32;
@@ -95,23 +94,6 @@ export function BillEnterModalLinesComponent({
}); });
}; };
const focusLineDescription = (index) => {
const lineDescription = lineDescriptionRefs.current[index];
if (typeof lineDescription?.focus === "function") {
lineDescription.focus({ preventScroll: true });
return;
}
lineDescription?.resizableTextArea?.textArea?.focus?.({ preventScroll: true });
};
const focusJobLineSelect = (index) => {
window.setTimeout(() => {
firstFieldRefs.current[index]?.focus?.({ preventScroll: true });
}, 0);
};
// Only fill actual_cost when the user forward-tabs out of Retail (actual_price) // Only fill actual_cost when the user forward-tabs out of Retail (actual_price)
const autofillActualCost = (index) => { const autofillActualCost = (index) => {
if (bodyshop.accountingconfig?.disableBillCostCalculation) return; if (bodyshop.accountingconfig?.disableBillCostCalculation) return;
@@ -213,12 +195,6 @@ export function BillEnterModalLinesComponent({
minHeight: `${CONTROL_HEIGHT}px` minHeight: `${CONTROL_HEIGHT}px`
}} }}
allowRemoved={form.getFieldValue("is_credit_memo") || false} allowRemoved={form.getFieldValue("is_credit_memo") || false}
onInputKeyDown={(event) => {
if (event.key !== "Tab" || event.shiftKey || event.defaultPrevented) return;
event.preventDefault();
focusLineDescription(index);
}}
onSelect={(value, opt) => { onSelect={(value, opt) => {
// IMPORTANT: // IMPORTANT:
// Do NOT autofill actual_cost here. It should only fill when the user forward-tabs // Do NOT autofill actual_cost here. It should only fill when the user forward-tabs
@@ -245,7 +221,6 @@ export function BillEnterModalLinesComponent({
}; };
}) })
}); });
focusJobLineSelect(index);
}} }}
/> />
) )
@@ -261,16 +236,7 @@ export function BillEnterModalLinesComponent({
label: t("billlines.fields.line_desc"), label: t("billlines.fields.line_desc"),
rules: [{ required: true }] rules: [{ required: true }]
}), }),
formInput: (record, index) => ( formInput: () => <Input.TextArea disabled={disabled} autoSize tabIndex={0} />
<Input.TextArea
ref={(el) => {
lineDescriptionRefs.current[index] = el;
}}
disabled={disabled}
autoSize
tabIndex={0}
/>
)
}, },
{ {

View File

@@ -8,7 +8,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import GlobalSearch from "../global-search/global-search.component"; import GlobalSearch from "../global-search/global-search.component";
import GlobalSearchOs from "../global-search/global-search-os.component"; import GlobalSearchOs from "../global-search/global-search-os.component";
import "./breadcrumbs.styles.scss"; import "./breadcrumbs.styles.scss";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
breadcrumbs: selectBreadcrumbs, breadcrumbs: selectBreadcrumbs,

View File

@@ -1,6 +1,6 @@
import { PictureFilled } from "@ant-design/icons"; import { PictureFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Badge, Popover } from "antd"; import { Badge, Popover } from "antd";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -5,7 +5,7 @@ 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, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser, currentUser: selectCurrentUser,

View File

@@ -26,7 +26,7 @@ import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import { DMS_MAP } from "../../utils/dmsUtils"; import { DMS_MAP } from "../../utils/dmsUtils";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
/** /**
* CDK-like DMS post form: * CDK-like DMS post form:

View File

@@ -9,7 +9,7 @@ import AlertComponent from "../alert/alert.component";
import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component"; import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-documents-gallery.external.component";
import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component"; import JobsDocumentsLocalGalleryExternalComponent from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component"; import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({

View File

@@ -24,28 +24,16 @@ const mapDispatchToProps = (dispatch) => ({
) )
}); });
export function EsignatureCustomDocument({ export function EsignatureCustomDocument({ bodyshop, jobId, setEsignatureContext }) {
bodyshop,
disabled = false,
jobId,
setEsignatureContext,
showUnavailable = false
}) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const notification = useNotification(); const notification = useNotification();
const { t } = useTranslation(); const { t } = useTranslation();
const esignatureEnabled = hasDocumensoApiKey(bodyshop);
const isDisabled = disabled || !esignatureEnabled;
if (!esignatureEnabled && !showUnavailable) { if (!hasDocumensoApiKey(bodyshop)) {
return null; return null;
} }
const uploadCustomDocument = async ({ file, onError, onSuccess }) => { const uploadCustomDocument = async ({ file, onError, onSuccess }) => {
if (isDisabled) {
return;
}
const formData = new FormData(); const formData = new FormData();
formData.append("document", file); formData.append("document", file);
formData.append("jobid", jobId); formData.append("jobid", jobId);
@@ -90,12 +78,11 @@ export function EsignatureCustomDocument({
return Upload.LIST_IGNORE; return Upload.LIST_IGNORE;
}} }}
customRequest={uploadCustomDocument} customRequest={uploadCustomDocument}
disabled={isDisabled}
maxCount={1} maxCount={1}
showUploadList={false} showUploadList={false}
multiple={false} multiple={false}
> >
<Button disabled={isDisabled} icon={<UploadOutlined />} loading={loading}> <Button icon={<UploadOutlined />} loading={loading}>
{t("esignature.actions.upload_document")} {t("esignature.actions.upload_document")}
</Button> </Button>
</Upload> </Upload>

View File

@@ -26,7 +26,6 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
const { open, context } = esignatureModal; const { open, context } = esignatureModal;
const { token, envelopeId, documentId, jobid } = context; const { token, envelopeId, documentId, jobid } = context;
const [distributing, setDistributing] = useState(false); const [distributing, setDistributing] = useState(false);
const hasToken = Boolean(token);
if (!hasDocumensoApiKey(bodyshop)) { if (!hasDocumensoApiKey(bodyshop)) {
return null; return null;
@@ -40,10 +39,6 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
rome: t("jobs.labels.esignature_rome") rome: t("jobs.labels.esignature_rome")
})} })}
onOk={async () => { onOk={async () => {
if (!hasToken) {
return;
}
try { try {
setDistributing(true); setDistributing(true);
await axios.post("/esign/distribute", { await axios.post("/esign/distribute", {
@@ -63,11 +58,6 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
setDistributing(false); setDistributing(false);
}} }}
onCancel={async () => { onCancel={async () => {
if (!hasToken) {
toggleModalVisible();
return;
}
try { try {
await axios.post("/esign/delete", { await axios.post("/esign/delete", {
documentId, documentId,
@@ -83,15 +73,13 @@ export function EsignatureModalContainer({ esignatureModal, toggleModalVisible,
}); });
} }
}} }}
okButtonProps={{ loading: distributing, style: hasToken ? undefined : { display: "none" } }} okButtonProps={{ loading: distributing }}
okText={t("esignature.actions.distribute")} okText={t("esignature.actions.distribute")}
destroyOnHidden destroyOnHidden
width="calc(100vw - 32px)" width={"80%"}
wrapClassName="esignature-modal"
styles={{ body: { overflow: "hidden", padding: 0 } }}
> >
<div className="esignature-modal-frame"> <div style={{ height: "80vh", width: "100%" }}>
{hasToken ? ( {token ? (
<EmbedUpdateDocumentV1 <EmbedUpdateDocumentV1
presignToken={token} presignToken={token}
host="https://sign.imex.online" host="https://sign.imex.online"

View File

@@ -5,7 +5,6 @@ import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx"; import WssStatusDisplayComponent from "../../components/wss-status-display/wss-status-display.component.jsx";
import { useTreatment } from "../../feature-flags/splitio-react-replacement.jsx";
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js"; import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
@@ -17,12 +16,6 @@ const mapStateToProps = createStructuredSelector({
export function GlobalFooter({ isPartsEntry }) { export function GlobalFooter({ isPartsEntry }) {
const { t } = useTranslation(); const { t } = useTranslation();
const testFlagTreatment = useTreatment({ name: "TEST_FLAG" });
const testFlagEnabled = testFlagTreatment === "on";
const testFlagIndicator = testFlagEnabled ? (
<div style={{ fontWeight: 600, marginTop: 4 }}>Test Feature Flag Enabled</div>
) : null;
if (isPartsEntry) { if (isPartsEntry) {
return ( return (
@@ -45,7 +38,6 @@ export function GlobalFooter({ isPartsEntry }) {
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}> <Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
Disclaimer & Notices Disclaimer & Notices
</Link> </Link>
{testFlagIndicator}
</div> </div>
</Footer> </Footer>
); );
@@ -82,7 +74,6 @@ export function GlobalFooter({ isPartsEntry }) {
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}> <Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
Disclaimer & Notices Disclaimer & Notices
</Link> </Link>
{testFlagIndicator}
</div> </div>
</Footer> </Footer>
); );

View File

@@ -2,7 +2,7 @@
import { BellFilled } from "@ant-design/icons"; import { BellFilled } from "@ant-design/icons";
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Badge, Layout, Menu, Spin, Tooltip } from "antd"; import { Badge, Layout, Menu, Spin, Tooltip } from "antd";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -1,5 +1,5 @@
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries"; import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";

View File

@@ -1,5 +1,5 @@
import { useApolloClient } from "@apollo/client/react"; import { useApolloClient } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Popconfirm } from "antd"; import { Button, Popconfirm } from "antd";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -30,7 +30,7 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
// import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container"; // import AllocationsAssignmentContainer from "../allocations-assignment/allocations-assignment.container";
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container"; // import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container"; // import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import _ from "lodash"; import _ from "lodash";
import { FaTasks } from "react-icons/fa"; import { FaTasks } from "react-icons/fa";
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors"; import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";

View File

@@ -63,9 +63,7 @@ export function JobLineDispatchButton({
} }
} }
//joblineids: selectedLines.map((l) => l.id), //joblineids: selectedLines.map((l) => l.id),
}, }
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID", "GET_JOB_BY_PK"],
awaitRefetchQueries: true
}); });
if (result.errors) { if (result.errors) {
console.log("🚀 ~ handleConvert ~ result.errors:", result.errors); console.log("🚀 ~ handleConvert ~ result.errors:", result.errors);

View File

@@ -1,4 +1,4 @@
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Form, Input, InputNumber, Modal, Select, Switch } from "antd"; import { Form, Input, InputNumber, Modal, Select, Switch } from "antd";
import { useEffect } from "react"; import { useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -1,5 +1,5 @@
import { useMutation } from "@apollo/client/react"; import { useMutation } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import Axios from "axios"; import Axios from "axios";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import { useState } from "react"; import { useState } from "react";

View File

@@ -16,7 +16,7 @@ import DataLabel from "../data-label/data-label.component";
import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component"; import PaymentExpandedRowComponent from "../payment-expanded-row/payment-expanded-row.component";
import PaymentsGenerateLink from "../payments-generate-link/payments-generate-link.component"; import PaymentsGenerateLink from "../payments-generate-link/payments-generate-link.component";
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component"; import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop

View File

@@ -4,7 +4,7 @@ import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../gra
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx"; import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
import { useIsEmployee } from "../../utils/useIsEmployee.js"; import { useIsEmployee } from "../../utils/useIsEmployee.js";

View File

@@ -1,6 +1,6 @@
import { useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client/react"; import { useApolloClient, useLazyQuery, useMutation, useQuery } from "@apollo/client/react";
import { gql } from "@apollo/client"; import { gql } from "@apollo/client";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Col, Row } from "antd"; import { Col, Row } from "antd";
import Axios from "axios"; import Axios from "axios";
import _ from "lodash"; import _ from "lodash";

View File

@@ -6,7 +6,7 @@ import { useCallback, useMemo, useState } from "react";
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 { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries"; import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions"; import { insertAuditTrail } from "../../redux/application/application.actions";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";

View File

@@ -1,6 +1,6 @@
import { DownCircleFilled } from "@ant-design/icons"; import { DownCircleFilled } from "@ant-design/icons";
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react"; import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Card, Dropdown, Form, Input, Modal, Popover, Select, Space } from "antd"; import { Button, Card, Dropdown, Form, Input, Modal, Popover, Select, Space } from "antd";
import axios from "axios"; import axios from "axios";
import parsePhoneNumber from "libphonenumber-js"; import parsePhoneNumber from "libphonenumber-js";

View File

@@ -6,7 +6,7 @@ import LaborAllocationsTableComponent from "../labor-allocations-table/labor-all
import TimeTicketList from "../time-ticket-list/time-ticket-list.component"; import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component"; import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,

View File

@@ -6,7 +6,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import cleanAxios from "../../utils/CleanAxios"; import cleanAxios from "../../utils/CleanAxios";
import formatBytes from "../../utils/formatbytes"; import formatBytes from "../../utils/formatbytes";
//import yauzl from "yauzl"; //import yauzl from "yauzl";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";

View File

@@ -7,7 +7,7 @@ import JobDocuments from "./jobs-documents-gallery.component";
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 { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop

View File

@@ -14,7 +14,7 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const LockWrapper = ({ featureName, bodyshop, children, disabled = true, bypass, locked }) => { const LockWrapper = ({ featureName, bodyshop, children, disabled = true, bypass }) => {
let renderedChildren = children; let renderedChildren = children;
//Mark the child prop as disabled. //Mark the child prop as disabled.
@@ -36,13 +36,11 @@ const LockWrapper = ({ featureName, bodyshop, children, disabled = true, bypass,
return <span>{children}</span>; return <span>{children}</span>;
} }
const hasAccess = typeof locked === "boolean" ? !locked : HasFeatureAccess({ featureName: featureName, bodyshop }); return HasFeatureAccess({ featureName: featureName, bodyshop }) ? (
return hasAccess ? (
children children
) : ( ) : (
<Space> <Space>
<LockOutlined style={{ color: "tomato" }} /> {!HasFeatureAccess({ featureName: featureName, bodyshop }) && <LockOutlined style={{ color: "tomato" }} />}
{renderedChildren} {renderedChildren}
</Space> </Space>
); );

View File

@@ -1,5 +1,5 @@
import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons"; import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd"; import { Button, Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";

View File

@@ -19,7 +19,7 @@ import { TemplateList } from "../../utils/TemplateConstants";
import AlertComponent from "../alert/alert.component"; import AlertComponent from "../alert/alert.component";
import PartsOrderModalComponent from "./parts-order-modal.component"; import PartsOrderModalComponent from "./parts-order-modal.component";
import axios from "axios"; import axios from "axios";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import _ from "lodash"; import _ from "lodash";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";

View File

@@ -1,4 +1,4 @@
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Form, Input, Radio, Select } from "antd"; import { Form, Input, Radio, Select } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";

View File

@@ -1,4 +1,4 @@
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Card, Col, Row, Space, Tooltip, Typography } from "antd"; import { Button, Card, Col, Row, Space, Tooltip, Typography } from "antd";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";

View File

@@ -1,6 +1,5 @@
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { CloseOutlined } from "@ant-design/icons"; import { Card, Col, Input, Row, Space, Typography } from "antd";
import { Alert, Button, Card, Col, Input, Row, Space, Typography, Tooltip } from "antd";
import _ from "lodash"; import _ from "lodash";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -11,14 +10,12 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { TemplateList } from "../../utils/TemplateConstants"; import { TemplateList } from "../../utils/TemplateConstants";
import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component"; import Jobd3RdPartyModal from "../job-3rd-party-modal/job-3rd-party-modal.component";
import EsignatureCustomDocument from "../esignature-custom-document/esignature-custom-document.component"; import EsignatureCustomDocument from "../esignature-custom-document/esignature-custom-document.component";
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
import PrintCenterItem from "../print-center-item/print-center-item.component"; import PrintCenterItem from "../print-center-item/print-center-item.component";
import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component"; import PrintCenterJobsLabels from "../print-center-jobs-labels/print-center-jobs-labels.component";
import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component"; import PrintCenterSpeedPrint from "../print-center-speed-print/print-center-speed-print.component";
import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils"; import { bodyshopHasDmsKey, DMS_MAP, getDmsMode } from "../../utils/dmsUtils";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
import { hasDocumensoApiKey } from "../../utils/esignature.js"; import { hasDocumensoApiKey } from "../../utils/esignature.js";
import useLocalStorage from "../../utils/useLocalStorage";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
printCenterModal: selectPrintCenter, printCenterModal: selectPrintCenter,
@@ -30,10 +27,6 @@ const mapDispatchToProps = () => ({});
export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technician }) { export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technician }) {
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [esignatureBannerDismissed, setEsignatureBannerDismissed] = useLocalStorage(
"print_center_esignature_banner_dismissed",
false
);
const { id: jobId, job } = printCenterModal.context; const { id: jobId, job } = printCenterModal.context;
const tempList = TemplateList("job", {}); const tempList = TemplateList("job", {});
const { t } = useTranslation(); const { t } = useTranslation();
@@ -48,7 +41,6 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
const dmsMode = getDmsMode(bodyshop, "off"); const dmsMode = getDmsMode(bodyshop, "off");
const isReynoldsMode = dmsMode === DMS_MAP.reynolds; const isReynoldsMode = dmsMode === DMS_MAP.reynolds;
const esignatureEnabled = hasDocumensoApiKey(bodyshop); const esignatureEnabled = hasDocumensoApiKey(bodyshop);
const showEsignatureBanner = !esignatureEnabled && !esignatureBannerDismissed;
const Templates = !hasDMSKey const Templates = !hasDMSKey
? Object.keys(tempList) ? Object.keys(tempList)
@@ -58,7 +50,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
.filter( .filter(
(temp) => (temp) =>
(!temp.regions || (!temp.regions ||
temp.regions?.[bodyshop.region_config] || (temp.regions && temp.regions[bodyshop.region_config]) ||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)) && (temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)) &&
(!temp.dms || temp.dms === false) (!temp.dms || temp.dms === false)
) )
@@ -70,7 +62,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
.filter( .filter(
(temp) => (temp) =>
!temp.regions || !temp.regions ||
temp.regions?.[bodyshop.region_config] || (temp.regions && temp.regions[bodyshop.region_config]) ||
(temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true) (temp.regions && bodyshop.region_config.includes(Object.keys(temp.regions)) === true)
) )
.filter((temp) => !isReynoldsMode || !temp.excludedDmsModes?.includes(dmsMode)) .filter((temp) => !isReynoldsMode || !temp.excludedDmsModes?.includes(dmsMode))
@@ -99,23 +91,6 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
return ( return (
<div> <div>
{showEsignatureBanner && (
<Alert
action={
<Button
aria-label={t("general.actions.close")}
icon={<CloseOutlined />}
onClick={() => setEsignatureBannerDismissed(true)}
size="small"
type="text"
/>
}
banner
title={t("printcenter.banners.esignature_promo")}
type="info"
className="print-center-esignature-banner"
/>
)}
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
<Col lg={8} md={12} sm={24}> <Col lg={8} md={12} sm={24}>
<PrintCenterSpeedPrint jobId={jobId} /> <PrintCenterSpeedPrint jobId={jobId} />
@@ -125,13 +100,7 @@ export function PrintCenterJobsComponent({ printCenterModal, bodyshop, technicia
extra={ extra={
<Space wrap> <Space wrap>
<PrintCenterJobsLabels jobId={jobId} /> <PrintCenterJobsLabels jobId={jobId} />
<Tooltip title={!esignatureEnabled ? t("esignature.tooltips.contact_sales") : null}> {esignatureEnabled && <EsignatureCustomDocument jobId={jobId} />}
<span>
<LockWrapperComponent locked={!esignatureEnabled} bodyshop={bodyshop}>
<EsignatureCustomDocument jobId={jobId} showUnavailable />
</LockWrapperComponent>
</span>
</Tooltip>
<Jobd3RdPartyModal jobId={jobId} job={job} /> <Jobd3RdPartyModal jobId={jobId} job={job} />
<Input.Search onChange={(e) => setSearch(e.target.value)} value={search} enterButton /> <Input.Search onChange={(e) => setSearch(e.target.value)} value={search} enterButton />
</Space> </Space>

View File

@@ -5,7 +5,3 @@
padding: 0; padding: 0;
} }
} }
.print-center-esignature-banner {
margin-bottom: 16px;
}

View File

@@ -11,7 +11,7 @@ import {
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries"; import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import ProductionBoardKanbanComponent from "./production-board-kanban.component"; import ProductionBoardKanbanComponent from "./production-board-kanban.component";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { useSocket } from "../../contexts/SocketIO/useSocket.js";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({

View File

@@ -5,7 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
technician: selectTechnician, technician: selectTechnician,

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries"; import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries"; import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import ProductionListColumns from "../production-list-columns/production-list-columns.data"; import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { isFunction } from "lodash"; import { isFunction } from "lodash";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";

View File

@@ -1,6 +1,6 @@
import { HolderOutlined, SyncOutlined } from "@ant-design/icons"; import { HolderOutlined, SyncOutlined } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd"; import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";

View File

@@ -9,7 +9,7 @@ import {
} from "../../graphql/jobs.queries"; } from "../../graphql/jobs.queries";
import ProductionListTable from "./production-list-table.component"; import ProductionListTable from "./production-list-table.component";
import _ from "lodash"; import _ from "lodash";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { useSocket } from "../../contexts/SocketIO/useSocket.js";
export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) { export default function ProductionListTableContainer({ bodyshop, subscriptionType = "direct" }) {

View File

@@ -1,5 +1,5 @@
import { useLazyQuery } from "@apollo/client/react"; import { useLazyQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography } from "antd"; import { Button, Card, Col, DatePicker, Form, Input, Radio, Row, Typography } from "antd";
import _ from "lodash"; import _ from "lodash";
import { useState } from "react"; import { useState } from "react";

View File

@@ -1,6 +1,6 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { useApolloClient, useMutation, useQuery } from "@apollo/client/react"; import { useApolloClient, useMutation, useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd"; import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component"; import ResponsiveTable from "../responsive-table/responsive-table.component";
import queryString from "query-string"; import queryString from "query-string";
@@ -12,11 +12,11 @@ import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
import { import {
CHECK_EMPLOYEE_EMAIL,
CHECK_EMPLOYEE_NUMBER, CHECK_EMPLOYEE_NUMBER,
DELETE_VACATION, DELETE_VACATION,
INSERT_EMPLOYEES, INSERT_EMPLOYEES,
QUERY_EMPLOYEE_BY_ID, QUERY_EMPLOYEE_BY_ID,
QUERY_USERS_BY_EMAIL,
UPDATE_EMPLOYEE UPDATE_EMPLOYEE
} from "../../graphql/employees.queries"; } from "../../graphql/employees.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
@@ -174,10 +174,9 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
const handleFinish = async (values) => { const handleFinish = async (values) => {
const submitAction = saveAndResetSubmitAction(); const submitAction = saveAndResetSubmitAction();
const userEmail = typeof values.user_email === "string" ? values.user_email.trim() : values.user_email;
const normalizedValues = { const normalizedValues = {
...values, ...values,
user_email: userEmail === "" ? null : userEmail user_email: values.user_email === "" ? null : values.user_email
}; };
if (search.employeeId && search.employeeId !== "new") { if (search.employeeId && search.employeeId !== "new") {
@@ -492,29 +491,18 @@ export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDi
rules={[ rules={[
({ getFieldValue }) => ({ ({ getFieldValue }) => ({
async validator(rule, value) { async validator(rule, value) {
const user_email = typeof value === "string" ? value.trim() : getFieldValue("user_email"); const user_email = getFieldValue("user_email");
if (user_email && value) { if (user_email && value) {
const response = await client.query({ const response = await client.query({
query: CHECK_EMPLOYEE_EMAIL, query: QUERY_USERS_BY_EMAIL,
variables: { variables: {
email: user_email, email: user_email
shopId: bodyshop.id
} }
}); });
if (response.data.users.length === 1) { if (response.data.users.length === 1) {
const matchingEmployees = response.data.employees_aggregate.nodes; return Promise.resolve();
const currentEmployeeId = form.getFieldValue("id") ?? search.employeeId;
if (
response.data.employees_aggregate.aggregate.count === 0 ||
matchingEmployees.every((employee) => employee.id === currentEmployeeId)
) {
return Promise.resolve();
}
return Promise.reject(t("employees.validation.unique_user_email"));
} }
return Promise.reject(t("bodyshop.validation.useremailmustexist")); return Promise.reject(t("bodyshop.validation.useremailmustexist"));
} else { } else {

View File

@@ -4,7 +4,6 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { useEffect } from "react"; import { useEffect } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { import {
CHECK_EMPLOYEE_EMAIL,
DELETE_VACATION, DELETE_VACATION,
INSERT_EMPLOYEES, INSERT_EMPLOYEES,
QUERY_EMPLOYEE_BY_ID, QUERY_EMPLOYEE_BY_ID,
@@ -17,7 +16,6 @@ const updateEmployeeMock = vi.fn();
const deleteVacationMock = vi.fn(); const deleteVacationMock = vi.fn();
const useQueryMock = vi.fn(); const useQueryMock = vi.fn();
const useMutationMock = vi.fn(); const useMutationMock = vi.fn();
const apolloClientQueryMock = vi.fn();
const navigateMock = vi.fn(); const navigateMock = vi.fn();
const notification = { const notification = {
error: vi.fn(), error: vi.fn(),
@@ -35,7 +33,7 @@ vi.mock("@apollo/client/react", async () => {
}; };
}); });
vi.mock("../../feature-flags/splitio-react-replacement", () => ({ vi.mock("@splitsoftware/splitio-react", () => ({
useTreatmentsWithConfig: () => ({ useTreatmentsWithConfig: () => ({
treatments: { treatments: {
Enhanced_Payroll: { Enhanced_Payroll: {
@@ -89,10 +87,6 @@ vi.mock("react-i18next", () => ({
return "Employee number must be unique"; return "Employee number must be unique";
} }
if (key === "employees.validation.unique_user_email") {
return "User email already assigned";
}
if (key === "bodyshop.validation.useremailmustexist") { if (key === "bodyshop.validation.useremailmustexist") {
return "User email must exist"; return "User email must exist";
} }
@@ -209,20 +203,18 @@ describe("ShopEmployeesFormComponent", () => {
return [vi.fn()]; return [vi.fn()];
}); });
apolloClientQueryMock.mockResolvedValue({
data: {
employees_aggregate: {
aggregate: {
count: 0
},
nodes: []
},
users: []
}
});
useApolloClient.mockReturnValue({ useApolloClient.mockReturnValue({
query: apolloClientQueryMock query: vi.fn().mockResolvedValue({
data: {
employees_aggregate: {
aggregate: {
count: 0
},
nodes: []
},
users: []
}
})
}); });
insertEmployeesMock.mockResolvedValue({ insertEmployeesMock.mockResolvedValue({
@@ -364,59 +356,4 @@ describe("ShopEmployeesFormComponent", () => {
title: "Saved" title: "Saved"
}); });
}); });
it("blocks saving when the user email belongs to another employee in the shop", async () => {
apolloClientQueryMock.mockImplementation(({ query }) => {
if (query === CHECK_EMPLOYEE_EMAIL) {
return Promise.resolve({
data: {
users: [{ email: "jamie@example.com" }],
employees_aggregate: {
aggregate: {
count: 1
},
nodes: [{ id: "other-employee" }]
}
}
});
}
return Promise.resolve({
data: {
employees_aggregate: {
aggregate: {
count: 0
},
nodes: []
},
users: []
}
});
});
fireEvent.change(screen.getByRole("textbox", { name: "First Name" }), {
target: { value: "Jamie" }
});
fireEvent.change(screen.getByRole("textbox", { name: "Last Name" }), {
target: { value: "Rivera" }
});
fireEvent.change(screen.getByRole("textbox", { name: "Employee Number" }), {
target: { value: "42" }
});
fireEvent.change(screen.getByRole("textbox", { name: "PIN" }), {
target: { value: "1234" }
});
fireEvent.change(screen.getByRole("textbox", { name: "Hire Date" }), {
target: { value: "2026-04-20" }
});
fireEvent.change(screen.getByRole("textbox", { name: "User Email" }), {
target: { value: "jamie@example.com" }
});
fireEvent.click(screen.getByRole("button", { name: "Save Employee" }));
expect(await screen.findByText("User email already assigned")).toBeInTheDocument();
expect(insertEmployeesMock).not.toHaveBeenCalled();
expect(notification.success).not.toHaveBeenCalled();
});
}); });

View File

@@ -1,4 +1,4 @@
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Card, Tabs } from "antd"; import { Button, Card, Tabs } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import { useRef } from "react"; import { useRef } from "react";

View File

@@ -4,7 +4,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
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 { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop

View File

@@ -1,4 +1,4 @@
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Form, InputNumber } from "antd"; import { Form, InputNumber } from "antd";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";

View File

@@ -1,5 +1,5 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Col, DatePicker, Divider, Form, Input, InputNumber, Radio, Row, Select, Space, Switch } from "antd"; import { Button, Col, DatePicker, Divider, Form, Input, InputNumber, Radio, Row, Select, Space, Switch } from "antd";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -16,7 +16,7 @@ import { DEFAULT_TRANSLUCENT_CARD_COLOR, getTintedCardSurfaceStyles } from "./sh
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 { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop

View File

@@ -7,7 +7,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import JobSearchSelect from "../job-search-select/job-search-select.component"; import JobSearchSelect from "../job-search-select/job-search-select.component";
import JobsDetailLaborContainer from "../jobs-detail-labor/jobs-detail-labor.container"; import JobsDetailLaborContainer from "../jobs-detail-labor/jobs-detail-labor.container";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,

View File

@@ -12,7 +12,7 @@ import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component"; import TechJobPrintTickets from "../tech-job-print-tickets/tech-job-print-tickets.component";
import TechClockInComponent from "./tech-job-clock-in-form.component"; import TechClockInComponent from "./tech-job-clock-in-form.component";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({

View File

@@ -14,7 +14,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility"; import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-out-delete.component"; import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-out-delete.component";
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component"; import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js"; import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";

View File

@@ -11,7 +11,7 @@ import { createStructuredSelector } from "reselect";
import { techLogout } from "../../redux/tech/tech.actions"; import { techLogout } from "../../redux/tech/tech.actions";
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
import { BsKanban } from "react-icons/bs"; import { BsKanban } from "react-icons/bs";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";

View File

@@ -1,5 +1,5 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons"; import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Card, Checkbox, Space } from "antd"; import { Button, Card, Checkbox, Space } from "antd";
import ResponsiveTable from "../responsive-table/responsive-table.component"; import ResponsiveTable from "../responsive-table/responsive-table.component";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";

View File

@@ -1,5 +1,5 @@
import { useLazyQuery } from "@apollo/client/react"; import { useLazyQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd"; import { Card, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -1,6 +1,6 @@
import { PageHeader } from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import { useMutation, useQuery } from "@apollo/client/react"; import { useMutation, useQuery } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Form, Modal, Space } from "antd"; import { Button, Form, Modal, Space } from "antd";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -1,6 +1,6 @@
import { DeleteFilled } from "@ant-design/icons"; import { DeleteFilled } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client/react"; import { useApolloClient } from "@apollo/client/react";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from "antd"; import { Button, Divider, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import { PageHeader } from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";

View File

@@ -14,7 +14,7 @@ import {
} from "../../graphql/notifications.queries.js"; } from "../../graphql/notifications.queries.js";
import { useMutation } from "@apollo/client/react"; import { useMutation } from "@apollo/client/react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FEATURE_FLAGS_CHANGED_EVENT, useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { INITIAL_NOTIFICATIONS, SocketContext } from "./useSocket.js"; import { INITIAL_NOTIFICATIONS, SocketContext } from "./useSocket.js";
const LIMIT = INITIAL_NOTIFICATIONS; const LIMIT = INITIAL_NOTIFICATIONS;
@@ -280,10 +280,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
} }
}; };
const handleFeatureFlagsChanged = (payload) => {
window.dispatchEvent(new CustomEvent(FEATURE_FLAGS_CHANGED_EVENT, { detail: payload }));
};
const syncCurrentTokenToSocket = async () => { const syncCurrentTokenToSocket = async () => {
try { try {
if (!auth.currentUser || !bodyshop?.id) return; if (!auth.currentUser || !bodyshop?.id) return;
@@ -578,7 +574,6 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
socketInstance.on("notification", handleNotification); socketInstance.on("notification", handleNotification);
socketInstance.on("sync-notification-read", handleSyncNotificationRead); socketInstance.on("sync-notification-read", handleSyncNotificationRead);
socketInstance.on("sync-all-notifications-read", handleSyncAllNotificationsRead); socketInstance.on("sync-all-notifications-read", handleSyncAllNotificationsRead);
socketInstance.on(FEATURE_FLAGS_CHANGED_EVENT, handleFeatureFlagsChanged);
socketInstance.on("token-updated", handleTokenUpdated); socketInstance.on("token-updated", handleTokenUpdated);
if (tokenSyncIntervalRef.current) { if (tokenSyncIntervalRef.current) {

View File

@@ -1,71 +0,0 @@
# Feature Flags
The app imports feature-flag hooks from `src/feature-flags/splitio-react-replacement.jsx`. That module keeps the old
Split-shaped component and hook API intact while removing the runtime dependency on Split.
Code should import this local module directly. We no longer rely on a Vite alias for the old Split package.
## Current storage contract
The compatibility layer reads the active shop from Redux, then fetches DB-backed assignments from:
```text
GET /feature-flags/bodyshops/:bodyshopId
```
That endpoint verifies the Firebase user can access the bodyshop through Hasura permissions, then returns cached Redis
data when present or refreshes from `feature_flags` + `bodyshop_feature_flags`.
On successful backend responses, the client stores the last-known flag payload in browser `localStorage` for the active
bodyshop. If the backend cannot be reached later, the client uses that bodyshop-scoped browser cache for up to 24 hours.
If there is no browser cache, unknown flags resolve to `"off"`.
Recommended backend payload shape:
```json
{
"flags": {
"Enhanced_Payroll": {
"treatment": "on",
"config": null,
"activeDate": null,
"deactiveDate": null
},
"Demo_Feature": {
"treatment": "on",
"config": null,
"activeDate": "2026-06-01T13:00:00-04:00",
"deactiveDate": "2026-06-05T17:00:00-04:00"
}
}
}
```
Supported values:
- `true`, `"true"`, `1`, `"on"` -> treatment `"on"`
- `false`, `"false"`, `0`, `"off"` -> treatment `"off"`
- ISO-ish future date strings -> `"on"` until the date passes
- `{ "treatment": "on" | "off" | "control" | "any-custom-treatment", "config": ... }`
- Scheduled demo windows using `activeDate` and `deactiveDate`
Unknown flags default to `"off"`.
## Backend registry
Canonical feature flag definitions live in the Hasura-backed `feature_flags` table and are exposed to the admin panel
through `GET /adm/feature-flags`.
Per-shop assignments live in `bodyshop_feature_flags`. The admin panel reads them through
`GET /adm/bodyshops/:bodyshopId/feature-flags` and saves them through `POST /adm/updateshop`.
Hasura invalidates the Redis cache through `/feature-flags/cache/invalidate` when `bodyshop_feature_flags` or
`feature_flags` changes. Assignment changes clear the affected shop cache for the current cache version; definition
changes increment a global feature flag cache version so old per-shop cache entries become invisible and expire by TTL.
The backend also emits `feature-flags-changed` over the existing Socket.IO connection. `SocketProvider` bridges that
socket message to a browser event, and `SplitFactoryProvider` refetches flags when the event is global or matches the
active bodyshop. This keeps already-open tabs in sync with admin edits and Hasura-triggered invalidation.
For manual frontend testing, the global footer displays `Test Feature Flag Enabled` when `TEST_FLAG` resolves to
the `on` treatment.

View File

@@ -1,411 +0,0 @@
import axios from "axios";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { useSelector } from "react-redux";
import { selectBodyshop } from "../redux/user/user.selectors";
const FeatureFlagContext = createContext({
config: {},
factory: null,
flags: {},
isReady: true,
source: "local"
});
const OFF_TREATMENT = Object.freeze({ treatment: "off", config: null });
const LOCAL_STORAGE_PREFIX = "bodyshop-feature-flags";
const LOCAL_STORAGE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
const FEATURE_FLAGS_REFRESH_DEBOUNCE_MS = 150;
const MAX_SCHEDULE_REFRESH_DELAY_MS = 2_147_483_647;
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
const hasSchedule = (value) => value.activeDate != null || value.deactiveDate != null;
export const FEATURE_FLAGS_CHANGED_EVENT = "feature-flags-changed";
/**
* Parses optional schedule timestamps into comparable epoch milliseconds.
*/
const parseDate = (value) => {
if (value == null || value === "") return null;
const timestamp = Date.parse(value);
return Number.isNaN(timestamp) ? null : timestamp;
};
/**
* Determines whether a scheduled feature flag assignment is active at the current time.
*/
const isWithinSchedule = (value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return true;
const now = Date.now();
const startsAt = parseDate(value.activeDate);
const endsAt = parseDate(value.deactiveDate);
if (startsAt != null && now < startsAt) return false;
if (endsAt != null && now >= endsAt) return false;
return true;
};
/**
* Normalizes backend config values into the object/string/null shape Split hooks expect.
*/
const normalizeConfig = (config) => {
if (config == null || config === "") return null;
if (typeof config === "string") {
try {
return JSON.parse(config);
} catch {
return config;
}
}
return config;
};
/**
* Converts legacy boolean-ish values and custom treatment strings into a stable treatment value.
*/
const normalizeTreatment = (value) => {
if (typeof value === "boolean") return value ? "on" : "off";
if (typeof value === "number") return value > 0 ? "on" : "off";
if (typeof value === "string") {
const normalized = value.trim();
const lowered = normalized.toLowerCase();
if (lowered === "true") return "on";
if (lowered === "false") return "off";
if (lowered === "on" || lowered === "off" || lowered === "control") return lowered;
const dateValue = Date.parse(normalized);
if (!Number.isNaN(dateValue)) return dateValue > Date.now() ? "on" : "off";
return normalized;
}
return value ? "on" : "off";
};
/**
* Converts any supported backend flag value into a Split-compatible treatment/config pair.
*/
const normalizeFlagValue = (value) => {
if (value == null) return OFF_TREATMENT;
if (typeof value === "object" && !Array.isArray(value)) {
if (!isWithinSchedule(value)) return OFF_TREATMENT;
if (hasOwn(value, "treatment")) {
return {
treatment: normalizeTreatment(value.treatment),
config: normalizeConfig(value.config)
};
}
if (hasOwn(value, "enabled")) {
return {
treatment: normalizeTreatment(value.enabled),
config: normalizeConfig(value.config)
};
}
if (hasSchedule(value)) {
return {
treatment: "on",
config: normalizeConfig(value.config)
};
}
}
return {
treatment: normalizeTreatment(value),
config: null
};
};
/**
* Checks whether a socket/browser feature-flag change event applies to the active bodyshop.
*/
const isFeatureFlagChangeRelevant = (detail, bodyshopId) => {
if (!detail || detail.scope === "global") return true;
if (!bodyshopId) return false;
return String(detail.bodyshopId) === String(bodyshopId);
};
/**
* Finds the next scheduled flag boundary that should force a local re-render.
*/
const getNextScheduleRefreshDelay = (flags = {}, now = Date.now()) => {
const nextTimestamp = Object.values(flags).reduce((next, value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) return next;
const timestamps = [parseDate(value.activeDate), parseDate(value.deactiveDate)].filter(
(timestamp) => timestamp != null && timestamp > now
);
if (!timestamps.length) return next;
const candidate = Math.min(...timestamps);
return next == null ? candidate : Math.min(next, candidate);
}, null);
if (nextTimestamp == null) return null;
return Math.min(Math.max(nextTimestamp - now + 50, 0), MAX_SCHEDULE_REFRESH_DELAY_MS);
};
/**
* Checks whether browser localStorage can be used in the current runtime.
*/
const isBrowserStorageAvailable = () => typeof window !== "undefined" && window.localStorage;
/**
* Builds the browser cache key for one bodyshop's feature flags.
*/
const getLocalStorageKey = (bodyshopId) => `${LOCAL_STORAGE_PREFIX}:${bodyshopId}`;
/**
* Reads a bodyshop-scoped last-known-good flag payload from browser storage.
*/
const readCachedFeatureFlags = (bodyshopId, now = Date.now()) => {
if (!bodyshopId || !isBrowserStorageAvailable()) return null;
try {
const rawValue = window.localStorage.getItem(getLocalStorageKey(bodyshopId));
if (!rawValue) return null;
const parsed = JSON.parse(rawValue);
if (!parsed?.flags || typeof parsed.flags !== "object" || Array.isArray(parsed.flags)) return null;
const cachedAt = Date.parse(parsed.cachedAt);
if (!parsed.cachedAt || Number.isNaN(cachedAt) || now - cachedAt > LOCAL_STORAGE_MAX_AGE_MS) return null;
return parsed.flags;
} catch {
return null;
}
};
/**
* Persists a successful backend flag payload for short-term browser fallback.
*/
const writeCachedFeatureFlags = (bodyshopId, flags) => {
if (!bodyshopId || !flags || !isBrowserStorageAvailable()) return;
try {
window.localStorage.setItem(
getLocalStorageKey(bodyshopId),
JSON.stringify({
cachedAt: new Date().toISOString(),
flags
})
);
} catch {
// localStorage may be unavailable, full, or blocked. Runtime flags still work without the browser cache.
}
};
/**
* Builds the local client object that mimics the Split client surface used by the app.
*/
const createFeatureFlagClient = ({ bodyshop, key, backendFlags }) => {
const attributes = {};
const getTreatmentWithConfig = (name) => normalizeFlagValue(backendFlags?.[name]);
return {
client: null,
isReady: true,
isReadyFromCache: true,
key: key || bodyshop?.imexshopid || "anon",
getTreatment: (name) => getTreatmentWithConfig(name).treatment,
getTreatmentWithConfig,
getTreatments: (names = []) =>
names.reduce((acc, name) => {
acc[name] = getTreatmentWithConfig(name).treatment;
return acc;
}, {}),
getTreatmentsWithConfig: (names = []) =>
names.reduce((acc, name) => {
acc[name] = getTreatmentWithConfig(name);
return acc;
}, {}),
setAttribute: (name, value) => {
attributes[name] = value;
return true;
},
setAttributes: (values = {}) => {
Object.assign(attributes, values);
return true;
},
getAttribute: (name) => attributes[name],
getAttributes: () => ({ ...attributes }),
ready: () => Promise.resolve(),
on: () => {},
off: () => {},
destroy: () => {}
};
};
/**
* Provides database-backed feature flags through a Split-shaped React context.
*/
export function SplitFactoryProvider({ children, config, factory }) {
const bodyshop = useSelector(selectBodyshop);
const [state, setState] = useState({ flags: {}, isReady: true, source: "local" });
const loadIdRef = useRef(0);
const refreshTimerRef = useRef(null);
const loadFeatureFlags = useCallback(async () => {
const loadId = (loadIdRef.current += 1);
if (!bodyshop?.id) {
setState({ flags: {}, isReady: true, source: "local" });
return;
}
setState((current) => ({ ...current, isReady: false }));
try {
const { data } = await axios.get(`/feature-flags/bodyshops/${bodyshop.id}`);
if (loadId !== loadIdRef.current) return;
const flags = data.flags || {};
writeCachedFeatureFlags(bodyshop.id, flags);
setState({
flags,
isReady: true,
source: data.source || "database"
});
} catch (error) {
if (loadId !== loadIdRef.current) return;
const cachedFlags = readCachedFeatureFlags(bodyshop.id);
console.warn("Feature flags backend fetch failed; falling back to last-known browser cache.", error);
setState({
flags: cachedFlags || {},
isReady: true,
source: cachedFlags ? "browser-cache" : "local"
});
}
}, [bodyshop?.id]);
useEffect(() => {
loadFeatureFlags();
return () => {
loadIdRef.current += 1;
};
}, [loadFeatureFlags]);
useEffect(() => {
if (!bodyshop?.id) return undefined;
const handleFeatureFlagsChanged = (event) => {
if (!isFeatureFlagChangeRelevant(event.detail, bodyshop.id)) return;
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
}
refreshTimerRef.current = setTimeout(() => {
refreshTimerRef.current = null;
loadFeatureFlags();
}, FEATURE_FLAGS_REFRESH_DEBOUNCE_MS);
};
window.addEventListener(FEATURE_FLAGS_CHANGED_EVENT, handleFeatureFlagsChanged);
return () => {
window.removeEventListener(FEATURE_FLAGS_CHANGED_EVENT, handleFeatureFlagsChanged);
if (refreshTimerRef.current) {
clearTimeout(refreshTimerRef.current);
refreshTimerRef.current = null;
}
};
}, [bodyshop?.id, loadFeatureFlags]);
useEffect(() => {
const delay = getNextScheduleRefreshDelay(state.flags);
if (delay == null) return undefined;
const timer = setTimeout(() => {
setState((current) => ({ ...current, flags: { ...current.flags } }));
}, delay);
return () => {
clearTimeout(timer);
};
}, [state.flags]);
const value = useMemo(
() => ({ config, factory, flags: state.flags, isReady: state.isReady, source: state.source }),
[config, factory, state.flags, state.isReady, state.source]
);
return <FeatureFlagContext.Provider value={value}>{children}</FeatureFlagContext.Provider>;
}
/**
* Returns a Split-compatible client backed by the local feature flag context.
*/
export function useSplitClient(options = {}) {
const bodyshop = useSelector(selectBodyshop);
const context = useContext(FeatureFlagContext);
const client = useMemo(() => {
const nextClient = createFeatureFlagClient({
bodyshop,
key: options.key,
backendFlags: context.flags
});
nextClient.client = nextClient;
nextClient.isReady = context.isReady;
nextClient.isReadyFromCache = context.source === "redis" || context.source === "browser-cache";
return nextClient;
}, [bodyshop, options.key, context.flags, context.isReady, context.source]);
return client;
}
/**
* Returns treatment/config pairs for several feature flags.
*/
export function useTreatmentsWithConfig({ names = [] } = {}) {
const client = useSplitClient();
return useMemo(
() => ({
treatments: client.getTreatmentsWithConfig(names),
isReady: client.isReady,
isReadyFromCache: client.isReadyFromCache,
lastUpdate: Date.now()
}),
[client, names]
);
}
/**
* Returns only the treatment string for one feature flag.
*/
export function useTreatment({ name } = {}) {
const client = useSplitClient();
return client.getTreatment(name);
}
/**
* Returns the treatment/config pair for one feature flag.
*/
export function useTreatmentWithConfig({ name } = {}) {
const client = useSplitClient();
return client.getTreatmentWithConfig(name);
}
export const FeatureFlagProvider = SplitFactoryProvider;
export const useFeatureFlagClient = useSplitClient;
export const SplitContext = FeatureFlagContext;
export const useSplitContext = () => useContext(FeatureFlagContext);
export const __featureFlagTesting = {
createFeatureFlagClient,
getNextScheduleRefreshDelay,
getLocalStorageKey,
isFeatureFlagChangeRelevant,
normalizeFlagValue,
readCachedFeatureFlags,
writeCachedFeatureFlags
};

View File

@@ -1,166 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { __featureFlagTesting } from "./splitio-react-replacement";
const {
createFeatureFlagClient,
getNextScheduleRefreshDelay,
getLocalStorageKey,
isFeatureFlagChangeRelevant,
normalizeFlagValue,
readCachedFeatureFlags,
writeCachedFeatureFlags
} = __featureFlagTesting;
beforeEach(() => {
window.localStorage.clear();
vi.useRealTimers();
});
describe("splitio-react-replacement feature flag normalization", () => {
it("returns off for unknown or null values", () => {
expect(normalizeFlagValue(null)).toEqual({ treatment: "off", config: null });
expect(normalizeFlagValue(undefined)).toEqual({ treatment: "off", config: null });
});
it("normalizes primitive values into Split-like treatments", () => {
expect(normalizeFlagValue(true)).toEqual({ treatment: "on", config: null });
expect(normalizeFlagValue(false)).toEqual({ treatment: "off", config: null });
expect(normalizeFlagValue(1)).toEqual({ treatment: "on", config: null });
expect(normalizeFlagValue(0)).toEqual({ treatment: "off", config: null });
expect(normalizeFlagValue("true")).toEqual({ treatment: "on", config: null });
expect(normalizeFlagValue("false")).toEqual({ treatment: "off", config: null });
expect(normalizeFlagValue("variant-a")).toEqual({ treatment: "variant-a", config: null });
});
it("preserves custom treatments and parses JSON config strings", () => {
expect(
normalizeFlagValue({
treatment: "demo",
config: "{\"limit\":25}"
})
).toEqual({
treatment: "demo",
config: { limit: 25 }
});
});
it("respects activeDate and deactiveDate windows", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-19T15:00:00.000Z"));
expect(
normalizeFlagValue({
treatment: "on",
activeDate: "2026-05-19T14:59:00.000Z",
deactiveDate: "2026-05-19T15:01:00.000Z"
})
).toEqual({ treatment: "on", config: null });
expect(
normalizeFlagValue({
treatment: "on",
activeDate: "2026-05-19T15:01:00.000Z"
})
).toEqual({ treatment: "off", config: null });
expect(
normalizeFlagValue({
treatment: "on",
deactiveDate: "2026-05-19T15:00:00.000Z"
})
).toEqual({ treatment: "off", config: null });
vi.useRealTimers();
});
});
describe("splitio-react-replacement feature flag client", () => {
it("uses backend flags", () => {
const client = createFeatureFlagClient({
bodyshop: {
imexshopid: "APPLE"
},
backendFlags: {
Enhanced_Payroll: { treatment: "on" }
}
});
expect(client.getTreatment("Enhanced_Payroll")).toBe("on");
});
it("ignores old bodyshop feature JSON fallback values", () => {
const client = createFeatureFlagClient({
bodyshop: {
imexshopid: "APPLE",
features: {
featureFlags: {
Enhanced_Payroll: { treatment: "on" }
}
}
},
backendFlags: {}
});
expect(client.getTreatment("Enhanced_Payroll")).toBe("off");
});
it("returns off for flags that are not present in any source", () => {
const client = createFeatureFlagClient({
bodyshop: { imexshopid: "APPLE", features: {} },
backendFlags: {}
});
expect(client.getTreatment("Missing_Flag")).toBe("off");
});
it("uses a bodyshop-scoped browser cache key", () => {
expect(getLocalStorageKey("shop-1")).toBe("bodyshop-feature-flags:shop-1");
});
it("stores and reads last-known backend flags from browser storage", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-19T15:00:00.000Z"));
writeCachedFeatureFlags("shop-1", {
Enhanced_Payroll: { treatment: "on", config: null }
});
expect(readCachedFeatureFlags("shop-1")).toEqual({
Enhanced_Payroll: { treatment: "on", config: null }
});
});
it("ignores expired browser cached flags", () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-19T15:00:00.000Z"));
writeCachedFeatureFlags("shop-1", {
Enhanced_Payroll: { treatment: "on", config: null }
});
expect(readCachedFeatureFlags("shop-1", Date.parse("2026-05-20T15:00:01.000Z"))).toBeNull();
});
});
describe("splitio-react-replacement live refresh helpers", () => {
it("matches global and bodyshop-scoped socket changes", () => {
expect(isFeatureFlagChangeRelevant({ scope: "global" }, "shop-1")).toBe(true);
expect(isFeatureFlagChangeRelevant({ bodyshopId: "shop-1", scope: "bodyshop" }, "shop-1")).toBe(true);
expect(isFeatureFlagChangeRelevant({ bodyshopId: "shop-2", scope: "bodyshop" }, "shop-1")).toBe(false);
});
it("finds the next active/deactive date boundary that needs a refresh", () => {
const now = Date.parse("2026-05-19T15:00:00.000Z");
expect(
getNextScheduleRefreshDelay(
{
Demo: { treatment: "on", activeDate: "2026-05-19T15:05:00.000Z" },
Expiring: { treatment: "on", deactiveDate: "2026-05-19T15:02:00.000Z" },
Expired: { treatment: "on", deactiveDate: "2026-05-19T14:59:00.000Z" }
},
now
)
).toBe(120050);
});
});

View File

@@ -49,22 +49,6 @@ export const CHECK_EMPLOYEE_NUMBER = gql`
} }
`; `;
export const CHECK_EMPLOYEE_EMAIL = gql`
query CHECK_EMPLOYEE_EMAIL($email: String!, $shopId: uuid!) {
users(where: { email: { _ilike: $email } }) {
email
}
employees_aggregate(where: { user_email: { _ilike: $email }, shopid: { _eq: $shopId } }) {
aggregate {
count
}
nodes {
id
}
}
}
`;
export const QUERY_ACTIVE_EMPLOYEES = gql` export const QUERY_ACTIVE_EMPLOYEES = gql`
query QUERY_ACTIVE_EMPLOYEES { query QUERY_ACTIVE_EMPLOYEES {
employees(where: { active: { _eq: true } }) { employees(where: { active: { _eq: true } }) {

View File

@@ -6,7 +6,7 @@ import { createStructuredSelector } from "reselect";
import queryString from "query-string"; import queryString from "query-string";
import { useQuery } from "@apollo/client/react"; import { useQuery } from "@apollo/client/react";
import { Button, Card, Col, Result, Row, Select, Space, Switch } from "antd"; import { Button, Card, Col, Result, Row, Select, Space, Switch } from "antd";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";

View File

@@ -4,7 +4,7 @@ import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { some } from "lodash"; import { some } from "lodash";
import axios from "axios"; import axios from "axios";
import AlertComponent from "../../components/alert/alert.component"; import AlertComponent from "../../components/alert/alert.component";

View File

@@ -22,7 +22,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
// import { useNavigate } from 'react-router-dom'; // import { useNavigate } from 'react-router-dom';
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import Dinero from "dinero.js"; import Dinero from "dinero.js";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";

View File

@@ -2,7 +2,7 @@ import ProductionBoardKanbanContainer from "../../components/production-board-ka
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 { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser

View File

@@ -3,7 +3,7 @@ import ProductionListTable from "../../components/production-list-table/producti
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 { selectBodyshop } from "../../redux/user/user.selectors";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop

View File

@@ -13,7 +13,7 @@ import { GET_UNACCEPTED_PARTS_DISPATCH } from "../../graphql/parts-dispatch.quer
import { selectTechnician } from "../../redux/tech/tech.selectors"; import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { alphaSort } from "../../utils/sorters"; import { alphaSort } from "../../utils/sorters";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser //currentUser: selectCurrentUser

View File

@@ -4,7 +4,7 @@ import JobsDocumentsContainer from "../../components/jobs-documents-gallery/jobs
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component"; import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries"; import { QUERY_TEMPORARY_DOCS } from "../../graphql/documents.queries";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container"; import JobsDocumentsLocalGallery from "../../components/jobs-documents-local-gallery/jobs-documents-local-gallery.container";

View File

@@ -18,7 +18,7 @@ import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/appli
import TimeTicketsCommit from "../../components/time-tickets-commit/time-tickets-commit.component"; import TimeTicketsCommit from "../../components/time-tickets-commit/time-tickets-commit.component";
import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component"; import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wrapper.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { useTreatmentsWithConfig } from "../../feature-flags/splitio-react-replacement"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component"; import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";

View File

@@ -1,4 +1,5 @@
import FingerprintJS from "@fingerprintjs/fingerprintjs"; import FingerprintJS from "@fingerprintjs/fingerprintjs";
//import { setUserId, setUserProperties } from "@firebase/analytics";
import { import {
checkActionCode, checkActionCode,
confirmPasswordReset, confirmPasswordReset,
@@ -8,9 +9,11 @@ import {
} from "@firebase/auth"; } from "@firebase/auth";
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore"; import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "@firebase/firestore";
import { getToken } from "@firebase/messaging"; import { getToken } from "@firebase/messaging";
// import * as Sentry from "@sentry/react";
import { notification } from "antd"; import { notification } from "antd";
import axios from "axios"; import axios from "axios";
import i18next from "i18next"; import i18next from "i18next";
//import LogRocket from "logrocket";
import { all, call, delay, put, select, takeLatest } from "redux-saga/effects"; import { all, call, delay, put, select, takeLatest } from "redux-saga/effects";
import { import {
auth, auth,
@@ -45,12 +48,8 @@ import {
validatePasswordResetSuccess validatePasswordResetSuccess
} from "./user.actions"; } from "./user.actions";
import UserActionTypes from "./user.types"; import UserActionTypes from "./user.types";
import { bodyshopHasDmsKey, determineDMSTypeByBodyshop, DMS_MAP } from "../../utils/dmsUtils";
//import { setUserId, setUserProperties } from "@firebase/analytics";
//import * as Sentry from "@sentry/react";
//import LogRocket from "logrocket";
//import posthog from "posthog-js"; //import posthog from "posthog-js";
import { bodyshopHasDmsKey, determineDMSTypeByBodyshop, DMS_MAP } from "../../utils/dmsUtils";
const fpPromise = FingerprintJS.load(); const fpPromise = FingerprintJS.load();

View File

@@ -1351,8 +1351,7 @@
"vacationadded": "Employee vacation added." "vacationadded": "Employee vacation added."
}, },
"validation": { "validation": {
"unique_employee_number": "You must enter a unique employee number.", "unique_employee_number": "You must enter a unique employee number."
"unique_user_email": "This email is already assigned to another employee."
} }
}, },
"esignature": { "esignature": {
@@ -1368,9 +1367,6 @@
"pdf_only": "Only PDF documents can be uploaded for e-signature.", "pdf_only": "Only PDF documents can be uploaded for e-signature.",
"upload_title": "Unable to prepare document for e-signature" "upload_title": "Unable to prepare document for e-signature"
}, },
"tooltips": {
"contact_sales": "E-signatures are not enabled for this shop. Contact sales to add this feature."
},
"fields": { "fields": {
"completed": "Completed?", "completed": "Completed?",
"completed_at": "Completed At", "completed_at": "Completed At",
@@ -3055,9 +3051,6 @@
"appointments": { "appointments": {
"appointment_confirmation": "Appointment Confirmation" "appointment_confirmation": "Appointment Confirmation"
}, },
"banners": {
"esignature_promo": "Tired of getting paper signatures? Try E-Signatures today. Contact support to add this feature."
},
"bills": { "bills": {
"inhouse_invoice": "In House Invoice" "inhouse_invoice": "In House Invoice"
}, },

View File

@@ -1351,8 +1351,7 @@
"vacationadded": "" "vacationadded": ""
}, },
"validation": { "validation": {
"unique_employee_number": "", "unique_employee_number": ""
"unique_user_email": "Este correo electrónico ya está asignado a otro empleado."
} }
}, },
"esignature": { "esignature": {
@@ -1368,9 +1367,6 @@
"pdf_only": "Only PDF documents can be uploaded for e-signature.", "pdf_only": "Only PDF documents can be uploaded for e-signature.",
"upload_title": "Unable to prepare document for e-signature" "upload_title": "Unable to prepare document for e-signature"
}, },
"tooltips": {
"contact_sales": "Las firmas electronicas no estan habilitadas para este taller. Contacte a ventas para agregar esta funcion."
},
"fields": { "fields": {
"completed": "", "completed": "",
"completed_at": "", "completed_at": "",
@@ -3055,9 +3051,6 @@
"appointments": { "appointments": {
"appointment_confirmation": "" "appointment_confirmation": ""
}, },
"banners": {
"esignature_promo": "¿Cansado de obtener firmas en papel? Prueba las firmas electrónicas hoy. Contacta a ventas para agregar esta función."
},
"bills": { "bills": {
"inhouse_invoice": "" "inhouse_invoice": ""
}, },

View File

@@ -1351,8 +1351,7 @@
"vacationadded": "" "vacationadded": ""
}, },
"validation": { "validation": {
"unique_employee_number": "", "unique_employee_number": ""
"unique_user_email": "Cette adresse courriel est déjà assignée à un autre employé."
} }
}, },
"esignature": { "esignature": {
@@ -1368,9 +1367,6 @@
"pdf_only": "Only PDF documents can be uploaded for e-signature.", "pdf_only": "Only PDF documents can be uploaded for e-signature.",
"upload_title": "Unable to prepare document for e-signature" "upload_title": "Unable to prepare document for e-signature"
}, },
"tooltips": {
"contact_sales": "Les signatures electroniques ne sont pas activees pour cet atelier. Contactez les ventes pour ajouter cette fonctionnalite."
},
"fields": { "fields": {
"completed": "", "completed": "",
"completed_at": "", "completed_at": "",
@@ -3055,9 +3051,6 @@
"appointments": { "appointments": {
"appointment_confirmation": "" "appointment_confirmation": ""
}, },
"banners": {
"esignature_promo": "Vous en avez assez des signatures papier? Essayez les signatures électroniques dès aujourd'hui. Communiquez avec les ventes pour ajouter cette fonction."
},
"bills": { "bills": {
"inhouse_invoice": "" "inhouse_invoice": ""
}, },

View File

@@ -236,8 +236,10 @@ export default defineConfig(({ command, mode }) => {
redux: ["redux"], redux: ["redux"],
lodash: ["lodash"], lodash: ["lodash"],
"@sentry/react": ["@sentry/react"], "@sentry/react": ["@sentry/react"],
"feature-flags": ["src/feature-flags/splitio-react-replacement.jsx"], "@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"],
logrocket: ["logrocket"],
firebase: [ firebase: [
"@firebase/analytics",
"@firebase/app", "@firebase/app",
"@firebase/firestore", "@firebase/firestore",
"@firebase/auth", "@firebase/auth",

View File

@@ -5,7 +5,6 @@ provider "registry.terraform.io/hashicorp/aws" {
version = "6.38.0" version = "6.38.0"
constraints = "~> 6.0" constraints = "~> 6.0"
hashes = [ hashes = [
"h1:IMf41BcW9huOeVcrt6XjQqadYR2xD8zkUpGLLERJ4NM=",
"h1:RDoKIzXmt7H1mNFcNIyRT+nA/gTJyO3+iW9QGN5I2eQ=", "h1:RDoKIzXmt7H1mNFcNIyRT+nA/gTJyO3+iW9QGN5I2eQ=",
"zh:143f118ae71059a7a7026c6b950da23fef04a06e2362ffa688bef75e43e869ed", "zh:143f118ae71059a7a7026c6b950da23fef04a06e2362ffa688bef75e43e869ed",
"zh:29ee220a017306effd877e1280f8b2934dc957e16e0e72ca0222e5514d0db522", "zh:29ee220a017306effd877e1280f8b2934dc957e16e0e72ca0222e5514d0db522",
@@ -29,7 +28,6 @@ provider "registry.terraform.io/hashicorp/random" {
version = "3.8.1" version = "3.8.1"
constraints = "~> 3.6" constraints = "~> 3.6"
hashes = [ hashes = [
"h1:osH3aBqEARwOz3VBJKdpFKJJCNIdgRC6k8vPojkLmlY=",
"h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=", "h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=",
"zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4", "zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4",
"zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae", "zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae",

View File

@@ -1,7 +1,7 @@
{ {
"version": 4, "version": 4,
"terraform_version": "1.15.4", "terraform_version": "1.14.3",
"serial": 111, "serial": 105,
"lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683", "lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683",
"outputs": { "outputs": {
"application_url": { "application_url": {
@@ -21,7 +21,7 @@
"type": "string" "type": "string"
}, },
"postgres_engine_version": { "postgres_engine_version": {
"value": "17.10", "value": "17.9",
"type": "string" "type": "string"
}, },
"secrets_manager_secret_name": { "secrets_manager_secret_name": {
@@ -118,7 +118,7 @@
"filter": null, "filter": null,
"has_major_target": null, "has_major_target": null,
"has_minor_target": null, "has_minor_target": null,
"id": "17.10", "id": "17.9",
"include_all": null, "include_all": null,
"latest": true, "latest": true,
"parameter_group_family": "postgres17", "parameter_group_family": "postgres17",
@@ -144,15 +144,15 @@
"supports_parallel_query": false, "supports_parallel_query": false,
"supports_read_replica": true, "supports_read_replica": true,
"valid_major_targets": [ "valid_major_targets": [
"18.4" "18.3"
], ],
"valid_minor_targets": [], "valid_minor_targets": [],
"valid_upgrade_targets": [ "valid_upgrade_targets": [
"18.4" "18.3"
], ],
"version": "17.10", "version": "17.9",
"version_actual": "17.10", "version_actual": "17.9",
"version_description": "PostgreSQL 17.10-R1" "version_description": "PostgreSQL 17.9-R1"
}, },
"sensitive_attributes": [], "sensitive_attributes": [],
"identity_schema_version": 0 "identity_schema_version": 0
@@ -1085,7 +1085,7 @@
"endpoint": "documenso-postgres.cfo5pnykioqq.ca-central-1.rds.amazonaws.com:5432", "endpoint": "documenso-postgres.cfo5pnykioqq.ca-central-1.rds.amazonaws.com:5432",
"engine": "postgres", "engine": "postgres",
"engine_lifecycle_support": "open-source-rds-extended-support", "engine_lifecycle_support": "open-source-rds-extended-support",
"engine_version": "17.10", "engine_version": "17.9",
"engine_version_actual": "17.9", "engine_version_actual": "17.9",
"final_snapshot_identifier": "documenso-final-03443461", "final_snapshot_identifier": "documenso-final-03443461",
"hosted_zone_id": "Z1JG78A3UK1DU3", "hosted_zone_id": "Z1JG78A3UK1DU3",
@@ -1096,7 +1096,7 @@
"instance_class": "db.t4g.micro", "instance_class": "db.t4g.micro",
"iops": 3000, "iops": 3000,
"kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9", "kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9",
"latest_restorable_time": "2026-05-25T20:16:55Z", "latest_restorable_time": "2026-05-01T17:49:36Z",
"license_model": "postgresql-license", "license_model": "postgresql-license",
"listener_endpoint": [], "listener_endpoint": [],
"maintenance_window": "tue:03:10-tue:03:40", "maintenance_window": "tue:03:10-tue:03:40",
@@ -1384,7 +1384,7 @@
"Application": "documenso", "Application": "documenso",
"ManagedBy": "Terraform" "ManagedBy": "Terraform"
}, },
"task_definition": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:9", "task_definition": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:8",
"timeouts": null, "timeouts": null,
"triggers": {}, "triggers": {},
"volume_configuration": [], "volume_configuration": [],
@@ -1451,9 +1451,9 @@
{ {
"schema_version": 1, "schema_version": 1,
"attributes": { "attributes": {
"arn": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:9", "arn": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task:8",
"arn_without_revision": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task", "arn_without_revision": "arn:aws:ecs:ca-central-1:714144183158:task-definition/documenso-task",
"container_definitions": "[{\"environment\":[{\"name\":\"NEXT_PRIVATE_INTERNAL_WEBAPP_URL\",\"value\":\"http://127.0.0.1:3000\"},{\"name\":\"NEXT_PRIVATE_SMTP_HOST\",\"value\":\"email-smtp.ca-central-1.amazonaws.com\"},{\"name\":\"NEXT_PRIVATE_SMTP_PORT\",\"value\":\"587\"},{\"name\":\"NEXT_PRIVATE_SMTP_SECURE\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_SMTP_TRANSPORT\",\"value\":\"smtp-auth\"},{\"name\":\"NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_BUCKET\",\"value\":\"documenso-714144183158-ca-central-1\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_REGION\",\"value\":\"ca-central-1\"},{\"name\":\"NEXT_PUBLIC_DISABLE_SIGNUP\",\"value\":\"true\"},{\"name\":\"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT\",\"value\":\"10\"},{\"name\":\"NEXT_PUBLIC_UPLOAD_TRANSPORT\",\"value\":\"s3\"},{\"name\":\"NEXT_PUBLIC_WEBAPP_URL\",\"value\":\"https://sign.imex.online\"},{\"name\":\"PORT\",\"value\":\"3000\"}],\"essential\":true,\"image\":\"documenso/documenso:2.11.0\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"/ecs/documenso\",\"awslogs-region\":\"ca-central-1\",\"awslogs-stream-prefix\":\"documenso\"}},\"mountPoints\":[],\"name\":\"documenso\",\"portMappings\":[{\"containerPort\":3000,\"hostPort\":3000,\"protocol\":\"tcp\"}],\"secrets\":[{\"name\":\"NEXTAUTH_SECRET\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXTAUTH_SECRET::\"},{\"name\":\"NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS::\"},{\"name\":\"NEXT_PRIVATE_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DIRECT_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DIRECT_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_PASSPHRASE\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_PASSPHRASE::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_ADDRESS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_ADDRESS::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_NAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_NAME::\"},{\"name\":\"NEXT_PRIVATE_SMTP_PASSWORD\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_PASSWORD::\"},{\"name\":\"NEXT_PRIVATE_SMTP_USERNAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_USERNAME::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY::\"}],\"systemControls\":[],\"volumesFrom\":[]}]", "container_definitions": "[{\"environment\":[{\"name\":\"NEXT_PRIVATE_INTERNAL_WEBAPP_URL\",\"value\":\"http://127.0.0.1:3000\"},{\"name\":\"NEXT_PRIVATE_SMTP_HOST\",\"value\":\"email-smtp.ca-central-1.amazonaws.com\"},{\"name\":\"NEXT_PRIVATE_SMTP_PORT\",\"value\":\"587\"},{\"name\":\"NEXT_PRIVATE_SMTP_SECURE\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_SMTP_TRANSPORT\",\"value\":\"smtp-auth\"},{\"name\":\"NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS\",\"value\":\"false\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_BUCKET\",\"value\":\"documenso-714144183158-ca-central-1\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_REGION\",\"value\":\"ca-central-1\"},{\"name\":\"NEXT_PUBLIC_DISABLE_SIGNUP\",\"value\":\"true\"},{\"name\":\"NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT\",\"value\":\"10\"},{\"name\":\"NEXT_PUBLIC_UPLOAD_TRANSPORT\",\"value\":\"s3\"},{\"name\":\"NEXT_PUBLIC_WEBAPP_URL\",\"value\":\"https://sign.imex.online\"},{\"name\":\"PORT\",\"value\":\"3000\"}],\"essential\":true,\"image\":\"documenso/documenso:2.10.0\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"/ecs/documenso\",\"awslogs-region\":\"ca-central-1\",\"awslogs-stream-prefix\":\"documenso\"}},\"mountPoints\":[],\"name\":\"documenso\",\"portMappings\":[{\"containerPort\":3000,\"hostPort\":3000,\"protocol\":\"tcp\"}],\"secrets\":[{\"name\":\"NEXTAUTH_SECRET\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXTAUTH_SECRET::\"},{\"name\":\"NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS::\"},{\"name\":\"NEXT_PRIVATE_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DIRECT_DATABASE_URL\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DIRECT_DATABASE_URL::\"},{\"name\":\"NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_KEY::\"},{\"name\":\"NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS::\"},{\"name\":\"NEXT_PRIVATE_SIGNING_PASSPHRASE\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SIGNING_PASSPHRASE::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_ADDRESS\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_ADDRESS::\"},{\"name\":\"NEXT_PRIVATE_SMTP_FROM_NAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_FROM_NAME::\"},{\"name\":\"NEXT_PRIVATE_SMTP_PASSWORD\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_PASSWORD::\"},{\"name\":\"NEXT_PRIVATE_SMTP_USERNAME\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_SMTP_USERNAME::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID::\"},{\"name\":\"NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY\",\"valueFrom\":\"arn:aws:secretsmanager:ca-central-1:714144183158:secret:documenso/sign-imex-online/app-DNl1NE:NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY::\"}],\"systemControls\":[],\"volumesFrom\":[]}]",
"cpu": "512", "cpu": "512",
"enable_fault_injection": false, "enable_fault_injection": false,
"ephemeral_storage": [], "ephemeral_storage": [],
@@ -1470,7 +1470,7 @@
"requires_compatibilities": [ "requires_compatibilities": [
"FARGATE" "FARGATE"
], ],
"revision": 9, "revision": 8,
"runtime_platform": [], "runtime_platform": [],
"skip_destroy": false, "skip_destroy": false,
"tags": { "tags": {
@@ -1498,7 +1498,7 @@
"account_id": "714144183158", "account_id": "714144183158",
"family": "documenso-task", "family": "documenso-task",
"region": "ca-central-1", "region": "ca-central-1",
"revision": 9 "revision": 8
}, },
"private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==", "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjEifQ==",
"dependencies": [ "dependencies": [

View File

@@ -1,7 +1,7 @@
{ {
"version": 4, "version": 4,
"terraform_version": "1.15.4", "terraform_version": "1.14.3",
"serial": 105, "serial": 101,
"lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683", "lineage": "2b49a6da-17c7-01da-d62f-9a13def4b683",
"outputs": { "outputs": {
"application_url": { "application_url": {
@@ -1096,7 +1096,7 @@
"instance_class": "db.t4g.micro", "instance_class": "db.t4g.micro",
"iops": 3000, "iops": 3000,
"kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9", "kms_key_id": "arn:aws:kms:ca-central-1:714144183158:key/1237b672-91b3-4d23-958d-1877c5d22eb9",
"latest_restorable_time": "2026-05-01T17:49:36Z", "latest_restorable_time": "2026-05-01T15:49:30Z",
"license_model": "postgresql-license", "license_model": "postgresql-license",
"listener_endpoint": [], "listener_endpoint": [],
"maintenance_window": "tue:03:10-tue:03:40", "maintenance_window": "tue:03:10-tue:03:40",
@@ -3551,7 +3551,7 @@
], ],
"description": "WAF protection for Documenso", "description": "WAF protection for Documenso",
"id": "04577153-2a1a-462c-94b8-b0a1804755bb", "id": "04577153-2a1a-462c-94b8-b0a1804755bb",
"lock_token": "417061f1-deea-4ac2-b932-9bea49265444", "lock_token": "e71f2816-492c-4afc-acc2-3700795c2657",
"name": "documenso-web-acl", "name": "documenso-web-acl",
"name_prefix": "", "name_prefix": "",
"region": "ca-central-1", "region": "ca-central-1",
@@ -3693,24 +3693,7 @@
{ {
"managed_rule_group_configs": [], "managed_rule_group_configs": [],
"name": "AWSManagedRulesCommonRuleSet", "name": "AWSManagedRulesCommonRuleSet",
"rule_action_override": [ "rule_action_override": [],
{
"action_to_use": [
{
"allow": [],
"block": [],
"captcha": [],
"challenge": [],
"count": [
{
"custom_request_handling": []
}
]
}
],
"name": "SizeRestrictions_BODY"
}
],
"scope_down_statement": [], "scope_down_statement": [],
"vendor_name": "AWS", "vendor_name": "AWS",
"version": "" "version": ""

View File

@@ -846,13 +846,6 @@
table: table:
name: exportlog name: exportlog
schema: public schema: public
- name: feature_flags
using:
foreign_key_constraint_on:
column: bodyshopid
table:
name: bodyshop_feature_flags
schema: public
- name: inventories - name: inventories
using: using:
foreign_key_constraint_on: foreign_key_constraint_on:
@@ -2746,114 +2739,6 @@
- end_date - end_date
- content - content
filter: {} filter: {}
- table:
name: bodyshop_feature_flags
schema: public
object_relationships:
- name: bodyshop
using:
foreign_key_constraint_on: bodyshopid
- name: feature_flag
using:
foreign_key_constraint_on: name
select_permissions:
- role: user
permission:
columns:
- id
- bodyshopid
- name
- treatment
- config
- activeDate
- deactiveDate
- created_at
- updated_at
filter:
_and:
- bodyshop:
associations:
_and:
- user:
authid:
_eq: X-Hasura-User-Id
- active:
_eq: true
- feature_flag:
active:
_eq: true
event_triggers:
- name: cache_bodyshop_feature_flags
definition:
delete:
columns: '*'
enable_manual: false
insert:
columns: '*'
update:
columns: '*'
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/feature-flags/cache/invalidate'
version: 2
- table:
name: feature_flags
schema: public
array_relationships:
- name: bodyshop_feature_flags
using:
foreign_key_constraint_on:
column: name
table:
name: bodyshop_feature_flags
schema: public
select_permissions:
- role: user
permission:
columns:
- name
- description
- default_treatment
- active
- created_at
- updated_at
filter:
active:
_eq: true
event_triggers:
- name: cache_feature_flags
definition:
delete:
columns: '*'
enable_manual: false
insert:
columns: '*'
update:
columns: '*'
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/feature-flags/cache/invalidate'
version: 2
- table: - table:
name: exportlog name: exportlog
schema: public schema: public

View File

@@ -1 +0,0 @@
DROP TABLE IF EXISTS "public"."feature_flags";

View File

@@ -1,43 +0,0 @@
CREATE TABLE "public"."feature_flags" (
"name" text NOT NULL,
"description" text NULL,
"default_treatment" text NOT NULL DEFAULT 'off',
"active" boolean NOT NULL DEFAULT true,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
CONSTRAINT "feature_flags_pkey" PRIMARY KEY ("name"),
CONSTRAINT "feature_flags_default_treatment_check" CHECK (length(btrim("default_treatment")) > 0)
);
INSERT INTO "public"."feature_flags" ("name", "description")
VALUES
('ADPPayroll', 'Enable ADP payroll flows and reporting.'),
('Allow_Negative_Jobline_Price', 'Allow negative pricing on job lines.'),
('Autohouse_Detail_line', 'Enable Autohouse detail line handling.'),
('Bill_OCR_AI', 'Enable AI bill OCR entry.'),
('ClosingPeriod', 'Enable closing period accounting restrictions.'),
('CriticalPartsScanning', 'Enable critical parts scanning workflows.'),
('Direct_Media_Download', 'Enable direct media downloads.'),
('DmsAp', 'Enable DMS accounts payable workflows.'),
('Enhanced_Payroll', 'Enable enhanced payroll and labor allocation features.'),
('Extended_Bill_Posting', 'Enable extended bill posting.'),
('Fortellis', 'Enable Fortellis-backed DMS flows.'),
('IOU_Tracking', 'Enable IOU tracking.'),
('ImEXPay', 'Enable ImEX Pay workflows.'),
('Imgproxy', 'Enable imgproxy-backed media rendering.'),
('LogRocket_Tracking', 'Enable LogRocket tracking.'),
('NewPhotoViewer', 'Enable the newer photo viewer experience.'),
('OEConnection', 'Enable OEConnection parts ordering.'),
('OEConnection_PriceChange', 'Enable OEConnection price changes.'),
('OpenSearch', 'Enable OpenSearch global search.'),
('OpenSearch_PaginatedScreens', 'Enable OpenSearch on paginated screens.'),
('Production_List_Status_Colors', 'Enable status colors on production list.'),
('Production_Use_View', 'Enable production view selection.'),
('Qb_Multi_Ar', 'Enable QuickBooks multi-AR payment options.'),
('Realtime_Notifications_UI', 'Enable realtime notification UI.'),
('Share_To_Teams', 'Enable sharing workflows to Microsoft Teams.'),
('Simple_Inventory', 'Enable simple inventory workflows.'),
('TEST_FLAG', 'Manual test flag used to verify frontend feature flag plumbing.'),
('Use_Graphql_RR', 'Enable GraphQL-backed Rome/RR flows.'),
('Websocket_Production', 'Toggle websocket production board/list behavior.')
ON CONFLICT ("name") DO NOTHING;

View File

@@ -1,3 +0,0 @@
DROP TRIGGER IF EXISTS "set_public_feature_flags_updated_at" ON "public"."feature_flags";
DROP TRIGGER IF EXISTS "set_public_bodyshop_feature_flags_updated_at" ON "public"."bodyshop_feature_flags";
DROP TABLE IF EXISTS "public"."bodyshop_feature_flags";

View File

@@ -1,89 +0,0 @@
CREATE TABLE "public"."bodyshop_feature_flags" (
"id" uuid NOT NULL DEFAULT public.gen_random_uuid(),
"bodyshopid" uuid NOT NULL,
"name" text NOT NULL,
"treatment" text NOT NULL DEFAULT 'off',
"config" jsonb NULL,
"activeDate" timestamptz NULL,
"deactiveDate" timestamptz NULL,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
CONSTRAINT "bodyshop_feature_flags_pkey" PRIMARY KEY ("id"),
CONSTRAINT "bodyshop_feature_flags_bodyshopid_name_key" UNIQUE ("bodyshopid", "name"),
CONSTRAINT "bodyshop_feature_flags_bodyshopid_fkey" FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "bodyshop_feature_flags_name_fkey" FOREIGN KEY ("name") REFERENCES "public"."feature_flags" ("name") ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT "bodyshop_feature_flags_treatment_check" CHECK (length(btrim("treatment")) > 0),
CONSTRAINT "bodyshop_feature_flags_dates_check" CHECK ("deactiveDate" IS NULL OR "activeDate" IS NULL OR "deactiveDate" > "activeDate")
);
CREATE INDEX "bodyshop_feature_flags_bodyshopid_idx" ON "public"."bodyshop_feature_flags" ("bodyshopid");
CREATE INDEX "bodyshop_feature_flags_name_idx" ON "public"."bodyshop_feature_flags" ("name");
INSERT INTO "public"."bodyshop_feature_flags" (
"bodyshopid",
"name",
"treatment",
"config",
"activeDate",
"deactiveDate"
)
SELECT
"bodyshops"."id",
"feature_flag"."name",
CASE
WHEN jsonb_typeof("feature_flag"."value") = 'object'
AND nullif(btrim("feature_flag"."value" ->> 'treatment'), '') IS NOT NULL
THEN btrim("feature_flag"."value" ->> 'treatment')
WHEN jsonb_typeof("feature_flag"."value") = 'boolean'
THEN CASE WHEN ("feature_flag"."value" #>> '{}')::boolean THEN 'on' ELSE 'off' END
WHEN jsonb_typeof("feature_flag"."value") = 'string'
AND nullif(btrim("feature_flag"."value" #>> '{}'), '') IS NOT NULL
THEN btrim("feature_flag"."value" #>> '{}')
ELSE 'on'
END,
CASE
WHEN jsonb_typeof("feature_flag"."value") = 'object'
THEN "feature_flag"."value" -> 'config'
ELSE NULL
END,
CASE
WHEN jsonb_typeof("feature_flag"."value") = 'object'
AND "feature_flag"."value" ->> 'activeDate' ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}'
THEN ("feature_flag"."value" ->> 'activeDate')::timestamptz
ELSE NULL
END,
CASE
WHEN jsonb_typeof("feature_flag"."value") = 'object'
AND "feature_flag"."value" ->> 'deactiveDate' ~ '^[0-9]{4}-[0-9]{2}-[0-9]{2}'
THEN ("feature_flag"."value" ->> 'deactiveDate')::timestamptz
ELSE NULL
END
FROM "public"."bodyshops"
CROSS JOIN LATERAL jsonb_each(
CASE
WHEN jsonb_typeof(COALESCE("bodyshops"."features" -> 'featureFlags', '{}'::jsonb)) = 'object'
THEN COALESCE("bodyshops"."features" -> 'featureFlags', '{}'::jsonb)
ELSE '{}'::jsonb
END
) AS "feature_flag"("name", "value")
INNER JOIN "public"."feature_flags" ON "feature_flags"."name" = "feature_flag"."name"
ON CONFLICT ("bodyshopid", "name") DO UPDATE
SET
"treatment" = EXCLUDED."treatment",
"config" = EXCLUDED."config",
"activeDate" = EXCLUDED."activeDate",
"deactiveDate" = EXCLUDED."deactiveDate";
CREATE TRIGGER "set_public_bodyshop_feature_flags_updated_at"
BEFORE UPDATE ON "public"."bodyshop_feature_flags"
FOR EACH ROW EXECUTE FUNCTION "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_bodyshop_feature_flags_updated_at" ON "public"."bodyshop_feature_flags"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
CREATE TRIGGER "set_public_feature_flags_updated_at"
BEFORE UPDATE ON "public"."feature_flags"
FOR EACH ROW EXECUTE FUNCTION "public"."set_current_timestamp_updated_at"();
COMMENT ON TRIGGER "set_public_feature_flags_updated_at" ON "public"."feature_flags"
IS 'trigger to set value of column "updated_at" to current timestamp on row update';

View File

@@ -1,11 +0,0 @@
ALTER TABLE "public"."feature_flags"
DROP CONSTRAINT IF EXISTS "feature_flags_default_treatment_check";
ALTER TABLE "public"."feature_flags"
ADD CONSTRAINT "feature_flags_default_treatment_check" CHECK ("default_treatment" IN ('on', 'off', 'control'));
ALTER TABLE "public"."bodyshop_feature_flags"
DROP CONSTRAINT IF EXISTS "bodyshop_feature_flags_treatment_check";
ALTER TABLE "public"."bodyshop_feature_flags"
ADD CONSTRAINT "bodyshop_feature_flags_treatment_check" CHECK ("treatment" IN ('on', 'off', 'control'));

View File

@@ -1,11 +0,0 @@
ALTER TABLE "public"."feature_flags"
DROP CONSTRAINT IF EXISTS "feature_flags_default_treatment_check";
ALTER TABLE "public"."feature_flags"
ADD CONSTRAINT "feature_flags_default_treatment_check" CHECK (length(btrim("default_treatment")) > 0);
ALTER TABLE "public"."bodyshop_feature_flags"
DROP CONSTRAINT IF EXISTS "bodyshop_feature_flags_treatment_check";
ALTER TABLE "public"."bodyshop_feature_flags"
ADD CONSTRAINT "bodyshop_feature_flags_treatment_check" CHECK (length(btrim("treatment")) > 0);

View File

@@ -1,2 +0,0 @@
DELETE FROM "public"."feature_flags"
WHERE "name" = 'TEST_FLAG';

View File

@@ -1,3 +0,0 @@
INSERT INTO "public"."feature_flags" ("name", "description")
VALUES ('TEST_FLAG', 'Manual test flag used to verify frontend feature flag plumbing.')
ON CONFLICT ("name") DO NOTHING;

2170
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,67 +15,66 @@
"lint:fix": "eslint . --fix", "lint:fix": "eslint . --fix",
"test:unit": "vitest run", "test:unit": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js", "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
"feature-flags:export-harness": "node scripts/export-harness-feature-flags.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.1053.0", "@aws-sdk/client-cloudwatch-logs": "^3.1020.0",
"@aws-sdk/client-elasticache": "^3.1053.0", "@aws-sdk/client-elasticache": "^3.1020.0",
"@aws-sdk/client-s3": "^3.1053.0", "@aws-sdk/client-s3": "^3.1020.0",
"@aws-sdk/client-secrets-manager": "^3.1053.0", "@aws-sdk/client-secrets-manager": "^3.1020.0",
"@aws-sdk/client-ses": "^3.1053.0", "@aws-sdk/client-ses": "^3.1020.0",
"@aws-sdk/client-sqs": "^3.1053.0", "@aws-sdk/client-sqs": "^3.1020.0",
"@aws-sdk/client-textract": "^3.1053.0", "@aws-sdk/client-textract": "^3.1020.0",
"@aws-sdk/credential-provider-node": "^3.972.44", "@aws-sdk/credential-provider-node": "^3.972.28",
"@aws-sdk/lib-storage": "^3.1053.0", "@aws-sdk/lib-storage": "^3.1020.0",
"@aws-sdk/s3-request-presigner": "^3.1053.0", "@aws-sdk/s3-request-presigner": "^3.1020.0",
"@documenso/sdk-typescript": "^0.8.1", "@documenso/sdk-typescript": "^0.8.0",
"@jsreport/nodejs-client": "^4.1.1", "@jsreport/nodejs-client": "^4.1.0",
"@opensearch-project/opensearch": "^2.13.0", "@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1", "@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"aws4": "^1.13.2", "aws4": "^1.13.2",
"axios": "^1.16.1", "axios": "^1.14.0",
"axios-curlirize": "^2.0.0", "axios-curlirize": "^2.0.0",
"better-queue": "^3.8.12", "better-queue": "^3.8.12",
"bullmq": "^5.77.3", "bullmq": "^5.71.1",
"chart.js": "^4.5.1", "chart.js": "^4.5.1",
"cloudinary": "^2.10.0", "cloudinary": "^2.9.0",
"compression": "^1.8.1", "compression": "^1.8.1",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.6", "cors": "^2.8.6",
"crisp-status-reporter": "^1.2.2", "crisp-status-reporter": "^1.2.2",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^17.4.2", "dotenv": "^17.3.1",
"express": "^4.21.1", "express": "^4.21.1",
"fast-xml-parser": "^5.8.0", "fast-xml-parser": "^5.5.9",
"firebase-admin": "^13.10.0", "firebase-admin": "^13.7.0",
"fuse.js": "^7.3.0", "fuse.js": "^7.1.0",
"graphql": "^16.14.0", "graphql": "^16.13.2",
"graphql-request": "^6.1.0", "graphql-request": "^6.1.0",
"intuit-oauth": "^4.2.3", "intuit-oauth": "^4.2.2",
"ioredis": "^5.10.1", "ioredis": "^5.10.1",
"json-2-csv": "^5.5.10", "json-2-csv": "^5.5.10",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"juice": "^11.1.1", "juice": "^11.1.1",
"lodash": "^4.18.1", "lodash": "^4.17.23",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.6.2", "moment-timezone": "^0.6.1",
"multer": "^2.1.1", "multer": "^2.1.1",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"node-persist": "^4.0.4", "node-persist": "^4.0.4",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
"normalize-url": "^9.0.1", "normalize-url": "^9.0.0",
"pdf-lib": "^1.17.1", "pdf-lib": "^1.17.1",
"phone": "^3.1.71", "phone": "^3.1.71",
"query-string": "7.1.3", "query-string": "7.1.3",
"recursive-diff": "^1.0.9", "recursive-diff": "^1.0.9",
"rimraf": "^6.1.3", "rimraf": "^6.1.3",
"skia-canvas": "^3.0.8", "skia-canvas": "^3.0.8",
"soap": "^1.9.3", "soap": "^1.8.0",
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"socket.io-adapter": "^2.5.7", "socket.io-adapter": "^2.5.6",
"ssh2-sftp-client": "^11.0.0", "ssh2-sftp-client": "^11.0.0",
"twilio": "^5.13.1", "twilio": "^5.13.1",
"uuid": "^11.1.0", "uuid": "^11.1.0",
@@ -90,11 +89,11 @@
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"globals": "^17.6.0", "globals": "^17.4.0",
"mock-require": "^3.0.3", "mock-require": "^3.0.3",
"p-limit": "^3.1.0", "p-limit": "^3.1.0",
"prettier": "^3.8.3", "prettier": "^3.8.1",
"supertest": "^7.2.2", "supertest": "^7.2.2",
"vitest": "^4.1.7" "vitest": "^4.1.2"
} }
} }

Some files were not shown because too many files have changed in this diff Show More