diff --git a/client/.env.development.imex b/client/.env.development.imex index 79d1b4e63..32dac9e44 100644 --- a/client/.env.development.imex +++ b/client/.env.development.imex @@ -17,4 +17,5 @@ TEST_PASSWORD="test123" VITE_PUBLIC_POSTHOG_KEY=phc_xtLmBIu0rjWwExY73Oj5DTH1bGbwq1G1Y8jnlTceien VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com VITE_APP_AMP_URL=https://vp8k908qy2.execute-api.ca-central-1.amazonaws.com -VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 \ No newline at end of file +VITE_APP_AMP_KEY=6228a598e57cd66875cfd41604f1f891 +VITE_ENABLE_COMPILER_IN_DEV=1 diff --git a/client/package-lock.json b/client/package-lock.json index 1dbdc5df3..be9269e14 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.1", "hasInstallScript": true, "dependencies": { - "@amplitude/analytics-browser": "^2.33.1", + "@amplitude/analytics-browser": "^2.33.2", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.0.13", "@emotion/is-prop-valid": "^1.4.0", @@ -51,7 +51,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.69", - "posthog-js": "^1.319.2", + "posthog-js": "^1.321.0", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", @@ -149,17 +149,17 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-browser": { - "version": "2.33.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.33.1.tgz", - "integrity": "sha512-93wZjuAFJ7QdyptF82i1pezm5jKuBWITHI++XshDgpks1RstJvJ9n11Ak8MnE4L2BGQ93XDN2aVEHfmQkt0/Pw==", + "version": "2.33.2", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.33.2.tgz", + "integrity": "sha512-TOVa3oqHQqKJbceMix+fucUvaAe70Mq3eMK2lANz3GHrry/xrzuc/M8HpxdSwDbR1XG6BGKrd4vHREc945z56g==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.35.0", - "@amplitude/plugin-autocapture-browser": "1.18.3", - "@amplitude/plugin-network-capture-browser": "1.7.3", - "@amplitude/plugin-page-url-enrichment-browser": "0.5.9", - "@amplitude/plugin-page-view-tracking-browser": "2.6.6", - "@amplitude/plugin-web-vitals-browser": "1.1.4", + "@amplitude/analytics-core": "2.35.1", + "@amplitude/plugin-autocapture-browser": "1.18.4", + "@amplitude/plugin-network-capture-browser": "1.7.4", + "@amplitude/plugin-page-url-enrichment-browser": "0.5.10", + "@amplitude/plugin-page-view-tracking-browser": "2.6.7", + "@amplitude/plugin-web-vitals-browser": "1.1.5", "tslib": "^2.4.1" } }, @@ -170,9 +170,9 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-core": { - "version": "2.35.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.35.0.tgz", - "integrity": "sha512-7RmHYELXCGu8yuO9D6lEXiqkMtiC5sePNhCWmwuP30dneDYHtH06gaYvAFH/YqOFuE6enwEEJfFYtcaPhyiqtA==", + "version": "2.35.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.35.1.tgz", + "integrity": "sha512-ZChD4oUtpbO6W5YhWZ0G9BbqVOx7DoX1+cPyAMFwFkglH6JrZCrKvUTrukhVpVB+wkLRRK1ZviN0PzP6mDaifw==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.6.4", @@ -181,53 +181,52 @@ } }, "node_modules/@amplitude/plugin-autocapture-browser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.18.3.tgz", - "integrity": "sha512-njYque5t1QCEEe5V8Ls4yVVklTM6V7OXxBk6pqznN/hj/Pc4X8Wjy898pZ2VtbnvpagBKKzGb5B6Syl8OXiicw==", + "version": "1.18.4", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.18.4.tgz", + "integrity": "sha512-D4BzLjTjT7+Q2TEA0US9THlKPDpukcyIkjknsa9jRhWyLhirnwKEnf5w3WhX+g2psfnq+zY0UjCboQ+WCDL0Zw==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.35.0", - "rxjs": "^7.8.1", + "@amplitude/analytics-core": "2.35.1", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-network-capture-browser": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.7.3.tgz", - "integrity": "sha512-zfWgAN7g6AigJAsgrGmlgVwydOHH6XvweBoxhU+qEvRydboiIVCDLSxuXczUsBG7kYVLWRdBK1DYoE5J7lqTGA==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.7.4.tgz", + "integrity": "sha512-en86lEWMNkQOPm64yYnBjOI3qyHxAmhKZF+zgxdxwHM4vOQ8M1ySxVitCyd0GJiLmdEHEWj0PWgPAVkkj7BjBQ==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.35.0", + "@amplitude/analytics-core": "2.35.1", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-url-enrichment-browser": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.5.9.tgz", - "integrity": "sha512-TqdELx4WrdRutCjHUFUzum/f/UjhbdTZw0UKkYFAj5gwAKDjaPEjL4waRvINOTaVLsne1A6ck4KEMfC8AKByFw==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.5.10.tgz", + "integrity": "sha512-lgp2uwz2UPXxJypYMgiQ5yHhoTIQ6QaZQu8yq//9sogkMkDt0ClybTYwRk3N1q/XVS1cR79vT68gtvzdLD62Lg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.35.0", + "@amplitude/analytics-core": "2.35.1", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.6.6.tgz", - "integrity": "sha512-dBcJlrdKgPzSgS3exDRRrMLqhIaOjwlIy7o8sEMn1PpMawERlbumSSdtfII6L4L67HYUPo4PY4Kp4acqSzaLvQ==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.6.7.tgz", + "integrity": "sha512-cSiKOAJkgqI/h3+rjVXVvegrK2cma9XPxtWnBvShGbmVzh+ekIUrKktUFLsmxxFzkY94VVsVWiSGovQsKa8RuA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.35.0", + "@amplitude/analytics-core": "2.35.1", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-web-vitals-browser": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.4.tgz", - "integrity": "sha512-XQXI9OjTNSz2yi0lXw2VYMensDzzSkMCfvXNniTb1LgnHwBcQ1JWPcTqHLPFrvvNckeIdOT78vjs7yA+c1FyzA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.5.tgz", + "integrity": "sha512-6tcaSi5nM5pd6/bcMl90+LSR4cCsqFLP2SG9RUy+bHQN/DCh+Nzq1X+5a1St+MqX8Qr6s4q6YHbkIUcEMHo+Zg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.35.0", + "@amplitude/analytics-core": "2.35.1", "tslib": "^2.4.1", "web-vitals": "5.1.0" } @@ -4566,9 +4565,9 @@ } }, "node_modules/@posthog/types": { - "version": "1.319.2", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.319.2.tgz", - "integrity": "sha512-mGyQx5T4mpX+r4hyFKXJ41sck7WkWSiPgq7NTDGPbFPNW9F2mtD0R+myDhXxHrQUxAEa9ZIgrIvysTY37UYagA==", + "version": "1.321.0", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.321.0.tgz", + "integrity": "sha512-dNxsez/AqV3dt/UO6h5aJ+qBj7Tj0a17hqc9zE1XvvlXxpVFuk0EFsSlxtrBNumWWxh29jINw0x0YitrozNqIQ==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -7325,6 +7324,12 @@ "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", "license": "MIT" }, + "node_modules/@types/zen-observable": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz", + "integrity": "sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw==", + "license": "MIT" + }, "node_modules/@umijs/route-utils": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@umijs/route-utils/-/route-utils-4.0.1.tgz", @@ -14694,9 +14699,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.319.2", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.319.2.tgz", - "integrity": "sha512-mYFoRPSYZ34Ywdz3Ph4ME/md5H60NoKc8I/DTEr31YEGIC6dYKOOWBRFO/MLMvnAny5C7VEir8YE5dQ9484vPw==", + "version": "1.321.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.321.0.tgz", + "integrity": "sha512-IFdm/iBoFHltHwdZ/qjtni4RAtFCU6NEt6QTNOzBcuAk5srAFQBb7o+8MxryGON7EXLKCbAA6hueksHFB/WY/A==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -14705,11 +14710,11 @@ "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", "@posthog/core": "1.9.1", - "@posthog/types": "1.319.2", + "@posthog/types": "1.321.0", "core-js": "^3.38.1", "dompurify": "^3.3.1", "fflate": "^0.4.8", - "preact": "^10.19.3", + "preact": "^10.28.0", "query-selector-shadow-dom": "^1.0.1", "web-vitals": "^4.2.4" } @@ -19198,11 +19203,12 @@ "license": "MIT" }, "node_modules/zen-observable-ts": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz", - "integrity": "sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz", + "integrity": "sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA==", "license": "MIT", "dependencies": { + "@types/zen-observable": "0.8.3", "zen-observable": "0.8.15" } }, diff --git a/client/package.json b/client/package.json index 51a533388..aef2aebf2 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "private": true, "proxy": "http://localhost:4000", "dependencies": { - "@amplitude/analytics-browser": "^2.33.1", + "@amplitude/analytics-browser": "^2.33.2", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.0.13", "@emotion/is-prop-valid": "^1.4.0", @@ -50,7 +50,7 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.69", - "posthog-js": "^1.319.2", + "posthog-js": "^1.321.0", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", diff --git a/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx b/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx index 63404a49c..c4afa697c 100644 --- a/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx +++ b/client/src/components/bill-line-search-select/bill-line-search-select.component.jsx @@ -1,9 +1,8 @@ import { Select } from "antd"; -import { forwardRef } from "react"; import { useTranslation } from "react-i18next"; import InstanceRenderMgr from "../../utils/instanceRenderMgr"; -const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => { +const BillLineSearchSelect = ({ options, disabled, allowRemoved, ref, ...restProps }) => { const { t } = useTranslation(); return ( @@ -68,4 +67,4 @@ function generateLineName(item) { ); } -export default forwardRef(BillLineSearchSelect); +export default BillLineSearchSelect; diff --git a/client/src/components/card-payment-modal/card-payment-modal.component.jsx b/client/src/components/card-payment-modal/card-payment-modal.component.jsx index 2c5862c24..ab3384fce 100644 --- a/client/src/components/card-payment-modal/card-payment-modal.component.jsx +++ b/client/src/components/card-payment-modal/card-payment-modal.component.jsx @@ -46,6 +46,8 @@ const CardPaymentModalComponent = ({ const [form] = Form.useForm(); const [paymentLink, setPaymentLink] = useState(); + const isMountedRef = useRef(true); + const [loading, setLoading] = useState(false); const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE); const { t } = useTranslation(); @@ -69,6 +71,16 @@ const CardPaymentModalComponent = ({ [called, loadRoAndOwnerByJobPks, refetch] ); + useEffect(() => { + return () => { + isMountedRef.current = false; + }; + }, []); + + const setLoadingSafe = useCallback((value) => { + if (isMountedRef.current) setLoading(value); + }, []); + // Watch form payments so we can query jobs when all jobids are filled (without side effects during render) const payments = Form.useWatch(["payments"], form); @@ -106,45 +118,74 @@ const CardPaymentModalComponent = ({ const SetIntellipayCallbackFunctions = () => { console.log("*** Set IntelliPay callback functions."); + const isLikelyUserCancel = (response) => { + const reason = String(response?.declinereason ?? "").toLowerCase(); + // Heuristics: adjust if IntelliPay gives you a known cancel code/message + return ( + reason.includes("cancel") || + reason.includes("canceled") || + reason.includes("closed") || + // many gateways won't have a paymentid if user cancels before submitting + !response?.paymentid + ); + }; + window.intellipay.runOnClose(() => { - //window.intellipay.initialize(); + // This is the path for Cancel / X + try { + // If IntelliPay uses this flag, clear it so initialize() won't reopen unexpectedly + window.intellipay.isAutoOpen = false; + } catch { + // ignore + } + + // Optional: if IntelliPay needs re-init after close, uncomment: + // try { window.intellipay.initialize?.(); } catch {} + + setLoadingSafe(false); }); window.intellipay.runOnApproval(() => { - // 2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback. - // Add a slight delay to allow the refetch to properly get the data. + // keep your existing behavior setTimeout(() => { if (actions?.refetch) actions.refetch(); - setLoading(false); + setLoadingSafe(false); toggleModalVisible(); }, 750); }); window.intellipay.runOnNonApproval(async (response) => { - // Mutate unsuccessful payment + try { + // If cancel is reported as "non-approval", don't record it as a failed payment + if (isLikelyUserCancel(response)) return; - const { payments } = form.getFieldsValue(); - await insertPaymentResponse({ - variables: { - paymentResponse: payments.map((payment) => ({ - amount: payment.amount, - bodyshopid: bodyshop.id, + const { payments } = form.getFieldsValue(); + + await insertPaymentResponse({ + variables: { + paymentResponse: payments.map((payment) => ({ + amount: payment.amount, + bodyshopid: bodyshop.id, + jobid: payment.jobid, + declinereason: response.declinereason, + ext_paymentid: response.paymentid?.toString?.() ?? null, + successful: false, + response + })) + } + }); + + payments.forEach((payment) => + insertAuditTrail({ jobid: payment.jobid, - declinereason: response.declinereason, - ext_paymentid: response.paymentid.toString(), - successful: false, - response - })) - } - }); - - payments.forEach((payment) => - insertAuditTrail({ - jobid: payment.jobid, - operation: AuditTrailMapping.failedpayment(), - type: "failedpayment" - }) - ); + operation: AuditTrailMapping.failedpayment(), + type: "failedpayment" + }) + ); + } finally { + // IMPORTANT: always clear loading, even on errors + setLoadingSafe(false); + } }); }; @@ -154,7 +195,7 @@ const CardPaymentModalComponent = ({ try { await form.validateFields(); } catch { - setLoading(false); + setLoadingSafe(false); return; } @@ -185,7 +226,7 @@ const CardPaymentModalComponent = ({ document.documentElement.appendChild(node); pollForIntelliPay(() => { SetIntellipayCallbackFunctions(); - // eslint-disable-next-line react-compiler/react-compiler + window.intellipay.isAutoOpen = true; window.intellipay.initialize(); }); @@ -194,7 +235,7 @@ const CardPaymentModalComponent = ({ notification.error({ title: t("job_payments.notifications.error.openingip") }); - setLoading(false); + setLoadingSafe(false); } }; @@ -204,7 +245,7 @@ const CardPaymentModalComponent = ({ try { await form.validateFields(); } catch { - setLoading(false); + setLoadingSafe(false); return; } @@ -227,12 +268,12 @@ const CardPaymentModalComponent = ({ await navigator.clipboard.writeText(response.data.shorUrl); message.success(t("general.actions.copied")); } - setLoading(false); + setLoadingSafe(false); } catch { notification.error({ title: t("job_payments.notifications.error.openingip") }); - setLoading(false); + setLoadingSafe(false); } }; diff --git a/client/src/components/contract-status-select/contract-status-select.component.jsx b/client/src/components/contract-status-select/contract-status-select.component.jsx index 69d38656a..a02664f6c 100644 --- a/client/src/components/contract-status-select/contract-status-select.component.jsx +++ b/client/src/components/contract-status-select/contract-status-select.component.jsx @@ -1,10 +1,10 @@ -import { forwardRef, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Select } from "antd"; import { useTranslation } from "react-i18next"; const { Option } = Select; -const ContractStatusComponent = forwardRef(({ value, onChange }, ref) => { +const ContractStatusComponent = ({ value, onChange, ref }) => { const [option, setOption] = useState(value); const { t } = useTranslation(); @@ -21,7 +21,7 @@ const ContractStatusComponent = forwardRef(({ value, onChange }, ref) => { ); -}); +}; ContractStatusComponent.displayName = "ContractStatusComponent"; diff --git a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx index a189e0660..ddfaa0845 100644 --- a/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx +++ b/client/src/components/courtesy-car-fuel-select/courtesy-car-fuel-select.component.jsx @@ -1,8 +1,7 @@ import { Slider } from "antd"; -import { forwardRef } from "react"; import { useTranslation } from "react-i18next"; -const CourtesyCarFuelComponent = (props, ref) => { +const CourtesyCarFuelComponent = ({ ref, ...props }) => { const { t } = useTranslation(); const marks = { @@ -63,4 +62,4 @@ const CourtesyCarFuelComponent = (props, ref) => { /> ); }; -export default forwardRef(CourtesyCarFuelComponent); +export default CourtesyCarFuelComponent; diff --git a/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx b/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx index c6ff8212d..eea8819d8 100644 --- a/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx +++ b/client/src/components/courtesy-car-readiness-select/courtesy-car-readiness-select.component.jsx @@ -1,10 +1,10 @@ import { Select } from "antd"; -import { forwardRef, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; const { Option } = Select; -const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => { +const CourtesyCarReadinessComponent = ({ value, onChange, ref }) => { const [option, setOption] = useState(value); const { t } = useTranslation(); @@ -29,4 +29,4 @@ const CourtesyCarReadinessComponent = ({ value, onChange }, ref) => { ); }; -export default forwardRef(CourtesyCarReadinessComponent); +export default CourtesyCarReadinessComponent; diff --git a/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx b/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx index 11dd09296..31f0216de 100644 --- a/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx +++ b/client/src/components/courtesy-car-status-select/courtesy-car-status-select.component.jsx @@ -1,10 +1,10 @@ -import { forwardRef, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Select } from "antd"; import { useTranslation } from "react-i18next"; const { Option } = Select; -const CourtesyCarStatusComponent = ({ value, onChange }, ref) => { +const CourtesyCarStatusComponent = ({ value, onChange, ref }) => { const [option, setOption] = useState(value); const { t } = useTranslation(); @@ -32,4 +32,4 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => { ); }; -export default forwardRef(CourtesyCarStatusComponent); +export default CourtesyCarStatusComponent; diff --git a/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx b/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx index 3005b0534..ad83ae557 100644 --- a/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx +++ b/client/src/components/employee-team-search-select/employee-team-search-select.component.jsx @@ -1,17 +1,17 @@ import { useQuery } from "@apollo/client/react"; import { Select } from "antd"; -import { forwardRef } from "react"; import { QUERY_TEAMS } from "../../graphql/employee_teams.queries"; import AlertComponent from "../alert/alert.component"; //To be used as a form element only. -const EmployeeTeamSearchSelect = ({ ...props }) => { +const EmployeeTeamSearchSelect = ({ ref, ...props }) => { const { loading, error, data } = useQuery(QUERY_TEAMS); if (error) return ; return ( ; } -export default forwardRef(FormItemPhone); +export default FormItemPhone; export const PhoneItemFormatterValidation = () => ({ async validator(rule, value) { diff --git a/client/src/components/job-search-select/job-search-select.component.jsx b/client/src/components/job-search-select/job-search-select.component.jsx index f70beb627..62d1f60f3 100644 --- a/client/src/components/job-search-select/job-search-select.component.jsx +++ b/client/src/components/job-search-select/job-search-select.component.jsx @@ -2,7 +2,7 @@ import { LoadingOutlined } from "@ant-design/icons"; import { useLazyQuery } from "@apollo/client/react"; import { Select, Space, Spin, Tag } from "antd"; import _ from "lodash"; -import { forwardRef, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries"; import AlertComponent from "../alert/alert.component"; @@ -10,10 +10,15 @@ import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-displ const { Option } = Select; -const JobSearchSelect = ( - { disabled, convertedOnly = false, notInvoiced = false, notExported = true, clm_no = false, ...restProps }, - ref -) => { +const JobSearchSelect = ({ + disabled, + convertedOnly = false, + notInvoiced = false, + notExported = true, + clm_no = false, + ref, + ...restProps +}) => { const { t } = useTranslation(); const [theOptions, setTheOptions] = useState([]); @@ -107,4 +112,4 @@ const JobSearchSelect = ( ); }; -export default forwardRef(JobSearchSelect); +export default JobSearchSelect; diff --git a/client/src/components/notification-center/notification-center.component.jsx b/client/src/components/notification-center/notification-center.component.jsx index 67fb7119d..0702447c7 100644 --- a/client/src/components/notification-center/notification-center.component.jsx +++ b/client/src/components/notification-center/notification-center.component.jsx @@ -5,31 +5,27 @@ import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import "./notification-center.styles.scss"; import day from "../../utils/day.js"; -import { forwardRef, useEffect, useRef } from "react"; +import { useEffect, useRef } from "react"; import { DateTimeFormat } from "../../utils/DateFormatter.jsx"; const { Text, Title } = Typography; /** * Notification Center Component - * @type {React.ForwardRefExoticComponent & React.RefAttributes>} */ -const NotificationCenterComponent = forwardRef( - ( - { - visible, - notifications, - loading, - showUnreadOnly, - toggleUnreadOnly, - markAllRead, - loadMore, - onNotificationClick, - unreadCount, - isEmployee - }, - ref - ) => { +const NotificationCenterComponent = ({ + visible, + notifications, + loading, + showUnreadOnly, + toggleUnreadOnly, + markAllRead, + loadMore, + onNotificationClick, + unreadCount, + isEmployee, + ref +}) => { const { t } = useTranslation(); const navigate = useNavigate(); const virtuosoRef = useRef(null); @@ -127,8 +123,7 @@ const NotificationCenterComponent = forwardRef( )} ); - } -); +}; NotificationCenterComponent.displayName = "NotificationCenterComponent"; diff --git a/client/src/components/owner-search-select/owner-search-select.component.jsx b/client/src/components/owner-search-select/owner-search-select.component.jsx index 6107ae764..d065f1db9 100644 --- a/client/src/components/owner-search-select/owner-search-select.component.jsx +++ b/client/src/components/owner-search-select/owner-search-select.component.jsx @@ -2,14 +2,14 @@ import { LoadingOutlined } from "@ant-design/icons"; import { useLazyQuery } from "@apollo/client/react"; import { Empty, Select } from "antd"; import _ from "lodash"; -import { forwardRef, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { SEARCH_OWNERS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_OWNERS_FOR_AUTOCOMPLETE } from "../../graphql/owners.queries"; import AlertComponent from "../alert/alert.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; const { Option } = Select; -const OwnerSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => { +const OwnerSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => { const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_OWNERS_FOR_AUTOCOMPLETE); const [callIdSearch, { loading: idLoading, error: idError, data: idData }] = useLazyQuery( @@ -85,4 +85,4 @@ const OwnerSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => { ); }; -export default forwardRef(OwnerSearchSelect); +export default OwnerSearchSelect; diff --git a/client/src/components/production-board-kanban/trello-board/components/ListComponent.jsx b/client/src/components/production-board-kanban/trello-board/components/ListComponent.jsx index 9d6da60d9..3abb90f70 100644 --- a/client/src/components/production-board-kanban/trello-board/components/ListComponent.jsx +++ b/client/src/components/production-board-kanban/trello-board/components/ListComponent.jsx @@ -1,10 +1,8 @@ -import { forwardRef } from "react"; - -const ListComponent = forwardRef(({ style, children, ...props }, ref) => ( +const ListComponent = ({ style, children, ref, ...props }) => (
{children}
-)); +); ListComponent.displayName = "ListComponent"; diff --git a/client/src/components/task-center/task-center.component.jsx b/client/src/components/task-center/task-center.component.jsx index 8b269f7b7..459e495fc 100644 --- a/client/src/components/task-center/task-center.component.jsx +++ b/client/src/components/task-center/task-center.component.jsx @@ -1,7 +1,7 @@ import { Virtuoso } from "react-virtuoso"; import { Badge, Button, Spin } from "antd"; import { useTranslation } from "react-i18next"; -import { forwardRef, useMemo, useRef } from "react"; +import { useMemo, useRef } from "react"; import day from "../../utils/day.js"; import "./task-center.styles.scss"; import { @@ -12,8 +12,18 @@ import { QuestionCircleOutlined } from "@ant-design/icons"; -const TaskCenterComponent = forwardRef( - ({ visible, tasks, loading, error, onTaskClick, onLoadMore, hasMore, createNewTask, incompleteTaskCount }, ref) => { +const TaskCenterComponent = ({ + visible, + tasks, + loading, + error, + onTaskClick, + onLoadMore, + hasMore, + createNewTask, + incompleteTaskCount, + ref +}) => { const { t } = useTranslation(); const virtuosoRef = useRef(null); @@ -149,8 +159,7 @@ const TaskCenterComponent = forwardRef( )} ); - } -); +}; TaskCenterComponent.displayName = "TaskCenterComponent"; export default TaskCenterComponent; diff --git a/client/src/components/vehicle-search-select/vehicle-search-select.component.jsx b/client/src/components/vehicle-search-select/vehicle-search-select.component.jsx index 698ad3807..f06db3124 100644 --- a/client/src/components/vehicle-search-select/vehicle-search-select.component.jsx +++ b/client/src/components/vehicle-search-select/vehicle-search-select.component.jsx @@ -2,7 +2,7 @@ import { LoadingOutlined } from "@ant-design/icons"; import { useLazyQuery } from "@apollo/client/react"; import { Empty, Select } from "antd"; import _ from "lodash"; -import { forwardRef, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { SEARCH_VEHICLES_BY_ID_FOR_AUTOCOMPLETE, SEARCH_VEHICLES_FOR_AUTOCOMPLETE @@ -11,7 +11,7 @@ import AlertComponent from "../alert/alert.component"; const { Option } = Select; -const VehicleSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => { +const VehicleSearchSelect = ({ value, onChange, onBlur, disabled, ref }) => { const [callSearch, { loading, error, data }] = useLazyQuery(SEARCH_VEHICLES_FOR_AUTOCOMPLETE); const [callIdSearch, { loading: idLoading, error: idError, data: idData }] = useLazyQuery( @@ -87,4 +87,4 @@ const VehicleSearchSelect = ({ value, onChange, onBlur, disabled }, ref) => { ); }; -export default forwardRef(VehicleSearchSelect); +export default VehicleSearchSelect; diff --git a/client/src/components/vendor-search-select/vendor-search-select.component.jsx b/client/src/components/vendor-search-select/vendor-search-select.component.jsx index cc366b7cd..a8c0d5a24 100644 --- a/client/src/components/vendor-search-select/vendor-search-select.component.jsx +++ b/client/src/components/vendor-search-select/vendor-search-select.component.jsx @@ -1,13 +1,13 @@ import { HeartOutlined } from "@ant-design/icons"; import { Select, Space, Tag } from "antd"; -import { forwardRef, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import PhoneNumberFormatter from "../../utils/PhoneFormatter"; const { Option } = Select; // To be used as a form element only. -const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone }, ref) => { +const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, preferredMake, showPhone, ref }) => { const [option, setOption] = useState(value); useEffect(() => { @@ -131,4 +131,4 @@ const VendorSearchSelect = ({ value, onChange, options, onSelect, disabled, pref ); }; -export default forwardRef(VendorSearchSelect); +export default VendorSearchSelect; diff --git a/client/vite.config.js b/client/vite.config.js index 2119dd104..1d9eb75ad 100644 --- a/client/vite.config.js +++ b/client/vite.config.js @@ -37,239 +37,258 @@ const getFormattedTimestamp = () => export const logger = createLogger("info", { allowClearScreen: false }); -export default defineConfig({ - base: "/", - plugins: [ - ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })), +// Read HTTPS certs once (used by both server + preview) +const httpsCerts = { + key: await fsPromises.readFile("../certs/key.pem"), + cert: await fsPromises.readFile("../certs/cert.pem") +}; - // PWA only for production builds (faster dev) - VitePWA({ - apply: "build", - injectRegister: "auto", - registerType: "prompt", - workbox: { - navigateFallbackDenylist: [/^\/api\//] // prevent caching API routes - }, - manifest: { - short_name: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "ImEX Online", - rome: "Rome Online" - }), - name: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "ImEX Online", - rome: "Rome Online" - }), - description: "The ultimate bodyshop management system.", - icons: [ - { - src: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "favicon.png", - rome: "ro-favicon.png" - }), - sizes: "64x64 32x32 24x24 16x16", - type: "image/x-icon" - }, - { - src: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "logo192.png", - rome: "logo192.png" - }), - type: "image/png", - sizes: "192x192" - }, - { - src: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "logo512.png", - rome: "ro-favicon.png" - }), - type: "image/png", - sizes: "512x512" - } - ], - theme_color: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "#1890ff", - rome: "#fff" - }), - background_color: "#fff", - gcm_sender_id: "103953800507" - } - }), +export default defineConfig(({ command, mode }) => { + // Only enable React Compiler on build in production/test (keeps dev as fast as possible) + const isBuild = command === "build"; + const isTestBuild = + mode === "test" || process.env.VITE_APP_IS_TEST === "true" || process.env.VITE_APP_IS_TEST === "1"; + const enableReactCompiler = + process.env.VITE_ENABLE_COMPILER_IN_DEV || (isBuild && (mode === "production" || isTestBuild)); - react({ - babel: { - plugins: [ - ['babel-plugin-react-compiler', { - // Exclude third-party drag-and-drop library from compilation - sources: (filename) => { - return !filename.includes('trello-board/dnd'); + console.log(enableReactCompiler ? "React Compiler enabled" : "React Compiler disabled"); + + return { + base: "/", + plugins: [ + ViteEjsPlugin((viteConfig) => ({ env: viteConfig.env })), + + // PWA only for production builds (faster dev) + VitePWA({ + apply: "build", + injectRegister: "auto", + registerType: "prompt", + workbox: { + navigateFallbackDenylist: [/^\/api\//] // prevent caching API routes + }, + manifest: { + short_name: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "ImEX Online", + rome: "Rome Online" + }), + name: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "ImEX Online", + rome: "Rome Online" + }), + description: "The ultimate bodyshop management system.", + icons: [ + { + src: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "favicon.png", + rome: "ro-favicon.png" + }), + sizes: "64x64 32x32 24x24 16x16", + type: "image/x-icon" + }, + { + src: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "logo192.png", + rome: "logo192.png" + }), + type: "image/png", + sizes: "192x192" + }, + { + src: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "logo512.png", + rome: "ro-favicon.png" + }), + type: "image/png", + sizes: "512x512" } - }] - ] - } - }), - eslint(), - - // Sentry only for production builds (no dev overhead) - sentryVitePlugin({ - apply: "build", - org: "imex", - reactComponentAnnotation: { enabled: true }, - release: { - name: `${process.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${commitHash}`.trim() - }, - project: InstanceRenderManager({ - instance: process.env.VITE_APP_INSTANCE, - imex: "imexonline", - rome: "rome-online" - }) - }) - ], - - define: { - APP_VERSION: JSON.stringify(process.env.npm_package_version), - __COMMIT_HASH__: JSON.stringify(commitHash) - }, - - server: { - host: true, - port: 3000, - open: true, - proxy: { - "/ws": { - target: "ws://localhost:4000", - secure: false, - ws: true - }, - "/wss": { - target: "ws://localhost:4000", - secure: false, - ws: true - }, - "/api": { - target: "http://localhost:4000", - changeOrigin: true, - secure: false, - ws: false, - rewrite: (path) => { - const replacedValue = path.replace(/^\/api/, ""); - logger.info( - `${chalk.grey.bold(getFormattedTimestamp())} ${chalk.cyan.bold("[vite]")} ${chalk.green.bold("[API]")} ${chalk.blue(replacedValue)}` - ); - return replacedValue; - } - } - }, - https: { - key: await fsPromises.readFile("../certs/key.pem"), - cert: await fsPromises.readFile("../certs/cert.pem") - } - }, - - preview: { - port: 6000, - host: true, - open: true, - https: { - key: await fsPromises.readFile("../certs/key.pem"), - cert: await fsPromises.readFile("../certs/cert.pem") - }, - proxy: { - "/ws": { - target: "ws://localhost:4000", - rewriteWsOrigin: true, - secure: false, - ws: true - }, - "/wss": { - target: "ws://localhost:4000", - rewriteWsOrigin: true, - secure: false, - ws: true - }, - "/api": { - target: "http://localhost:4000", - changeOrigin: true, - secure: false, - ws: false - } - } - }, - - build: { - sourcemap: true, - - rollupOptions: { - output: { - manualChunks: { - antd: ["antd"], - "react-redux": ["react-redux"], - redux: ["redux"], - lodash: ["lodash"], - "@sentry/react": ["@sentry/react"], - "@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"], - logrocket: ["logrocket"], - firebase: [ - "@firebase/analytics", - "@firebase/app", - "@firebase/firestore", - "@firebase/auth", - "@firebase/messaging" ], - markerjs2: ["markerjs2"], - "@apollo/client": ["@apollo/client"], - "libphonenumber-js": ["libphonenumber-js"], - recharts: ["recharts"] + theme_color: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "#1890ff", + rome: "#fff" + }), + background_color: "#fff", + gcm_sender_id: "103953800507" + } + }), + + react( + enableReactCompiler + ? { + babel: { + plugins: [ + [ + "babel-plugin-react-compiler", + { + // Exclude third-party drag-and-drop library from compilation + sources: (filename) => { + return !filename.includes("trello-board/dnd"); + } + } + ] + ] + } + } + : undefined + ), + + eslint(), + + // Sentry only for production builds (no dev overhead) + sentryVitePlugin({ + apply: "build", + org: "imex", + reactComponentAnnotation: { enabled: true }, + release: { + name: `${process.env.VITE_APP_IS_TEST ? "test" : "production"}-${currentDatePST}-${commitHash}`.trim() + }, + project: InstanceRenderManager({ + instance: process.env.VITE_APP_INSTANCE, + imex: "imexonline", + rome: "rome-online" + }) + }) + ], + + define: { + APP_VERSION: JSON.stringify(process.env.npm_package_version), + __COMMIT_HASH__: JSON.stringify(commitHash) + }, + + server: { + host: true, + port: 3000, + open: true, + proxy: { + "/ws": { + target: "ws://localhost:4000", + secure: false, + ws: true + }, + "/wss": { + target: "ws://localhost:4000", + secure: false, + ws: true + }, + "/api": { + target: "http://localhost:4000", + changeOrigin: true, + secure: false, + ws: false, + rewrite: (path) => { + const replacedValue = path.replace(/^\/api/, ""); + logger.info( + `${chalk.grey.bold(getFormattedTimestamp())} ${chalk.cyan.bold("[vite]")} ${chalk.green.bold("[API]")} ${chalk.blue(replacedValue)}` + ); + return replacedValue; + } + } + }, + https: httpsCerts + }, + + preview: { + port: 6000, + host: true, + open: true, + https: httpsCerts, + proxy: { + "/ws": { + target: "ws://localhost:4000", + rewriteWsOrigin: true, + secure: false, + ws: true + }, + "/wss": { + target: "ws://localhost:4000", + rewriteWsOrigin: true, + secure: false, + ws: true + }, + "/api": { + target: "http://localhost:4000", + changeOrigin: true, + secure: false, + ws: false } } }, - cssMinify: "lightningcss" - }, + build: { + sourcemap: true, - // Strip console/debugger in prod to shrink bundles - esbuild: { - //drop: ["console", "debugger"] - }, + rollupOptions: { + output: { + manualChunks: { + antd: ["antd"], + "react-redux": ["react-redux"], + redux: ["redux"], + lodash: ["lodash"], + "@sentry/react": ["@sentry/react"], + "@splitsoftware/splitio-react": ["@splitsoftware/splitio-react"], + logrocket: ["logrocket"], + firebase: [ + "@firebase/analytics", + "@firebase/app", + "@firebase/firestore", + "@firebase/auth", + "@firebase/messaging" + ], + markerjs2: ["markerjs2"], + "@apollo/client": ["@apollo/client"], + "libphonenumber-js": ["libphonenumber-js"], + recharts: ["recharts"] + } + } + }, - optimizeDeps: { - include: [ - "react", - "react-dom", - "antd", - "lodash", - "@sentry/react", - "@apollo/client", - "@reduxjs/toolkit", - "axios", - "react-router-dom", - "dayjs", - "redux", - "react-redux", - "@firebase/app", - "@firebase/analytics", - "@firebase/firestore", - "@firebase/auth", - "@firebase/messaging", - "@firebase/util" - ], - esbuildOptions: { - loader: { ".jsx": "jsx", ".tsx": "tsx" } - } - }, - - css: { - transformer: "lightningcss", - lightningcss: { - targets: lightningCssTargets + cssMinify: "lightningcss" }, - preprocessorOptions: { - scss: { quietDeps: true } + + // Strip console/debugger in prod to shrink bundles + esbuild: { + //drop: ["console", "debugger"] + }, + + optimizeDeps: { + include: [ + "react", + "react-dom", + "antd", + "lodash", + "@sentry/react", + "@apollo/client", + "@reduxjs/toolkit", + "axios", + "react-router-dom", + "dayjs", + "redux", + "react-redux", + "@firebase/app", + "@firebase/analytics", + "@firebase/firestore", + "@firebase/auth", + "@firebase/messaging", + "@firebase/util" + ], + esbuildOptions: { + loader: { ".jsx": "jsx", ".tsx": "tsx" } + } + }, + + css: { + transformer: "lightningcss", + lightningcss: { + targets: lightningCssTargets + }, + preprocessorOptions: { + scss: { quietDeps: true } + } } - } + }; });