diff --git a/bodyshop_translations.babel b/bodyshop_translations.babel index 8969c93eb..399953905 100644 --- a/bodyshop_translations.babel +++ b/bodyshop_translations.babel @@ -12791,27 +12791,6 @@ - - allow_text_message - false - - - - - - en-US - false - - - es-MX - false - - - fr-CA - false - - - checklist false @@ -42614,27 +42593,6 @@ - - allow_text_message - false - - - - - - en-US - false - - - es-MX - false - - - fr-CA - false - - - name false diff --git a/client/package-lock.json b/client/package-lock.json index d0cd3d682..42436e972 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13,19 +13,19 @@ "@apollo/client": "^3.13.6", "@emotion/is-prop-valid": "^1.3.1", "@fingerprintjs/fingerprintjs": "^4.6.1", - "@firebase/analytics": "^0.10.13", - "@firebase/app": "^0.12.1", - "@firebase/auth": "^1.10.2", - "@firebase/firestore": "^4.7.12", - "@firebase/messaging": "^0.12.18", + "@firebase/analytics": "^0.10.16", + "@firebase/app": "^0.13.0", + "@firebase/auth": "^1.10.6", + "@firebase/firestore": "^4.7.16", + "@firebase/messaging": "^0.12.21", "@jsreport/browser-client": "^3.1.0", - "@reduxjs/toolkit": "^2.8.1", - "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.18.0", - "@sentry/vite-plugin": "^3.4.0", + "@reduxjs/toolkit": "^2.8.2", + "@sentry/cli": "^2.46.0", + "@sentry/react": "^9.23.0", + "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", - "antd": "^5.25.1", + "antd": "^5.25.3", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^4.3.0", "autosize": "^6.0.1", @@ -48,8 +48,9 @@ "memoize-one": "^6.0.0", "normalize-url": "^8.0.1", "object-hash": "^3.0.0", + "phone": "^3.1.59", "prop-types": "^15.8.1", - "query-string": "^9.1.2", + "query-string": "^9.2.0", "raf-schd": "^4.0.3", "react": "^18.3.1", "react-big-calendar": "^1.18.0", @@ -59,7 +60,7 @@ "react-drag-listview": "^2.0.0", "react-grid-gallery": "^1.0.1", "react-grid-layout": "1.3.4", - "react-i18next": "^15.4.1", + "react-i18next": "^15.5.2", "react-icons": "^5.5.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", @@ -78,7 +79,7 @@ "redux-saga": "^1.3.0", "redux-state-sync": "^3.1.4", "reselect": "^5.1.1", - "sass": "^1.88.0", + "sass": "^1.89.0", "socket.io-client": "^4.8.1", "styled-components": "^6.1.18", "subscriptions-transport-ws": "^0.11.0", @@ -90,16 +91,16 @@ "@ant-design/icons": "^6.0.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.27.1", - "@dotenvx/dotenvx": "^1.44.0", + "@dotenvx/dotenvx": "^1.44.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", - "@eslint/js": "^9.26.0", + "@eslint/js": "^9.27.0", "@playwright/test": "^1.51.1", - "@sentry/webpack-plugin": "^3.4.0", + "@sentry/webpack-plugin": "^3.5.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^4.5.0", "browserslist": "^4.24.5", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.4.1", @@ -108,7 +109,7 @@ "eslint-plugin-react": "^7.37.5", "globals": "^15.15.0", "jsdom": "^26.0.0", - "memfs": "^4.17.1", + "memfs": "^4.17.2", "os-browserify": "^0.3.0", "playwright": "^1.51.1", "react-error-overlay": "^6.1.0", @@ -120,7 +121,7 @@ "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-pwa": "^1.0.0", "vite-plugin-style-import": "^2.0.0", - "vitest": "^3.1.3", + "vitest": "^3.1.4", "workbox-window": "^7.3.0" }, "engines": { @@ -2587,9 +2588,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.44.0.tgz", - "integrity": "sha512-18Aa+7KP/L2Kj9lxmT4EJZnsCq/xGIHgzU26rdzsKMhjpeT3YY+qin/dNAnIaVHPZnee7kXpZL55M9htd30r7Q==", + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.44.1.tgz", + "integrity": "sha512-j1QImCqf/XJmhIjC1OPpgiZV9g370HG9MNT9s/CDwCKsoYzNCPEKK+GfsidahJx7yIlBbm+4dPLlGec+bKn7oA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2912,13 +2913,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.26.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", - "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "version": "9.27.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", + "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@fingerprintjs/fingerprintjs": { @@ -2931,15 +2935,15 @@ } }, "node_modules/@firebase/analytics": { - "version": "0.10.13", - "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.13.tgz", - "integrity": "sha512-X+6wMOPgA9l0AeeMdMcMfaCP4XKPvrhx55MGuMrfHvUrOvFKldpzBum7KkoGJMoexKmqmKP+mCmJMY9Fb8K6Hw==", + "version": "0.10.16", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.16.tgz", + "integrity": "sha512-cMtp19He7Fd6uaj/nDEul+8JwvJsN8aRSJyuA1QN3QrKvfDDp+efjVurJO61sJpkVftw9O9nNMdhFbRcTmTfRQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", - "@firebase/installations": "0.6.14", + "@firebase/component": "0.6.17", + "@firebase/installations": "0.6.17", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "tslib": "^2.1.0" }, "peerDependencies": { @@ -2947,14 +2951,14 @@ } }, "node_modules/@firebase/app": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.12.1.tgz", - "integrity": "sha512-ASExOlmmjRMdwOQ65Oj6R9JBqa7iiT1/LgZjtbU7FqxoJZNWHrt39NJ/z2bjyYDdAHX8jkY7muFqzahScCXgfA==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.0.tgz", + "integrity": "sha512-Vj3MST245nq+V5UmmfEkB3isIgPouyUr8yGJlFeL9Trg/umG5ogAvrjAYvQ8gV7daKDoQSRnJKWI2JFpQqRsuQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", + "@firebase/component": "0.6.17", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -2963,14 +2967,14 @@ } }, "node_modules/@firebase/auth": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.2.tgz", - "integrity": "sha512-HHudcj3CJyXpoMKslNOVHGSNJdAUjvy5xBA/G/uPb32QFqvx5F3EW9RDYvve2IHEN7Vpc1QTkk/28J32x83UGA==", + "version": "1.10.6", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.6.tgz", + "integrity": "sha512-cFbo2FymQltog4atI9cKTO6CxKxS0dOMXslTQrlNZRH7qhDG44/d7QeI6GXLweFZtrnlecf52ESnNz1DU6ek8w==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", + "@firebase/component": "0.6.17", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "tslib": "^2.1.0" }, "engines": { @@ -2987,12 +2991,12 @@ } }, "node_modules/@firebase/component": { - "version": "0.6.14", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.14.tgz", - "integrity": "sha512-kf/zAT8GQJ9nYoHuj0mv7twp1QzifKYrO+GsmsVHHM+Hi9KkmI7E3B3J0CtihHpb34vinl4gbJrYJ2p2wfvc9A==", + "version": "0.6.17", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.17.tgz", + "integrity": "sha512-M6DOg7OySrKEFS8kxA3MU5/xc37fiOpKPMz6cTsMUcsuKB6CiZxxNAvgFta8HGRgEpZbi8WjGIj6Uf+TpOhyzg==", "license": "Apache-2.0", "dependencies": { - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "tslib": "^2.1.0" }, "engines": { @@ -3000,14 +3004,14 @@ } }, "node_modules/@firebase/firestore": { - "version": "4.7.12", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.12.tgz", - "integrity": "sha512-50KRdSp8xA7+G0wfWxlnCoEN951mt8BVdLMxeP57Rehj2DqIb41q6Fc6JH0dfQ4TlMqWua1YfVY1jPEAaHVF9w==", + "version": "4.7.16", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.16.tgz", + "integrity": "sha512-5OpvlwYVUTLEnqewOlXmtIpH8t2ISlZHDW0NDbKROM2D0ATMqFkMHdvl+/wz9zOAcb8GMQYlhCihOnVAliUbpQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", + "@firebase/component": "0.6.17", "@firebase/logger": "0.4.4", - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "@firebase/webchannel-wrapper": "1.0.3", "@grpc/grpc-js": "~1.9.0", "@grpc/proto-loader": "^0.7.8", @@ -3021,13 +3025,13 @@ } }, "node_modules/@firebase/installations": { - "version": "0.6.14", - "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.14.tgz", - "integrity": "sha512-uE837g9+sv6PfjWPgOfG3JtjZ+hJ7KBHO4UVenVsvhzgOxFkvLjO/bgE7fyvsaD3fOHSXunx3adRIg4eUEMPyA==", + "version": "0.6.17", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.17.tgz", + "integrity": "sha512-zfhqCNJZRe12KyADtRrtOj+SeSbD1H/K8J24oQAJVv/u02eQajEGlhZtcx9Qk7vhGWF5z9dvIygVDYqLL4o1XQ==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", - "@firebase/util": "1.11.1", + "@firebase/component": "0.6.17", + "@firebase/util": "1.12.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -3048,15 +3052,15 @@ } }, "node_modules/@firebase/messaging": { - "version": "0.12.18", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.18.tgz", - "integrity": "sha512-2MGhUGoCZloB7ysoYzG/T2nnRmHYLT+AcqYouZuD6APabpkDhF8lHsmSQq4MFSlXhI3DKFOXxjuvbY8ec4C2JQ==", + "version": "0.12.21", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.21.tgz", + "integrity": "sha512-bYJ2Evj167Z+lJ1ach6UglXz5dUKY1zrJZd15GagBUJSR7d9KfiM1W8dsyL0lDxcmhmA/sLaBYAAhF1uilwN0g==", "license": "Apache-2.0", "dependencies": { - "@firebase/component": "0.6.14", - "@firebase/installations": "0.6.14", + "@firebase/component": "0.6.17", + "@firebase/installations": "0.6.17", "@firebase/messaging-interop-types": "0.2.3", - "@firebase/util": "1.11.1", + "@firebase/util": "1.12.0", "idb": "7.1.1", "tslib": "^2.1.0" }, @@ -3071,9 +3075,9 @@ "license": "Apache-2.0" }, "node_modules/@firebase/util": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.1.tgz", - "integrity": "sha512-RXg4WE8C2LUrvoV/TMGRTu223zZf9Dq9MR8yHZio9nF9TpLnpCPURw9VWWB2WATDl6HfIdWfl2x2SJYtHkN4hw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.12.0.tgz", + "integrity": "sha512-Z4rK23xBCwgKDqmzGVMef+Vb4xso2j5Q8OG0vVL4m4fA5ZjPMYQazu8OJJC3vtQRC3SQ/Pgx/6TPNVsCd70QRw==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3836,9 +3840,9 @@ "license": "MIT" }, "node_modules/@reduxjs/toolkit": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.1.tgz", - "integrity": "sha512-GLjHS13LiBdiuxSJvfWs3+Cx5yt97mCbuVlDteTusS6VRksPhoWviO8L1e3Re1G94m6lkw/l4pjEEyyNaGf19g==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -3882,6 +3886,13 @@ "react": ">=16.8.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", + "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -4458,88 +4469,88 @@ "license": "MIT" }, "node_modules/@sentry-internal/browser-utils": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.18.0.tgz", - "integrity": "sha512-TwSlmgYpHhe55JpOcVApkM0XcXZh1/cYuEPKPFgeaaPD8BrQrLJJvwKxnonSWXOhdnkJxi4GgK7j7mw57PS4aA==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.23.0.tgz", + "integrity": "sha512-hyN2Q6mh7ggw8sDVHeRyWz5LR6gjvf8zHSzQnMaF7QkeSyaeGM/SVSL4ODwqR9TRH7U2ku6nZFMbKhaBPV+Hfg==", "license": "MIT", "dependencies": { - "@sentry/core": "9.18.0" + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.18.0.tgz", - "integrity": "sha512-QlrB8oQK+5bfhbgK6yHF6rLwLNJ9XuGblTc51yVkm4d4jn4W/HDyaNqMfQF+JXdTiFatl8oz2xdKR8kGK8kXyg==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.23.0.tgz", + "integrity": "sha512-Xf+KqV69TBiPo1gk2EsU6O/dumuTMxWOF52uVWJddQYI3pQTU5DqSeoZ5AY76bIIhV9n6AEFDGqNPXmuj4Acrw==", "license": "MIT", "dependencies": { - "@sentry/core": "9.18.0" + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.18.0.tgz", - "integrity": "sha512-2A32FFwrlZtdpBruvpcLEfucu6BpyqOk3F4Bo5smM/5q7u0pa7q5d9FSY5l3nwKEAFAoLGv3hcCb+8wxMm50xA==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.23.0.tgz", + "integrity": "sha512-0/q15tvSboaK7/05BFQhs71bqgHKejJoDJgXmH0lySqgsRh/S18867ZxQNiuYhuVt337h07u1QaCyjnNJKHfuA==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.18.0", - "@sentry/core": "9.18.0" + "@sentry-internal/browser-utils": "9.23.0", + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.18.0.tgz", - "integrity": "sha512-3DEyQLmHcYgcwJ8n8eMhI6bhhawPuMc2xTT+Az8gXMqCO/X9ZACpipAmhXFjYP9Ptl+w0Vh3nllJw+gXc/DOsg==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.23.0.tgz", + "integrity": "sha512-cYlw5svJjyPequm0PJjFGLpee86L1NieONEHlujOXkIG6IEriiorMm+8bNpGsHRuyvg41B+4P/YmcQAGtEGxXg==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "9.18.0", - "@sentry/core": "9.18.0" + "@sentry-internal/replay": "9.23.0", + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz", - "integrity": "sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz", + "integrity": "sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==", "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/@sentry/browser": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.18.0.tgz", - "integrity": "sha512-0SWfp4J2+mH4lZOcHfyIwt9VoGD7yCGQE1cm0BPcLwKnrVQeXHtUXNYNy8HTHSjTGyoFDhEAYelE/tdA3OLcWQ==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.23.0.tgz", + "integrity": "sha512-QRkNxWys8e088260vByztoTsEVZ0W6v/XnZfKT6wkPPGn0aFeOrg/xjgxfI8D5huqZCxT28Cf23akOOly4FXjg==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "9.18.0", - "@sentry-internal/feedback": "9.18.0", - "@sentry-internal/replay": "9.18.0", - "@sentry-internal/replay-canvas": "9.18.0", - "@sentry/core": "9.18.0" + "@sentry-internal/browser-utils": "9.23.0", + "@sentry-internal/feedback": "9.23.0", + "@sentry-internal/replay": "9.23.0", + "@sentry-internal/replay-canvas": "9.23.0", + "@sentry/core": "9.23.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry/bundler-plugin-core": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz", - "integrity": "sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz", + "integrity": "sha512-zDzPrhJqAAy2VzV4g540qAZH4qxzisstK2+NIJPZUUKztWRWUV2cMHsyUtdctYgloGkLyGpZJBE3RE6dmP/xqQ==", "license": "MIT", "dependencies": { "@babel/core": "^7.18.5", - "@sentry/babel-plugin-component-annotate": "3.4.0", + "@sentry/babel-plugin-component-annotate": "3.5.0", "@sentry/cli": "2.42.2", "dotenv": "^16.3.1", "find-up": "^5.0.0", @@ -4717,9 +4728,9 @@ } }, "node_modules/@sentry/cli": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.45.0.tgz", - "integrity": "sha512-4sWu7zgzgHAjIxIjXUA/66qgeEf5ZOlloO+/JaGD5qXNSW0G7KMTR6iYjReNKMgdBCTH6bUUt9qiuA+Ex9Masw==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.46.0.tgz", + "integrity": "sha512-nqoPl7UCr446QFkylrsRrUXF51x8Z9dGquyf4jaQU+OzbOJMqclnYEvU6iwbwvaw3tu/2DnoZE/Og+Nq1h63sA==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -4736,20 +4747,20 @@ "node": ">= 10" }, "optionalDependencies": { - "@sentry/cli-darwin": "2.45.0", - "@sentry/cli-linux-arm": "2.45.0", - "@sentry/cli-linux-arm64": "2.45.0", - "@sentry/cli-linux-i686": "2.45.0", - "@sentry/cli-linux-x64": "2.45.0", - "@sentry/cli-win32-arm64": "2.45.0", - "@sentry/cli-win32-i686": "2.45.0", - "@sentry/cli-win32-x64": "2.45.0" + "@sentry/cli-darwin": "2.46.0", + "@sentry/cli-linux-arm": "2.46.0", + "@sentry/cli-linux-arm64": "2.46.0", + "@sentry/cli-linux-i686": "2.46.0", + "@sentry/cli-linux-x64": "2.46.0", + "@sentry/cli-win32-arm64": "2.46.0", + "@sentry/cli-win32-i686": "2.46.0", + "@sentry/cli-win32-x64": "2.46.0" } }, "node_modules/@sentry/cli-darwin": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.45.0.tgz", - "integrity": "sha512-p4Uxfv/L2fQdP3/wYnKVVz9gzZJf/1Xp9D+6raax/3Bu5y87yHYUqcdt98y/VAXQD4ofp2QgmhGUVPofvQNZmg==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.46.0.tgz", + "integrity": "sha512-5Ll+e5KAdIk9OYiZO8aifMBRNWmNyPjSqdjaHlBC1Qfh7pE3b1zyzoHlsUazG0bv0sNrSGea8e7kF5wIO1hvyg==", "license": "BSD-3-Clause", "optional": true, "os": [ @@ -4760,9 +4771,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.45.0.tgz", - "integrity": "sha512-6sEskFLlFKJ+e0MOYgIclBTUX5jYMyYhHIxXahEkI/4vx6JO0uvpyRAkUJRpJkRh/lPog0FM+tbP3so+VxB2qQ==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.46.0.tgz", + "integrity": "sha512-WRrLNq/TEX/TNJkGqq6Ad0tGyapd5dwlxtsPbVBrIdryuL1mA7VCBoaHBr3kcwJLsgBHFH0lmkMee2ubNZZdkg==", "cpu": [ "arm" ], @@ -4770,16 +4781,17 @@ "optional": true, "os": [ "linux", - "freebsd" + "freebsd", + "android" ], "engines": { "node": ">=10" } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.45.0.tgz", - "integrity": "sha512-gUcLoEjzg7AIc4QQGEZwRHri+EHf3Gcms9zAR1VHiNF3/C/jL4WeDPJF2YiWAQt6EtH84tHiyhw1Ab/R8XFClg==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.46.0.tgz", + "integrity": "sha512-OEJN8yAjI9y5B4telyqzu27Hi3+S4T8VxZCqJz1+z2Mp0Q/MZ622AahVPpcrVq/5bxrnlZR16+lKh8L1QwNFPg==", "cpu": [ "arm64" ], @@ -4787,16 +4799,17 @@ "optional": true, "os": [ "linux", - "freebsd" + "freebsd", + "android" ], "engines": { "node": ">=10" } }, "node_modules/@sentry/cli-linux-i686": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.45.0.tgz", - "integrity": "sha512-VmmOaEAzSW23YdGNdy/+oQjCNAMY+HmOGA77A25/ep/9AV7PQB6FI7xO5Y1PVvlkxZFJ23e373njSsEeg4uDZw==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.46.0.tgz", + "integrity": "sha512-xko3/BVa4LX8EmRxVOCipV+PwfcK5Xs8lP6lgF+7NeuAHMNL4DqF6iV9rrN8gkGUHCUI9RXSve37uuZnFy55+Q==", "cpu": [ "x86", "ia32" @@ -4805,16 +4818,17 @@ "optional": true, "os": [ "linux", - "freebsd" + "freebsd", + "android" ], "engines": { "node": ">=10" } }, "node_modules/@sentry/cli-linux-x64": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.45.0.tgz", - "integrity": "sha512-a0Oj68mrb25a0WjX/ShZ6AAd4PPiuLcgyzQr7bl2+DvYxIOajwkGbR+CZFEhOVZcfhTnixKy/qIXEzApEPHPQg==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.46.0.tgz", + "integrity": "sha512-hJ1g5UEboYcOuRia96LxjJ0jhnmk8EWLDvlGnXLnYHkwy3ree/L7sNgdp/QsY8Z4j2PGO5f22Va+UDhSjhzlfQ==", "cpu": [ "x64" ], @@ -4822,16 +4836,17 @@ "optional": true, "os": [ "linux", - "freebsd" + "freebsd", + "android" ], "engines": { "node": ">=10" } }, "node_modules/@sentry/cli-win32-arm64": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.45.0.tgz", - "integrity": "sha512-vn+CwS4p+52pQSLNPoi20ZOrQmv01ZgAmuMnjkh1oUZfTyBAwWLrAh6Cy4cztcN8DfL5dOWKQBo8DBKURE4ttg==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.46.0.tgz", + "integrity": "sha512-mN7cpPoCv2VExFRGHt+IoK11yx4pM4ADZQGEso5BAUZ5duViXB2WrAXCLd8DrwMnP0OE978a7N8OtzsFqjkbNA==", "cpu": [ "arm64" ], @@ -4845,9 +4860,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.45.0.tgz", - "integrity": "sha512-8mMoDdlwxtcdNIMtteMK7dbi7054jak8wKSHJ5yzMw8UmWxC5thc/gXBc1uPduiaI56VjoJV+phWHBKCD+6I4w==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.46.0.tgz", + "integrity": "sha512-6F73AUE3lm71BISUO19OmlnkFD5WVe4/wA1YivtLZTc1RU3eUYJLYxhDfaH3P77+ycDppQ2yCgemLRaA4A8mNQ==", "cpu": [ "x86", "ia32" @@ -4862,9 +4877,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "2.45.0", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.45.0.tgz", - "integrity": "sha512-ZvK9cIqFaq7vZ0jkHJ/xh5au6902Dr+AUxSk6L6vCL7JCe2p93KGL/4d8VFB5PD/P7Y9b+105G/e0QIFKzpeOw==", + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.46.0.tgz", + "integrity": "sha512-yuGVcfepnNL84LGA0GjHzdMIcOzMe0bjPhq/rwPsPN+zu11N+nPR2wV2Bum4U0eQdqYH3iAlMdL5/BEQfuLJww==", "cpu": [ "x64" ], @@ -4899,22 +4914,22 @@ } }, "node_modules/@sentry/core": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.18.0.tgz", - "integrity": "sha512-kRVH8BqMiaU2FTHYa68zNlAloS43jl4XtIEHkLKVH/7gUtwRmM4Gqj8P7RTrZdO1Lo7ksYnGj+AG05Z09CRbOw==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.23.0.tgz", + "integrity": "sha512-9846pn/BvASGgl7WsnKY4xry98WreP9ToeLfCQTQOf+pNfD/qNPn1/0xPInGni3LVMAXRtfHHMPm2Ghz255N7A==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.18.0.tgz", - "integrity": "sha512-1cCLYZrZ2gu6H8eE83DC47kLf+pzD1Rim3dDoOEvwt1F5cD3K/DBeIhoHZaXqBeQxuVyHXOOLXSAC/CIuas5Aw==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.23.0.tgz", + "integrity": "sha512-2J/oOx8jd7Jr2koYIe5IcJyStHBXpjkQnxawo54Zyyvzc96MftyM2Dv5TeYdz7fChU1NIXw7BVbEpkQ9XEQlqg==", "license": "MIT", "dependencies": { - "@sentry/browser": "9.18.0", - "@sentry/core": "9.18.0", + "@sentry/browser": "9.23.0", + "@sentry/core": "9.23.0", "hoist-non-react-statics": "^3.3.2" }, "engines": { @@ -4925,12 +4940,12 @@ } }, "node_modules/@sentry/vite-plugin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.4.0.tgz", - "integrity": "sha512-pUFBGrKsHuc8K6A7B1wU2nx65n9aIzvTlcHX9yZ1qvjEO0cZFih0JCwu1Fcav/yrtT9RMN44L/ugu/kMBHQhjQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.5.0.tgz", + "integrity": "sha512-jUnpTdpicG8wefamw7eNo2uO+Q3KCbOAiF76xH4gfNHSW6TN2hBfOtmLu7J+ive4c0Al3+NEHz19bIPR0lkwWg==", "license": "MIT", "dependencies": { - "@sentry/bundler-plugin-core": "3.4.0", + "@sentry/bundler-plugin-core": "3.5.0", "unplugin": "1.0.1" }, "engines": { @@ -4938,13 +4953,13 @@ } }, "node_modules/@sentry/webpack-plugin": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.4.0.tgz", - "integrity": "sha512-i+nAxxniJV5ovijojjTF5n+Yj08Xk8my+vm8+oo0C0I7xcnI2gOKft6B0sJOq01CNbo85X5m/3/edL0PKoWE9w==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.5.0.tgz", + "integrity": "sha512-xvclj0QY2HyU7uJLzOlHSrZQBDwfnGKJxp8mmlU4L7CwmK+8xMCqlO7tYZoqE4K/wU3c2xpXql70x8qmvNMxzQ==", "dev": true, "license": "MIT", "dependencies": { - "@sentry/bundler-plugin-core": "3.4.0", + "@sentry/bundler-plugin-core": "3.5.0", "unplugin": "1.0.1", "uuid": "^9.0.0" }, @@ -5790,15 +5805,16 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz", + "integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.26.10", "@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@rolldown/pluginutils": "1.0.0-beta.9", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -5810,14 +5826,14 @@ } }, "node_modules/@vitest/expect": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz", - "integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz", + "integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -5826,13 +5842,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz", - "integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz", + "integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.1.3", + "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -5873,9 +5889,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz", - "integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz", + "integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==", "dev": true, "license": "MIT", "dependencies": { @@ -5886,13 +5902,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz", - "integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz", + "integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.1.3", + "@vitest/utils": "3.1.4", "pathe": "^2.0.3" }, "funding": { @@ -5907,13 +5923,13 @@ "license": "MIT" }, "node_modules/@vitest/snapshot": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz", - "integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz", + "integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -5939,9 +5955,9 @@ "license": "MIT" }, "node_modules/@vitest/spy": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz", - "integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz", + "integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==", "dev": true, "license": "MIT", "dependencies": { @@ -5952,13 +5968,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz", - "integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.1.3", + "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, @@ -6088,12 +6104,12 @@ } }, "node_modules/antd": { - "version": "5.25.1", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.25.1.tgz", - "integrity": "sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==", + "version": "5.25.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.25.3.tgz", + "integrity": "sha512-tBBcAFRjmWM3sitxrL/FEbQL+MTQntYY5bGa5c1ZZZHXWCynkhS3Ch/gy25mGMUY1M/9Uw3pH029v/RGht1x3w==", "license": "MIT", "dependencies": { - "@ant-design/colors": "^7.2.0", + "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", @@ -6128,11 +6144,11 @@ "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", - "rc-select": "~14.16.7", + "rc-select": "~14.16.8", "rc-slider": "~11.1.8", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", - "rc-table": "~7.50.4", + "rc-table": "~7.50.5", "rc-tabs": "~15.6.1", "rc-textarea": "~1.10.0", "rc-tooltip": "~6.4.0", @@ -6153,9 +6169,9 @@ } }, "node_modules/antd/node_modules/@ant-design/colors": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz", - "integrity": "sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==", + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz", + "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==", "license": "MIT", "dependencies": { "@ant-design/fast-color": "^2.0.6" @@ -11833,9 +11849,9 @@ } }, "node_modules/memfs": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.1.tgz", - "integrity": "sha512-thuTRd7F4m4dReCIy7vv4eNYnU6XI/tHMLSMMHLiortw/Y0QxqKtinG523U2aerzwYWGi606oBP4oMPy4+edag==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", + "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -13263,6 +13279,15 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT" }, + "node_modules/phone": { + "version": "3.1.59", + "resolved": "https://registry.npmjs.org/phone/-/phone-3.1.59.tgz", + "integrity": "sha512-CUv22jw0Zgrb/h7v3sEd262zJXS/66h7zyCCRIynx+2FswAJuuFsXsJkIxMUT4UcosKxDx1bJwdZeGnDELLsCw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -13569,9 +13594,9 @@ } }, "node_modules/query-string": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.2.tgz", - "integrity": "sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.2.0.tgz", + "integrity": "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ==", "license": "MIT", "dependencies": { "decode-uri-component": "^0.4.1", @@ -14024,9 +14049,9 @@ } }, "node_modules/rc-select": { - "version": "14.16.7", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.7.tgz", - "integrity": "sha512-lT9kO5gFHQdJzu9a0btcOtNaJHkhenSl8H5mcpgXN9VIMXP59rnkpbdHmPrteixWs1D5zFOTyoTYX3b7joADIQ==", + "version": "14.16.8", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz", + "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -14097,9 +14122,9 @@ } }, "node_modules/rc-table": { - "version": "7.50.4", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.4.tgz", - "integrity": "sha512-Y+YuncnQqoS5e7yHvfvlv8BmCvwDYDX/2VixTBEhkMDk9itS9aBINp4nhzXFKiBP/frG4w0pS9d9Rgisl0T1Bw==", + "version": "7.50.5", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.5.tgz", + "integrity": "sha512-FDZu8aolhSYd3v9KOc3lZOVAU77wmRRu44R0Wfb8Oj1dXRUsloFaXMSl6f7yuWZUxArJTli7k8TEOX2mvhDl4A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -14400,9 +14425,9 @@ } }, "node_modules/react-i18next": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", - "integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", + "version": "15.5.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.2.tgz", + "integrity": "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.0", @@ -15375,9 +15400,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.88.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", - "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", + "version": "1.89.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz", + "integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -17533,9 +17558,9 @@ } }, "node_modules/vite-node": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz", - "integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz", + "integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==", "dev": true, "license": "MIT", "dependencies": { @@ -17731,19 +17756,19 @@ } }, "node_modules/vitest": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz", - "integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz", + "integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.1.3", - "@vitest/mocker": "3.1.3", - "@vitest/pretty-format": "^3.1.3", - "@vitest/runner": "3.1.3", - "@vitest/snapshot": "3.1.3", - "@vitest/spy": "3.1.3", - "@vitest/utils": "3.1.3", + "@vitest/expect": "3.1.4", + "@vitest/mocker": "3.1.4", + "@vitest/pretty-format": "^3.1.4", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "@vitest/spy": "3.1.4", + "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", @@ -17756,7 +17781,7 @@ "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.1.3", + "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "bin": { @@ -17772,8 +17797,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.1.3", - "@vitest/ui": "3.1.3", + "@vitest/browser": "3.1.4", + "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, diff --git a/client/package.json b/client/package.json index 1779b36c5..61af4372d 100644 --- a/client/package.json +++ b/client/package.json @@ -12,19 +12,19 @@ "@apollo/client": "^3.13.6", "@emotion/is-prop-valid": "^1.3.1", "@fingerprintjs/fingerprintjs": "^4.6.1", - "@firebase/analytics": "^0.10.13", - "@firebase/app": "^0.12.1", - "@firebase/auth": "^1.10.2", - "@firebase/firestore": "^4.7.12", - "@firebase/messaging": "^0.12.18", + "@firebase/analytics": "^0.10.16", + "@firebase/app": "^0.13.0", + "@firebase/auth": "^1.10.6", + "@firebase/firestore": "^4.7.16", + "@firebase/messaging": "^0.12.21", "@jsreport/browser-client": "^3.1.0", - "@reduxjs/toolkit": "^2.8.1", - "@sentry/cli": "^2.45.0", - "@sentry/react": "^9.18.0", - "@sentry/vite-plugin": "^3.4.0", + "@reduxjs/toolkit": "^2.8.2", + "@sentry/cli": "^2.46.0", + "@sentry/react": "^9.23.0", + "@sentry/vite-plugin": "^3.5.0", "@splitsoftware/splitio-react": "^2.1.1", "@tanem/react-nprogress": "^5.0.53", - "antd": "^5.25.1", + "antd": "^5.25.3", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^4.3.0", "autosize": "^6.0.1", @@ -47,8 +47,9 @@ "memoize-one": "^6.0.0", "normalize-url": "^8.0.1", "object-hash": "^3.0.0", + "phone": "^3.1.59", "prop-types": "^15.8.1", - "query-string": "^9.1.2", + "query-string": "^9.2.0", "raf-schd": "^4.0.3", "react": "^18.3.1", "react-big-calendar": "^1.18.0", @@ -58,7 +59,7 @@ "react-drag-listview": "^2.0.0", "react-grid-gallery": "^1.0.1", "react-grid-layout": "1.3.4", - "react-i18next": "^15.4.1", + "react-i18next": "^15.5.2", "react-icons": "^5.5.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", @@ -77,7 +78,7 @@ "redux-saga": "^1.3.0", "redux-state-sync": "^3.1.4", "reselect": "^5.1.1", - "sass": "^1.88.0", + "sass": "^1.89.0", "socket.io-client": "^4.8.1", "styled-components": "^6.1.18", "subscriptions-transport-ws": "^0.11.0", @@ -130,16 +131,16 @@ "@ant-design/icons": "^6.0.0", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.27.1", - "@dotenvx/dotenvx": "^1.44.0", + "@dotenvx/dotenvx": "^1.44.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", - "@eslint/js": "^9.26.0", + "@eslint/js": "^9.27.0", "@playwright/test": "^1.51.1", - "@sentry/webpack-plugin": "^3.4.0", + "@sentry/webpack-plugin": "^3.5.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^4.5.0", "browserslist": "^4.24.5", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.4.1", @@ -148,7 +149,7 @@ "eslint-plugin-react": "^7.37.5", "globals": "^15.15.0", "jsdom": "^26.0.0", - "memfs": "^4.17.1", + "memfs": "^4.17.2", "os-browserify": "^0.3.0", "playwright": "^1.51.1", "react-error-overlay": "^6.1.0", @@ -160,7 +161,7 @@ "vite-plugin-node-polyfills": "^0.23.0", "vite-plugin-pwa": "^1.0.0", "vite-plugin-style-import": "^2.0.0", - "vitest": "^3.1.3", + "vitest": "^3.1.4", "workbox-window": "^7.3.0" } } diff --git a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx index 6d296edd1..c48875f4c 100644 --- a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx +++ b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx @@ -3,6 +3,7 @@ import { Button, Form, InputNumber, Popover, Space } from "antd"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { logImEXEvent } from "../../firebase/firebase.utils"; + export default function CABCpvrtCalculator({ disabled, form }) { const [visibility, setVisibility] = useState(false); @@ -39,7 +40,7 @@ export default function CABCpvrtCalculator({ disabled, form }) { ); return ( - + diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index 9885b8551..b370b221c 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -34,16 +34,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) { SubscribeToTopicForFCMNotification(); - //Register WS handlers + // Register WebSocket handlers if (socket && socket.connected) { registerMessagingHandlers({ socket, client }); - } - return () => { - if (socket && socket.connected) { + return () => { unregisterMessagingHandlers({ socket }); - } - }; + }; + } }, [bodyshop, socket, t, client]); if (!bodyshop || !bodyshop.messagingservicesid) return <>; diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index f2d9d1ef9..88e0ac9df 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -202,8 +202,6 @@ export const registerMessagingHandlers = ({ socket, client }) => { text: message.text }; - // Add cases for other known message types as needed - default: // Log a warning for unhandled message types logLocal("handleMessageChanged - Unhandled message type", { type: message.type }); @@ -211,7 +209,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { } } - return messageRef; // Keep other messages unchanged + return messageRef; }); } } @@ -245,11 +243,8 @@ export const registerMessagingHandlers = ({ socket, client }) => { }); const updatedList = existingList?.conversations - ? [ - newConversation, - ...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates - ] - : [newConversation]; + ? [newConversation, ...existingList.conversations.filter((conv) => conv.id !== newConversation.id)] + : [newConversation]; // Prevent duplicates client.cache.writeQuery({ query: CONVERSATION_LIST_QUERY, @@ -403,6 +398,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { } break; + default: logLocal("handleConversationChanged - Unhandled type", { type }); client.cache.modify({ @@ -419,10 +415,95 @@ export const registerMessagingHandlers = ({ socket, client }) => { } }; + // Existing handler for phone number opt-out + const handlePhoneNumberOptedOut = async (data) => { + const { bodyshopid, phone_number } = data; + logLocal("handlePhoneNumberOptedOut - Start", data); + + try { + client.cache.modify({ + id: "ROOT_QUERY", + fields: { + phone_number_opt_out(existing = [], { readField }) { + const phoneNumberExists = existing.some( + (ref) => readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid + ); + + if (phoneNumberExists) { + logLocal("handlePhoneNumberOptedOut - Phone number already in cache", { phone_number, bodyshopid }); + return existing; + } + + const newOptOut = { + __typename: "phone_number_opt_out", + id: `temporary-${phone_number}-${Date.now()}`, + bodyshopid, + phone_number, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + }; + + return [...existing, newOptOut]; + } + }, + broadcast: true + }); + + client.cache.evict({ + id: "ROOT_QUERY", + fieldName: "phone_number_opt_out", + args: { bodyshopid, search: phone_number } + }); + client.cache.gc(); + + logLocal("handlePhoneNumberOptedOut - Cache updated successfully", data); + } catch (error) { + console.error("Error updating cache for phone number opt-out:", error); + logLocal("handlePhoneNumberOptedOut - Error", { error: error.message }); + } + }; + + // New handler for phone number opt-in + const handlePhoneNumberOptedIn = async (data) => { + const { bodyshopid, phone_number } = data; + logLocal("handlePhoneNumberOptedIn - Start", data); + + try { + // Update the Apollo cache for GET_PHONE_NUMBER_OPT_OUTS by removing the phone number + client.cache.modify({ + id: "ROOT_QUERY", + fields: { + phone_number_opt_out(existing = [], { readField }) { + // Filter out the phone number from the opt-out list + return existing.filter( + (ref) => !(readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid) + ); + } + }, + broadcast: true // Trigger UI updates + }); + + // Evict the cache entry to force a refetch on next query + client.cache.evict({ + id: "ROOT_QUERY", + fieldName: "phone_number_opt_out", + args: { bodyshopid, search: phone_number } + }); + client.cache.gc(); + + logLocal("handlePhoneNumberOptedIn - Cache updated successfully", data); + } catch (error) { + console.error("Error updating cache for phone number opt-in:", error); + logLocal("handlePhoneNumberOptedIn - Error", { error: error.message }); + } + }; + socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); socket.on("message-changed", handleMessageChanged); socket.on("conversation-changed", handleConversationChanged); + socket.on("phone-number-opted-out", handlePhoneNumberOptedOut); + socket.on("phone-number-opted-in", handlePhoneNumberOptedIn); }; export const unregisterMessagingHandlers = ({ socket }) => { @@ -431,4 +512,6 @@ export const unregisterMessagingHandlers = ({ socket }) => { socket.off("new-message-detailed"); socket.off("message-changed"); socket.off("conversation-changed"); + socket.off("phone-number-opted-out"); + socket.off("phone-number-opted-in"); }; diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx index 16d4c0bf1..e5fe4b9ca 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx +++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx @@ -1,5 +1,5 @@ -import { Badge, Card, List, Space, Tag } from "antd"; -import React, { useEffect, useState } from "react"; +import { Badge, Card, List, Space, Tag, Tooltip } from "antd"; +import { useEffect, useMemo, useState } from "react"; import { connect } from "react-redux"; import { Virtuoso } from "react-virtuoso"; import { createStructuredSelector } from "reselect"; @@ -9,36 +9,62 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import _ from "lodash"; +import { ExclamationCircleOutlined } from "@ant-design/icons"; import "./chat-conversation-list.styles.scss"; +import { useQuery } from "@apollo/client"; +import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js"; +import { phone } from "phone"; +import { useTranslation } from "react-i18next"; +import { selectBodyshop } from "../../redux/user/user.selectors"; const mapStateToProps = createStructuredSelector({ - selectedConversation: selectSelectedConversation + selectedConversation: selectSelectedConversation, + bodyshop: selectBodyshop }); const mapDispatchToProps = (dispatch) => ({ setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId)) }); -function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) { - // That comma is there for a reason, do not remove it +function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) { + const { t } = useTranslation(); const [, forceUpdate] = useState(false); - // Re-render every minute + const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "")); + + const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, { + variables: { + bodyshopid: bodyshop.id, + phone_numbers: phoneNumbers + }, + skip: !conversationList.length, + fetchPolicy: "cache-and-network" + }); + + const optOutMap = useMemo(() => { + const map = new Map(); + optOutData?.phone_number_opt_out?.forEach((optOut) => { + map.set(optOut.phone_number, true); + }); + return map; + }, [optOutData?.phone_number_opt_out]); + useEffect(() => { const interval = setInterval(() => { - forceUpdate((prev) => !prev); // Toggle state to trigger re-render - }, 60000); // 1 minute in milliseconds - - return () => clearInterval(interval); // Cleanup on unmount + forceUpdate((prev) => !prev); + }, 60000); + return () => clearInterval(interval); }, []); - // Memoize the sorted conversation list - const sortedConversationList = React.useMemo(() => { + const sortedConversationList = useMemo(() => { return _.orderBy(conversationList, ["updated_at"], ["desc"]); }, [conversationList]); - const renderConversation = (index) => { + const renderConversation = (index, t) => { const item = sortedConversationList[index]; + const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""); + const hasOptOutEntry = optOutMap.has(normalizedPhone); + const cardContentRight = {item.updated_at}; const cardContentLeft = item.job_conversations.length > 0 @@ -60,7 +86,18 @@ function ChatConversationListComponent({ conversationList, selectedConversation, ); - const cardExtra = ; + const cardExtra = ( + <> + + {hasOptOutEntry && ( + + }> + {t("messaging.labels.no_consent")} + + + )} + + ); const getCardStyle = () => item.id === selectedConversation @@ -73,9 +110,25 @@ function ChatConversationListComponent({ conversationList, selectedConversation, onClick={() => setSelectedConversation(item.id)} className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`} > - -
{cardContentLeft}
-
{cardContentRight}
+ +
+ {cardContentLeft} +
+
+ {cardContentRight} +
); @@ -85,7 +138,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
renderConversation(index)} + itemContent={(index) => renderConversation(index, t)} style={{ height: "100%", width: "100%" }} />
diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss index e6169777c..86cf06152 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss +++ b/client/src/components/chat-conversation-list/chat-conversation-list.styles.scss @@ -24,7 +24,7 @@ /* Add spacing and better alignment for items */ .chat-list-item { - padding: 0.5rem 0; /* Add spacing between list items */ + padding: 0.2rem 0; /* Add spacing between list items */ .ant-card { border-radius: 8px; /* Slight rounding for card edges */ diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index f951aabd1..e53ec8172 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -58,6 +58,7 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) { userid created_at read + is_system } `, data: message diff --git a/client/src/components/chat-media-selector/chat-media-selector.component.jsx b/client/src/components/chat-media-selector/chat-media-selector.component.jsx index b7e7d64a3..95a917e7e 100644 --- a/client/src/components/chat-media-selector/chat-media-selector.component.jsx +++ b/client/src/components/chat-media-selector/chat-media-selector.component.jsx @@ -13,13 +13,14 @@ import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-document import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component"; import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; +import "./chat-media-selector.styles.scss"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); -const mapDispatchToProps = (dispatch) => ({ - //setUserLanguage: language => dispatch(setUserLanguage(language)) -}); + +const mapDispatchToProps = (dispatch) => ({}); + export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector); export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) { @@ -37,9 +38,8 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c fetchPolicy: "network-only", nextFetchPolicy: "network-only", variables: { - jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid + jobId: conversation.job_conversations[0]?.jobid }, - skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0 }); @@ -56,25 +56,25 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c //If Imageproxy is on, rely only on the LMS selector //If not on, use the old methods. const content = ( -
+
{loading && } {error && } {selectedMedia.filter((s) => s.isSelected).length >= 10 ? ( -
{t("messaging.labels.maxtenimages")}
+
{t("messaging.labels.maxtenimages")}
) : null} {Imgproxy.treatment === "on" ? ( <> {!bodyshop.uselocalmediaserver && ( )} {bodyshop.uselocalmediaserver && open && ( )} @@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c {bodyshop.uselocalmediaserver && open && ( )} @@ -100,12 +100,17 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c return ( {t("messaging.errors.noattachedjobs")}
: content + conversation.job_conversations.length === 0 ? ( +
{t("messaging.errors.noattachedjobs")}
+ ) : ( + content + ) } title={t("messaging.labels.selectmedia")} trigger="click" open={open} onOpenChange={handleVisibleChange} + overlayClassName="media-selector-popover" > s.isSelected).length}> diff --git a/client/src/components/chat-media-selector/chat-media-selector.styles.scss b/client/src/components/chat-media-selector/chat-media-selector.styles.scss new file mode 100644 index 000000000..58e2d06b4 --- /dev/null +++ b/client/src/components/chat-media-selector/chat-media-selector.styles.scss @@ -0,0 +1,52 @@ +.media-selector-popover { + .ant-popover-inner-content { + max-width: 640px; + max-height: 480px; + overflow-y: auto; + padding: 8px; + background-color: #fff; + border-radius: 8px; + } +} + +.media-selector-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.error-message { + color: red; + font-size: 12px; + text-align: center; + margin-bottom: 8px; +} + +.no-jobs-message { + font-size: 14px; + color: #888; + text-align: center; + padding: 8px; +} + +/* Style images within gallery components */ +.media-selector-content img { + + object-fit: cover; + border-radius: 4px; + margin: 4px; + cursor: pointer; + transition: transform 0.2s; + + &:hover { + transform: scale(1.05); + } +} + +/* Grid layout for gallery components */ +.media-selector-content .ant-image, /* Assuming gallery components use Ant Design's Image */ +.media-selector-content .gallery-container { /* Fallback for custom gallery classes */ + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 4px; +} diff --git a/client/src/components/chat-messages-list/chat-message-list.styles.scss b/client/src/components/chat-messages-list/chat-message-list.styles.scss index 98f5ab8a4..969de0c82 100644 --- a/client/src/components/chat-messages-list/chat-message-list.styles.scss +++ b/client/src/components/chat-messages-list/chat-message-list.styles.scss @@ -4,13 +4,16 @@ height: 100%; width: 100%; } + .archive-button { height: 20px; border-radius: 4px; } + .chat-title { margin-bottom: 5px; } + .messages { display: flex; flex-direction: column; @@ -37,11 +40,13 @@ gap: 8px; } } -.chat-send-message-button{ + +.chat-send-message-button { margin: 0.3rem; padding-left: 0.5rem; - + } + .message-icon { position: absolute; bottom: 0.1rem; @@ -125,6 +130,37 @@ } } +.system { + align-items: center; + margin: 0.5rem 10%; + + .message { + background-color: #f5f5f5; + border-radius: 10px; + padding: 0.5rem 1rem; + text-align: center; + font-style: italic; + color: #555; + width: fit-content; + max-width: 80%; + } + + .system-label { + font-size: 0.75rem; + color: #888; + margin-bottom: 0.2rem; + display: block; + } + + .system-date { + font-size: 0.75rem; + color: #888; + margin-top: 0.2rem; + text-align: center; + } +} + + .virtuoso-container { flex: 1; overflow: auto; diff --git a/client/src/components/chat-messages-list/renderMessage.jsx b/client/src/components/chat-messages-list/renderMessage.jsx index b94e69ee0..2d1e1ce75 100644 --- a/client/src/components/chat-messages-list/renderMessage.jsx +++ b/client/src/components/chat-messages-list/renderMessage.jsx @@ -2,17 +2,29 @@ import Icon from "@ant-design/icons"; import { Tooltip } from "antd"; import i18n from "i18next"; import dayjs from "../../utils/day"; -import { MdDone, MdDoneAll } from "react-icons/md"; +import { MdClose, MdDone, MdDoneAll } from "react-icons/md"; import { DateTimeFormatter } from "../../utils/DateFormatter"; export const renderMessage = (messages, index) => { const message = messages[index]; + const isSystem = message.is_system; + + // Determine message class + const messageClass = isSystem ? "system messages" : message.isoutbound ? "mine messages" : "yours messages"; + + // Tooltip content based on message type + const tooltipTitle = isSystem ? ( + i18n.t("consent.text_body") + ) : ( + {message.created_at} + ); return ( -
+
- +
+ {isSystem && System} {/* Render images if available */} {message.image && message.image_path?.length > 0 && (
@@ -26,20 +38,31 @@ export const renderMessage = (messages, index) => {
)} {/* Render text if available */} - {message.text &&
{message.text}
} + {message.text &&
{message.text}
} + {/* Render date for system messages */} + {isSystem && ( +
+ {message.created_at} +
+ )}
- {/* Message status icons */} - {message.status && (message.status === "sent" || message.status === "delivered") && ( -
- -
- )} + {/* Message status icons for non-system messages */} + {!isSystem && + message.status && + (message.status === "sent" || message.status === "delivered" || message.status === "failed") && ( +
+ +
+ )}
- - {/* Outbound message metadata */} - {message.isoutbound && ( + {/* Outbound message metadata for non-system messages */} + {!isSystem && message.isoutbound && (
{i18n.t("messaging.labels.sentby", { by: message.userid, diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx index 824d5e591..5b4d31d14 100644 --- a/client/src/components/chat-send-message/chat-send-message.component.jsx +++ b/client/src/components/chat-send-message/chat-send-message.component.jsx @@ -1,6 +1,6 @@ -import { LoadingOutlined, SendOutlined } from "@ant-design/icons"; -import { Input, Spin } from "antd"; -import React, { useEffect, useRef, useState } from "react"; +import { ExclamationCircleOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons"; +import { Alert, Input, Space, Spin, Tooltip } from "antd"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -10,6 +10,9 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging. import { selectBodyshop } from "../../redux/user/user.selectors"; import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component"; import ChatPresetsComponent from "../chat-presets/chat-presets.component"; +import { useQuery } from "@apollo/client"; +import { phone } from "phone"; +import { GET_PHONE_NUMBER_OPT_OUT } from "../../graphql/phone-number-opt-out.queries"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -25,16 +28,24 @@ const mapDispatchToProps = (dispatch) => ({ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) { const inputArea = useRef(null); const [selectedMedia, setSelectedMedia] = useState([]); + const { t } = useTranslation(); + + const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, ""); + const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUT, { + variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone }, + fetchPolicy: "cache-and-network" + }); + + const isOptedOut = !!optOutData?.phone_number_opt_out?.[0]; useEffect(() => { inputArea.current.focus(); }, [isSending, setMessage]); - const { t } = useTranslation(); - const handleEnter = () => { const selectedImages = selectedMedia.filter((i) => i.isSelected); if ((message === "" || !message) && selectedImages.length === 0) return; + if (isOptedOut) return; // Prevent sending if phone number is opted out logImEXEvent("messaging_send_message"); if (selectedImages.length < 11) { @@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi messagingServiceSid: bodyshop.messagingservicesid, conversationid: conversation.id, selectedMedia: selectedImages, - imexshopid: bodyshop.imexshopid + imexshopid: bodyshop.imexshopid, + bodyshopid: bodyshop.id }; sendMessage(newMessage); setSelectedMedia( @@ -56,47 +68,67 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi }; return ( -
- - - - setMessage(e.target.value)} - onPressEnter={(event) => { - event.preventDefault(); - if (!!!event.shiftKey) handleEnter(); - }} - /> - - - + {isOptedOut && ( + + } + message={t("messaging.errors.no_consent")} + type="error" /> - } - /> -
+ + )} +
+ {!isOptedOut && ( + <> + + + + )} + + setMessage(e.target.value)} + onPressEnter={(event) => { + event.preventDefault(); + if (!event.shiftKey && !isOptedOut) handleEnter(); + }} + /> + + {!isOptedOut && ( + + )} + + + } + /> +
+ ); } diff --git a/client/src/components/feature-wrapper/feature-wrapper.component.jsx b/client/src/components/feature-wrapper/feature-wrapper.component.jsx index 7dabea2ac..507a05923 100644 --- a/client/src/components/feature-wrapper/feature-wrapper.component.jsx +++ b/client/src/components/feature-wrapper/feature-wrapper.component.jsx @@ -81,8 +81,9 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false } return ( bodyshop?.features?.allAccess || - bodyshop?.features?.[featureName] || - dayjs(bodyshop?.features[featureName]).isAfter(dayjs()) + (typeof bodyshop?.features?.[featureName] === "boolean" + ? bodyshop?.features?.[featureName] + : dayjs(bodyshop?.features?.[featureName]).isAfter(dayjs())) ); } diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index a13c258e1..826edea54 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -15,6 +15,7 @@ import { HomeFilled, ImportOutlined, LineChartOutlined, + OneToOneOutlined, PaperClipOutlined, PhoneOutlined, PlusCircleOutlined, @@ -24,6 +25,7 @@ import { TeamOutlined, ToolFilled, UnorderedListOutlined, + UsergroupAddOutlined, UserOutlined } from "@ant-design/icons"; import { useQuery } from "@apollo/client"; @@ -40,6 +42,7 @@ import { RiSurveyLine } from "react-icons/ri"; import { connect } from "react-redux"; import { Link } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js"; import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; @@ -47,11 +50,10 @@ import { signOutStart } from "../../redux/user/user.actions"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import day from "../../utils/day.js"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; +import { useIsEmployee } from "../../utils/useIsEmployee.js"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import LockWrapper from "../lock-wrapper/lock-wrapper.component"; import NotificationCenterContainer from "../notification-center/notification-center.container.jsx"; -import { useSocket } from "../../contexts/SocketIO/useSocket.js"; -import { useIsEmployee } from "../../utils/useIsEmployee.js"; // Redux mappings const mapStateToProps = createStructuredSelector({ @@ -642,17 +644,32 @@ function Header({ label: t("menus.header.help"), onClick: () => window.open("https://help.imex.online/", "_blank") }, - ...(InstanceRenderManager({ imex: true, rome: false }) - ? [ - { - key: "rescue", - id: "header-rescue", - icon: , - label: t("menus.header.rescueme"), - onClick: () => window.open("https://imexrescue.com/", "_blank") - } - ] - : []), + { + key: "remoteassist", + id: "header-remote-assist", + icon: , + label: t("menus.header.remoteassist"), + children: [ + ...(InstanceRenderManager({ imex: true, rome: false }) + ? [ + { + key: "rescue", + id: "header-rescue", + icon: , + label: t("menus.header.rescueme"), + onClick: () => window.open("https://imexrescue.com/", "_blank") + } + ] + : []), + { + key: "rescue-zoho", + id: "header-rescue-zoho", + icon: , + label: t("menus.header.rescuemezoho"), + onClick: () => window.open("https://join.zoho.com/", "_blank") + } + ] + }, { key: "shiftclock", id: "header-shiftclock", diff --git a/client/src/components/job-at-change/schedule-event.component.jsx b/client/src/components/job-at-change/schedule-event.component.jsx index 6f7780ba8..6438308da 100644 --- a/client/src/components/job-at-change/schedule-event.component.jsx +++ b/client/src/components/job-at-change/schedule-event.component.jsx @@ -395,32 +395,33 @@ export function ScheduleEventComponent({ ) : ( )} - {event.isintake && HasFeatureAccess({ featureName: "checklist", bodyshop }) ? ( - - - - ) : ( - { - if (event.job?.id) { - e.stopPropagation(); - getJobDetails(); - } - }} - getPopupContainer={(trigger) => trigger.parentNode} - trigger="click" - > - - - )} + {event.job && + (HasFeatureAccess({ featureName: "checklist", bodyshop }) ? ( + + + + ) : ( + { + if (event.job?.id) { + e.stopPropagation(); + getJobDetails(); + } + }} + getPopupContainer={(trigger) => trigger.parentNode} + trigger="click" + > + + + ))}
); diff --git a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx index 18f5d025e..29ced4f09 100644 --- a/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx +++ b/client/src/components/job-checklist/components/job-checklist-form/job-checklist-form.component.jsx @@ -1,7 +1,7 @@ import { useMutation } from "@apollo/client"; import { Button, Card, Form, Input, Switch } from "antd"; import queryString from "query-string"; -import React, { useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { useLocation, useNavigate, useParams } from "react-router-dom"; @@ -9,7 +9,6 @@ import { createStructuredSelector } from "reselect"; import { logImEXEvent } from "../../../../firebase/firebase.utils"; import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries"; import { UPDATE_JOB } from "../../../../graphql/jobs.queries"; -import { UPDATE_OWNER } from "../../../../graphql/owners.queries"; import { insertAuditTrail } from "../../../../redux/application/application.actions"; import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors"; import AuditTrailMapping from "../../../../utils/AuditTrailMappings"; @@ -32,7 +31,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren const [loading, setLoading] = useState(false); const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED); const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED); - const [updateOwner] = useMutation(UPDATE_OWNER); const notification = useNotification(); const { jobId } = useParams(); @@ -129,24 +127,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren } } - if (type === "intake" && job.owner && job.owner.id) { - //Updae Owner Allow to Text - const updateOwnerResult = await updateOwner({ - variables: { - ownerId: job.owner.id, - owner: { allow_text_message: values.allow_text_message } - } - }); - - if (!!updateOwnerResult.errors) { - notification["error"]({ - message: t("checklist.errors.complete", { - error: JSON.stringify(result.errors) - }) - }); - } - } - setLoading(false); if (!!!result.errors) { @@ -189,7 +169,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren initialValues={{ ...(type === "intake" && { addToProduction: true, - allow_text_message: job.owner && job.owner.allow_text_message, scheduled_completion: (job && job.scheduled_completion && dayjs(job.scheduled_completion)) || (job && @@ -228,14 +207,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren > - - - + {body ? ( diff --git a/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx b/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx index d6ee2076f..1a532b484 100644 --- a/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx +++ b/client/src/components/jobs-create-owner-info/jobs-create-owner-info.new.component.jsx @@ -1,4 +1,4 @@ -import { Form, Input, Switch } from "antd"; +import { Form, Input } from "antd"; import React, { useContext } from "react"; import { useTranslation } from "react-i18next"; import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; @@ -129,13 +129,6 @@ export default function JobsCreateOwnerInfoNewComponent() { - - -
); diff --git a/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx b/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx index 338e873dd..5f0c3a2dc 100644 --- a/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx +++ b/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx @@ -72,7 +72,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) { open={open} placement="left" onOpenChange={handleOpenChange} - destroyTooltipOnHide + destroyOnHidden > diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx index 1db81a3b1..ec1ec32b6 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx @@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; +import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries"; @@ -32,7 +33,6 @@ import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx"; import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production"; -import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -1078,17 +1078,26 @@ export function JobsDetailHeaderActions({ menuItems.push({ key: "deletejob", id: "job-actions-deletejob", - label: ( - e.stopPropagation()} - onConfirm={handleDeleteJob} - > - {t("menus.jobsactions.deletejob")} - - ) + label: + job.job_watchers.length === 0 ? ( + e.stopPropagation()} + onConfirm={handleDeleteJob} + > + {t("menus.jobsactions.deletejob")} + + ) : ( + e.stopPropagation()} + showCancel={false} + > + {t("menus.jobsactions.deletejob")} + + ) }); } @@ -1109,8 +1118,8 @@ export function JobsDetailHeaderActions({ e.stopPropagation()} onConfirm={handleVoidJob} > diff --git a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.toggle-production.jsx b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.toggle-production.jsx index c14099f89..2a331769f 100644 --- a/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.toggle-production.jsx +++ b/client/src/components/jobs-detail-header-actions/jobs-detail-header-actions.toggle-production.jsx @@ -167,7 +167,18 @@ export function JobsDetailHeaderActionsToggleProduction({ - + diff --git a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx index 746b40abd..141cab456 100644 --- a/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx +++ b/client/src/components/jobs-detail-header/jobs-detail-header.component.jsx @@ -1,17 +1,20 @@ import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons"; +import { useMutation } from "@apollo/client"; import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link } from "react-router-dom"; -import { useMutation } from "@apollo/client"; import { createStructuredSelector } from "reselect"; +import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import { insertAuditTrail } from "../../redux/application/application.actions.js"; import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; import { selectBodyshop } from "../../redux/user/user.selectors"; -import { UPDATE_JOB } from "../../graphql/jobs.queries"; +import AuditTrailMapping from "../../utils/AuditTrailMappings.js"; import CurrencyFormatter from "../../utils/CurrencyFormatter"; -import { DateTimeFormatter } from "../../utils/DateFormatter"; +import { DateTimeFormatter, DateTimeFormatterFunction } from "../../utils/DateFormatter"; import dayjs from "../../utils/day"; import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import ChatOpenButton from "../chat-open-button/chat-open-button.component"; @@ -24,7 +27,6 @@ import ProductionListColumnComment from "../production-list-columns/production-l import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import "./jobs-detail-header.styles.scss"; -import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; const mapStateToProps = createStructuredSelector({ jobRO: selectJobReadOnly, @@ -38,6 +40,14 @@ const mapDispatchToProps = (dispatch) => ({ context: context, modal: "printCenter" }) + ), + insertAuditTrail: ({ jobid, operation, type }) => + dispatch( + insertAuditTrail({ + jobid, + operation, + type + }) ) }); @@ -49,7 +59,7 @@ const colSpan = { xl: { span: 6 } }; -export function JobsDetailHeader({ job, bodyshop, disabled }) { +export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail }) { const { t } = useTranslation(); const { notification } = useNotification(); const [notesClamped, setNotesClamped] = useState(true); @@ -66,7 +76,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { const handleCheckboxChange = async (field, checked) => { const value = checked ? dayjs().toISOString() : null; try { - await updateJob({ + const ret = await updateJob({ variables: { jobId: job.id, job: { [field]: value } @@ -74,6 +84,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) { refetchQueries: ["GET_JOB_BY_PK"], awaitRefetchQueries: true }); + insertAuditTrail({ + jobid: job.id, + operation: AuditTrailMapping.jobfieldchange( + field, + ret.data.update_jobs.returning[0][field] + ? DateTimeFormatterFunction(ret.data.update_jobs.returning[0][field]) + : checked + ), + type: "jobfieldchange" + }); } catch (error) { notification.error({ message: t("jobs.errors.saving", { error: error.message }) diff --git a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx index 5bbd95f94..ea4489426 100644 --- a/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx +++ b/client/src/components/jobs-documents-gallery/jobs-documents-gallery.container.jsx @@ -42,7 +42,7 @@ export function JobsDocumentsContainer({ variables: { jobId: jobId }, fetchPolicy: "network-only", nextFetchPolicy: "network-only", - skip: Imgproxy.treatment === "on" || !!billId + skip: !!billId }); if (loading) return ; diff --git a/client/src/components/owner-detail-form/owner-detail-form.component.jsx b/client/src/components/owner-detail-form/owner-detail-form.component.jsx index 5de4231f7..99bacd566 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.component.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.component.jsx @@ -1,14 +1,15 @@ -import { Form, Input, Switch } from "antd"; -import React from "react"; +import { Form, Input, Tooltip } from "antd"; +import { CloseCircleFilled } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component"; -import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; -export default function OwnerDetailFormComponent({ form, loading }) { +export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedOut, isPhone2OptedOut }) { const { t } = useTranslation(); const { getFieldValue } = form; + return (
@@ -26,7 +27,7 @@ export default function OwnerDetailFormComponent({ form, loading }) { - + @@ -50,9 +51,6 @@ export default function OwnerDetailFormComponent({ form, loading }) { - - - - PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]} - > - + +
+ PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]} + > + + + {isPhone1OptedOut && ( + + + + )} +
- PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]} - > - + +
+ PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]} + > + + + {isPhone2OptedOut && ( + + + + )} +
diff --git a/client/src/components/owner-detail-form/owner-detail-form.container.jsx b/client/src/components/owner-detail-form/owner-detail-form.container.jsx index fdf2df339..d78a11f04 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.container.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.container.jsx @@ -1,69 +1,115 @@ import { Button, Form, Popconfirm } from "antd"; import { PageHeader } from "@ant-design/pro-layout"; - -import React, { useState } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { useMutation } from "@apollo/client"; +import { useApolloClient, useMutation } from "@apollo/client"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; // Adjust path +import { phoneNumberOptOutService } from "../../utils/phoneOptOutService.js"; // Adjust path import OwnerDetailFormComponent from "./owner-detail-form.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { phone } from "phone"; // Import phone utility for formatting -function OwnerDetailFormContainer({ owner, refetch }) { +// Connect to Redux to access bodyshop +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +function OwnerDetailFormContainer({ owner, refetch, bodyshop }) { const { t } = useTranslation(); const [form] = Form.useForm(); - const history = useNavigate(); + const navigate = useNavigate(); const [loading, setLoading] = useState(false); + const [optedOutPhones, setOptedOutPhones] = useState(new Set()); const [updateOwner] = useMutation(UPDATE_OWNER); const [deleteOwner] = useMutation(DELETE_OWNER); const notification = useNotification(); + const apolloClient = useApolloClient(); + + // Fetch opt-out status on mount + useEffect(() => { + const fetchOptOutStatus = async () => { + if (bodyshop?.id && bodyshop?.messagingservicesid && (owner?.ownr_ph1 || owner?.ownr_ph2)) { + const phoneNumbers = [owner.ownr_ph1, owner.ownr_ph2].filter(Boolean); + const optOutSet = await phoneNumberOptOutService(apolloClient, bodyshop.id, phoneNumbers); + setOptedOutPhones(optOutSet); + } else { + setOptedOutPhones(new Set()); + } + }; + + fetchOptOutStatus(); + }, [apolloClient, bodyshop?.id, bodyshop?.messagingservicesid, owner?.ownr_ph1, owner?.ownr_ph2]); + + // Reset form fields when owner changes + useEffect(() => { + form.setFieldsValue({ + ownr_ph1: owner?.ownr_ph1, + ownr_ph2: owner?.ownr_ph2, + ...owner + }); + }, [owner, form]); const handleDelete = async () => { setLoading(true); - const result = await deleteOwner({ - variables: { id: owner.id } - }); - console.log(result); - if (result.errors) { - notification["error"]({ + try { + const result = await deleteOwner({ + variables: { id: owner.id } + }); + if (result.errors) { + notification.error({ + message: t("owners.errors.deleting", { + error: JSON.stringify(result.errors) + }) + }); + } else { + notification.success({ + message: t("owners.successes.delete") + }); + navigate(`/manage/owners`); + } + } catch (error) { + notification.error({ message: t("owners.errors.deleting", { - error: JSON.stringify(result.errors) + error: error.message }) }); + } finally { setLoading(false); - } else { - notification["success"]({ - message: t("owners.successes.delete") - }); - setLoading(false); - history(`/manage/owners`); } }; const handleFinish = async (values) => { setLoading(true); - const result = await updateOwner({ - variables: { ownerId: owner.id, owner: values } - }); - - if (!!result.errors) { - notification["error"]({ + try { + const result = await updateOwner({ + variables: { ownerId: owner.id, owner: values } + }); + if (result.errors) { + notification.error({ + message: t("owners.errors.saving", { + error: JSON.stringify(result.errors) + }) + }); + } else { + notification.success({ + message: t("owners.successes.save") + }); + if (refetch) await refetch(); + form.resetFields(); + } + } catch (error) { + notification.error({ message: t("owners.errors.saving", { - error: JSON.stringify(result.errors) + error: error.message }) }); + } finally { setLoading(false); - return; } - - notification["success"]({ - message: t("owners.successes.save") - }); - - if (refetch) await refetch(); - form.resetFields(); - form.resetFields(); - setLoading(false); }; return ( @@ -72,6 +118,7 @@ function OwnerDetailFormContainer({ owner, refetch }) { title={t("menus.header.owners")} extra={[ , - ]} />
- + ); } -export default OwnerDetailFormContainer; +export default connect(mapStateToProps)(OwnerDetailFormContainer); diff --git a/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx b/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx index d8895803b..7fd2cf2e4 100644 --- a/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx +++ b/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx @@ -75,7 +75,7 @@ export function PartsOrderBackorderEta({ ); return ( - + {backordered_eta} {isAlreadyBackordered && } {loading && } diff --git a/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx b/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx index 26910643c..e38b30ecb 100644 --- a/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx +++ b/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx @@ -84,7 +84,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j ); return ( - + diff --git a/client/src/components/phone-number-consent/phone-number-consent.component.jsx b/client/src/components/phone-number-consent/phone-number-consent.component.jsx new file mode 100644 index 000000000..9a9e32e51 --- /dev/null +++ b/client/src/components/phone-number-consent/phone-number-consent.component.jsx @@ -0,0 +1,146 @@ +import { useQuery } from "@apollo/client"; +import { Input, Table, Typography } from "antd"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; +import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries"; +import { TimeAgoFormatter } from "../../utils/DateFormatter"; +import ChatOpenButton from "../chat-open-button/chat-open-button.component"; +import { useTranslation } from "react-i18next"; +import { useState } from "react"; + +const { Paragraph } = Typography; + +// Commented out Associated Owners section for now +//import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +//import { Link } from "react-router-dom"; +//import { useMemo, useState } from "react"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + currentUser: selectCurrentUser +}); + +const mapDispatchToProps = () => ({}); + +function PhoneNumberConsentList({ bodyshop, currentUser }) { + const { t } = useTranslation(); + const [search, setSearch] = useState(""); + + // Fetch opt-out phone numbers + const { loading: optOutLoading, data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, { + variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }, + fetchPolicy: "network-only" + }); + + // Commented out Associated Owners section for now + /*// Prepare phone numbers for owner query + const phoneNumbers = useMemo(() => { + return optOutData?.phone_number_opt_out?.map((item) => item.phone_number) || []; + }, [optOutData?.phone_number_opt_out]); + const allPhoneNumbers = useMemo(() => { + const normalized = phoneNumbers; + const withPlusOne = phoneNumbers.map((num) => `+1${num}`); + return [...normalized, ...withPlusOne].filter(Boolean); + }, [phoneNumbers]); + + // Fetch owners for all phone numbers + const { loading: ownersLoading, data: ownersData } = useQuery(SEARCH_OWNERS_BY_PHONE_NUMBERS, { + variables: { bodyshopid: bodyshop.id, phone_numbers: allPhoneNumbers }, + skip: allPhoneNumbers.length === 0 || !bodyshop.id, + fetchPolicy: "network-only" + }); + + // Map phone numbers to their associated owners and identify phone field + const getAssociatedOwners = (phoneNumber) => { + if (!ownersData?.owners) return []; + const normalizedPhone = phoneNumber.replace(/^\+1/, ""); + return ownersData.owners + .filter( + (owner) => + owner.ownr_ph1 === phoneNumber || + owner.ownr_ph2 === phoneNumber || + owner.ownr_ph1 === normalizedPhone || + owner.ownr_ph2 === normalizedPhone || + owner.ownr_ph1 === `+1${phoneNumber}` || + owner.ownr_ph2 === `+1${phoneNumber}` + ) + .map((owner) => ({ + ...owner, + phoneField: + [owner.ownr_ph1, owner.ownr_ph2].includes(phoneNumber) || + [owner.ownr_ph1, owner.ownr_ph2].includes(normalizedPhone) || + [owner.ownr_ph1, owner.ownr_ph2].includes(`+1${phoneNumber}`) + ? owner.ownr_ph1 === phoneNumber || + owner.ownr_ph1 === normalizedPhone || + owner.ownr_ph1 === `+1${phoneNumber}` + ? t("consent.phone_1") + : t("consent.phone_2") + : null + })); + };*/ + + const columns = [ + { + title: t("consent.phone_number"), + dataIndex: "phone_number", + render: (text) => , + sorter: (a, b) => a.phone_number.localeCompare(b.phone_number) + }, + // Commented out Associated Owners section for now + /*{ + title: t("consent.associated_owners"), + dataIndex: "phone_number", + render: (phoneNumber) => { + const owners = getAssociatedOwners(phoneNumber); + if (!owners || owners.length === 0) { + return t("consent.no_owners"); + } + return owners.map((owner) => ( +
+ + + + + ({owner.phoneField}) + +
+ )); + }, + sorter: (a, b) => { + const aOwners = getAssociatedOwners(a.phone_number); + const bOwners = getAssociatedOwners(b.phone_number); + const aName = aOwners[0] ? `${aOwners[0].ownr_fn} ${aOwners[0].ownr_ln}` : ""; + const bName = bOwners[0] ? `${bOwners[0].ownr_fn} ${bOwners[0].ownr_ln}` : ""; + return aName.localeCompare(bName); + } + },*/ + { + title: t("consent.created_at"), + dataIndex: "created_at", + render: (text) => {text}, + sorter: (a, b) => new Date(a.created_at) - new Date(b.created_at) + } + ]; + + return ( +
+ {t("consent.text_body")} + setSearch(value)} + style={{ marginBottom: 16 }} + /> + + + + ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(PhoneNumberConsentList); diff --git a/client/src/components/production-board-kanban/settings/LayoutSettings.jsx b/client/src/components/production-board-kanban/settings/LayoutSettings.jsx index 888503df2..8155a4ace 100644 --- a/client/src/components/production-board-kanban/settings/LayoutSettings.jsx +++ b/client/src/components/production-board-kanban/settings/LayoutSettings.jsx @@ -1,7 +1,15 @@ import { Card, Col, Form, Radio, Row } from "antd"; import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../../redux/user/user.selectors"; +import { HasFeatureAccess } from "../../feature-wrapper/feature-wrapper.component"; -const LayoutSettings = ({ t }) => ( +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const LayoutSettings = ({ t, bodyshop }) => ( {[ @@ -30,14 +38,18 @@ const LayoutSettings = ({ t }) => ( { value: false, label: t("production.labels.wide") } ] }, - { - name: "cardcolor", - label: t("production.labels.cardcolor"), - options: [ - { value: true, label: t("production.labels.on") }, - { value: false, label: t("production.labels.off") } - ] - }, + ...(HasFeatureAccess({ bodyshop, featureName: "smartscheduling" }) + ? [ + { + name: "cardcolor", + label: t("production.labels.cardcolor"), + options: [ + { value: true, label: t("production.labels.on") }, + { value: false, label: t("production.labels.off") } + ] + } + ] + : []), { name: "kiosk", label: t("production.labels.kiosk_mode"), @@ -67,4 +79,4 @@ LayoutSettings.propTypes = { t: PropTypes.func.isRequired }; -export default LayoutSettings; +export default connect(mapStateToProps)(LayoutSettings); diff --git a/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx b/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx index 3e7cdbdcd..e326a455f 100644 --- a/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx @@ -140,7 +140,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]); return ( - + {record[type] ? (
diff --git a/client/src/components/report-center-modal/report-center-modal.component.jsx b/client/src/components/report-center-modal/report-center-modal.component.jsx index ddea0aab2..336124eaa 100644 --- a/client/src/components/report-center-modal/report-center-modal.component.jsx +++ b/client/src/components/report-center-modal/report-center-modal.component.jsx @@ -6,6 +6,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; +import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { QUERY_ACTIVE_EMPLOYEES, QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL } from "../../graphql/employees.queries"; import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries"; import { selectReportCenter } from "../../redux/modals/modals.selectors"; @@ -18,11 +19,10 @@ import EmployeeSearchSelectEmail from "../employee-search-select/employee-search import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component"; import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; +import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component"; import "./report-center-modal.styles.scss"; -import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component"; -import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; const mapStateToProps = createStructuredSelector({ reportCenterModal: selectReportCenter, @@ -389,5 +389,7 @@ const restrictedReports = [ { key: "job_costing_ro_date_detail", days: 183 }, { key: "job_costing_ro_estimator", days: 183 }, { key: "job_lifecycle_date_detail", days: 183 }, - { key: "job_lifecycle_date_summary", days: 183 } + { key: "job_lifecycle_date_summary", days: 183 }, + { key: "customer_list", days: 183 }, + { key: "customer_list_excel", days: 183 } ]; diff --git a/client/src/components/shop-info/shop-info.consent.component.jsx b/client/src/components/shop-info/shop-info.consent.component.jsx new file mode 100644 index 000000000..15754c4e6 --- /dev/null +++ b/client/src/components/shop-info/shop-info.consent.component.jsx @@ -0,0 +1,25 @@ +import { Typography } from "antd"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors"; +import PhoneNumberConsentList from "../phone-number-consent/phone-number-consent.component"; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +const mapDispatchToProps = (dispatch) => ({}); + +function ShopInfoConsentComponent({ bodyshop }) { + const { t } = useTranslation(); + + return ( +
+ {t("settings.title")} + {} +
+ ); +} + +export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoConsentComponent); diff --git a/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx index c5b174f26..be74b1f99 100644 --- a/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx +++ b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx @@ -107,7 +107,7 @@ export default function TimeTicketCalculatorComponent({ open={visible} onOpenChange={handleOpenChange} placement="right" - destroyTooltipOnHide + destroyOnHidden >