diff --git a/client/eslint.config.js b/client/eslint.config.js
index b1f7dd1c2..0ced5d379 100644
--- a/client/eslint.config.js
+++ b/client/eslint.config.js
@@ -3,10 +3,19 @@ import pluginJs from "@eslint/js";
import pluginReact from "eslint-plugin-react";
/** @type {import('eslint').Linter.Config[]} */
+
export default [
- { files: ["**/*.{js,mjs,cjs,jsx}"] },
+ {
+ files: ["**/*.{js,mjs,cjs,jsx}"]
+ },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
- pluginReact.configs.flat.recommended, // This is not a plugin object, but a shareable config object
- pluginReact.configs.flat["jsx-runtime"] // Add this if you are using React 17+
+ {
+ ...pluginReact.configs.flat.recommended,
+ rules: {
+ ...pluginReact.configs.flat.recommended.rules,
+ "react/prop-types": 0
+ }
+ },
+ pluginReact.configs.flat["jsx-runtime"]
];
diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss
index 827d0dfc7..8d44de002 100644
--- a/client/src/App/App.styles.scss
+++ b/client/src/App/App.styles.scss
@@ -173,3 +173,9 @@
.muted-button:hover {
color: darkgrey;
}
+
+.blur-feature {
+ filter: blur(5px);
+ user-select: none;
+ pointer-events: none;
+}
diff --git a/client/src/components/feature-wrapper/blur-wrapper.component.jsx b/client/src/components/feature-wrapper/blur-wrapper.component.jsx
new file mode 100644
index 000000000..53d3ae0fb
--- /dev/null
+++ b/client/src/components/feature-wrapper/blur-wrapper.component.jsx
@@ -0,0 +1,57 @@
+import Dinero from "dinero.js";
+import React from "react";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectBodyshop } from "../../redux/user/user.selectors";
+import { HasFeatureAccess } from "./feature-wrapper.component";
+
+const mapStateToProps = createStructuredSelector({
+ bodyshop: selectBodyshop
+});
+const blurringProps = {
+ filter: "blur(6px)"
+};
+
+export function BlurWrapper({
+ bodyshop,
+ featureName,
+ styleProp = "style",
+ valueProp = "value",
+ overrideValue = true,
+ overrideValueFunction,
+ children
+}) {
+ if (!HasFeatureAccess({ featureName, bodyshop })) {
+ const childrenWithBlurProps = React.Children.map(children, (child) => {
+ if (React.isValidElement(child)) {
+ //Clone the child, and spread in our props to overwrite it.
+ let newValueProp;
+ if (!overrideValue) {
+ newValueProp = child.props[valueProp];
+ } else {
+ if (typeof overrideValueFunction === "function") {
+ newValueProp = overrideValueFunction();
+ } else if (overrideValueFunction === "RandomDinero") {
+ newValueProp = RandomDinero();
+ } else {
+ newValueProp = "This is some random text. Nothing interesting here.";
+ }
+ }
+
+ return React.cloneElement(child, {
+ [valueProp]: newValueProp,
+ [styleProp]: { ...child.props[styleProp], ...blurringProps }
+ });
+ }
+ return child;
+ });
+
+ return childrenWithBlurProps;
+ }
+ return children;
+}
+export default connect(mapStateToProps, null)(BlurWrapper);
+
+function RandomDinero() {
+ return Dinero({ amount: Math.round(Math.exp(Math.random() * 100, 2)) }).toFormat();
+}
diff --git a/client/src/components/feature-wrapper/feature-wrapper.component.jsx b/client/src/components/feature-wrapper/feature-wrapper.component.jsx
index 8f2336369..2efc7430f 100644
--- a/client/src/components/feature-wrapper/feature-wrapper.component.jsx
+++ b/client/src/components/feature-wrapper/feature-wrapper.component.jsx
@@ -11,24 +11,36 @@ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
-function FeatureWrapper({ bodyshop, featureName, noauth, children, ...restProps }) {
+function FeatureWrapper({ bodyshop, featureName, noauth, blurContent = false, children, ...restProps }) {
const { t } = useTranslation();
if (HasFeatureAccess({ featureName, bodyshop })) return children;
- return (
- noauth || (
-
- )
- );
+ if (blurContent) {
+ const childrenWithBlurProps = React.Children.map(children, (child) => {
+ // Checking isValidElement is the safe way and avoids a
+ // typescript error too.
+ if (React.isValidElement(child)) {
+ return React.cloneElement(child, { blur: true });
+ }
+ return child;
+ });
+ return childrenWithBlurProps;
+ } else {
+ return (
+ noauth || (
+
+ )
+ );
+ }
}
export function HasFeatureAccess({ featureName, bodyshop }) {
diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx
index 6457de46d..3d814d211 100644
--- a/client/src/components/header/header.component.jsx
+++ b/client/src/components/header/header.component.jsx
@@ -26,7 +26,7 @@ import Icon, {
UserOutlined
} from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
-import { Layout, Menu } from "antd";
+import { Layout, Menu, Space } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs";
@@ -44,6 +44,7 @@ import { signOutStart } from "../../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
+import LockWrapper from "../lock-wrapper/locker-wrapper.component";
const mapStateToProps = createStructuredSelector({
currentUser: selectCurrentUser,
@@ -128,33 +129,39 @@ function Header({
const accountingChildren = [];
- if (
- InstanceRenderManager({
- imex: HasFeatureAccess({ featureName: "bills", bodyshop }),
- rome: "USE_IMEX"
- })
- ) {
- accountingChildren.push(
- {
- key: "bills",
- id: "header-accounting-bills",
- icon: ,
- label: {t("menus.header.bills")}
- },
- {
- key: "enterbills",
- id: "header-accounting-enterbills",
- icon: ,
- label: t("menus.header.enterbills"),
- onClick: () => {
+ accountingChildren.push(
+ {
+ key: "bills",
+ id: "header-accounting-bills",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.bills")}
+
+
+ )
+ },
+ {
+ key: "enterbills",
+ id: "header-accounting-enterbills",
+ icon: ,
+ label: (
+
+
+ {t(t("menus.header.enterbills"))}
+
+
+ ),
+ onClick: () => {
+ HasFeatureAccess({ featureName: "bills", bodyshop }) &&
setBillEnterContext({
actions: {},
context: {}
});
- }
}
- );
- }
+ }
+ );
if (Simple_Inventory.treatment === "on") {
accountingChildren.push(
@@ -169,36 +176,41 @@ function Header({
}
);
}
- if (
- InstanceRenderManager({
- imex: HasFeatureAccess({ featureName: "payments", bodyshop }),
- rome: "USE_IMEX"
- })
- ) {
- accountingChildren.push(
- {
- type: "divider"
- },
- {
- key: "allpayments",
- id: "header-accounting-allpayments",
- icon: ,
- label: {t("menus.header.allpayments")}
- },
- {
- key: "enterpayments",
- id: "header-accounting-enterpayments",
- icon: ,
- label: t("menus.header.enterpayment"),
- onClick: () => {
+
+ accountingChildren.push(
+ {
+ type: "divider"
+ },
+ {
+ key: "allpayments",
+ id: "header-accounting-allpayments",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.allpayments")}
+
+
+ )
+ },
+ {
+ key: "enterpayments",
+ id: "header-accounting-enterpayments",
+ icon: ,
+ label: (
+
+ {t("menus.header.enterpayment")}
+
+ ),
+ onClick: () => {
+ HasFeatureAccess({ featureName: "payments", bodyshop }) &&
setPaymentContext({
actions: {},
context: null
});
- }
}
- );
- }
+ }
+ );
if (ImEXPay.treatment === "on") {
accountingChildren.push({
@@ -215,39 +227,44 @@ function Header({
});
}
- if (
- InstanceRenderManager({
- imex: HasFeatureAccess({ featureName: "timetickets", bodyshop }),
- rome: "USE_IMEX"
- })
- ) {
- accountingChildren.push(
- {
- type: "divider"
- },
- {
- key: "timetickets",
- id: "header-accounting-timetickets",
- icon: ,
- label: {t("menus.header.timetickets")}
- }
- );
-
- if (bodyshop?.md_tasks_presets?.use_approvals) {
- accountingChildren.push({
- key: "ttapprovals",
- id: "header-accounting-ttapprovals",
- icon: ,
- label: {t("menus.header.ttapprovals")}
- });
+ accountingChildren.push(
+ {
+ type: "divider"
+ },
+ {
+ key: "timetickets",
+ id: "header-accounting-timetickets",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.timetickets")}
+
+
+ )
}
- accountingChildren.push(
- {
- key: "entertimetickets",
- icon: ,
- label: t("menus.header.entertimeticket"),
- id: "header-accounting-entertimetickets",
- onClick: () => {
+ );
+
+ if (bodyshop?.md_tasks_presets?.use_approvals) {
+ accountingChildren.push({
+ key: "ttapprovals",
+ id: "header-accounting-ttapprovals",
+ icon: ,
+ label: {t("menus.header.ttapprovals")}
+ });
+ }
+ accountingChildren.push(
+ {
+ key: "entertimetickets",
+ icon: ,
+ label: (
+
+ {t("menus.header.entertimeticket")}
+
+ ),
+ id: "header-accounting-entertimetickets",
+ onClick: () => {
+ HasFeatureAccess({ featureName: "timetickets", bodyshop }) &&
setTimeTicketContext({
actions: {},
context: {
@@ -256,19 +273,24 @@ function Header({
: currentUser.email
}
});
- }
- },
- {
- type: "divider"
}
- );
- }
+ },
+ {
+ type: "divider"
+ }
+ );
const accountingExportChildren = [
{
key: "receivables",
id: "header-accounting-receivables",
- label: {t("menus.header.accounting-receivables")}
+ label: (
+
+
+ {t("menus.header.accounting-receivables")}
+
+
+ )
}
];
@@ -276,7 +298,13 @@ function Header({
accountingExportChildren.push({
key: "payables",
id: "header-accounting-payables",
- label: {t("menus.header.accounting-payables")}
+ label: (
+
+
+ {t("menus.header.accounting-payables")}
+
+
+ )
});
}
@@ -284,7 +312,13 @@ function Header({
accountingExportChildren.push({
key: "payments",
id: "header-accounting-payments",
- label: {t("menus.header.accounting-payments")}
+ label: (
+
+
+ {t("menus.header.accounting-payments")}
+
+
+ )
});
}
@@ -295,24 +329,27 @@ function Header({
{
key: "exportlogs",
id: "header-accounting-exportlogs",
- label: {t("menus.header.export-logs")}
+ label: (
+
+
+ {t("menus.header.export-logs")}
+
+
+ )
}
);
- if (
- InstanceRenderManager({
- imex: HasFeatureAccess({ featureName: "export", bodyshop }),
- rome: "USE_IMEX"
- })
- ) {
- accountingChildren.push({
- key: "accountingexport",
- id: "header-accounting-export",
- icon: ,
- label: t("menus.header.export"),
- children: accountingExportChildren
- });
- }
+ accountingChildren.push({
+ key: "accountingexport",
+ id: "header-accounting-export",
+ icon: ,
+ label: (
+
+ {t("menus.header.export")}
+
+ ),
+ children: accountingExportChildren
+ });
const menuItems = [
{
@@ -381,36 +418,35 @@ function Header({
icon: ,
label: {t("menus.header.productionlist")}
},
- ...(InstanceRenderManager({
- imex: HasFeatureAccess({ featureName: "visualboard", bodyshop }),
- rome: "USE_IMEX"
- })
- ? [
- {
- key: "productionboard",
- id: "header-production-board",
- icon: ,
- label: {t("menus.header.productionboard")}
- }
- ]
- : []),
- ...(InstanceRenderManager({
- imex: HasFeatureAccess({ featureName: "scoreboard", bodyshop }),
- rome: "USE_IMEX"
- })
- ? [
- {
- type: "divider"
- },
- {
- key: "scoreboard",
- id: "header-scoreboard",
- icon: ,
- label: {t("menus.header.scoreboard")}
- }
- ]
- : [])
+ {
+ key: "productionboard",
+ id: "header-production-board",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.productionboard")}
+
+
+ )
+ },
+
+ {
+ type: "divider"
+ },
+ {
+ key: "scoreboard",
+ id: "header-scoreboard",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.scoreboard")}
+
+
+ )
+ }
]
},
{
@@ -433,39 +469,54 @@ function Header({
}
]
},
- ...(InstanceRenderManager({
- imex: HasFeatureAccess({ featureName: "courtesycars", bodyshop }),
- rome: "USE_IMEX"
- })
- ? [
- {
- key: "ccs",
- id: "header-css",
- icon: ,
- label: t("menus.header.courtesycars"),
- children: [
- {
- key: "courtesycarsall",
- id: "header-courtesycars-all",
- icon: ,
- label: {t("menus.header.courtesycars-all")}
- },
- {
- key: "contracts",
- id: "header-contracts",
- icon: ,
- label: {t("menus.header.courtesycars-contracts")}
- },
- {
- key: "newcontract",
- id: "header-newcontract",
- icon: ,
- label: {t("menus.header.courtesycars-newcontract")}
- }
- ]
- }
- ]
- : []),
+ {
+ key: "ccs",
+ id: "header-css",
+ icon: ,
+ label: (
+
+ {t("menus.header.courtesycars")}
+
+ ),
+ children: [
+ {
+ key: "courtesycarsall",
+ id: "header-courtesycars-all",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.courtesycars-all")}
+
+
+ )
+ },
+ {
+ key: "contracts",
+ id: "header-contracts",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.courtesycars-contracts")}
+
+
+ )
+ },
+ {
+ key: "newcontract",
+ id: "header-newcontract",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.courtesycars-newcontract")}
+
+
+ )
+ }
+ ]
+ },
...(accountingChildren.length > 0
? [
@@ -484,19 +535,20 @@ function Header({
icon: ,
label: {t("menus.header.phonebook")}
},
- ...(InstanceRenderManager({
- imex: HasFeatureAccess({ featureName: "media", bodyshop }),
- rome: "USE_IMEX"
- })
- ? [
- {
- key: "temporarydocs",
- id: "header-temporarydocs",
- icon: ,
- label: {t("menus.header.temporarydocs")}
- }
- ]
- : []),
+
+ {
+ key: "temporarydocs",
+ id: "header-temporarydocs",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.temporarydocs")}
+
+
+ )
+ },
+
{
key: "tasks",
id: "tasks",
@@ -562,19 +614,19 @@ function Header({
icon: ,
label: {t("menus.header.shop_vendors")}
},
- ...(InstanceRenderManager({
- imex: HasFeatureAccess({ featureName: "csi", bodyshop }),
- rome: "USE_IMEX"
- })
- ? [
- {
- key: "shop-csi",
- id: "header-shop-csi",
- icon: ,
- label: {t("menus.header.shop_csi")}
- }
- ]
- : [])
+
+ {
+ key: "shop-csi",
+ id: "header-shop-csi",
+ icon: ,
+ label: (
+
+
+ {t("menus.header.shop_csi")}
+
+
+ )
+ }
]
},
{
@@ -630,7 +682,13 @@ function Header({
key: "shiftclock",
id: "header-shiftclock",
icon: ,
- label: {t("menus.header.shiftclock")}
+ label: (
+
+
+ {t("menus.header.shiftclock")}
+
+
+ )
}
]
: []),
diff --git a/client/src/components/job-bills-total/job-bills-total.component.jsx b/client/src/components/job-bills-total/job-bills-total.component.jsx
index ed123fa9c..140326283 100644
--- a/client/src/components/job-bills-total/job-bills-total.component.jsx
+++ b/client/src/components/job-bills-total/job-bills-total.component.jsx
@@ -6,6 +6,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import "./job-bills-total.styles.scss";
+import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
export default function JobBillsTotalComponent({
loading,
@@ -18,7 +19,7 @@ export default function JobBillsTotalComponent({
const { t } = useTranslation();
if (loading) return ;
- if (!!!jobTotals) {
+ if (!jobTotals) {
if (showWarning && warningCallback && typeof warningCallback === "function") {
warningCallback({ key: "bills", warning: t("jobs.errors.nofinancial") });
}
@@ -97,7 +98,7 @@ export default function JobBillsTotalComponent({
.add(
InstanceRenderManager({
imex: Dinero(),
- rome: Dinero(totals.additional.additionalCosts),
+ rome: Dinero(totals.additional.additionalCosts)
})
); // Additional costs were captured for Rome, but not imex.
diff --git a/client/src/components/lock-wrapper/locker-wrapper.component.jsx b/client/src/components/lock-wrapper/locker-wrapper.component.jsx
new file mode 100644
index 000000000..8b8711089
--- /dev/null
+++ b/client/src/components/lock-wrapper/locker-wrapper.component.jsx
@@ -0,0 +1,38 @@
+import { LockOutlined } from "@ant-design/icons";
+import { Space } from "antd";
+import React from "react";
+import { connect } from "react-redux";
+import { createStructuredSelector } from "reselect";
+import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
+import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
+import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
+
+const mapStateToProps = createStructuredSelector({
+ currentUser: selectCurrentUser,
+ recentItems: selectRecentItems,
+ selectedHeader: selectSelectedHeader,
+ bodyshop: selectBodyshop
+});
+
+const LockWrapper = ({ featureName, bodyshop, children, disabled = true }) => {
+ let renderedChildren = children;
+
+ if (disabled) {
+ renderedChildren = React.Children.map(children, (child) => {
+ if (React.isValidElement(child)) {
+ return React.cloneElement(child, {
+ disabled: true
+ });
+ }
+ return child;
+ });
+ }
+
+ return (
+
+ {!HasFeatureAccess({ featureName: featureName, bodyshop }) && }
+ {renderedChildren}
+
+ );
+};
+export default connect(mapStateToProps, null)(LockWrapper);