diff --git a/client/package-lock.json b/client/package-lock.json index 4ed331a94..fbd87949f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,27 +9,27 @@ "version": "0.2.1", "hasInstallScript": true, "dependencies": { - "@ant-design/pro-layout": "^7.22.0", - "@apollo/client": "^3.12.6", + "@ant-design/pro-layout": "^7.22.3", + "@apollo/client": "^3.13.1", "@emotion/is-prop-valid": "^1.3.1", - "@fingerprintjs/fingerprintjs": "^4.5.1", + "@fingerprintjs/fingerprintjs": "^4.6.1", "@jsreport/browser-client": "^3.1.0", - "@reduxjs/toolkit": "^2.5.0", + "@reduxjs/toolkit": "^2.6.0", "@sentry/cli": "^2.42.2", "@sentry/react": "^9.3.0", - "@sentry/vite-plugin": "^3.2.1", + "@sentry/vite-plugin": "^3.2.2", "@splitsoftware/splitio-react": "^1.13.0", "@tanem/react-nprogress": "^5.0.53", "@vitejs/plugin-react": "^4.3.4", - "antd": "^5.23.1", + "antd": "^5.24.2", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^4.1.0", "autosize": "^6.0.1", - "axios": "^1.7.9", + "axios": "^1.8.1", "classnames": "^2.5.1", "css-box-model": "^1.2.1", "dayjs": "^1.11.13", - "dayjs-business-days2": "^1.2.3", + "dayjs-business-days2": "^1.3.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.7", "env-cmd": "^10.1.0", @@ -37,9 +37,9 @@ "firebase": "^10.13.2", "graphql": "^16.10.0", "i18next": "^23.15.1", - "i18next-browser-languagedetector": "^8.0.2", + "i18next-browser-languagedetector": "^8.0.4", "immutability-helper": "^3.1.1", - "libphonenumber-js": "^1.11.18", + "libphonenumber-js": "^1.12.4", "logrocket": "^8.1.2", "markerjs2": "^2.32.3", "memoize-one": "^6.0.0", @@ -49,7 +49,7 @@ "query-string": "^9.1.1", "raf-schd": "^4.0.3", "react": "^18.3.1", - "react-big-calendar": "^1.17.1", + "react-big-calendar": "^1.18.0", "react-color": "^2.19.3", "react-cookie": "^7.2.2", "react-dom": "^18.3.1", @@ -57,7 +57,7 @@ "react-grid-gallery": "^1.0.1", "react-grid-layout": "1.3.4", "react-i18next": "^14.1.3", - "react-icons": "^5.4.0", + "react-icons": "^5.5.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^9.0.3", "react-number-format": "^5.4.3", @@ -65,9 +65,9 @@ "react-product-fruits": "^2.2.61", "react-redux": "^9.2.0", "react-resizable": "^3.0.5", - "react-router-dom": "^6.26.2", + "react-router-dom": "^6.30.0", "react-sticky": "^6.0.3", - "react-virtuoso": "^4.10.4", + "react-virtuoso": "^4.12.5", "recharts": "^2.15.0", "redux": "^5.0.1", "redux-actions": "^3.0.3", @@ -75,24 +75,24 @@ "redux-saga": "^1.3.0", "redux-state-sync": "^3.1.4", "reselect": "^5.1.1", - "sass": "^1.83.4", + "sass": "^1.85.1", "socket.io-client": "^4.8.1", - "styled-components": "^6.1.14", + "styled-components": "^6.1.15", "subscriptions-transport-ws": "^0.11.0", "use-memo-one": "^1.1.3", - "userpilot": "^1.3.6", + "userpilot": "^1.3.8", "vite-plugin-ejs": "^1.7.0", "web-vitals": "^3.5.2" }, "devDependencies": { - "@ant-design/icons": "^5.5.2", + "@ant-design/icons": "^5.6.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.26.3", - "@dotenvx/dotenvx": "^1.33.0", + "@dotenvx/dotenvx": "^1.38.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", - "@eslint/js": "^9.18.0", - "@sentry/webpack-plugin": "^3.2.1", + "@eslint/js": "^9.21.0", + "@sentry/webpack-plugin": "^3.2.2", "@testing-library/cypress": "^10.0.2", "browserslist": "^4.24.4", "browserslist-to-esbuild": "^2.1.1", @@ -103,13 +103,13 @@ "eslint-config-react-app": "^7.0.1", "eslint-plugin-cypress": "^2.15.1", "eslint-plugin-react": "^7.37.4", - "globals": "^15.14.0", + "globals": "^15.15.0", "memfs": "^4.17.0", "os-browserify": "^0.3.0", - "react-error-overlay": "6.0.11", + "react-error-overlay": "^6.1.0", "redux-logger": "^3.0.6", "source-map-explorer": "^2.5.3", - "vite": "^6.0.7", + "vite": "^6.2.0", "vite-plugin-babel": "^1.3.0", "vite-plugin-eslint": "^1.8.1", "vite-plugin-node-polyfills": "^0.23.0", @@ -193,9 +193,9 @@ } }, "node_modules/@ant-design/icons": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.0.tgz", - "integrity": "sha512-Mb6QkQmPLZsmIHJ6oBsoyKrrT8/kAUdQ6+8q38e2bQSclROi69SiDlI4zZroaIPseae1w110RJH0zGrphAvlSQ==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.6.1.tgz", + "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==", "license": "MIT", "dependencies": { "@ant-design/colors": "^7.0.0", @@ -219,15 +219,15 @@ "license": "MIT" }, "node_modules/@ant-design/pro-layout": { - "version": "7.22.1", - "resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.1.tgz", - "integrity": "sha512-aOwdd/u/yz/0ZHPDpnoVUIDGneZUDw62lCo3C4s6o0SGQrCOkYvY0efG4yLC4RA8eSFvXwnMt5pB7ii7f46KLQ==", + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@ant-design/pro-layout/-/pro-layout-7.22.3.tgz", + "integrity": "sha512-di/EOMDuoMDRjBweqesYyCxEYr2LCmO82y6A4bSwmmJ6ehxN7HGC73Wx4RuBkzDR7kHLTOXt7WxI6875ENT8mg==", "license": "MIT", "dependencies": { "@ant-design/cssinjs": "^1.21.1", "@ant-design/icons": "^5.0.0", "@ant-design/pro-provider": "2.15.3", - "@ant-design/pro-utils": "2.16.3", + "@ant-design/pro-utils": "2.16.4", "@babel/runtime": "^7.18.0", "@umijs/route-utils": "^4.0.0", "@umijs/use-params": "^1.0.9", @@ -266,9 +266,9 @@ } }, "node_modules/@ant-design/pro-utils": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.16.3.tgz", - "integrity": "sha512-uNjKh51v/SUlCJbWfhg2lRQB/TB0MyNMCQkFZ8mZBQ2rk3Ew47Sly6VssVVWMjIWBLE+g9fOgPg0C1IVeilIXA==", + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/@ant-design/pro-utils/-/pro-utils-2.16.4.tgz", + "integrity": "sha512-PFxqF0fsUsLj8ORvJSuMgVv9NDHwAxZaglzPN/u3jZX7rWYcrHD04EMJEXooZaSyT6Q4+j7SqXDx6oBsdb9zNw==", "license": "MIT", "dependencies": { "@ant-design/icons": "^5.0.0", @@ -323,9 +323,9 @@ } }, "node_modules/@apollo/client": { - "version": "3.12.8", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.12.8.tgz", - "integrity": "sha512-053z5eCM7zNl81Lxs/SqHv8e+x3sszrtmGS2TNuPW0ZTZzmKATMsIsoblx1Kt0E67Ze3jb99JSPiQHG9ozGc3g==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.13.1.tgz", + "integrity": "sha512-HaAt62h3jNUXpJ1v5HNgUiCzPP1c5zc2Q/FeTb2cTk/v09YlhoqKKHQFJI7St50VCJ5q8JVIc03I5bRcBrQxsg==", "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", @@ -344,7 +344,7 @@ }, "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", - "graphql-ws": "^5.5.5", + "graphql-ws": "^5.5.5 || ^6.0.3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" @@ -2426,9 +2426,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.34.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.34.0.tgz", - "integrity": "sha512-+Dp/xaI3IZ4eKv+b2vg4V89VnqLKbmJ7UZ7unnZxMu9SNLOSc2jYaXey1YHCJM+67T0pOr2Gbej3TewnuoqTWQ==", + "version": "1.38.3", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.38.3.tgz", + "integrity": "sha512-6tquYDfAiJbgQbYwWfL0jJHiUumY5EiFXVswk9sTwn5lWICMwOPmMOrM9TEVLzesfNMYwDyUiMp5WAA6yXs+SQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2632,9 +2632,9 @@ "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", "cpu": [ "ppc64" ], @@ -2649,9 +2649,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", "cpu": [ "arm" ], @@ -2666,9 +2666,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", "cpu": [ "arm64" ], @@ -2683,9 +2683,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", "cpu": [ "x64" ], @@ -2700,9 +2700,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", "cpu": [ "arm64" ], @@ -2717,9 +2717,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", "cpu": [ "x64" ], @@ -2734,9 +2734,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", "cpu": [ "arm64" ], @@ -2751,9 +2751,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", "cpu": [ "x64" ], @@ -2768,9 +2768,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", "cpu": [ "arm" ], @@ -2785,9 +2785,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", "cpu": [ "arm64" ], @@ -2802,9 +2802,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", "cpu": [ "ia32" ], @@ -2819,9 +2819,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", "cpu": [ "loong64" ], @@ -2836,9 +2836,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", "cpu": [ "mips64el" ], @@ -2853,9 +2853,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", "cpu": [ "ppc64" ], @@ -2870,9 +2870,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", "cpu": [ "riscv64" ], @@ -2887,9 +2887,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", "cpu": [ "s390x" ], @@ -2904,9 +2904,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", "cpu": [ "x64" ], @@ -2921,9 +2921,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", "cpu": [ "arm64" ], @@ -2938,9 +2938,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", "cpu": [ "x64" ], @@ -2955,9 +2955,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", "cpu": [ "arm64" ], @@ -2972,9 +2972,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", "cpu": [ "x64" ], @@ -2989,9 +2989,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", "cpu": [ "x64" ], @@ -3006,9 +3006,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", "cpu": [ "arm64" ], @@ -3023,9 +3023,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", "cpu": [ "ia32" ], @@ -3039,6 +3039,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -3142,9 +3159,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.19.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz", - "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", "dev": true, "license": "MIT", "engines": { @@ -3152,9 +3169,9 @@ } }, "node_modules/@fingerprintjs/fingerprintjs": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-4.5.1.tgz", - "integrity": "sha512-hKJaRoLHNeUUPhb+Md3pTlY/Js2YR4aXjroaDHpxrjoM8kGnEFyZVZxXo6l3gRyKnQN52Uoqsycd3M73eCdMzw==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@fingerprintjs/fingerprintjs/-/fingerprintjs-4.6.1.tgz", + "integrity": "sha512-62TPnX6fXXMlxS7SOR3DJWEOKab7rCALwSWkuKWYMRrnsZ/jD9Ju4CUyy9VWDUYuhQ2ZW1RGLwOZJXTXR6K1pg==", "license": "BUSL-1.1", "dependencies": { "tslib": "^2.4.1" @@ -4652,9 +4669,9 @@ "license": "MIT" }, "node_modules/@reduxjs/toolkit": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz", - "integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.0.tgz", + "integrity": "sha512-mWJCYpewLRyTuuzRSEC/IwIBBkYg2dKtQas8mty5MaV2iXzcmicS3gW554FDeOvLnY3x13NIk8MB1e8wHO7rqQ==", "license": "MIT", "dependencies": { "immer": "^10.0.3", @@ -4676,9 +4693,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", - "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -5294,9 +5311,9 @@ } }, "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.1.tgz", - "integrity": "sha512-tUp2e+CERpRFzTftjPxt7lg4BF0R3K+wGfeJyIqrc0tbJ2y6duT8OD0ArWoOi1g8xQ73NDn1/mEeS8pC+sbjTQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.2.tgz", + "integrity": "sha512-D+SKQ266ra/wo87s9+UI/rKQi3qhGPCR8eSCDe0VJudhjHsqyNU+JJ5lnIGCgmZaWFTXgdBP/gdr1Iz1zqGs4Q==", "license": "MIT", "engines": { "node": ">= 14" @@ -5319,13 +5336,13 @@ } }, "node_modules/@sentry/bundler-plugin-core": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.2.1.tgz", - "integrity": "sha512-1wId05LXf6LyTeNwqyhSDSWYbYtFT/NQRqq3sW7hcL4nZuAgzT82PSvxeeCgR/D2qXOj7RCYXXZtyWzzo3wtXA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.2.2.tgz", + "integrity": "sha512-YGrtmqQ2jMixccX2slVG/Lw7pCGJL3DGB3clmY9mO8QBEBIN3/gEANiHJVWwRidpUOS/0b7yVVGAdwZ87oPwTg==", "license": "MIT", "dependencies": { "@babel/core": "^7.18.5", - "@sentry/babel-plugin-component-annotate": "3.2.1", + "@sentry/babel-plugin-component-annotate": "3.2.2", "@sentry/cli": "2.42.2", "dotenv": "^16.3.1", "find-up": "^5.0.0", @@ -5529,12 +5546,12 @@ } }, "node_modules/@sentry/vite-plugin": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.2.1.tgz", - "integrity": "sha512-A/R9PAWPkWR6iqbJJ4C9BygcET0HAq5irEKy7xPmzB0mjW5XbDwbhQtHHnb6C1q/JrfzufB3TZWrG2XfrBRazg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.2.2.tgz", + "integrity": "sha512-WSkHOhZszMrIE9zmx2l4JhMnMlZmN/yAoHyf59pwFLIMctuZak6lNPbTbIFkFHDzIJ9Nut5RAVsw1qjmWc1PTA==", "license": "MIT", "dependencies": { - "@sentry/bundler-plugin-core": "3.2.1", + "@sentry/bundler-plugin-core": "3.2.2", "unplugin": "1.0.1" }, "engines": { @@ -5542,13 +5559,13 @@ } }, "node_modules/@sentry/webpack-plugin": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.2.1.tgz", - "integrity": "sha512-wP/JDljhB9pCFc62rSwWbIglF2Os8FLV68pQuyJnmImM9cjGjlK6UO+qKa2pOLYsmAcnn+t3Bhu77bbzPIStCg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.2.2.tgz", + "integrity": "sha512-6OkVKNOjKk8P9j7oh6svZ+kEP1i9YIHBC2aGWL2XsgeZTIrMBxJAXtOf+qSrfMAxEtibSroGVOMQc/y3WJTQtg==", "dev": true, "license": "MIT", "dependencies": { - "@sentry/bundler-plugin-core": "3.2.1", + "@sentry/bundler-plugin-core": "3.2.2", "unplugin": "1.0.1", "uuid": "^9.0.0" }, @@ -6542,16 +6559,16 @@ } }, "node_modules/antd": { - "version": "5.23.3", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.23.3.tgz", - "integrity": "sha512-xDvwl7C43/NZ9rTOS1bkbuKoSxqZKf6FlaSW/BRsV8QST3Ce2jGx7dJzYahKIZwe3WNSgvEXAlTrckBHMKHcgQ==", + "version": "5.24.2", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.24.2.tgz", + "integrity": "sha512-7Z9HsE3ZIK3sE/WuUqii3w7Gl1IJuRL21sDUTtkN95JS5KhRYP8ISv7m/HxsJ3Mn/yxgojBCgLPJ212+Dn+aPw==", "license": "MIT", "dependencies": { "@ant-design/colors": "^7.2.0", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", - "@ant-design/icons": "^5.6.0", + "@ant-design/icons": "^5.6.1", "@ant-design/react-slick": "~1.1.2", "@babel/runtime": "^7.26.0", "@rc-component/color-picker": "~2.0.1", @@ -6573,27 +6590,27 @@ "rc-input": "~1.7.2", "rc-input-number": "~9.4.0", "rc-mentions": "~2.19.1", - "rc-menu": "~9.16.0", + "rc-menu": "~9.16.1", "rc-motion": "^2.9.5", - "rc-notification": "~5.6.2", - "rc-pagination": "~5.0.0", - "rc-picker": "~4.9.2", + "rc-notification": "~5.6.3", + "rc-pagination": "~5.1.0", + "rc-picker": "~4.11.2", "rc-progress": "~4.0.0", - "rc-rate": "~2.13.0", + "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", "rc-select": "~14.16.6", "rc-slider": "~11.1.8", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", - "rc-table": "~7.50.2", - "rc-tabs": "~15.5.0", + "rc-table": "~7.50.3", + "rc-tabs": "~15.5.1", "rc-textarea": "~1.9.0", - "rc-tooltip": "~6.3.2", + "rc-tooltip": "~6.4.0", "rc-tree": "~5.13.0", "rc-tree-select": "~5.27.0", "rc-upload": "~4.8.1", - "rc-util": "^5.44.3", + "rc-util": "^5.44.4", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, @@ -7020,9 +7037,9 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -8713,9 +8730,9 @@ "license": "MIT" }, "node_modules/dayjs-business-days2": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/dayjs-business-days2/-/dayjs-business-days2-1.2.3.tgz", - "integrity": "sha512-xNT6cjlERxb2zAByQ+60BIQmI+Jn3q333mUF6+2wImSwC2m6XZmNI9Yj+8EKgKJ0TdRk5Z0EaAp7I0hSy76jGw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/dayjs-business-days2/-/dayjs-business-days2-1.3.0.tgz", + "integrity": "sha512-OgDBnsNmlk9+vmRQaP4yFisXs29WDk0ItUUctIagmO6OIoxhf4vArTov5i+G4vjT9Sz8NXOLMLrOVP0X0lG/Hw==", "license": "MIT", "dependencies": { "dayjs": "^1.11.13" @@ -9396,9 +9413,9 @@ } }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9409,48 +9426,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" - } - }, - "node_modules/esbuild/node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" } }, "node_modules/escalade": { @@ -10887,9 +10887,9 @@ "integrity": "sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==" }, "node_modules/globals": { - "version": "15.14.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", - "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", "engines": { @@ -11298,9 +11298,9 @@ } }, "node_modules/i18next-browser-languagedetector": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.2.tgz", - "integrity": "sha512-shBvPmnIyZeD2VU5jVGIOWP7u9qNG3Lj7mpaiPFpbJ3LVfHZJvVzKR4v1Cb91wAOFpNw442N+LGPzHOHsten2g==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.4.tgz", + "integrity": "sha512-f3frU3pIxD50/Tz20zx9TD9HobKYg47fmAETb117GKGPrhwcSSPJDoCposXlVycVebQ9GQohC3Efbpq7/nnJ5w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2" @@ -12486,9 +12486,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.19.tgz", - "integrity": "sha512-bW/Yp/9dod6fmyR+XqSUL1N5JE7QRxQ3KrBIbYS1FTv32e5i3SEtQVX+71CYNv8maWNSOgnlCoNp9X78f/cKiA==", + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.4.tgz", + "integrity": "sha512-vLmhg7Gan7idyAKfc6pvCtNzvar4/eIzrVVk3hjNFH5+fGqyjD0gQRovdTrDl20wsmZhBtmZpcsR0tOfquwb8g==", "license": "MIT" }, "node_modules/lines-and-columns": { @@ -14471,9 +14471,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -14491,8 +14491,8 @@ "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -14960,9 +14960,9 @@ } }, "node_modules/rc-menu": { - "version": "9.16.0", - "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.0.tgz", - "integrity": "sha512-vAL0yqPkmXWk3+YKRkmIR8TYj3RVdEt3ptG2jCJXWNAvQbT0VJJdRyHZ7kG/l1JsZlB+VJq/VcYOo69VR4oD+w==", + "version": "9.16.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.16.1.tgz", + "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -14993,9 +14993,9 @@ } }, "node_modules/rc-notification": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.2.tgz", - "integrity": "sha512-Id4IYMoii3zzrG0lB0gD6dPgJx4Iu95Xu0BQrhHIbp7ZnAZbLqdqQ73aIWH0d0UFcElxwaKjnzNovTjo7kXz7g==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.3.tgz", + "integrity": "sha512-42szwnn8VYQoT6GnjO00i1iwqV9D1TTMvxObWsuLwgl0TsOokzhkYiufdtQBsJMFjJravS1hfDKVMHLKLcPE4g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -15028,9 +15028,9 @@ } }, "node_modules/rc-pagination": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.0.0.tgz", - "integrity": "sha512-QjrPvbAQwps93iluvFM62AEYglGYhWW2q/nliQqmvkTi4PXP4HHoh00iC1Sa5LLVmtWQHmG73fBi2x6H6vFHRg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-5.1.0.tgz", + "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -15043,9 +15043,9 @@ } }, "node_modules/rc-picker": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.9.2.tgz", - "integrity": "sha512-SLW4PRudODOomipKI0dvykxW4P8LOqtMr17MOaLU6NQJhkh9SZeh44a/8BMxwv5T6e3kiIeYc9k5jFg2Mv35Pg==", + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.11.3.tgz", + "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.7", @@ -15097,9 +15097,9 @@ } }, "node_modules/rc-rate": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.0.tgz", - "integrity": "sha512-oxvx1Q5k5wD30sjN5tqAyWTvJfLNNJn7Oq3IeS4HxWfAiC4BOXMITNAsw7u/fzdtO4MS8Ki8uRLOzcnEuoQiAw==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.1.tgz", + "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -15220,9 +15220,9 @@ } }, "node_modules/rc-table": { - "version": "7.50.2", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.2.tgz", - "integrity": "sha512-+nJbzxzstBriLb5sr9U7Vjs7+4dO8cWlouQbMwBVYghk2vr508bBdkHJeP/z9HVjAIKmAgMQKxmtbgDd3gc5wA==", + "version": "7.50.3", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.3.tgz", + "integrity": "sha512-Z4/zNCzjv7f/XzPRecb+vJU0DJKdsYt4YRkDzNl4G05m7JmxrKGYC2KqN1Ew6jw2zJq7cxVv3z39qyZOHMuf7A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", @@ -15241,9 +15241,9 @@ } }, "node_modules/rc-tabs": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.0.tgz", - "integrity": "sha512-NrDcTaUJLh9UuDdMBkjKTn97U9iXG44s9D03V5NHkhEDWO5/nC6PwC3RhkCWFMKB9hh+ryqgZ+TIr1b9Jd/hnQ==", + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.5.1.tgz", + "integrity": "sha512-yiWivLAjEo5d1v2xlseB2dQocsOhkoVSfo1krS8v8r+02K+TBUjSjXIf7dgyVSxp6wRIPv5pMi5hanNUlQMgUA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.2", @@ -15280,14 +15280,15 @@ } }, "node_modules/rc-tooltip": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.3.2.tgz", - "integrity": "sha512-oA4HZIiZJbUQ5ojigM0y4XtWxaH/aQlJSzknjICRWNpqyemy1sL3X3iEQV2eSPBWEq+bqU3+aSs81z+28j9luA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.4.0.tgz", + "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/trigger": "^2.0.0", - "classnames": "^2.3.1" + "classnames": "^2.3.1", + "rc-util": "^5.44.3" }, "peerDependencies": { "react": ">=16.9.0", @@ -15347,9 +15348,9 @@ } }, "node_modules/rc-util": { - "version": "5.44.3", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.3.tgz", - "integrity": "sha512-q6KCcOFk3rv/zD3MckhJteZxb0VjAIFuf622B7ElK4vfrZdAzs16XR5p3VTdy3+U5jfJU5ACz4QnhLSuAGe5dA==", + "version": "5.44.4", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.44.4.tgz", + "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", @@ -15398,9 +15399,9 @@ } }, "node_modules/react-big-calendar": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.17.1.tgz", - "integrity": "sha512-LltUAMSGODWQBKx4013bRe6R0jaINV9hrs970+F860KedpozwRGGMT66esV9mA3mAhfSKoazF/QH1WCyLkXYZA==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/react-big-calendar/-/react-big-calendar-1.18.0.tgz", + "integrity": "sha512-bGrCdyfnCGe2qnIdEoGkGgQdEFOiGO1Tq7RLkI1a2t8ZudyEAKekFtneO2/ltKQEQK6zH76YdJ7vR9UMyD+ULw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.7", @@ -15421,8 +15422,8 @@ "uncontrollable": "^7.2.1" }, "peerDependencies": { - "react": "^16.14.0 || ^17 || ^18", - "react-dom": "^16.14.0 || ^17 || ^18" + "react": "^16.14.0 || ^17 || ^18 || ^19", + "react-dom": "^16.14.0 || ^17 || ^18 || ^19" } }, "node_modules/react-color": { @@ -15495,9 +15496,9 @@ } }, "node_modules/react-error-overlay": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz", + "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==", "dev": true, "license": "MIT" }, @@ -15550,9 +15551,9 @@ } }, "node_modules/react-icons": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.4.0.tgz", - "integrity": "sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", "license": "MIT", "peerDependencies": { "react": "*" @@ -15734,12 +15735,12 @@ } }, "node_modules/react-router": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.29.0.tgz", - "integrity": "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ==", + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.22.0" + "@remix-run/router": "1.23.0" }, "engines": { "node": ">=14.0.0" @@ -15749,13 +15750,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.29.0.tgz", - "integrity": "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ==", + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.22.0", - "react-router": "6.29.0" + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" }, "engines": { "node": ">=14.0.0" @@ -15811,16 +15812,13 @@ } }, "node_modules/react-virtuoso": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.12.3.tgz", - "integrity": "sha512-6X1p/sU7hecmjDZMAwN+r3go9EVjofKhwkUbVlL8lXhBZecPv9XVCkZ/kBPYOr0Mv0Vl5+Ziwgexg9Kh7+NNXQ==", + "version": "4.12.5", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.12.5.tgz", + "integrity": "sha512-YeCbRRsC9CLf0buD0Rct7WsDbzf+yBU1wGbo05/XjbcN2nJuhgh040m3y3+6HVogTZxEqVm45ac9Fpae4/MxRQ==", "license": "MIT", - "engines": { - "node": ">=10" - }, "peerDependencies": { - "react": ">=16 || >=17 || >= 18", - "react-dom": ">=16 || >=17 || >= 18" + "react": ">=16 || >=17 || >= 18 || >= 19", + "react-dom": ">=16 || >=17 || >= 18 || >=19" } }, "node_modules/reactcss": { @@ -16535,9 +16533,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", - "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", + "version": "1.85.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz", + "integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==", "license": "MIT", "dependencies": { "chokidar": "^4.0.0", @@ -17449,9 +17447,9 @@ } }, "node_modules/styled-components": { - "version": "6.1.14", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.14.tgz", - "integrity": "sha512-KtfwhU5jw7UoxdM0g6XU9VZQFV4do+KrM8idiVCH5h4v49W+3p3yMe0icYwJgZQZepa5DbH04Qv8P0/RdcLcgg==", + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.15.tgz", + "integrity": "sha512-PpOTEztW87Ua2xbmLa7yssjNyUF9vE7wdldRfn1I2E6RTkqknkBYpj771OxM/xrvRGinLy2oysa7GOd7NcZZIA==", "license": "MIT", "dependencies": { "@emotion/is-prop-valid": "1.2.2", @@ -17459,7 +17457,7 @@ "@types/stylis": "4.2.5", "css-to-react-native": "3.2.0", "csstype": "3.1.3", - "postcss": "8.4.38", + "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.2", "tslib": "2.6.2" @@ -17598,9 +17596,9 @@ } }, "node_modules/swr": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz", - "integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.2.tgz", + "integrity": "sha512-RosxFpiabojs75IwQ316DGoDRmOqtiAj0tg8wCcbEu4CiLZBs/a9QNtHV7TUfDXmmlgqij/NqzKq/eLelyv9xA==", "license": "MIT", "dependencies": { "dequal": "^2.0.3", @@ -18580,9 +18578,9 @@ } }, "node_modules/userpilot": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/userpilot/-/userpilot-1.3.6.tgz", - "integrity": "sha512-NK/4sQTnWrpER164PkWzLdLjUc2766B4yeLdLiFDoRfyLNAc3SecLWszZH6oPlv67B+XcYzqtmzEalE86bkljw==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/userpilot/-/userpilot-1.3.8.tgz", + "integrity": "sha512-Hoym2S7j5IvGzb3n/eOwX3FE5PgzMjk5148uU1WTNM5iw784u6+LZiu3DC7NuovVrwYzI99qy5Ossqmft9c74A==", "license": "MIT", "dependencies": { "@ndhoule/includes": "^2.0.1", @@ -18693,15 +18691,15 @@ } }, "node_modules/vite": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.11.tgz", - "integrity": "sha512-4VL9mQPKoHy4+FE0NnRE/kbY51TOfaknxAjt3fJbGJxhIpBZiqVzlZDEesWWsuREXHwNdAoOFZ9MkPEVXczHwg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" }, "bin": { "vite": "bin/vite.js" @@ -18925,9 +18923,9 @@ } }, "node_modules/vite/node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { diff --git a/client/package.json b/client/package.json index e3576dfb1..e54a6a098 100644 --- a/client/package.json +++ b/client/package.json @@ -8,27 +8,27 @@ "private": true, "proxy": "http://localhost:4000", "dependencies": { - "@ant-design/pro-layout": "^7.22.0", - "@apollo/client": "^3.12.6", + "@ant-design/pro-layout": "^7.22.3", + "@apollo/client": "^3.13.1", "@emotion/is-prop-valid": "^1.3.1", - "@fingerprintjs/fingerprintjs": "^4.5.1", + "@fingerprintjs/fingerprintjs": "^4.6.1", "@jsreport/browser-client": "^3.1.0", - "@reduxjs/toolkit": "^2.5.0", + "@reduxjs/toolkit": "^2.6.0", "@sentry/cli": "^2.42.2", "@sentry/react": "^9.3.0", - "@sentry/vite-plugin": "^3.2.1", + "@sentry/vite-plugin": "^3.2.2", "@splitsoftware/splitio-react": "^1.13.0", "@tanem/react-nprogress": "^5.0.53", "@vitejs/plugin-react": "^4.3.4", - "antd": "^5.23.1", + "antd": "^5.24.2", "apollo-link-logger": "^2.0.1", "apollo-link-sentry": "^4.1.0", "autosize": "^6.0.1", - "axios": "^1.7.9", + "axios": "^1.8.1", "classnames": "^2.5.1", "css-box-model": "^1.2.1", "dayjs": "^1.11.13", - "dayjs-business-days2": "^1.2.3", + "dayjs-business-days2": "^1.3.0", "dinero.js": "^1.9.1", "dotenv": "^16.4.7", "env-cmd": "^10.1.0", @@ -36,9 +36,9 @@ "firebase": "^10.13.2", "graphql": "^16.10.0", "i18next": "^23.15.1", - "i18next-browser-languagedetector": "^8.0.2", + "i18next-browser-languagedetector": "^8.0.4", "immutability-helper": "^3.1.1", - "libphonenumber-js": "^1.11.18", + "libphonenumber-js": "^1.12.4", "logrocket": "^8.1.2", "markerjs2": "^2.32.3", "memoize-one": "^6.0.0", @@ -48,7 +48,7 @@ "query-string": "^9.1.1", "raf-schd": "^4.0.3", "react": "^18.3.1", - "react-big-calendar": "^1.17.1", + "react-big-calendar": "^1.18.0", "react-color": "^2.19.3", "react-cookie": "^7.2.2", "react-dom": "^18.3.1", @@ -56,7 +56,7 @@ "react-grid-gallery": "^1.0.1", "react-grid-layout": "1.3.4", "react-i18next": "^14.1.3", - "react-icons": "^5.4.0", + "react-icons": "^5.5.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^9.0.3", "react-number-format": "^5.4.3", @@ -64,9 +64,9 @@ "react-product-fruits": "^2.2.61", "react-redux": "^9.2.0", "react-resizable": "^3.0.5", - "react-router-dom": "^6.26.2", + "react-router-dom": "^6.30.0", "react-sticky": "^6.0.3", - "react-virtuoso": "^4.10.4", + "react-virtuoso": "^4.12.5", "recharts": "^2.15.0", "redux": "^5.0.1", "redux-actions": "^3.0.3", @@ -74,12 +74,12 @@ "redux-saga": "^1.3.0", "redux-state-sync": "^3.1.4", "reselect": "^5.1.1", - "sass": "^1.83.4", + "sass": "^1.85.1", "socket.io-client": "^4.8.1", - "styled-components": "^6.1.14", + "styled-components": "^6.1.15", "subscriptions-transport-ws": "^0.11.0", "use-memo-one": "^1.1.3", - "userpilot": "^1.3.6", + "userpilot": "^1.3.8", "vite-plugin-ejs": "^1.7.0", "web-vitals": "^3.5.2" }, @@ -120,14 +120,14 @@ "@rollup/rollup-linux-x64-gnu": "4.6.1" }, "devDependencies": { - "@ant-design/icons": "^5.5.2", + "@ant-design/icons": "^5.6.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.26.3", - "@dotenvx/dotenvx": "^1.33.0", + "@dotenvx/dotenvx": "^1.38.3", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", - "@eslint/js": "^9.18.0", - "@sentry/webpack-plugin": "^3.2.1", + "@eslint/js": "^9.21.0", + "@sentry/webpack-plugin": "^3.2.2", "@testing-library/cypress": "^10.0.2", "browserslist": "^4.24.4", "browserslist-to-esbuild": "^2.1.1", @@ -138,13 +138,13 @@ "eslint-config-react-app": "^7.0.1", "eslint-plugin-cypress": "^2.15.1", "eslint-plugin-react": "^7.37.4", - "globals": "^15.14.0", + "globals": "^15.15.0", "memfs": "^4.17.0", "os-browserify": "^0.3.0", - "react-error-overlay": "6.0.11", + "react-error-overlay": "^6.1.0", "redux-logger": "^3.0.6", "source-map-explorer": "^2.5.3", - "vite": "^6.0.7", + "vite": "^6.2.0", "vite-plugin-babel": "^1.3.0", "vite-plugin-eslint": "^1.8.1", "vite-plugin-node-polyfills": "^0.23.0", diff --git a/client/src/App/App.jsx b/client/src/App/App.jsx index 26f44fc8d..af3c906a8 100644 --- a/client/src/App/App.jsx +++ b/client/src/App/App.jsx @@ -1,10 +1,10 @@ import { useSplitClient } from "@splitsoftware/splitio-react"; import { Button, Result } from "antd"; import LogRocket from "logrocket"; -import React, { lazy, Suspense, useEffect, useState } from "react"; +import { lazy, Suspense, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; -import { Route, Routes } from "react-router-dom"; +import { Route, Routes, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import DocumentEditorContainer from "../components/document-editor/document-editor.container"; import ErrorBoundary from "../components/error-boundary/error-boundary.component"; // Component Imports @@ -21,7 +21,7 @@ import "./App.styles.scss"; import Eula from "../components/eula/eula.component"; import InstanceRenderMgr from "../utils/instanceRenderMgr"; import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx"; -import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx"; +import { SocketProvider } from "../contexts/SocketIO/useSocket.jsx"; import { NotificationProvider } from "../contexts/Notifications/notificationContext.jsx"; const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component")); @@ -46,6 +46,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline const client = useSplitClient().client; const [listenersAdded, setListenersAdded] = useState(false); const { t } = useTranslation(); + const navigate = useNavigate(); + + const scenarioNotificationsOn = client?.getTreatment("Realtime_Notifications_UI") === "on"; useEffect(() => { if (!navigator.onLine) { @@ -200,7 +203,12 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline path="/manage/*" element={ - + @@ -212,7 +220,12 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline path="/tech/*" element={ - + diff --git a/client/src/App/App.styles.scss b/client/src/App/App.styles.scss index a2cd5bea6..9ea0a8d24 100644 --- a/client/src/App/App.styles.scss +++ b/client/src/App/App.styles.scss @@ -180,3 +180,13 @@ .muted-button:hover { color: darkgrey; } + +.notification-alert-unordered-list { + cursor: pointer; + padding: 0; + margin: 0; + + .notification-alert-unordered-list-item { + margin-right: 0; + } +} diff --git a/client/src/components/chat-affix/chat-affix.container.jsx b/client/src/components/chat-affix/chat-affix.container.jsx index ec3c79cd7..c757598c6 100644 --- a/client/src/components/chat-affix/chat-affix.container.jsx +++ b/client/src/components/chat-affix/chat-affix.container.jsx @@ -1,9 +1,9 @@ import { useApolloClient } from "@apollo/client"; import { getToken } from "@firebase/messaging"; import axios from "axios"; -import React, { useContext, useEffect } from "react"; +import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import SocketContext from "../../contexts/SocketIO/socketContext"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; import { messaging, requestForToken } from "../../firebase/firebase.utils"; import ChatPopupComponent from "../chat-popup/chat-popup.component"; import "./chat-affix.styles.scss"; @@ -12,7 +12,7 @@ import { registerMessagingHandlers, unregisterMessagingHandlers } from "./regist export function ChatAffixContainer({ bodyshop, chatVisible }) { const { t } = useTranslation(); const client = useApolloClient(); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); useEffect(() => { if (!bodyshop || !bodyshop.messagingservicesid) return; diff --git a/client/src/components/chat-archive-button/chat-archive-button.component.jsx b/client/src/components/chat-archive-button/chat-archive-button.component.jsx index 66d25b787..24eb9cea4 100644 --- a/client/src/components/chat-archive-button/chat-archive-button.component.jsx +++ b/client/src/components/chat-archive-button/chat-archive-button.component.jsx @@ -1,9 +1,9 @@ import { useMutation } from "@apollo/client"; import { Button } from "antd"; -import React, { useContext, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors.js"; import { connect } from "react-redux"; @@ -18,7 +18,7 @@ export function ChatArchiveButton({ conversation, bodyshop }) { const [loading, setLoading] = useState(false); const { t } = useTranslation(); const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const handleToggleArchive = async () => { setLoading(true); diff --git a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx index 4203e8f5b..e85ed6270 100644 --- a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx +++ b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx @@ -1,11 +1,10 @@ import { useMutation } from "@apollo/client"; import { Tag } from "antd"; -import React, { useContext } from "react"; import { Link } from "react-router-dom"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors.js"; import { connect } from "react-redux"; @@ -18,7 +17,7 @@ const mapDispatchToProps = () => ({}); export function ChatConversationTitleTags({ jobConversations, bodyshop }) { const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const handleRemoveTag = async (jobId) => { const convId = jobConversations[0].conversationid; diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 4a2992f6d..853c4851f 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -1,10 +1,10 @@ import { gql, useApolloClient, useQuery, useSubscription } from "@apollo/client"; import axios from "axios"; -import React, { useCallback, useContext, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; -import SocketContext from "../../contexts/SocketIO/socketContext"; -import { GET_CONVERSATION_DETAILS, CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; +import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import ChatConversationComponent from "./chat-conversation.component"; @@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({ function ChatConversationContainer({ bodyshop, selectedConversation }) { const client = useApolloClient(); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false); // Fetch conversation details diff --git a/client/src/components/chat-label/chat-label.component.jsx b/client/src/components/chat-label/chat-label.component.jsx index ddd367798..6fccd1390 100644 --- a/client/src/components/chat-label/chat-label.component.jsx +++ b/client/src/components/chat-label/chat-label.component.jsx @@ -1,10 +1,10 @@ import { PlusOutlined } from "@ant-design/icons"; import { useMutation } from "@apollo/client"; import { Input, Spin, Tag, Tooltip } from "antd"; -import React, { useContext, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors.js"; import { connect } from "react-redux"; @@ -20,7 +20,7 @@ export function ChatLabel({ conversation, bodyshop }) { const [loading, setLoading] = useState(false); const [editing, setEditing] = useState(false); const [value, setValue] = useState(conversation.label); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const notification = useNotification(); const { t } = useTranslation(); diff --git a/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx b/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx index e45ffb9b6..8806974bf 100644 --- a/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx +++ b/client/src/components/chat-new-conversation/chat-new-conversation.component.jsx @@ -1,12 +1,11 @@ import { PlusCircleFilled } from "@ant-design/icons"; import { Button, Form, Popover } from "antd"; -import React, { useContext } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { openChatByPhone } from "../../redux/messaging/messaging.actions"; import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -18,7 +17,7 @@ const mapDispatchToProps = (dispatch) => ({ export function ChatNewConversation({ openChatByPhone }) { const { t } = useTranslation(); const [form] = Form.useForm(); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const handleFinish = (values) => { openChatByPhone({ phone_num: values.phoneNumber, socket }); diff --git a/client/src/components/chat-open-button/chat-open-button.component.jsx b/client/src/components/chat-open-button/chat-open-button.component.jsx index 42cd5dc82..68b9cd8b2 100644 --- a/client/src/components/chat-open-button/chat-open-button.component.jsx +++ b/client/src/components/chat-open-button/chat-open-button.component.jsx @@ -1,5 +1,4 @@ import parsePhoneNumber from "libphonenumber-js"; -import React, { useContext } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { openChatByPhone } from "../../redux/messaging/messaging.actions"; @@ -8,7 +7,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import { searchingForConversation } from "../../redux/messaging/messaging.selectors"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; const mapStateToProps = createStructuredSelector({ @@ -22,7 +21,7 @@ const mapDispatchToProps = (dispatch) => ({ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) { const { t } = useTranslation(); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const notification = useNotification(); if (!phone) return <>; diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index 0226cc354..2ae16b129 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -1,7 +1,7 @@ import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons"; import { useApolloClient, useLazyQuery, useQuery } from "@apollo/client"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; -import React, { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -12,8 +12,9 @@ import ChatConversationListComponent from "../chat-conversation-list/chat-conver import ChatConversationContainer from "../chat-conversation/chat-conversation.container"; import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; + import "./chat-popup.styles.scss"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; const mapStateToProps = createStructuredSelector({ selectedConversation: selectSelectedConversation, @@ -27,7 +28,7 @@ const mapDispatchToProps = (dispatch) => ({ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) { const { t } = useTranslation(); const [pollInterval, setPollInterval] = useState(0); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const client = useApolloClient(); // Apollo Client instance for cache operations // Lazy query for conversations diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx index 63839b263..2a12d96db 100644 --- a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx +++ b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx @@ -2,13 +2,13 @@ import { PlusOutlined } from "@ant-design/icons"; import { useLazyQuery, useMutation } from "@apollo/client"; import { Tag } from "antd"; import _ from "lodash"; -import React, { useContext, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries"; import ChatTagRo from "./chat-tag-ro.component"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors.js"; import { connect } from "react-redux"; @@ -22,7 +22,7 @@ const mapDispatchToProps = () => ({}); export function ChatTagRoContainer({ conversation, bodyshop }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS); diff --git a/client/src/components/error-boundary/error-boundary.component.jsx b/client/src/components/error-boundary/error-boundary.component.jsx index 13525d7fe..ef6020ccc 100644 --- a/client/src/components/error-boundary/error-boundary.component.jsx +++ b/client/src/components/error-boundary/error-boundary.component.jsx @@ -123,7 +123,7 @@ class ErrorBoundary extends React.Component { - +
{this.state.error.message}
diff --git a/client/src/components/eula/eula.component.jsx b/client/src/components/eula/eula.component.jsx index 389911961..ae503b09a 100644 --- a/client/src/components/eula/eula.component.jsx +++ b/client/src/components/eula/eula.component.jsx @@ -78,9 +78,7 @@ const Eula = ({ currentEula, currentUser, acceptEula }) => { } catch (err) { notification.error({ message: t("eula.errors.acceptance.message"), - description: t("eula.errors.acceptance.description"), - placement: "bottomRight", - duration: 5000 + description: t("eula.errors.acceptance.description") }); console.log(`${t("eula.errors.acceptance.message")}`); console.dir({ diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index edd444fd5..a5cc39e21 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -1,6 +1,19 @@ -import Icon, { +import { Badge, Layout, Menu, Spin } from "antd"; +import { useTranslation } from "react-i18next"; +import { useEffect, useState } from "react"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { Link } from "react-router-dom"; +import { useQuery } from "@apollo/client"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; +import NotificationCenterContainer from "../notification-center/notification-center.container.jsx"; +import LockWrapper from "../lock-wrapper/lock-wrapper.component"; +import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; +import { BankFilled, BarChartOutlined, + BellFilled, CarFilled, CheckCircleOutlined, ClockCircleFilled, @@ -25,26 +38,21 @@ import Icon, { UnorderedListOutlined, UserOutlined } from "@ant-design/icons"; -import { useSplitTreatments } from "@splitsoftware/splitio-react"; -import { Layout, Menu, Space } from "antd"; -import { useTranslation } from "react-i18next"; import { BsKanban } from "react-icons/bs"; import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa"; import { FiLogOut } from "react-icons/fi"; import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi"; import { IoBusinessOutline } from "react-icons/io5"; import { RiSurveyLine } from "react-icons/ri"; -import { connect } from "react-redux"; -import { Link } from "react-router-dom"; -import { createStructuredSelector } from "reselect"; +import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js"; import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; import { setModalContext } from "../../redux/modals/modals.actions"; import { signOutStart } from "../../redux/user/user.actions"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; -import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; -import LockWrapper from "../lock-wrapper/lock-wrapper.component"; +import day from "../../utils/day.js"; +// Redux mappings const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser, recentItems: selectRecentItems, @@ -53,43 +61,13 @@ const mapStateToProps = createStructuredSelector({ }); const mapDispatchToProps = (dispatch) => ({ - setBillEnterContext: (context) => - dispatch( - setModalContext({ - context: context, - modal: "billEnter" - }) - ), - setTimeTicketContext: (context) => - dispatch( - setModalContext({ - context: context, - modal: "timeTicket" - }) - ), - setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })), - setReportCenterContext: (context) => - dispatch( - setModalContext({ - context: context, - modal: "reportCenter" - }) - ), + setBillEnterContext: (context) => dispatch(setModalContext({ context, modal: "billEnter" })), + setTimeTicketContext: (context) => dispatch(setModalContext({ context, modal: "timeTicket" })), + setPaymentContext: (context) => dispatch(setModalContext({ context, modal: "payment" })), + setReportCenterContext: (context) => dispatch(setModalContext({ context, modal: "reportCenter" })), signOutStart: () => dispatch(signOutStart()), - setCardPaymentContext: (context) => - dispatch( - setModalContext({ - context: context, - modal: "cardPayment" - }) - ), - setTaskUpsertContext: (context) => - dispatch( - setModalContext({ - context: context, - modal: "taskUpsert" - }) - ) + setCardPaymentContext: (context) => dispatch(setModalContext({ context, modal: "cardPayment" })), + setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" })) }); function Header({ @@ -115,24 +93,46 @@ function Header({ }); const { t } = useTranslation(); + const { isConnected, scenarioNotificationsOn } = useSocket(); + const [notificationVisible, setNotificationVisible] = useState(false); - // const deleteBetaCookie = () => { - // const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`)); - // if (cookieExists) { - // const domain = window.location.hostname.split(".").slice(-2).join("."); - // document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`; - // } - // }; - // - // deleteBetaCookie(); + const userAssociationId = bodyshop?.associations?.[0]?.id; - const accountingChildren = []; + const { + data: unreadData, + refetch: refetchUnread, + loading: unreadLoading + } = useQuery(GET_UNREAD_COUNT, { + variables: { associationid: userAssociationId }, + fetchPolicy: "network-only", + pollInterval: isConnected ? 0 : day.duration(60, "seconds").asMilliseconds(), + skip: !userAssociationId || !scenarioNotificationsOn + }); - accountingChildren.push( + const unreadCount = unreadData?.notifications_aggregate?.aggregate?.count ?? 0; + + useEffect(() => { + if (userAssociationId) { + refetchUnread().catch((e) => console.error(`Error fetching unread notifications: ${e?.message}`)); + } + }, [refetchUnread, userAssociationId]); + + useEffect(() => { + if (!isConnected && !unreadLoading && userAssociationId) { + refetchUnread().catch((e) => console.error(`Error fetching unread notifications: ${e?.message}`)); + } + }, [isConnected, unreadLoading, refetchUnread, userAssociationId]); + + const handleNotificationClick = (e) => { + setNotificationVisible(!notificationVisible); + if (handleMenuClick) handleMenuClick(e); + }; + + const accountingChildren = [ { key: "bills", id: "header-accounting-bills", - icon: , + icon: , label: ( @@ -144,42 +144,31 @@ function Header({ { key: "enterbills", id: "header-accounting-enterbills", - icon: , + icon: , label: ( - - - {t("menus.header.enterbills")} - - + + {t("menus.header.enterbills")} + ), - onClick: () => { + onClick: () => HasFeatureAccess({ featureName: "bills", bodyshop }) && - setBillEnterContext({ - actions: {}, - context: {} - }); - } - } - ); - - if (Simple_Inventory.treatment === "on") { - accountingChildren.push( - { - type: "divider" - }, - { - key: "inventory", - id: "header-accounting-inventory", - icon: , - label: {t("menus.header.inventory")} - } - ); - } - - accountingChildren.push( - { - type: "divider" + setBillEnterContext({ + actions: {}, + context: {} + }) }, + ...(Simple_Inventory.treatment === "on" + ? [ + { type: "divider" }, + { + key: "inventory", + id: "header-accounting-inventory", + icon: , + label: {t("menus.header.inventory")} + } + ] + : []), + { type: "divider" }, { key: "allpayments", id: "header-accounting-allpayments", @@ -195,41 +184,31 @@ function Header({ { key: "enterpayments", id: "header-accounting-enterpayments", - icon: , + icon: , label: ( {t("menus.header.enterpayment")} ), - onClick: () => { + onClick: () => HasFeatureAccess({ featureName: "payments", bodyshop }) && - setPaymentContext({ - actions: {}, - context: null - }); - } - } - ); - - if (ImEXPay.treatment === "on") { - accountingChildren.push({ - key: "entercardpayments", - id: "header-accounting-entercardpayments", - icon: , - label: t("menus.header.entercardpayment"), - onClick: () => { - setCardPaymentContext({ + setPaymentContext({ actions: {}, - context: {} - }); - } - }); - } - - accountingChildren.push( - { - type: "divider" + context: null + }) }, + ...(ImEXPay.treatment === "on" + ? [ + { + key: "entercardpayments", + id: "header-accounting-entercardpayments", + icon: , + label: t("menus.header.entercardpayment"), + onClick: () => setCardPaymentContext({ actions: {}, context: {} }) + } + ] + : []), + { type: "divider" }, { key: "timetickets", id: "header-accounting-timetickets", @@ -241,132 +220,124 @@ function Header({ ) - } - ); - - if (bodyshop?.md_tasks_presets?.use_approvals) { - accountingChildren.push({ - key: "ttapprovals", - id: "header-accounting-ttapprovals", - icon: , - label: {t("menus.header.ttapprovals")} - }); - } - accountingChildren.push( + }, + ...(bodyshop?.md_tasks_presets?.use_approvals + ? [ + { + key: "ttapprovals", + id: "header-accounting-ttapprovals", + icon: , + label: {t("menus.header.ttapprovals")} + } + ] + : []), { key: "entertimetickets", - icon: , + id: "header-accounting-entertimetickets", + icon: , label: ( {t("menus.header.entertimeticket")} ), - id: "header-accounting-entertimetickets", - onClick: () => { + onClick: () => HasFeatureAccess({ featureName: "timetickets", bodyshop }) && - setTimeTicketContext({ - actions: {}, - context: { - created_by: currentUser.displayName - ? currentUser.email.concat(" | ", currentUser.displayName) - : currentUser.email - } - }); - } + setTimeTicketContext({ + actions: {}, + context: { + created_by: currentUser.displayName + ? `${currentUser.email} | ${currentUser.displayName}` + : currentUser.email + } + }) }, + { type: "divider" }, { - type: "divider" - } - ); - - const accountingExportChildren = [ - { - key: "receivables", - id: "header-accounting-receivables", + key: "accountingexport", + id: "header-accounting-export", + icon: , label: ( - - - {t("menus.header.accounting-receivables")} - - - ) + + {t("menus.header.export")} + + ), + children: [ + { + key: "receivables", + id: "header-accounting-receivables", + label: ( + + + {t("menus.header.accounting-receivables")} + + + ) + }, + ...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || + DmsAp.treatment === "on" + ? [ + { + key: "payables", + id: "header-accounting-payables", + label: ( + + + {t("menus.header.accounting-payables")} + + + ) + } + ] + : []), + ...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) + ? [ + { + key: "payments", + id: "header-accounting-payments", + label: ( + + + {t("menus.header.accounting-payments")} + + + ) + } + ] + : []), + { type: "divider" }, + { + key: "exportlogs", + id: "header-accounting-exportlogs", + label: ( + + + {t("menus.header.export-logs")} + + + ) + } + ] } ]; - if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") { - accountingExportChildren.push({ - key: "payables", - id: "header-accounting-payables", - label: ( - - - {t("menus.header.accounting-payables")} - - - ) - }); - } - - if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) { - accountingExportChildren.push({ - key: "payments", - id: "header-accounting-payments", - label: ( - - - {t("menus.header.accounting-payments")} - - - ) - }); - } - - accountingExportChildren.push( - { - type: "divider" - }, - { - key: "exportlogs", - id: "header-accounting-exportlogs", - label: ( - - - {t("menus.header.export-logs")} - - - ) - } - ); - - accountingChildren.push({ - key: "accountingexport", - id: "header-accounting-export", - icon: , - label: ( - - {t("menus.header.export")} - - ), - children: accountingExportChildren - }); - - const menuItems = [ + // Left menu items (includes original navigation items) + const leftMenuItems = [ { key: "home", - icon: , id: "header-home", + icon: , label: {t("menus.header.home")} }, { key: "schedule", id: "header-schedule", - icon: , + icon: , label: {t("menus.header.schedule")} }, { key: "jobssubmenu", id: "header-jobs", - icon: , + icon: , label: t("menus.header.jobs"), children: [ { @@ -399,31 +370,24 @@ function Header({ icon: , label: {t("menus.header.newjob")} }, - { - type: "divider", - id: "header-jobs-divider" - }, + { type: "divider" }, { key: "alljobs", id: "header-all-jobs", icon: , label: {t("menus.header.alljobs")} }, - { - type: "divider", - id: "header-jobs-divider2" - }, + { type: "divider" }, { key: "productionlist", id: "header-production-list", icon: , label: {t("menus.header.productionlist")} }, - { key: "productionboard", id: "header-production-board", - icon: , + icon: , label: ( @@ -432,11 +396,7 @@ function Header({ ) }, - - { - type: "divider", - id: "header-jobs-divider3" - }, + { type: "divider" }, { key: "scoreboard", id: "header-scoreboard", @@ -453,8 +413,8 @@ function Header({ }, { key: "customers", - icon: , id: "header-customers", + icon: , label: t("menus.header.customers"), children: [ { @@ -519,7 +479,6 @@ function Header({ } ] }, - ...(accountingChildren.length > 0 ? [ { @@ -537,7 +496,6 @@ function Header({ icon: , label: {t("menus.header.phonebook")} }, - { key: "temporarydocs", id: "header-temporarydocs", @@ -550,7 +508,6 @@ function Header({ ) }, - { key: "tasks", id: "tasks", @@ -562,12 +519,7 @@ function Header({ id: "header-create-task", icon: , label: t("menus.header.create_task"), - onClick: () => { - setTaskUpsertContext({ - actions: {}, - context: {} - }); - } + onClick: () => setTaskUpsertContext({ actions: {}, context: {} }) }, { key: "mytasks", @@ -592,7 +544,7 @@ function Header({ { key: "shop", id: "header-shop", - icon: , + icon: , label: {t("menus.header.shop_config")} }, { @@ -610,24 +562,18 @@ function Header({ id: "header-reportcenter", icon: , label: t("menus.header.reportcenter"), - onClick: () => { - setReportCenterContext({ - actions: {}, - context: {} - }); - } + onClick: () => setReportCenterContext({ actions: {}, context: {} }) }, { key: "shop-vendors", id: "header-shop-vendors", - icon: , + icon: , label: {t("menus.header.shop_vendors")} }, - { key: "shop-csi", id: "header-shop-csi", - icon: , + icon: , label: ( @@ -638,14 +584,25 @@ function Header({ } ] }, + { + key: "recent", + id: "header-recent", + icon: , + children: recentItems.map((i, idx) => ({ + key: idx, + id: `header-recent-${idx}`, + label: {i.label} + })) + }, { key: "user", - label: currentUser.displayName || currentUser.email || t("general.labels.unknown"), + id: "header-user", + icon: , children: [ { key: "signout", id: "header-signout", - icon: , + icon: , danger: true, label: t("user.actions.signout"), onClick: () => signOutStart() @@ -653,33 +610,25 @@ function Header({ { key: "help", id: "header-help", - icon: , + icon: , label: t("menus.header.help"), - onClick: () => { - window.open("https://help.imex.online/", "_blank"); - } + onClick: () => window.open("https://help.imex.online/", "_blank") }, - ...(InstanceRenderManager({ - imex: true, - rome: false - }) + ...(InstanceRenderManager({ imex: true, rome: false }) ? [ { key: "rescue", id: "header-rescue", - icon: , + icon: , label: t("menus.header.rescueme"), - onClick: () => { - window.open("https://imexrescue.com/", "_blank"); - } + onClick: () => window.open("https://imexrescue.com/", "_blank") } ] : []), - { key: "shiftclock", id: "header-shiftclock", - icon: , + icon: , label: ( @@ -688,64 +637,79 @@ function Header({ ) }, - { key: "profile", id: "header-profile", icon: , label: {t("menus.currentuser.profile")} } - // { - // key: 'langselecter', - // label: t("menus.currentuser.languageselector"), - // children: [ - // { - // key: 'en-US', - // label: t("general.languages.english"), - // onClick: () => { - // window.location.href = "/?lang=en-US"; - // } - // }, - // { - // key: 'fr-CA', - // label: t("general.languages.french"), - // onClick: () => { - // window.location.href = "/?lang=fr-CA"; - // } - // }, - // { - // key: 'es-MX', - // label: t("general.languages.spanish"), - // onClick: () => { - // window.location.href = "/?lang=es-MX"; - // } - // }, - // ] - // }, ] - }, - { - key: "recent", - icon: , - id: "header-recent", - children: recentItems.map((i, idx) => ({ - key: idx, - id: `header-recent-${idx}`, - label: {i.label} - })) } ]; + // Notifications item (always on the right) + const notificationItem = scenarioNotificationsOn + ? [ + { + key: "notifications", + id: "header-notifications", + icon: unreadLoading ? ( + + ) : ( + + + + ), + onClick: handleNotificationClick + } + ] + : []; + return ( - - + +
+ + {scenarioNotificationsOn && ( + + )} +
+ {scenarioNotificationsOn && ( + setNotificationVisible(false)} + unreadCount={unreadCount} + /> + )}
); } diff --git a/client/src/components/header/header.container.jsx b/client/src/components/header/header.container.jsx index 6920348ed..4bdb1e015 100644 --- a/client/src/components/header/header.container.jsx +++ b/client/src/components/header/header.container.jsx @@ -1,30 +1,7 @@ import { connect } from "react-redux"; import HeaderComponent from "./header.component"; -// const mapDispatchToProps = (dispatch) => ({ -// setUserLanguage: (language) => dispatch(setUserLanguage(language)) -// }); - -// setUserLanguage was removed from signature because it is not used in the component, and it is throwing a deprecation warning export function HeaderContainer() { - // Commented out the handleMenuClick function because it is not used in the component, and it is throwing a deprecation warning - - /* const handleMenuClick = (e) => { - if (e.item.props.actiontype === "lang-select") { - i18next.changeLanguage(e.key, (err, t) => { - if (err) { - logImEXEvent("language_change_error", { error: err }); - - return console.log("Error encountered when changing languages.", err); - } - logImEXEvent("language_change", { language: e.key }); - - setUserLanguage(e.key); - }); - } - };*/ - // return ; - return ; } 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 97eded476..eb53dffd4 100644 --- a/client/src/components/job-at-change/schedule-event.component.jsx +++ b/client/src/components/job-at-change/schedule-event.component.jsx @@ -3,12 +3,12 @@ import { useMutation } from "@apollo/client"; import { Button, Divider, Dropdown, Form, Input, Popover, Select, Space } from "antd"; import parsePhoneNumber from "libphonenumber-js"; import queryString from "query-string"; -import React, { useContext, useState } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries"; import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions"; import { setModalContext } from "../../redux/modals/modals.actions"; @@ -51,7 +51,7 @@ export function ScheduleEventComponent({ const searchParams = queryString.parse(useLocation().search); const [updateAppointment] = useMutation(UPDATE_APPOINTMENT); const [title, setTitle] = useState(event.title); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const notification = useNotification(); const blockContent = ( diff --git a/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx b/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx index 6beffacb3..0ec9a0311 100644 --- a/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx +++ b/client/src/components/job-close-ro-guard/job-close-ro-guard.container.jsx @@ -216,7 +216,7 @@ export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) { - + diff --git a/client/src/components/job-totals-table/job-totals-table.component.jsx b/client/src/components/job-totals-table/job-totals-table.component.jsx index e475ef5fd..8f390105d 100644 --- a/client/src/components/job-totals-table/job-totals-table.component.jsx +++ b/client/src/components/job-totals-table/job-totals-table.component.jsx @@ -69,7 +69,7 @@ export function JobsTotalsTableComponent({ jobRO, currentUser, job }) { - +
                           {JSON.stringify(
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 ee644654a..325a16ffc 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
@@ -4,12 +4,12 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react";
 import { Button, Card, Dropdown, Form, Input, Modal, Popconfirm, Popover, Select, Space } from "antd";
 import axios from "axios";
 import parsePhoneNumber from "libphonenumber-js";
-import { useContext, useMemo, useState } from "react";
+import { useMemo, useState } from "react";
 import { useTranslation } from "react-i18next";
 import { connect } from "react-redux";
 import { Link, useNavigate } from "react-router-dom";
 import { createStructuredSelector } from "reselect";
-import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
+import { useSocket } from "../../contexts/SocketIO/useSocket.jsx";
 import { auth, logImEXEvent } from "../../firebase/firebase.utils";
 import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
 import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
@@ -130,7 +130,7 @@ export function JobsDetailHeaderActions({
   const [updateJob] = useMutation(UPDATE_JOB);
   const [voidJob] = useMutation(VOID_JOB);
   const [cancelAllAppointments] = useMutation(CANCEL_APPOINTMENTS_BY_JOB_ID);
-  const { socket } = useContext(SocketContext);
+  const { socket } = useSocket();
   const notification = useNotification();
 
   const {
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 ffe729b51..dd49ffee0 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
@@ -119,7 +119,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
               
                 {job.cccontracts.map((c, index) => (
                   
-                    
+                    
                       {`${c.agreementnumber} - ${c.courtesycar.fleetnumber} ${c.courtesycar.year} ${c.courtesycar.make} ${c.courtesycar.model}`}
                       {index !== job.cccontracts.length - 1 ? "," : null}
                     
diff --git a/client/src/components/notification-center/notification-center.component.jsx b/client/src/components/notification-center/notification-center.component.jsx
new file mode 100644
index 000000000..5d9371f49
--- /dev/null
+++ b/client/src/components/notification-center/notification-center.component.jsx
@@ -0,0 +1,103 @@
+import { Virtuoso } from "react-virtuoso";
+import { Alert, Badge, Button, Space, Spin, Tooltip, Typography } from "antd";
+import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router-dom";
+import "./notification-center.styles.scss";
+import day from "../../utils/day.js";
+
+const { Text, Title } = Typography;
+
+const NotificationCenterComponent = ({
+  visible,
+  onClose,
+  notifications,
+  loading,
+  error,
+  showUnreadOnly,
+  toggleUnreadOnly,
+  markAllRead,
+  loadMore,
+  onNotificationClick,
+  unreadCount
+}) => {
+  const { t } = useTranslation();
+
+  const renderNotification = (index, notification) => {
+    return (
+      
!notification.read && onNotificationClick(notification.id)} + > + +
+ + <Link + to={`/manage/jobs/${notification.jobid}`} + onClick={(e) => { + e.stopPropagation(); + if (!notification.read) { + onNotificationClick(notification.id); + } + }} + className="ro-number" + > + {t("notifications.labels.ro-number", { ro_number: notification.roNumber })} + </Link> + <Text type="secondary" className="relative-time"> + {day(notification.created_at).fromNow()} + </Text> + + +
    + {notification.scenarioText.map((text, idx) => ( +
  • {text}
  • + ))} +
+
+
+
+
+ ); + }; + + return ( +
+
+ +

{t("notifications.labels.notification-center")}

+ {loading && !error && } +
+
+ +
+
+ {error && onClose()} />} + +
+ ); +}; + +export default NotificationCenterComponent; diff --git a/client/src/components/notification-center/notification-center.container.jsx b/client/src/components/notification-center/notification-center.container.jsx new file mode 100644 index 000000000..dfadf3a84 --- /dev/null +++ b/client/src/components/notification-center/notification-center.container.jsx @@ -0,0 +1,181 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useQuery } from "@apollo/client"; +import { connect } from "react-redux"; +import NotificationCenterComponent from "./notification-center.component"; +import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries"; +import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.jsx"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop } from "../../redux/user/user.selectors.js"; +import day from "../../utils/day.js"; + +// This will be used to poll for notifications when the socket is disconnected +const NOTIFICATION_POLL_INTERVAL_SECONDS = 60; + +export function NotificationCenterContainer({ visible, onClose, bodyshop, unreadCount }) { + const [showUnreadOnly, setShowUnreadOnly] = useState(false); + const [notifications, setNotifications] = useState([]); + const [error, setError] = useState(null); + const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket(); + + const userAssociationId = bodyshop?.associations?.[0]?.id; + + const baseWhereClause = useMemo(() => { + return { associationid: { _eq: userAssociationId } }; + }, [userAssociationId]); + + const whereClause = useMemo(() => { + return showUnreadOnly ? { ...baseWhereClause, read: { _is_null: true } } : baseWhereClause; + }, [baseWhereClause, showUnreadOnly]); + + const { + data, + fetchMore, + loading, + error: queryError, + refetch + } = useQuery(GET_NOTIFICATIONS, { + variables: { + limit: INITIAL_NOTIFICATIONS, + offset: 0, + where: whereClause + }, + fetchPolicy: "cache-and-network", + notifyOnNetworkStatusChange: true, + pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(), + skip: !userAssociationId, + onError: (err) => { + setError(err.message); + console.error(`Error polling Notifications in notification-center: ${err?.message || ""}`); + setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds()); + } + }); + + useEffect(() => { + if (data?.notifications) { + const processedNotifications = data.notifications + .map((notif) => { + let scenarioText; + let scenarioMeta; + try { + scenarioText = notif.scenario_text ? JSON.parse(notif.scenario_text) : []; + scenarioMeta = notif.scenario_meta ? JSON.parse(notif.scenario_meta) : {}; + } catch (e) { + console.error("Error parsing JSON for notification:", notif.id, e); + scenarioText = [notif.fcm_text || "Invalid notification data"]; + scenarioMeta = {}; + } + if (!Array.isArray(scenarioText)) scenarioText = [scenarioText]; + const roNumber = notif.job.ro_number; + if (!Array.isArray(scenarioMeta)) scenarioMeta = [scenarioMeta]; + return { + id: notif.id, + jobid: notif.jobid, + associationid: notif.associationid, + scenarioText, + scenarioMeta, + roNumber, + created_at: notif.created_at, + read: notif.read, + __typename: notif.__typename + }; + }) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); + setNotifications(processedNotifications); + setError(null); + } + }, [data]); + + useEffect(() => { + if (queryError) { + setError(queryError.message); + } + }, [queryError]); + + const loadMore = useCallback(() => { + if (!loading && data?.notifications.length) { + fetchMore({ + variables: { offset: data.notifications.length, where: whereClause }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) return prev; + return { + notifications: [...prev.notifications, ...fetchMoreResult.notifications] + }; + } + }).catch((err) => { + setError(err.message); + console.error("Fetch more error:", err); + }); + } + }, [data?.notifications?.length, fetchMore, loading, whereClause]); + + const handleToggleUnreadOnly = (value) => { + setShowUnreadOnly(value); + }; + + const handleMarkAllRead = useCallback(() => { + markAllNotificationsRead() + .then(() => { + const timestamp = new Date().toISOString(); + setNotifications((prev) => { + const updatedNotifications = prev.map((notif) => + notif.read === null && notif.associationid === userAssociationId + ? { + ...notif, + read: timestamp + } + : notif + ); + return [...updatedNotifications]; + }); + }) + .catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`)); + }, [markAllNotificationsRead, userAssociationId]); + + const handleNotificationClick = useCallback( + (notificationId) => { + markNotificationRead({ + variables: { id: notificationId } + }) + .then(() => { + const timestamp = new Date().toISOString(); + setNotifications((prev) => { + return prev.map((notif) => + notif.id === notificationId && !notif.read ? { ...notif, read: timestamp } : notif + ); + }); + }) + .catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`)); + }, + [markNotificationRead] + ); + + useEffect(() => { + if (visible && !isConnected) { + refetch().catch( + (err) => `Something went wrong re-fetching notifications in the notification-center: ${err?.message || ""}` + ); + } + }, [visible, isConnected, refetch]); + + return ( + + ); +} + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +export default connect(mapStateToProps, null)(NotificationCenterContainer); diff --git a/client/src/components/notification-center/notification-center.styles.scss b/client/src/components/notification-center/notification-center.styles.scss new file mode 100644 index 000000000..008642931 --- /dev/null +++ b/client/src/components/notification-center/notification-center.styles.scss @@ -0,0 +1,138 @@ +.notification-center { + position: absolute; + top: 64px; + right: 0; + width: 400px; + max-width: 400px; + background: #fff; + color: rgba(0, 0, 0, 0.85); + border: 1px solid #d9d9d9; + border-radius: 6px; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08), 0 3px 6px rgba(0, 0, 0, 0.06); + z-index: 1000; + display: none; + overflow-x: hidden; /* Prevent horizontal overflow */ + + &.visible { + display: block; + } + + .notification-header { + padding: 4px 16px; + border-bottom: 1px solid #f0f0f0; + display: flex; + justify-content: space-between; + align-items: center; + background: #fafafa; + + h3 { + margin: 0; + font-size: 14px; + color: rgba(0, 0, 0, 0.85); + } + + .notification-controls { + display: flex; + align-items: center; + gap: 8px; + + .ant-btn-link { + padding: 0; + color: #1677ff; + + &:hover { + color: #69b1ff; + } + + &:disabled { + color: rgba(0, 0, 0, 0.25); + cursor: not-allowed; + } + + &.active { + color: #0958d9; + } + } + } + } + + .notification-read { + background: #fff; + color: rgba(0, 0, 0, 0.65); + } + + .notification-unread { + background: #f5f5f5; + color: rgba(0, 0, 0, 0.85); + } + + .notification-item { + padding: 8px 16px; + border-bottom: 1px solid #f0f0f0; + display: block; + overflow: visible; + width: 100%; + box-sizing: border-box; + + .notification-content { + width: 100%; + } + + .notification-title { + margin: 0; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + box-sizing: border-box; + + .ro-number { + margin: 0; + color: #1677ff; + flex-shrink: 0; + white-space: nowrap; + } + + .relative-time { + margin: 0; + font-size: 12px; + color: rgba(0, 0, 0, 0.45); + white-space: nowrap; + flex-shrink: 0; + margin-left: auto; + } + } + + .notification-body { + margin-top: 4px; + + .ant-typography { + color: inherit; + } + + ul { + margin: 0; + padding: 0; + } + + li { + margin-bottom: 2px; + } + } + } + + .ant-badge { + width: 100%; /* Ensure Badge takes full width to allow .notification-title to stretch properly */ + } + + .ant-alert { + margin: 8px; + background: #fff1f0; + color: rgba(0, 0, 0, 0.85); + border: 1px solid #ffa39e; + + .ant-alert-message { + color: #ff4d4f; + } + } +} diff --git a/client/src/components/payments-generate-link/payments-generate-link.component.jsx b/client/src/components/payments-generate-link/payments-generate-link.component.jsx index f9d84d10f..7e121621f 100644 --- a/client/src/components/payments-generate-link/payments-generate-link.component.jsx +++ b/client/src/components/payments-generate-link/payments-generate-link.component.jsx @@ -2,15 +2,15 @@ import { CopyFilled } from "@ant-design/icons"; import { Button, Form, message, Popover, Space } from "antd"; import axios from "axios"; import Dinero from "dinero.js"; -import { parsePhoneNumber } from "libphonenumber-js"; -import React, { useContext, useState } from "react"; +import { parsePhoneNumberWithError, ParseError } from "libphonenumber-js"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component"; -import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop, @@ -29,22 +29,34 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); const [paymentLink, setPaymentLink] = useState(null); - const { socket } = useContext(SocketContext); + const { socket } = useSocket(); const handleFinish = async ({ amount }) => { setLoading(true); let p; try { - p = parsePhoneNumber(job.ownr_ph1 || "", "CA"); + // Updated to use parsePhoneNumberWithError + p = parsePhoneNumberWithError(job.ownr_ph1 || "", "CA"); } catch (error) { - console.log("Unable to parse phone number"); + if (error instanceof ParseError) { + // Handle specific parsing errors + console.log(`Phone number parsing failed: ${error.message}`); + } else { + // Handle other unexpected errors + console.log("Unexpected error while parsing phone number:", error); + } } setLoading(true); const response = await axios.post("/intellipay/generate_payment_url", { bodyshop, amount: amount, account: job.ro_number, - comment: btoa(JSON.stringify({ payments: [{ jobid: job.id, amount }], userEmail: currentUser.email })) + comment: btoa( + JSON.stringify({ + payments: [{ jobid: job.id, amount }], + userEmail: currentUser.email + }) + ) }); setLoading(false); setPaymentLink(response.data.shorUrl); @@ -106,7 +118,20 @@ export function PaymentsGenerateLink({ bodyshop, currentUser, callback, job, ope
+ + + + } + > + + + + ); +} + +NotificationSettingsForm.propTypes = { + currentUser: PropTypes.shape({ + email: PropTypes.string.isRequired + }).isRequired +}; + +const mapStateToProps = createStructuredSelector({ + currentUser: selectCurrentUser +}); + +export default connect(mapStateToProps)(NotificationSettingsForm); diff --git a/client/src/components/profile-my/profile-my.component.jsx b/client/src/components/profile-my/profile-my.component.jsx index f53646bee..df5a49e19 100644 --- a/client/src/components/profile-my/profile-my.component.jsx +++ b/client/src/components/profile-my/profile-my.component.jsx @@ -1,6 +1,5 @@ import { Button, Card, Col, Form, Input } from "antd"; import { LockOutlined } from "@ant-design/icons"; -import React from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -9,6 +8,8 @@ import { selectCurrentUser } from "../../redux/user/user.selectors"; import { logImEXEvent, updateCurrentPassword } from "../../firebase/firebase.utils"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import NotificationSettingsForm from "./notification-settings.component.jsx"; +import { useSocket } from "../../contexts/SocketIO/useSocket.jsx"; const mapStateToProps = createStructuredSelector({ currentUser: selectCurrentUser @@ -22,6 +23,7 @@ export default connect( )(function ProfileMyComponent({ currentUser, updateUserDetails }) { const { t } = useTranslation(); const notification = useNotification(); + const { scenarioNotificationsOn } = useSocket(); const handleFinish = (values) => { logImEXEvent("profile_update"); @@ -117,6 +119,11 @@ export default connect( + {scenarioNotificationsOn && ( + + + + )} ); }); diff --git a/client/src/components/update-alert/update-alert.component.jsx b/client/src/components/update-alert/update-alert.component.jsx index 22b290a5c..e3273ded6 100644 --- a/client/src/components/update-alert/update-alert.component.jsx +++ b/client/src/components/update-alert/update-alert.component.jsx @@ -1,7 +1,7 @@ import { AlertOutlined } from "@ant-design/icons"; import { Alert, Button, Col, Row, Space } from "antd"; import i18n from "i18next"; -import React, { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -81,8 +81,7 @@ export function UpdateAlert({ updateAvailable }) { imex: "$t(titles.imexonline)", rome: "$t(titles.romeonline)" }) - }), - placement: "bottomRight" + }) }); } if (needRefresh && timerStarted && timeLeft <= 0) { diff --git a/client/src/contexts/Notifications/notificationContext.jsx b/client/src/contexts/Notifications/notificationContext.jsx index 59bcdf4fe..52fd4eeeb 100644 --- a/client/src/contexts/Notifications/notificationContext.jsx +++ b/client/src/contexts/Notifications/notificationContext.jsx @@ -1,5 +1,4 @@ -// NotificationProvider.jsx -import React, { createContext, useContext } from "react"; +import { createContext, useContext } from "react"; import { notification } from "antd"; /** @@ -22,7 +21,10 @@ export const useNotification = () => { * - Provide `api` via the NotificationContext. */ export const NotificationProvider = ({ children }) => { - const [api, contextHolder] = notification.useNotification(); + const [api, contextHolder] = notification.useNotification({ + placement: "bottomRight", + showProgress: true + }); return ( diff --git a/client/src/contexts/SocketIO/socketContext.jsx b/client/src/contexts/SocketIO/socketContext.jsx deleted file mode 100644 index e0f0e61fc..000000000 --- a/client/src/contexts/SocketIO/socketContext.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { createContext } from "react"; -import useSocket from "./useSocket"; // Import the custom hook - -// Create the SocketContext -const SocketContext = createContext(null); - -export const SocketProvider = ({ children, bodyshop }) => { - const { socket, clientId } = useSocket(bodyshop); - - return {children}; -}; - -export default SocketContext; diff --git a/client/src/contexts/SocketIO/useSocket.js b/client/src/contexts/SocketIO/useSocket.js deleted file mode 100644 index c13141ca7..000000000 --- a/client/src/contexts/SocketIO/useSocket.js +++ /dev/null @@ -1,125 +0,0 @@ -import { useEffect, useRef, useState } from "react"; -import SocketIO from "socket.io-client"; -import { auth } from "../../firebase/firebase.utils"; -import { store } from "../../redux/store"; -import { addAlerts, setWssStatus } from "../../redux/application/application.actions"; - -const useSocket = (bodyshop) => { - const socketRef = useRef(null); - const [clientId, setClientId] = useState(null); - - useEffect(() => { - const initializeSocket = async (token) => { - if (!bodyshop || !bodyshop.id) return; - - const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : ""; - - const socketInstance = SocketIO(endpoint, { - path: "/wss", - withCredentials: true, - auth: { token }, - reconnectionAttempts: Infinity, - reconnectionDelay: 2000, - reconnectionDelayMax: 10000 - }); - - socketRef.current = socketInstance; - - // Handle socket events - const handleBodyshopMessage = (message) => { - if (!message || !message.type) return; - - switch (message.type) { - case "alert-update": - store.dispatch(addAlerts(message.payload)); - break; - default: - break; - } - - if (!import.meta.env.DEV) return; - console.log(`Received message for bodyshop ${bodyshop.id}:`, message); - }; - - const handleConnect = () => { - socketInstance.emit("join-bodyshop-room", bodyshop.id); - setClientId(socketInstance.id); - store.dispatch(setWssStatus("connected")); - }; - - const handleReconnect = () => { - store.dispatch(setWssStatus("connected")); - }; - - const handleConnectionError = (err) => { - console.error("Socket connection error:", err); - - // Handle token expiration - if (err.message.includes("auth/id-token-expired")) { - console.warn("Token expired, refreshing..."); - auth.currentUser?.getIdToken(true).then((newToken) => { - socketInstance.auth = { token: newToken }; // Update socket auth - socketInstance.connect(); // Retry connection - }); - } else { - store.dispatch(setWssStatus("error")); - } - }; - - const handleDisconnect = (reason) => { - console.warn("Socket disconnected:", reason); - store.dispatch(setWssStatus("disconnected")); - - // Manually trigger reconnection if necessary - if (!socketInstance.connected && reason !== "io server disconnect") { - setTimeout(() => { - if (socketInstance.disconnected) { - console.log("Manually triggering reconnection..."); - socketInstance.connect(); - } - }, 2000); // Retry after 2 seconds - } - }; - - // Register event handlers - socketInstance.on("connect", handleConnect); - socketInstance.on("reconnect", handleReconnect); - socketInstance.on("connect_error", handleConnectionError); - socketInstance.on("disconnect", handleDisconnect); - socketInstance.on("bodyshop-message", handleBodyshopMessage); - }; - - const unsubscribe = auth.onIdTokenChanged(async (user) => { - if (user) { - const token = await user.getIdToken(); - - if (socketRef.current) { - // Update token if socket exists - socketRef.current.emit("update-token", token); - } else { - // Initialize socket if not already connected - initializeSocket(token); - } - } else { - // User is not authenticated - if (socketRef.current) { - socketRef.current.disconnect(); - socketRef.current = null; - } - } - }); - - // Clean up on unmount - return () => { - unsubscribe(); - if (socketRef.current) { - socketRef.current.disconnect(); - socketRef.current = null; - } - }; - }, [bodyshop]); - - return { socket: socketRef.current, clientId }; -}; - -export default useSocket; diff --git a/client/src/contexts/SocketIO/useSocket.jsx b/client/src/contexts/SocketIO/useSocket.jsx new file mode 100644 index 000000000..a8b28a145 --- /dev/null +++ b/client/src/contexts/SocketIO/useSocket.jsx @@ -0,0 +1,483 @@ +import { createContext, useContext, useEffect, useRef, useState } from "react"; +import SocketIO from "socket.io-client"; +import { auth } from "../../firebase/firebase.utils"; +import { store } from "../../redux/store"; +import { addAlerts, setWssStatus } from "../../redux/application/application.actions"; +import client from "../../utils/GraphQLClient"; +import { useNotification } from "../Notifications/notificationContext.jsx"; +import { + GET_NOTIFICATIONS, + GET_UNREAD_COUNT, + MARK_ALL_NOTIFICATIONS_READ, + MARK_NOTIFICATION_READ +} from "../../graphql/notifications.queries.js"; +import { gql, useMutation } from "@apollo/client"; +import { useTranslation } from "react-i18next"; + +const SocketContext = createContext(null); + +const INITIAL_NOTIFICATIONS = 10; + +/** + * Socket Provider - Scenario Notifications / Web Socket related items + * @param children + * @param bodyshop + * @param navigate + * @param currentUser + * @param scenarioNotificationsOn + * @returns {JSX.Element} + * @constructor + */ +const SocketProvider = ({ children, bodyshop, navigate, currentUser, scenarioNotificationsOn }) => { + const socketRef = useRef(null); + const [clientId, setClientId] = useState(null); + const [isConnected, setIsConnected] = useState(false); + const notification = useNotification(); + const userAssociationId = bodyshop?.associations?.[0]?.id; + const { t } = useTranslation(); + + const [markNotificationRead] = useMutation(MARK_NOTIFICATION_READ, { + update: (cache, { data: { update_notifications } }) => { + const timestamp = new Date().toISOString(); + const updatedNotification = update_notifications.returning[0]; + + cache.modify({ + fields: { + notifications(existing = [], { readField }) { + return existing.map((notif) => + readField("id", notif) === updatedNotification.id + ? { + ...notif, + read: timestamp + } + : notif + ); + } + } + }); + + const unreadCountQuery = cache.readQuery({ + query: GET_UNREAD_COUNT, + variables: { associationid: userAssociationId } + }); + + if (unreadCountQuery?.notifications_aggregate?.aggregate?.count > 0) { + cache.writeQuery({ + query: GET_UNREAD_COUNT, + variables: { associationid: userAssociationId }, + data: { + notifications_aggregate: { + ...unreadCountQuery.notifications_aggregate, + aggregate: { + ...unreadCountQuery.notifications_aggregate.aggregate, + count: unreadCountQuery.notifications_aggregate.aggregate.count - 1 + } + } + } + }); + } + + if (socketRef.current && isConnected) { + socketRef.current.emit("sync-notification-read", { + email: currentUser?.email, + bodyshopId: bodyshop.id, + notificationId: updatedNotification.id + }); + } + }, + onError: (err) => console.error("MARK_NOTIFICATION_READ error:", err) + }); + + const [markAllNotificationsRead] = useMutation(MARK_ALL_NOTIFICATIONS_READ, { + variables: { associationid: userAssociationId }, + update: (cache) => { + const timestamp = new Date().toISOString(); + cache.modify({ + fields: { + notifications(existing = [], { readField }) { + return existing.map((notif) => + readField("read", notif) === null && readField("associationid", notif) === userAssociationId + ? { ...notif, read: timestamp } + : notif + ); + }, + notifications_aggregate() { + return { aggregate: { count: 0, __typename: "notifications_aggregate_fields" } }; + } + } + }); + + const baseWhereClause = { associationid: { _eq: userAssociationId } }; + const cachedNotifications = cache.readQuery({ + query: GET_NOTIFICATIONS, + variables: { limit: INITIAL_NOTIFICATIONS, offset: 0, where: baseWhereClause } + }); + + if (cachedNotifications?.notifications) { + cache.writeQuery({ + query: GET_NOTIFICATIONS, + variables: { limit: INITIAL_NOTIFICATIONS, offset: 0, where: baseWhereClause }, + data: { + notifications: cachedNotifications.notifications.map((notif) => + notif.read === null ? { ...notif, read: timestamp } : notif + ) + } + }); + } + + if (socketRef.current && isConnected) { + socketRef.current.emit("sync-all-notifications-read", { + email: currentUser?.email, + bodyshopId: bodyshop.id + }); + } + }, + onError: (err) => console.error("MARK_ALL_NOTIFICATIONS_READ error:", err) + }); + + useEffect(() => { + const initializeSocket = async (token) => { + if (!bodyshop || !bodyshop.id || socketRef.current) return; + + const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : ""; + const socketInstance = SocketIO(endpoint, { + path: "/wss", + withCredentials: true, + auth: { token, bodyshopId: bodyshop.id }, + reconnectionAttempts: Infinity, + reconnectionDelay: 2000, + reconnectionDelayMax: 10000 + }); + + socketRef.current = socketInstance; + + const handleBodyshopMessage = (message) => { + if (!message || !message.type) return; + switch (message.type) { + case "alert-update": + store.dispatch(addAlerts(message.payload)); + break; + default: + break; + } + }; + + const handleConnect = () => { + socketInstance.emit("join-bodyshop-room", bodyshop.id); + setClientId(socketInstance.id); + setIsConnected(true); + store.dispatch(setWssStatus("connected")); + }; + + const handleReconnect = () => { + setIsConnected(true); + store.dispatch(setWssStatus("connected")); + }; + + const handleConnectionError = (err) => { + console.error("Socket connection error:", err); + setIsConnected(false); + if (err.message.includes("auth/id-token-expired")) { + console.warn("Token expired, refreshing..."); + auth.currentUser?.getIdToken(true).then((newToken) => { + socketInstance.auth = { token: newToken }; + socketInstance.connect(); + }); + } else { + store.dispatch(setWssStatus("error")); + } + }; + + const handleDisconnect = (reason) => { + console.warn("Socket disconnected:", reason); + setIsConnected(false); + store.dispatch(setWssStatus("disconnected")); + if (!socketInstance.connected && reason !== "io server disconnect") { + setTimeout(() => { + if (socketInstance.disconnected) { + console.log("Manually triggering reconnection..."); + socketInstance.connect(); + } + }, 2000); + } + }; + + const handleNotification = (data) => { + // Scenario Notifications have been disabled, bail. + if (!scenarioNotificationsOn) { + return; + } + + const { jobId, jobRoNumber, notificationId, associationId, notifications } = data; + if (associationId !== userAssociationId) return; + + const newNotification = { + __typename: "notifications", + id: notificationId, + jobid: jobId, + associationid: associationId, + scenario_text: JSON.stringify(notifications.map((notif) => notif.body)), + fcm_text: notifications.map((notif) => notif.body).join(". ") + ".", + scenario_meta: JSON.stringify(notifications.map((notif) => notif.variables || {})), + created_at: new Date(notifications[0].timestamp).toISOString(), + read: null, + job: { ro_number: jobRoNumber } + }; + + const baseVariables = { + limit: INITIAL_NOTIFICATIONS, + offset: 0, + where: { associationid: { _eq: userAssociationId } } + }; + + try { + const existingNotifications = + client.cache.readQuery({ + query: GET_NOTIFICATIONS, + variables: baseVariables + })?.notifications || []; + if (!existingNotifications.some((n) => n.id === newNotification.id)) { + client.cache.writeQuery({ + query: GET_NOTIFICATIONS, + variables: baseVariables, + data: { + notifications: [newNotification, ...existingNotifications].sort( + (a, b) => new Date(b.created_at) - new Date(a.created_at) + ) + }, + broadcast: true + }); + + const unreadVariables = { + ...baseVariables, + where: { ...baseVariables.where, read: { _is_null: true } } + }; + const unreadNotifications = + client.cache.readQuery({ + query: GET_NOTIFICATIONS, + variables: unreadVariables + })?.notifications || []; + if (newNotification.read === null && !unreadNotifications.some((n) => n.id === newNotification.id)) { + client.cache.writeQuery({ + query: GET_NOTIFICATIONS, + variables: unreadVariables, + data: { + notifications: [newNotification, ...unreadNotifications].sort( + (a, b) => new Date(b.created_at) - new Date(a.created_at) + ) + }, + broadcast: true + }); + } + + client.cache.modify({ + id: "ROOT_QUERY", + fields: { + notifications_aggregate(existing = { aggregate: { count: 0 } }) { + return { + ...existing, + aggregate: { + ...existing.aggregate, + count: existing.aggregate.count + (newNotification.read === null ? 1 : 0) + } + }; + } + } + }); + + notification.info({ + message: t("notifications.labels.notification-popup-title", { ro_number: jobRoNumber }), + description: ( +
    { + markNotificationRead({ variables: { id: notificationId } }) + .then(() => navigate(`/manage/jobs/${jobId}`)) + .catch((e) => console.error(`Error marking notification read: ${e?.message || ""}`)); + }} + > + {notifications.map((notif, index) => ( +
  • + {notif.body} +
  • + ))} +
+ ) + }); + } + } catch (error) { + console.error(`Error handling new notification: ${error?.message || ""}`); + } + }; + + const handleSyncNotificationRead = ({ notificationId, timestamp }) => { + // Scenario Notifications have been disabled, bail. + if (!scenarioNotificationsOn) { + return; + } + + try { + const notificationRef = client.cache.identify({ + __typename: "notifications", + id: notificationId + }); + client.cache.writeFragment({ + id: notificationRef, + fragment: gql` + fragment UpdateNotificationRead on notifications { + read + } + `, + data: { read: timestamp } + }); + + const unreadCountData = client.cache.readQuery({ + query: GET_UNREAD_COUNT, + variables: { associationid: userAssociationId } + }); + if (unreadCountData?.notifications_aggregate?.aggregate?.count > 0) { + const newCount = Math.max(unreadCountData.notifications_aggregate.aggregate.count - 1, 0); + client.cache.writeQuery({ + query: GET_UNREAD_COUNT, + variables: { associationid: userAssociationId }, + data: { + notifications_aggregate: { + __typename: "notifications_aggregate", + aggregate: { + __typename: "notifications_aggregate_fields", + count: newCount + } + } + } + }); + } + } catch (error) { + console.error("Error in handleSyncNotificationRead:", error); + } + }; + + const handleSyncAllNotificationsRead = ({ timestamp }) => { + // Scenario Notifications have been disabled, bail. + if (!scenarioNotificationsOn) { + return; + } + + try { + const queryVars = { + limit: INITIAL_NOTIFICATIONS, + offset: 0, + where: { associationid: { _eq: userAssociationId } } + }; + const cachedData = client.cache.readQuery({ + query: GET_NOTIFICATIONS, + variables: queryVars + }); + + if (cachedData?.notifications) { + cachedData.notifications.forEach((notif) => { + if (!notif.read) { + const notifRef = client.cache.identify({ __typename: "notifications", id: notif.id }); + client.cache.writeFragment({ + id: notifRef, + fragment: gql` + fragment UpdateNotificationRead on notifications { + read + } + `, + data: { read: timestamp } + }); + } + }); + } + + client.cache.writeQuery({ + query: GET_UNREAD_COUNT, + variables: { associationid: userAssociationId }, + data: { + notifications_aggregate: { + __typename: "notifications_aggregate", + aggregate: { + __typename: "notifications_aggregate_fields", + count: 0 + } + } + } + }); + } catch (error) { + console.error(`Error In HandleSyncAllNotificationsRead: ${error?.message || ""}`); + } + }; + + socketInstance.on("connect", handleConnect); + socketInstance.on("reconnect", handleReconnect); + socketInstance.on("connect_error", handleConnectionError); + socketInstance.on("disconnect", handleDisconnect); + socketInstance.on("bodyshop-message", handleBodyshopMessage); + socketInstance.on("notification", handleNotification); + socketInstance.on("sync-notification-read", handleSyncNotificationRead); + socketInstance.on("sync-all-notifications-read", handleSyncAllNotificationsRead); + }; + + const unsubscribe = auth.onIdTokenChanged(async (user) => { + if (user) { + const token = await user.getIdToken(); + if (socketRef.current) { + socketRef.current.emit("update-token", { token, bodyshopId: bodyshop.id }); + } else { + initializeSocket(token).catch((err) => + console.error(`Something went wrong Initializing Sockets: ${err?.message || ""}`) + ); + } + } else { + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + setIsConnected(false); + } + } + }); + + return () => { + unsubscribe(); + if (socketRef.current) { + socketRef.current.disconnect(); + socketRef.current = null; + setIsConnected(false); + } + }; + }, [ + bodyshop, + notification, + userAssociationId, + markNotificationRead, + markAllNotificationsRead, + navigate, + currentUser, + scenarioNotificationsOn, + t + ]); + + return ( + + {children} + + ); +}; + +const useSocket = () => { + const context = useContext(SocketContext); + // NOTE: Not sure if we absolutely require this, does cause slipups on dev env + if (!context) throw new Error("useSocket must be used within a SocketProvider"); + return context; +}; + +export { SocketContext, SocketProvider, INITIAL_NOTIFICATIONS, useSocket }; diff --git a/client/src/graphql/bodyshop.queries.js b/client/src/graphql/bodyshop.queries.js index 670fe963e..32a93980f 100644 --- a/client/src/graphql/bodyshop.queries.js +++ b/client/src/graphql/bodyshop.queries.js @@ -349,3 +349,13 @@ export const QUERY_STRIPE_ID = gql` } } `; + +export const GET_ACTIVE_EMPLOYEES_IN_SHOP = gql` + query GetActiveEmployeesInShop($shopid: uuid!) { + associations(where: { shopid: { _eq: $shopid } }) { + id + useremail + shopid + } + } +`; diff --git a/client/src/graphql/jobs.queries.js b/client/src/graphql/jobs.queries.js index 6b417ba36..ddfd26a2f 100644 --- a/client/src/graphql/jobs.queries.js +++ b/client/src/graphql/jobs.queries.js @@ -524,6 +524,10 @@ export const GET_JOB_BY_PK = gql` invoice_final_note iouparent job_totals + job_watchers { + id + user_email + } joblines(where: { removed: { _eq: false } }, order_by: { line_no: asc }) { act_price act_price_before_ppc @@ -2567,3 +2571,34 @@ export const GET_JOB_BY_PK_QUICK_INTAKE = gql` } } `; + +export const GET_JOB_WATCHERS = gql` + query GET_JOB_WATCHERS($jobid: uuid!) { + job_watchers(where: { jobid: { _eq: $jobid } }) { + id + user_email + } + } +`; + +export const ADD_JOB_WATCHER = gql` + mutation ADD_JOB_WATCHER($jobid: uuid!, $userEmail: String!) { + insert_job_watchers_one(object: { jobid: $jobid, user_email: $userEmail }) { + id + jobid + user_email + } + } +`; + +export const REMOVE_JOB_WATCHER = gql` + mutation REMOVE_JOB_WATCHER($jobid: uuid!, $userEmail: String!) { + delete_job_watchers(where: { jobid: { _eq: $jobid }, user_email: { _eq: $userEmail } }) { + affected_rows + returning { + id + user_email + } + } + } +`; diff --git a/client/src/graphql/notifications.queries.js b/client/src/graphql/notifications.queries.js new file mode 100644 index 000000000..71be7b931 --- /dev/null +++ b/client/src/graphql/notifications.queries.js @@ -0,0 +1,52 @@ +import { gql } from "@apollo/client"; + +export const GET_NOTIFICATIONS = gql` + query GetNotifications($limit: Int!, $offset: Int!, $where: notifications_bool_exp) { + notifications(limit: $limit, offset: $offset, order_by: { created_at: desc }, where: $where) { + id + jobid + associationid + scenario_text + fcm_text + scenario_meta + created_at + read + job { + id + ro_number + } + } + } +`; + +export const GET_UNREAD_COUNT = gql` + query GetUnreadCount($associationid: uuid!) { + notifications_aggregate(where: { read: { _is_null: true }, associationid: { _eq: $associationid } }) { + aggregate { + count + } + } + } +`; + +export const MARK_ALL_NOTIFICATIONS_READ = gql` + mutation MarkAllNotificationsRead($associationid: uuid!) { + update_notifications( + where: { read: { _is_null: true }, associationid: { _eq: $associationid } } + _set: { read: "now()" } + ) { + affected_rows + } + } +`; + +export const MARK_NOTIFICATION_READ = gql` + mutation MarkNotificationRead($id: uuid!) { + update_notifications(where: { id: { _eq: $id } }, _set: { read: "now()" }) { + returning { + id + read + } + } + } +`; diff --git a/client/src/graphql/user.queries.js b/client/src/graphql/user.queries.js index bd9b9b1cb..266059c09 100644 --- a/client/src/graphql/user.queries.js +++ b/client/src/graphql/user.queries.js @@ -85,3 +85,21 @@ export const UPDATE_KANBAN_SETTINGS = gql` } } `; + +export const QUERY_NOTIFICATION_SETTINGS = gql` + query QUERY_NOTIFICATION_SETTINGS($email: String!) { + associations(where: { _and: { useremail: { _eq: $email }, active: { _eq: true } } }) { + id + notification_settings + } + } +`; + +export const UPDATE_NOTIFICATION_SETTINGS = gql` + mutation UPDATE_NOTIFICATION_SETTINGS($id: uuid!, $ns: jsonb) { + update_associations_by_pk(pk_columns: { id: $id }, _set: { notification_settings: $ns }) { + id + notification_settings + } + } +`; diff --git a/client/src/pages/jobs-detail/job-watcher-toggle.component.jsx b/client/src/pages/jobs-detail/job-watcher-toggle.component.jsx new file mode 100644 index 000000000..882c5966e --- /dev/null +++ b/client/src/pages/jobs-detail/job-watcher-toggle.component.jsx @@ -0,0 +1,231 @@ +import { useCallback, useMemo, useState } from "react"; +import { useMutation, useQuery } from "@apollo/client"; +import { EyeFilled, EyeOutlined, UserOutlined } from "@ant-design/icons"; +import { ADD_JOB_WATCHER, GET_JOB_WATCHERS, REMOVE_JOB_WATCHER } from "../../graphql/jobs.queries.js"; +import { Avatar, Button, Divider, List, Popover, Select, Tooltip, Typography } from "antd"; +import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; +import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js"; +import EmployeeSearchSelectComponent from "../../components/employee-search-select/employee-search-select.component.jsx"; +import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component.jsx"; +import { useSplitTreatments } from "@splitsoftware/splitio-react"; + +const { Text } = Typography; + +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop, + currentUser: selectCurrentUser +}); + +const JobWatcherToggle = ({ job, currentUser, bodyshop }) => { + const { t } = useTranslation(); + const { + treatments: { Enhanced_Payroll } + } = useSplitTreatments({ + attributes: {}, + names: ["Enhanced_Payroll"], + splitKey: bodyshop && bodyshop.imexshopid + }); + + const userEmail = currentUser.email; + const jobid = job.id; + + const [open, setOpen] = useState(false); + const [selectedWatcher, setSelectedWatcher] = useState(null); + const [selectedTeam, setSelectedTeam] = useState(null); + + // Fetch current watchers + const { data: watcherData, loading: watcherLoading } = useQuery(GET_JOB_WATCHERS, { variables: { jobid } }); + + // Extract watchers list + const jobWatchers = useMemo(() => watcherData?.job_watchers || [], [watcherData]); + + const isWatching = jobWatchers.some((w) => w.user_email === userEmail); + + // Add watcher mutation with cache update + const [addWatcher, { loading: adding }] = useMutation(ADD_JOB_WATCHER, { + update(cache, { data: { insert_job_watchers_one } }) { + const existingData = cache.readQuery({ + query: GET_JOB_WATCHERS, + variables: { jobid } + }); + + const updatedWatchers = [...(existingData?.job_watchers || []), insert_job_watchers_one]; + + cache.writeQuery({ + query: GET_JOB_WATCHERS, + variables: { jobid }, + data: { + ...existingData, + job_watchers: updatedWatchers + } + }); + } + }); + + // Remove watcher mutation with cache update + const [removeWatcher, { loading: removing }] = useMutation(REMOVE_JOB_WATCHER, { + update(cache, { data: { delete_job_watchers } }) { + const existingData = cache.readQuery({ + query: GET_JOB_WATCHERS, + variables: { jobid } + }); + + const deletedWatcher = delete_job_watchers.returning[0]; // Safely assume one row deleted + const updatedWatchers = deletedWatcher + ? (existingData?.job_watchers || []).filter((watcher) => watcher.user_email !== deletedWatcher.user_email) + : existingData?.job_watchers || []; // No change if nothing deleted + + cache.writeQuery({ + query: GET_JOB_WATCHERS, + variables: { jobid }, + data: { + ...existingData, + job_watchers: updatedWatchers + } + }); + } + }); + + // Toggle watcher for self + const handleToggleSelf = useCallback(() => { + (isWatching + ? removeWatcher({ variables: { jobid, userEmail } }) + : addWatcher({ variables: { jobid, userEmail } }) + ).catch((err) => console.error(`Error updating job watcher: ${err.message}`)); + }, [isWatching, addWatcher, removeWatcher, jobid, userEmail]); + + // Handle removing a watcher + const handleRemoveWatcher = (userEmail) => { + removeWatcher({ variables: { jobid, userEmail } }).catch((err) => + console.error(`Error removing job watcher: ${err.message}`) + ); + }; + + const handleWatcherSelect = (selectedUser) => { + const employee = bodyshop.employees.find((e) => e.id === selectedUser); + if (!employee) return; + + const isAlreadyWatching = jobWatchers.some((w) => w.user_email === employee.user_email); + + if (isAlreadyWatching) { + handleRemoveWatcher(employee.user_email); + } else { + addWatcher({ variables: { jobid, userEmail: employee.user_email } }).catch((err) => + console.error(`Error adding job watcher: ${err.message}`) + ); + } + + setSelectedWatcher(null); + }; + + const handleTeamSelect = (team) => { + const selectedTeamMembers = JSON.parse(team); + + const newWatchers = selectedTeamMembers.filter( + (email) => !jobWatchers.some((watcher) => watcher.user_email === email) + ); + + newWatchers.forEach((email) => { + addWatcher({ variables: { jobid, userEmail: email } }).catch((err) => + console.error(`Error adding job watcher: ${err.message}`) + ); + }); + + setSelectedTeam(null); + }; + + const handleRenderItem = (watcher) => { + const employee = bodyshop.employees.find((e) => e.user_email === watcher.user_email); + const displayName = employee ? `${employee.first_name} ${employee.last_name}` : watcher.user_email; + + return ( + handleRemoveWatcher(watcher.user_email)}> + {t("notifications.actions.remove")} + + ]} + > + } />} + title={{displayName}} + description={watcher.user_email} + /> + + ); + }; + + const popoverContent = ( +
+ + + {t("notifications.labels.watching-issue")} + + {watcherLoading ? : } + + + {t("notifications.labels.add-watchers")} + jobWatchers.every((w) => w.user_email !== e.user_email))} + placeholder={t("notifications.labels.employee-search")} + value={selectedWatcher} + onChange={(value) => { + setSelectedWatcher(value); + handleWatcherSelect(value); + }} + /> + {Enhanced_Payroll && bodyshop?.employee_teams?.length > 0 && ( + <> + + {t("notifications.labels.add-watchers-team")} +