diff --git a/client/package-lock.json b/client/package-lock.json index e9c5f52b6..8ff1ef726 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.1", "hasInstallScript": true, "dependencies": { - "@amplitude/analytics-browser": "^2.37.0", + "@amplitude/analytics-browser": "^2.38.0", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -25,29 +25,29 @@ "@firebase/messaging": "^0.12.25", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", - "@sentry/cli": "^3.3.3", - "@sentry/react": "^10.45.0", + "@sentry/cli": "^3.3.5", + "@sentry/react": "^10.47.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", - "antd": "^6.3.3", + "antd": "^6.3.5", "apollo-link-logger": "^3.0.0", "autosize": "^6.0.1", - "axios": "^1.13.6", + "axios": "^1.14.0", "classnames": "^2.5.1", "css-box-model": "^1.2.1", "dayjs": "^1.11.20", - "dayjs-business-days2": "^1.3.2", + "dayjs-business-days2": "^1.3.3", "dinero.js": "^1.9.1", "dotenv": "^17.3.1", "env-cmd": "^11.0.0", "exifr": "^7.1.3", - "graphql": "^16.13.1", - "graphql-ws": "^6.0.7", - "i18next": "^25.10.5", + "graphql": "^16.13.2", + "graphql-ws": "^6.0.8", + "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", - "libphonenumber-js": "^1.12.40", + "libphonenumber-js": "^1.12.41", "lightningcss": "^1.32.0", "logrocket": "^12.1.0", "markerjs2": "^2.32.7", @@ -55,18 +55,18 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.363.2", + "posthog-js": "^1.364.4", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", "react": "^19.2.4", "react-big-calendar": "^1.19.4", "react-color": "^2.19.3", - "react-cookie": "^8.0.1", + "react-cookie": "^8.1.0", "react-dom": "^19.2.4", "react-grid-gallery": "^1.0.1", - "react-grid-layout": "^2.2.2", - "react-i18next": "^16.6.2", + "react-grid-layout": "^2.2.3", + "react-i18next": "^16.6.6", "react-icons": "^5.6.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", @@ -78,7 +78,7 @@ "react-router-dom": "^7.13.2", "react-sticky": "^6.0.3", "react-virtuoso": "^4.18.3", - "recharts": "^3.8.0", + "recharts": "^3.8.1", "redux": "^5.0.1", "redux-actions": "^3.0.3", "redux-persist": "^6.0.0", @@ -90,13 +90,13 @@ "socket.io-client": "^4.8.3", "styled-components": "^6.3.12", "vite-plugin-ejs": "^1.7.0", - "web-vitals": "^5.1.0" + "web-vitals": "^5.2.0" }, "devDependencies": { - "@ant-design/icons": "^6.1.0", + "@ant-design/icons": "^6.1.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", - "@dotenvx/dotenvx": "^1.57.2", + "@dotenvx/dotenvx": "^1.59.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.39.2", @@ -106,7 +106,7 @@ "@testing-library/react": "^16.3.2", "@vitejs/plugin-react": "^5.1.4", "babel-plugin-react-compiler": "^1.0.0", - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.6.2", "eslint": "^9.39.2", @@ -123,10 +123,10 @@ "vite": "^7.3.1", "vite-plugin-babel": "^1.6.0", "vite-plugin-eslint": "^1.8.1", - "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-node-polyfills": "^0.26.0", "vite-plugin-pwa": "^1.2.0", "vite-plugin-style-import": "^2.0.0", - "vitest": "^4.1.0", + "vitest": "^4.1.2", "workbox-window": "^7.4.0" }, "engines": { @@ -151,18 +151,18 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-browser": { - "version": "2.37.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.37.0.tgz", - "integrity": "sha512-/BWDneHRfq6+9bcPQC09Ep79SEj7aRJLZ1jJrPHtxA9KZJUz2au2COlJc1ReCaNzCcrA1xXv/MQ0Fv7TwoBglg==", + "version": "2.38.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.38.0.tgz", + "integrity": "sha512-MhqyEkr1gGAR4s4GSSflDhFVheIx9Nv3FfElQu9NlNrXB2Hh3BEOyVgdK7hgfi6NJwFyfw30+t5lym+njtA8hA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", - "@amplitude/plugin-autocapture-browser": "1.24.1", - "@amplitude/plugin-custom-enrichment-browser": "0.1.0", - "@amplitude/plugin-network-capture-browser": "1.9.9", - "@amplitude/plugin-page-url-enrichment-browser": "0.7.0", - "@amplitude/plugin-page-view-tracking-browser": "2.9.1", - "@amplitude/plugin-web-vitals-browser": "1.1.24", + "@amplitude/analytics-core": "2.44.0", + "@amplitude/plugin-autocapture-browser": "1.25.0", + "@amplitude/plugin-custom-enrichment-browser": "0.1.2", + "@amplitude/plugin-network-capture-browser": "1.9.11", + "@amplitude/plugin-page-url-enrichment-browser": "0.7.3", + "@amplitude/plugin-page-view-tracking-browser": "2.9.4", + "@amplitude/plugin-web-vitals-browser": "1.1.26", "tslib": "^2.4.1" } }, @@ -173,9 +173,9 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-core": { - "version": "2.43.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.43.0.tgz", - "integrity": "sha512-rcDqi4cmI9Ro7hN5wjAuTm92IdN2i0lhIDAj+JOd9BP3SRMrhhiw2lzcScj3owig8CiV9X7EHPTuZe6XCTfIgQ==", + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.44.0.tgz", + "integrity": "sha512-z9QuTxLqEQ8KIeAT6Vmy6K48rP9TUmjnb4GwUMYoV/fxu3B9ClTaN18zqXQMmDw9HwUiIreHiVbwTb7OQRN5aA==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.6.4", @@ -186,66 +186,72 @@ } }, "node_modules/@amplitude/plugin-autocapture-browser": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.24.1.tgz", - "integrity": "sha512-cvjOFew2MFNBDTbk3+H7WNi3D0Jdp476m6faCaVhY99M5zqRCHDMRS7dC4HczvL9zYXlAcW9jAWucwES2m3TiQ==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.25.0.tgz", + "integrity": "sha512-YuWsz8XmJuKu3NlMxkvlhLey/5tGCeOwwfsROHficR0yDWO9gNG0WtHl7A0Pw1PUc9iaXjqfG2AjYumAtiq16Q==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-custom-enrichment-browser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-custom-enrichment-browser/-/plugin-custom-enrichment-browser-0.1.0.tgz", - "integrity": "sha512-y3VmqZvCP1Z3jNgo/mtKVHON9L0P2SyqkMmUsbbFuLu1+TKIkicotnVq/lzlLU1TrW68mkInOM+We8JngasZBA==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-custom-enrichment-browser/-/plugin-custom-enrichment-browser-0.1.2.tgz", + "integrity": "sha512-ZX9BKqs1E1OI7l7QCGu9JnB/1kqLN+zqIePgM2tuEhZNFQJaw4NhAMUaMRqvNnaCkHlmpVRISzSj/4D3tWMRtA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-network-capture-browser": { - "version": "1.9.9", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.9.tgz", - "integrity": "sha512-SJIOQN04Mk9vCsnVd9QRcIvkMV7XSGZIKfbaKNQY5O3ueV33Kc8opm7YjPg2sWcxdzTcJijbCkOI0wCwOaRolg==", + "version": "1.9.11", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.9.11.tgz", + "integrity": "sha512-49o3zYnKUmRdrxgAEcr1iHnXR1um40e1icO0hzugSq04k19hs27zcl3zpEk9geO+nNKwO744ryE1q93gqVbHrQ==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-url-enrichment-browser": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.7.0.tgz", - "integrity": "sha512-MkM7TDq24k7ilUDNZISqjDSkVfmDJxWcnUagwYEXjLILhno5hGm7wdgFvVXXzKlZQHEogBxkbnq7wZXS9/YsMw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.7.3.tgz", + "integrity": "sha512-3UZq/zKg4lcsRgziWAPSEeaUsNsbyjjxmsAE9kSDi/hIj5RaWnwWhY6TGhv45UAReugTA4vVZyFRg9btf3c/Fg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.9.1.tgz", - "integrity": "sha512-jkxz2lkJDAfsjj7mpbPUZx9N3qJssC3uYyv8Nk73z+p+v0wjBikWdOoKuNQkcuP09701zRdXp9ziU8+qwkGusw==", + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.9.4.tgz", + "integrity": "sha512-J16zmEadnzNpkHSmzpTiQN2q9pGJ/4SkHONA9O8KxUsMU/MYTDgof3rAYY/w5B5rmvdxfMRCjqWtvnkizzgZ6w==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1" } }, "node_modules/@amplitude/plugin-web-vitals-browser": { - "version": "1.1.24", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.24.tgz", - "integrity": "sha512-7AaytUK78RKdyDsblYJCKYan1lQi3Qzsp1WHItHJ+RSXPccmi4mCcvNtx0e8T9LmNJlUnsmYeEGR/6FaWvyvFg==", + "version": "1.1.26", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.26.tgz", + "integrity": "sha512-wiD4vy+f2fepr+8Lnn26TYYjDEnWsmlGhJog99x+xfbZ/D+stGdaCIOz5AOjU1TpTRvxvamEu2XuOh+8EZOCSA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "2.43.0", + "@amplitude/analytics-core": "2.44.0", "tslib": "^2.4.1", "web-vitals": "5.1.0" } }, + "node_modules/@amplitude/plugin-web-vitals-browser/node_modules/web-vitals": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", + "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "license": "Apache-2.0" + }, "node_modules/@ant-design/colors": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", @@ -327,9 +333,9 @@ } }, "node_modules/@ant-design/icons": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz", - "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.1.tgz", + "integrity": "sha512-AMT4N2y++TZETNHiM77fs4a0uPVCJGuL5MTonk13Pvv7UN7sID1cNEZOc1qNqx6zLKAOilTEFAdAoAFKa0U//Q==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.0", @@ -2588,9 +2594,9 @@ } }, "node_modules/@dotenvx/dotenvx": { - "version": "1.57.2", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.57.2.tgz", - "integrity": "sha512-lv9+UZPnl/KOvShepevLWm3+/wc1It5kgO5Q580evnvOFMZcgKVEYFwxlL7Ohl9my1yjTsWo28N3PJYUEO8wFQ==", + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.59.1.tgz", + "integrity": "sha512-Qg+meC+XFxliuVSDlEPkKnaUjdaJKK6FNx/Wwl2UxhQR8pyPIuLhMavsF7ePdB9qFZUWV1jEK3ckbJir/WmF4w==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -4864,18 +4870,18 @@ } }, "node_modules/@posthog/core": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.24.1.tgz", - "integrity": "sha512-e8AciAnc6MRFws89ux8lJKFAaI03yEon0ASDoUO7yS91FVqbUGXYekObUUR3LHplcg+pmyiJBI0jolY0SFbGRA==", + "version": "1.24.4", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.24.4.tgz", + "integrity": "sha512-S+TolwBHSSJz7WWtgaELQWQqXviSm3uf1e+qorWUts0bZcgPwWzhnmhCUZAhvn0NVpTQHDJ3epv+hHbPLl5dHg==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" } }, "node_modules/@posthog/types": { - "version": "1.363.2", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.363.2.tgz", - "integrity": "sha512-UcUwHEd2LXxWq4bW/I4TbwYcA+BHO/cSuHcNpGXjRCp76eJk1eOuQnm/a3MrfHtbt2X11CQu+eWpqiSgcv+X6A==", + "version": "1.364.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.364.4.tgz", + "integrity": "sha512-U7NpIy9XWrzz1q/66xyDu8Wm12a7avNRKRn5ISPT5kuCJQRaeAaHuf+dpgrFnuqjCCgxg+oIY/ReJdlZ+8/z4Q==", "license": "MIT" }, "node_modules/@protobufjs/aspromise": { @@ -5139,9 +5145,9 @@ } }, "node_modules/@rc-component/form": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.7.2.tgz", - "integrity": "sha512-5C90rXH7aZvvvxB4M5ew+QxROvimdL/lqhSshR8NsyiR7HKOoGQYSitxdfENnH6/0KNFxEy2ranVe2LrTnHZIw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.0.tgz", + "integrity": "sha512-eUD5KKYnIZWmJwRA0vnyO/ovYUfHGU1svydY1OrqU5fw8Oz9Tdqvxvrlh0wl6xI/EW69dT7II49xpgOWzK3T5A==", "license": "MIT", "dependencies": { "@rc-component/async-validator": "^5.1.0", @@ -5166,14 +5172,14 @@ } }, "node_modules/@rc-component/image": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.6.0.tgz", - "integrity": "sha512-tSfn2ZE/oP082g4QIOxeehkmgnXB7R+5AFj/lIFr4k7pEuxHBdyGIq9axoCY9qea8NN0DY6p4IB/F07tLqaT5A==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.8.0.tgz", + "integrity": "sha512-Dr41bFevLB5NgVaJhEUmNvbEf+ynAhim6W98ZW2xvCsdFISc2TYP4ZvCVdie3eaZdum2kieVcvpNHu+UrzAAHA==", "license": "MIT", "dependencies": { "@rc-component/motion": "^1.0.0", "@rc-component/portal": "^2.1.2", - "@rc-component/util": "^1.3.0", + "@rc-component/util": "^1.10.0", "clsx": "^2.1.1" }, "peerDependencies": { @@ -5303,9 +5309,9 @@ } }, "node_modules/@rc-component/motion": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.1.tgz", - "integrity": "sha512-Wo1mkd0tCcHtvYvpPOmlYJz546z16qlsiwaygmW7NPJpOZOF9GBjhGzdzZSsC2lEJ1IUkWLF4gMHlRA1aSA+Yw==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.2.tgz", + "integrity": "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ==", "license": "MIT", "dependencies": { "@rc-component/util": "^1.2.0", @@ -5555,9 +5561,9 @@ } }, "node_modules/@rc-component/resize-observer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.1.tgz", - "integrity": "sha512-NfXXMmiR+SmUuKE1NwJESzEUYUFWIDUn2uXpxCTOLwiRUUakd62DRNFjRJArgzyFW8S5rsL4aX5XlyIXyC/vRA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", + "integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==", "license": "MIT", "dependencies": { "@rc-component/util": "^1.2.0" @@ -5938,9 +5944,9 @@ } }, "node_modules/@rc-component/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.9.0.tgz", - "integrity": "sha512-5uW6AfhIigCWeEQDthTozlxiT4Prn6xYQWeO0xokjcaa186OtwPRHBZJ2o0T0FhbjGhZ3vXdbkv0sx3gAYW7Vg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.10.0.tgz", + "integrity": "sha512-aY9GLBuiUdpyfIUpAWSYer4Tu3mVaZCo5A0q9NtXcazT3MRiI3/WNHCR+DUn5VAtR6iRRf0ynCqQUcHli5UdYw==", "license": "MIT", "dependencies": { "is-mobile": "^5.0.0", @@ -6712,50 +6718,50 @@ ] }, "node_modules/@sentry-internal/browser-utils": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.45.0.tgz", - "integrity": "sha512-ZPZpeIarXKScvquGx2AfNKcYiVNDA4wegMmjyGVsTA2JPmP0TrJoO3UybJS6KGDeee8V3I3EfD/ruauMm7jOFQ==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.47.0.tgz", + "integrity": "sha512-bVFRAeJWMBcBCvJKIFCMJ1/yQToL4vPGqfmlnDZeypcxkqUDKQ/Y3ziLHXoDL2sx0lagcgU2vH1QhCQ67Aujjw==", "license": "MIT", "dependencies": { - "@sentry/core": "10.45.0" + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/feedback": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.45.0.tgz", - "integrity": "sha512-vCSurazFVq7RUeYiM5X326jA5gOVrWYD6lYX2fbjBOMcyCEhDnveNxMT62zKkZDyNT/jyD194nz/cjntBUkyWA==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.47.0.tgz", + "integrity": "sha512-pdvMmi4dQpX5S/vAAzrhHPIw3T3HjUgDNgUiCBrlp7N9/6zGO2gNPhUnNekP+CjgI/z0rvf49RLqlDenpNrMOg==", "license": "MIT", "dependencies": { - "@sentry/core": "10.45.0" + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.45.0.tgz", - "integrity": "sha512-vjosRoGA1bzhVAEO1oce+CsRdd70quzBeo7WvYqpcUnoLe/Rv8qpOMqWX3j26z7XfFHMExWQNQeLxmtYOArvlw==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.47.0.tgz", + "integrity": "sha512-ScdovxP7hJxgMt70+7hFvwT02GIaIUAxdEM/YPsayZBeCoAukPW8WiwztJfoKtsfPyKJ5A6f0H3PIxTPcA9Row==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.45.0", - "@sentry/core": "10.45.0" + "@sentry-internal/browser-utils": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.45.0.tgz", - "integrity": "sha512-nvq/AocdZTuD7y0KSiWi3gVaY0s5HOFy86mC/v1kDZmT/jsBAzN5LDkk/f1FvsWma1peqQmpUqxvhC+YIW294Q==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.47.0.tgz", + "integrity": "sha512-A5OY8friSe6g8WAK4L8IeOPiEd9D3Ps40DzRH5j2f6SUja0t90mKMvHRcRf8zq0d4BkdB+JM7tjOkwxpuv8heA==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "10.45.0", - "@sentry/core": "10.45.0" + "@sentry-internal/replay": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" @@ -6771,16 +6777,16 @@ } }, "node_modules/@sentry/browser": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.45.0.tgz", - "integrity": "sha512-e/a8UMiQhqqv706McSIcG6XK+AoQf9INthi2pD+giZfNRTzXTdqHzUT5OIO5hg8Am6eF63nDJc+vrYNPhzs51Q==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.47.0.tgz", + "integrity": "sha512-rC0agZdxKA5XWfL4VwPOr/rJMogXDqZgnVzr93YWpFn9DMZT/7LzxSJVPIJwRUjx3bFEby3PcTa3YaX7pxm1AA==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.45.0", - "@sentry-internal/feedback": "10.45.0", - "@sentry-internal/replay": "10.45.0", - "@sentry-internal/replay-canvas": "10.45.0", - "@sentry/core": "10.45.0" + "@sentry-internal/browser-utils": "10.47.0", + "@sentry-internal/feedback": "10.47.0", + "@sentry-internal/replay": "10.47.0", + "@sentry-internal/replay-canvas": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" @@ -7004,9 +7010,9 @@ } }, "node_modules/@sentry/cli": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.3.3.tgz", - "integrity": "sha512-4CZtfgiOraX+BntMjYQhfLDArXwpqt3sEo5Zdj2pqWSZSd4yI3ncfQ21CsxLcI/sUQrjmD5Vzidu4/1OShyxtA==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-3.3.5.tgz", + "integrity": "sha512-eyLHTj0rpeCsOUX+1ZU8UEWRXy6nXvTXNdhtAt1t6YXan9gSsAexZf28zVmDcYcP8WRbK0D2JMLp7NcaQCQgEA==", "hasInstallScript": true, "license": "FSL-1.1-MIT", "dependencies": { @@ -7022,20 +7028,20 @@ "node": ">= 18" }, "optionalDependencies": { - "@sentry/cli-darwin": "3.3.3", - "@sentry/cli-linux-arm": "3.3.3", - "@sentry/cli-linux-arm64": "3.3.3", - "@sentry/cli-linux-i686": "3.3.3", - "@sentry/cli-linux-x64": "3.3.3", - "@sentry/cli-win32-arm64": "3.3.3", - "@sentry/cli-win32-i686": "3.3.3", - "@sentry/cli-win32-x64": "3.3.3" + "@sentry/cli-darwin": "3.3.5", + "@sentry/cli-linux-arm": "3.3.5", + "@sentry/cli-linux-arm64": "3.3.5", + "@sentry/cli-linux-i686": "3.3.5", + "@sentry/cli-linux-x64": "3.3.5", + "@sentry/cli-win32-arm64": "3.3.5", + "@sentry/cli-win32-i686": "3.3.5", + "@sentry/cli-win32-x64": "3.3.5" } }, "node_modules/@sentry/cli-darwin": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.3.3.tgz", - "integrity": "sha512-P8DoL79eX5fhKCfBHHl7xwwTShDPOb2drJC8lizZ3v1iS1JLPrNweM1KEzDefR30zH1wghbLSwsYv/svWdM3wA==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-3.3.5.tgz", + "integrity": "sha512-E/SIY6+j2nt6Ri9nMt78sYle3LiF6uZyz4HGmvcEMU6HXjegmAayhy0J10JST+vZTzN6VixD8sUsa5UeJiOPcg==", "license": "FSL-1.1-MIT", "optional": true, "os": [ @@ -7046,9 +7052,9 @@ } }, "node_modules/@sentry/cli-linux-arm": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-3.3.3.tgz", - "integrity": "sha512-a7o/huozveLIImXHe0HDwEMVhvDopOP2tLcopvV7sQsVE8f/QOShR5FudKjmiaZz2opdLzPJO9pv5WuF9jAZPg==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-3.3.5.tgz", + "integrity": "sha512-EGuEIvC2OQyar/vu+jAQEmovTMgxpoxdx5knnzL5dLhIemjEUNqwv/sXq+m/Aj+ThqCMofcTWB2TOZXsTtl0Tw==", "cpu": [ "arm" ], @@ -7064,9 +7070,9 @@ } }, "node_modules/@sentry/cli-linux-arm64": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.3.3.tgz", - "integrity": "sha512-9jaX9RGyTpjo9u2urNi5ciBDpRdTt107YJpFXev+BFHJ6Lwz/owgRuYzPRfAen8hKkOOFheZ3iy07kl576eZzw==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-3.3.5.tgz", + "integrity": "sha512-/W7HTk2OFKD0bguTvQR1ue6pkFQWaGiqPafOSIQKyq0aGfbZhBn/Uj+IRefgMZMhJQ29xRz0y/iGRGKE+ef4Vg==", "cpu": [ "arm64" ], @@ -7082,9 +7088,9 @@ } }, "node_modules/@sentry/cli-linux-i686": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-3.3.3.tgz", - "integrity": "sha512-VngQYzR2kDm2oojCuYF20ebLTK8HKvEwxe785J6gxob8Ef9JvZkERyUqENYppBa9aVgN0pandqPAqOECWykTMA==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-3.3.5.tgz", + "integrity": "sha512-qODMEWLEeUNp3IUlwwISB37EXSo8qgMmHQuLKfxDjpIKw+7NAFfptOloqPrHkLWK3TzFr+Nv643wIKZaYrz28Q==", "cpu": [ "x86", "ia32" @@ -7101,9 +7107,9 @@ } }, "node_modules/@sentry/cli-linux-x64": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-3.3.3.tgz", - "integrity": "sha512-rBxXQeIYGefUNI2cXHxEr0y3bhxDQjOD4G6j/gqLz/Dj+l8gJ/iKP64kTudnoViNIpn0pdYccG69th7zmzM/Fg==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-3.3.5.tgz", + "integrity": "sha512-DCz7lQ4PySjQ1WvWOQ/uTdwauRo1D7hSHazxZ+fUAnK/epSPM9qLkjDMlD8uM5CaLoR8+ZTs7N94vV5LZs2QpA==", "cpu": [ "x64" ], @@ -7119,9 +7125,9 @@ } }, "node_modules/@sentry/cli-win32-arm64": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.3.3.tgz", - "integrity": "sha512-c52g+YS6BO0rzH8AEHqQPmpqZrw0GJjMWqy0tQ5jcqaGdaLVnxk0mMEubv8R6Dv5MR2LShoKjiNsaeVfrWIMUg==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-3.3.5.tgz", + "integrity": "sha512-VMNsHiyZcP8Ft3fcK/1zoO4L66soe1eSfXg2tglFQSc/2MYA5v1Br9B1GtjBwDIc3EmdPtFZhOGLyqIzszMxJw==", "cpu": [ "arm64" ], @@ -7135,9 +7141,9 @@ } }, "node_modules/@sentry/cli-win32-i686": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-3.3.3.tgz", - "integrity": "sha512-DygYzSY/+tS7oFj/mfeg/yzYxsQx3fO8cI+IWc2pns/at+JcJ9O5xyM/x/q55wOxpnwla7RL1D3rsqK2mqkYfg==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-3.3.5.tgz", + "integrity": "sha512-BE6aHOIpsm4jgavsvvXNcSikAr/8NSva3rk1N3BzoOLuX+dcFxBI60K1i2VzB1vsgtivJJo9YySNCi60dBgWTg==", "cpu": [ "x86", "ia32" @@ -7152,9 +7158,9 @@ } }, "node_modules/@sentry/cli-win32-x64": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-3.3.3.tgz", - "integrity": "sha512-i0glPcHwkqbVA2Y+0Yz7CD/l8TSkfft1a+lTU9yk/+DDU8WGkyArEAxAji9bGo4p+k5HIFC8OC2MwpKdcdFM4Q==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-3.3.5.tgz", + "integrity": "sha512-MSU+PtBuiLjEbiPFOvxk4CI3TCagwkIg9kvJ+DrI3+pBY0Sga/dOyeWKTIgb01xSVcfjdw0UkpU52VCvzTT9ew==", "cpu": [ "x64" ], @@ -7189,22 +7195,22 @@ } }, "node_modules/@sentry/core": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.45.0.tgz", - "integrity": "sha512-s69UXxvefeQxuZ5nY7/THtTrIEvJxNVCp3ns4kwoCw1qMpgpvn/296WCKVmM7MiwnaAdzEKnAvLAwaxZc2nM7Q==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.47.0.tgz", + "integrity": "sha512-nsYRAx3EWezDut+Zl+UwwP07thh9uY7CfSAi2whTdcJl5hu1nSp2z8bba7Vq/MGbNLnazkd3A+GITBEML924JA==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/@sentry/react": { - "version": "10.45.0", - "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.45.0.tgz", - "integrity": "sha512-jLezuxi4BUIU3raKyAPR5xMbQG/nhwnWmKo5p11NCbLmWzkS+lxoyDTUB4B8TAKZLfdtdkKLOn1S0tFc8vbUHw==", + "version": "10.47.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.47.0.tgz", + "integrity": "sha512-ZtJV6xxF8jUVE9e3YQUG3Do0XapG1GjniyLyqMPgN6cNvs/HaRJODf7m60By+VGqcl5XArEjEPTvx8CdPUXDfA==", "license": "MIT", "dependencies": { - "@sentry/browser": "10.45.0", - "@sentry/core": "10.45.0" + "@sentry/browser": "10.47.0", + "@sentry/core": "10.47.0" }, "engines": { "node": ">=18" @@ -7751,31 +7757,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", - "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "chai": "^6.2.2", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", - "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.0", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -7784,7 +7790,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -7816,26 +7822,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", - "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", - "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.0", + "@vitest/utils": "4.1.2", "pathe": "^2.0.3" }, "funding": { @@ -7850,14 +7856,14 @@ "license": "MIT" }, "node_modules/@vitest/snapshot": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", - "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -7883,9 +7889,9 @@ "license": "MIT" }, "node_modules/@vitest/spy": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", - "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "dev": true, "license": "MIT", "funding": { @@ -7893,15 +7899,15 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", - "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.0", + "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -8036,16 +8042,16 @@ } }, "node_modules/antd": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.3.tgz", - "integrity": "sha512-T8FAQelw36zS96cZw2U/qEjpYny5yFc7hg+1W7DvVr8xMoSXWvyB8WvmiDVH0nS0LPYV4y2sxetsJoGZt7rhhw==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.3.5.tgz", + "integrity": "sha512-8BPz9lpZWQm42PTx7yL4KxWAotVuqINiKcoYRcLtdd5BFmAcAZicVyFTnBJyRDlzGZFZeRW3foGu6jXYFnej6Q==", "license": "MIT", "dependencies": { "@ant-design/colors": "^8.0.1", "@ant-design/cssinjs": "^2.1.2", "@ant-design/cssinjs-utils": "^2.1.2", "@ant-design/fast-color": "^3.0.1", - "@ant-design/icons": "^6.1.0", + "@ant-design/icons": "^6.1.1", "@ant-design/react-slick": "~2.0.0", "@babel/runtime": "^7.28.4", "@rc-component/cascader": "~1.14.0", @@ -8055,13 +8061,13 @@ "@rc-component/dialog": "~1.8.4", "@rc-component/drawer": "~1.4.2", "@rc-component/dropdown": "~1.0.2", - "@rc-component/form": "~1.7.2", - "@rc-component/image": "~1.6.0", + "@rc-component/form": "~1.8.0", + "@rc-component/image": "~1.8.0", "@rc-component/input": "~1.1.2", "@rc-component/input-number": "~1.6.2", "@rc-component/mentions": "~1.6.0", "@rc-component/menu": "~1.2.0", - "@rc-component/motion": "^1.3.1", + "@rc-component/motion": "^1.3.2", "@rc-component/mutate-observer": "^2.0.1", "@rc-component/notification": "~1.2.0", "@rc-component/pagination": "~1.2.0", @@ -8069,9 +8075,9 @@ "@rc-component/progress": "~1.0.2", "@rc-component/qrcode": "~1.1.1", "@rc-component/rate": "~1.0.1", - "@rc-component/resize-observer": "^1.1.1", + "@rc-component/resize-observer": "^1.1.2", "@rc-component/segmented": "~1.3.0", - "@rc-component/select": "~1.6.14", + "@rc-component/select": "~1.6.15", "@rc-component/slider": "~1.0.1", "@rc-component/steps": "~1.2.2", "@rc-component/switch": "~1.0.3", @@ -8084,7 +8090,7 @@ "@rc-component/tree-select": "~1.8.0", "@rc-component/trigger": "^3.9.0", "@rc-component/upload": "~1.1.0", - "@rc-component/util": "^1.9.0", + "@rc-component/util": "^1.10.0", "clsx": "^2.1.1", "dayjs": "^1.11.11", "scroll-into-view-if-needed": "^3.1.0", @@ -8420,14 +8426,23 @@ } }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" } }, "node_modules/babel-plugin-macros": { @@ -8545,12 +8560,15 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.12.tgz", - "integrity": "sha512-Mij6Lij93pTAIsSYy5cyBQ975Qh9uLEc5rwGTpomiZeXZL9yIS6uORJakb3ScHgfs0serMMfIbXzokPMuEiRyw==", + "version": "2.10.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.12.tgz", + "integrity": "sha512-qyq26DxfY4awP2gIRXhhLWfwzwI+N5Nxk6iQi8EFizIaWIjqicQTE4sLnZZVdeKPRcVNoJOkkpfzoIYuvCKaIQ==", "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bidi-js": { @@ -8782,9 +8800,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -8801,11 +8819,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -8972,9 +8990,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001762", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", - "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "version": "1.0.30001782", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001782.tgz", + "integrity": "sha512-dZcaJLJeDMh4rELYFw1tvSn1bhZWYFOt468FcbHHxx/Z/dFidd1I6ciyFdi3iwfQCyOjqo9upF6lGQYtMiJWxw==", "funding": [ { "type": "opencollective", @@ -9738,12 +9756,12 @@ "license": "MIT" }, "node_modules/dayjs-business-days2": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dayjs-business-days2/-/dayjs-business-days2-1.3.2.tgz", - "integrity": "sha512-UDJcMw5tM6hoIu8QgP4ASKuVPrnFqB8WMbtfmLe2WNMX/n6zmXfPKwULJS2CKpS+N/+Jdq5Vmo8dkke0sIaV1A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/dayjs-business-days2/-/dayjs-business-days2-1.3.3.tgz", + "integrity": "sha512-ogedXtGep3W1rl/rhzrSbZU7cOA7Cr3s9HY7iiXDqkDv/LarERc1AEI3kvQ+sF43K1HLoXfByu2XQvU7jfqF9w==", "license": "MIT", "dependencies": { - "dayjs": "^1.11.19" + "dayjs": "^1.11.20" } }, "node_modules/debug": { @@ -10088,9 +10106,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.329", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.329.tgz", + "integrity": "sha512-/4t+AS1l4S3ZC0Ja7PHFIWeBIxGA3QGqV8/yKsP36v7NcyUCl+bIcmw6s5zVuMIECWwBrAK/6QLzTmbJChBboQ==", "license": "ISC" }, "node_modules/elliptic": { @@ -11411,9 +11429,9 @@ "license": "ISC" }, "node_modules/graphql": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.1.tgz", - "integrity": "sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ==", + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -11435,9 +11453,9 @@ } }, "node_modules/graphql-ws": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.7.tgz", - "integrity": "sha512-yoLRW+KRlDmnnROdAu7sX77VNLC0bsFoZyGQJLy1cF+X/SkLg/fWkRGrEEYQK8o2cafJ2wmEaMqMEZB3U3DYDg==", + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.8.tgz", + "integrity": "sha512-m3EOaNsUBXwAnkBWbzPfe0Nq8pXUfxsWnolC54sru3FzHvhTZL0Ouf/BoQsaGAXqM+YPerXOJ47BUnmgmoupCw==", "license": "MIT", "engines": { "node": ">=20" @@ -11784,9 +11802,9 @@ } }, "node_modules/i18next": { - "version": "25.10.5", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.5.tgz", - "integrity": "sha512-jRnF7eRNsdcnh7AASSgaU3lj/8lJZuHkfsouetnLEDH0xxE1vVi7qhiJ9RhdSPUyzg4ltb7P7aXsFlTk9sxL2w==", + "version": "25.10.10", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.10.tgz", + "integrity": "sha512-cqUW2Z3EkRx7NqSyywjkgCLK7KLCL6IFVFcONG7nVYIJ3ekZ1/N5jUsihHV6Bq37NfhgtczxJcxduELtjTwkuQ==", "funding": [ { "type": "individual", @@ -11806,7 +11824,7 @@ "@babel/runtime": "^7.29.2" }, "peerDependencies": { - "typescript": "^5" + "typescript": "^5 || ^6" }, "peerDependenciesMeta": { "typescript": { @@ -12927,9 +12945,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.40", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.40.tgz", - "integrity": "sha512-HKGs7GowShNls3Zh+7DTr6wYpPk5jC78l508yQQY3e8ZgJChM3A9JZghmMJZuK+5bogSfuTafpjksGSR3aMIEg==", + "version": "1.12.41", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", + "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", "license": "MIT" }, "node_modules/lightningcss": { @@ -14310,9 +14328,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "license": "MIT" }, "node_modules/node-stdlib-browser": { @@ -15070,9 +15088,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.363.2", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.363.2.tgz", - "integrity": "sha512-4ZEWMrymlFzjgDSmh25VeJQT//2XUFbfKqEPDNUW4dxcqWiVMo1+gJFy5YhJgVYS46OAXLbMcJgmuZBCnDIgVg==", + "version": "1.364.4", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.364.4.tgz", + "integrity": "sha512-T71zr06gH5YcrjS7c+sdzqfZKMxqqXC/a0w++zMQIPbL1ejvF9PdfUi0Kyd6Sy78Ocbb2smobdzBh8vXLwC+lQ==", "license": "SEE LICENSE IN LICENSE", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -15080,8 +15098,8 @@ "@opentelemetry/exporter-logs-otlp-http": "^0.208.0", "@opentelemetry/resources": "^2.2.0", "@opentelemetry/sdk-logs": "^0.208.0", - "@posthog/core": "1.24.1", - "@posthog/types": "1.363.2", + "@posthog/core": "1.24.4", + "@posthog/types": "1.364.4", "core-js": "^3.38.1", "dompurify": "^3.3.2", "fflate": "^0.4.8", @@ -15447,12 +15465,12 @@ } }, "node_modules/react-cookie": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.0.1.tgz", - "integrity": "sha512-QNdAd0MLuAiDiLcDU/2s/eyKmmfMHtjPUKJ2dZ/5CcQ9QKUium4B3o61/haq6PQl/YWFqC5PO8GvxeHKhy3GFA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-8.1.0.tgz", + "integrity": "sha512-Qs+gD3gpQmUXnJUZafhJtNWhhNdi8OYbOAF5YQRAZa/D171ILOIEMfXDz/tmhkE+nOthllmqryHH6I/qmvIYWQ==", "license": "MIT", "dependencies": { - "@types/hoist-non-react-statics": "^3.3.6", + "@types/hoist-non-react-statics": "^3.3.7", "hoist-non-react-statics": "^3.3.2", "universal-cookie": "^8.0.0" }, @@ -15512,16 +15530,16 @@ } }, "node_modules/react-grid-layout": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.2.tgz", - "integrity": "sha512-yNo9pxQWoxHWRAwHGSVT4DEGELYPyQ7+q9lFclb5jcqeFzva63/2F72CryS/jiTIr/SBIlTaDdyjqH+ODg8oBw==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.3.tgz", + "integrity": "sha512-OAEJHBxmfuxQfVtZwRzmsokijGlBgzYIJ7MUlLk/VSa43SaGzu15w5D0P2RDrfX5EvP9POMbL6bFrai/huDzbQ==", "license": "MIT", "dependencies": { "clsx": "^2.1.1", "fast-equals": "^4.0.3", "prop-types": "^15.8.1", "react-draggable": "^4.4.6", - "react-resizable": "^3.0.5", + "react-resizable": "^3.1.3", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { @@ -15545,9 +15563,9 @@ "license": "MIT" }, "node_modules/react-i18next": { - "version": "16.6.2", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.2.tgz", - "integrity": "sha512-/S/GPzElTqEi5o2kzd0/O2627hPDmE6OGhJCCwCfUaQ3syyu+kaYH8/PYFtZeWc25NzfxTN/2fD1QjvrTgrFfA==", + "version": "16.6.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.6.6.tgz", + "integrity": "sha512-ZgL2HUoW34UKUkOV7uSQFE1CDnRPD+tCR3ywSuWH7u2iapnz86U8Bi3Vrs620qNDzCf1F47NxglCEkchCTDOHw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.29.2", @@ -15555,9 +15573,9 @@ "use-sync-external-store": "^1.6.0" }, "peerDependencies": { - "i18next": ">= 25.6.2", + "i18next": ">= 25.10.9", "react": ">= 16.8.0", - "typescript": "^5" + "typescript": "^5 || ^6" }, "peerDependenciesMeta": { "react-dom": { @@ -15858,9 +15876,9 @@ } }, "node_modules/recharts": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", - "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", "license": "MIT", "workspaces": [ "www" @@ -18507,9 +18525,9 @@ } }, "node_modules/vite-plugin-node-polyfills": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.25.0.tgz", - "integrity": "sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.26.0.tgz", + "integrity": "sha512-BAe5YzJf368XGev02hDvioidx4uVH8dqEJlG73bjQSxM26/AQnGcKFomq9n3vGq5yqpSHKN4h1XQNxx9l98mBg==", "dev": true, "license": "MIT", "dependencies": { @@ -18520,7 +18538,7 @@ "url": "https://github.com/sponsors/davidmyersdev" }, "peerDependencies": { - "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/vite-plugin-pwa": { @@ -18613,19 +18631,19 @@ } }, "node_modules/vitest": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", - "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.0", - "@vitest/mocker": "4.1.0", - "@vitest/pretty-format": "4.1.0", - "@vitest/runner": "4.1.0", - "@vitest/snapshot": "4.1.0", - "@vitest/spy": "4.1.0", - "@vitest/utils": "4.1.0", + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -18636,8 +18654,8 @@ "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -18653,13 +18671,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.0", - "@vitest/browser-preview": "4.1.0", - "@vitest/browser-webdriverio": "4.1.0", - "@vitest/ui": "4.1.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -18757,9 +18775,9 @@ } }, "node_modules/web-vitals": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", - "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.2.0.tgz", + "integrity": "sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==", "license": "Apache-2.0" }, "node_modules/webidl-conversions": { diff --git a/client/package.json b/client/package.json index 4f03ad75d..7e95380a6 100644 --- a/client/package.json +++ b/client/package.json @@ -8,7 +8,7 @@ "private": true, "proxy": "http://localhost:4000", "dependencies": { - "@amplitude/analytics-browser": "^2.37.0", + "@amplitude/analytics-browser": "^2.38.0", "@ant-design/pro-layout": "^7.22.6", "@apollo/client": "^4.1.6", "@dnd-kit/core": "^6.3.1", @@ -24,29 +24,29 @@ "@firebase/messaging": "^0.12.25", "@jsreport/browser-client": "^3.1.0", "@reduxjs/toolkit": "^2.11.2", - "@sentry/cli": "^3.3.3", - "@sentry/react": "^10.45.0", + "@sentry/cli": "^3.3.5", + "@sentry/react": "^10.47.0", "@sentry/vite-plugin": "^4.9.1", "@splitsoftware/splitio-react": "^2.6.1", "@tanem/react-nprogress": "^5.0.63", - "antd": "^6.3.3", + "antd": "^6.3.5", "apollo-link-logger": "^3.0.0", "autosize": "^6.0.1", - "axios": "^1.13.6", + "axios": "^1.14.0", "classnames": "^2.5.1", "css-box-model": "^1.2.1", "dayjs": "^1.11.20", - "dayjs-business-days2": "^1.3.2", + "dayjs-business-days2": "^1.3.3", "dinero.js": "^1.9.1", "dotenv": "^17.3.1", "env-cmd": "^11.0.0", "exifr": "^7.1.3", - "graphql": "^16.13.1", - "graphql-ws": "^6.0.7", - "i18next": "^25.10.5", + "graphql": "^16.13.2", + "graphql-ws": "^6.0.8", + "i18next": "^25.10.10", "i18next-browser-languagedetector": "^8.2.1", "immutability-helper": "^3.1.1", - "libphonenumber-js": "^1.12.40", + "libphonenumber-js": "^1.12.41", "lightningcss": "^1.32.0", "logrocket": "^12.1.0", "markerjs2": "^2.32.7", @@ -54,18 +54,18 @@ "normalize-url": "^8.1.1", "object-hash": "^3.0.0", "phone": "^3.1.71", - "posthog-js": "^1.363.2", + "posthog-js": "^1.364.4", "prop-types": "^15.8.1", "query-string": "^9.3.1", "raf-schd": "^4.0.3", "react": "^19.2.4", "react-big-calendar": "^1.19.4", "react-color": "^2.19.3", - "react-cookie": "^8.0.1", + "react-cookie": "^8.1.0", "react-dom": "^19.2.4", "react-grid-gallery": "^1.0.1", - "react-grid-layout": "^2.2.2", - "react-i18next": "^16.6.2", + "react-grid-layout": "^2.2.3", + "react-i18next": "^16.6.6", "react-icons": "^5.6.0", "react-image-lightbox": "^5.1.4", "react-markdown": "^10.1.0", @@ -77,7 +77,7 @@ "react-router-dom": "^7.13.2", "react-sticky": "^6.0.3", "react-virtuoso": "^4.18.3", - "recharts": "^3.8.0", + "recharts": "^3.8.1", "redux": "^5.0.1", "redux-actions": "^3.0.3", "redux-persist": "^6.0.0", @@ -89,7 +89,7 @@ "socket.io-client": "^4.8.3", "styled-components": "^6.3.12", "vite-plugin-ejs": "^1.7.0", - "web-vitals": "^5.1.0" + "web-vitals": "^5.2.0" }, "scripts": { "postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'", @@ -137,10 +137,10 @@ "@rollup/rollup-linux-x64-gnu": "4.6.1" }, "devDependencies": { - "@ant-design/icons": "^6.1.0", + "@ant-design/icons": "^6.1.1", "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/preset-react": "^7.28.5", - "@dotenvx/dotenvx": "^1.57.2", + "@dotenvx/dotenvx": "^1.59.1", "@emotion/babel-plugin": "^11.13.5", "@emotion/react": "^11.14.0", "@eslint/js": "^9.39.2", @@ -150,7 +150,7 @@ "@testing-library/react": "^16.3.2", "@vitejs/plugin-react": "^5.1.4", "babel-plugin-react-compiler": "^1.0.0", - "browserslist": "^4.28.1", + "browserslist": "^4.28.2", "browserslist-to-esbuild": "^2.1.1", "chalk": "^5.6.2", "eslint": "^9.39.2", @@ -167,10 +167,10 @@ "vite": "^7.3.1", "vite-plugin-babel": "^1.6.0", "vite-plugin-eslint": "^1.8.1", - "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-node-polyfills": "^0.26.0", "vite-plugin-pwa": "^1.2.0", "vite-plugin-style-import": "^2.0.0", - "vitest": "^4.1.0", + "vitest": "^4.1.2", "workbox-window": "^7.4.0" } } diff --git a/client/src/components/alert/alert.component.jsx b/client/src/components/alert/alert.component.jsx index 439822f68..f57dcb5fd 100644 --- a/client/src/components/alert/alert.component.jsx +++ b/client/src/components/alert/alert.component.jsx @@ -1,5 +1,5 @@ import { Alert } from "antd"; -export default function AlertComponent(props) { - return ; +export default function AlertComponent({ title, message, ...props }) { + return ; } diff --git a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx index 0f265d7db..69c5a1831 100644 --- a/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx +++ b/client/src/components/form-fields-changed-alert/form-fields-changed-alert.component.jsx @@ -4,20 +4,203 @@ import AlertComponent from "../alert/alert.component"; import "./form-fields-changed.styles.scss"; import Prompt from "../../utils/prompt"; -export default function FormsFieldChanged({ form, skipPrompt }) { +export default function FormsFieldChanged({ form, skipPrompt, onErrorNavigate, onReset, onDirtyChange }) { const { t } = useTranslation(); + const normalizeNamePath = (namePath) => (Array.isArray(namePath) ? namePath.filter((part) => part !== undefined) : [namePath]); + + const getFieldIdCandidates = (namePath) => { + const normalizedNamePath = normalizeNamePath(namePath).map((part) => String(part)); + const underscoreId = normalizedNamePath.join("_"); + const dashId = normalizedNamePath.join("-"); + const dotName = normalizedNamePath.join("."); + + return [underscoreId, dashId, dotName].filter(Boolean); + }; + + const clearFormMeta = () => { + const fieldMeta = form.getFieldsError().map(({ name }) => ({ + name, + touched: false, + validating: false, + errors: [], + warnings: [] + })); + + if (fieldMeta.length > 0) { + form.setFields(fieldMeta); + } + + onDirtyChange?.(false); + }; + const handleReset = () => { - form.resetFields(); + if (onReset) { + onReset(); + } else { + form.resetFields(); + } + + window.requestAnimationFrame(() => { + clearFormMeta(); + }); + }; + + const getFieldDomNode = (namePath) => { + const fieldInstance = form.getFieldInstance?.(namePath); + const fieldIdCandidates = getFieldIdCandidates(namePath); + const domCandidates = [ + fieldInstance?.nativeElement, + fieldInstance?.input, + fieldInstance?.resizableTextArea?.textArea, + fieldInstance + ]; + + fieldIdCandidates.forEach((fieldId) => { + const escapedFieldId = CSS.escape(fieldId); + const directNode = document.getElementById(fieldId) || document.querySelector(`#${escapedFieldId}`); + const labelNode = document.querySelector(`label[for="${escapedFieldId}"]`); + const namedNode = document.querySelector(`[name="${escapedFieldId}"]`); + const formItemNode = + directNode?.closest?.(".ant-form-item") || + labelNode?.closest?.(".ant-form-item") || + namedNode?.closest?.(".ant-form-item"); + + domCandidates.push(directNode); + domCandidates.push(namedNode); + domCandidates.push(formItemNode); + domCandidates.push(formItemNode?.querySelector?.("input, textarea, select, .ant-select-selector")); + }); + + return domCandidates.find((candidate) => candidate instanceof HTMLElement) ?? null; + }; + + const waitForAnimationFrames = (frameCount = 1) => + new Promise((resolve) => { + let remainingFrames = frameCount; + const nextFrame = () => { + if (remainingFrames <= 0) { + resolve(); + return; + } + remainingFrames -= 1; + window.requestAnimationFrame(nextFrame); + }; + window.requestAnimationFrame(nextFrame); + }); + + const getFieldOwningTabMeta = (namePath) => { + const fieldDomNode = getFieldDomNode(namePath); + const owningTabPane = fieldDomNode?.closest?.(".ant-tabs-tabpane"); + const paneId = owningTabPane?.getAttribute?.("id") || null; + const owningTabButton = paneId + ? document.querySelector(`[role="tab"][aria-controls="${paneId.replace(/"/g, '\\"')}"]`) + : null; + const tabLabel = owningTabButton?.textContent?.trim() || null; + + return { + owningTabPane, + owningTabButton, + tabLabel + }; + }; + + const openFieldOwningTab = async (namePath) => { + const { owningTabPane, owningTabButton } = getFieldOwningTabMeta(namePath); + if (!owningTabPane || owningTabPane.classList.contains("ant-tabs-tabpane-active")) return false; + + if (!(owningTabButton instanceof HTMLElement)) return false; + + owningTabButton.click(); + + for (let index = 0; index < 24; index += 1) { + await waitForAnimationFrames(); + if (owningTabPane.classList.contains("ant-tabs-tabpane-active")) return true; + } + + return owningTabPane.classList.contains("ant-tabs-tabpane-active"); + }; + + const scrollToErrorField = (namePath) => { + const normalizedNamePath = normalizeNamePath(namePath); + if (!normalizedNamePath.length) return; + + try { + form.scrollToField(normalizedNamePath, { + behavior: "smooth", + block: "center", + focus: true + }); + window.requestAnimationFrame(() => { + const fallbackNode = getFieldDomNode(normalizedNamePath); + fallbackNode?.focus?.(); + }); + return; + } catch { + const fallbackTarget = document.getElementById(normalizedNamePath[0]?.toString?.() ?? ""); + fallbackTarget?.scrollIntoView({ + behavior: "smooth", + block: "center" + }); + } + }; + + const handleErrorClick = async (namePath) => { + const normalizedNamePath = normalizeNamePath(namePath); + if (!normalizedNamePath.length) return; + + const switchedTab = await openFieldOwningTab(normalizedNamePath); + if (!switchedTab) { + const navigationDelayMs = onErrorNavigate?.(normalizedNamePath) ?? 0; + if (navigationDelayMs > 0) { + window.setTimeout(() => { + window.requestAnimationFrame(() => { + scrollToErrorField(normalizedNamePath); + }); + }, navigationDelayMs); + return; + } + } + + await waitForAnimationFrames(switchedTab ? 2 : 1); + scrollToErrorField(normalizedNamePath); }; //if (!form.isFieldsTouched()) return <>; return ( {() => { - const errors = form.getFieldsError().filter((e) => e.errors.length > 0); + const errors = form + .getFieldsError() + .filter((fieldError) => fieldError.errors.length > 0) + .flatMap((fieldError) => { + const tabMeta = getFieldOwningTabMeta(fieldError.name); + + return fieldError.errors.map((errorMessage, errorIndex) => ({ + key: `${(fieldError.name || []).join(".")}-${errorIndex}-${errorMessage}`, + message: errorMessage, + namePath: fieldError.name, + tabLabel: tabMeta.tabLabel + })); + }); + + const groupedErrors = errors.reduce((groups, error) => { + const groupKey = error.tabLabel || "__ungrouped__"; + if (!groups[groupKey]) { + groups[groupKey] = { + key: groupKey, + label: error.tabLabel, + errors: [] + }; + } + groups[groupKey].errors.push(error); + return groups; + }, {}); + const errorGroups = Object.values(groupedErrors); + const hasTabbedErrorGroups = errorGroups.some((group) => Boolean(group.label)); + if (form.isFieldsTouched()) return ( - + 0 && ( -
    {errors.map((e, idx) => e.errors.map((e2, idx2) =>
  • {e2}
  • ))}
+
+ {errorGroups.map((group) => ( +
+ {hasTabbedErrorGroups && group.label ? ( +
{group.label}
+ ) : null} +
    + {group.errors.map((error) => ( +
  • + {Array.isArray(error.namePath) && error.namePath.length > 0 ? ( + + ) : ( + error.message + )} +
  • + ))} +
+
+ ))}
} showIcon diff --git a/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss b/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss index 155407907..cb3e3940f 100644 --- a/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss +++ b/client/src/components/form-fields-changed-alert/form-fields-changed.styles.scss @@ -4,4 +4,47 @@ min-height: unset !important; } } + + &__error-list { + margin: 0; + padding-left: 18px; + } + + &__error-groups { + display: grid; + gap: 10px; + } + + &__error-group { + display: grid; + gap: 4px; + } + + &__error-group-title { + font-weight: 600; + } + + &__error-link { + display: inline; + padding: 0; + border: 0; + background: none; + color: inherit; + font: inherit; + line-height: inherit; + text-align: left; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 2px; + + &:hover { + color: color-mix(in srgb, var(--ant-color-error) 82%, var(--ant-color-text)); + } + + &:focus-visible { + outline: 2px solid color-mix(in srgb, var(--ant-color-error) 32%, transparent); + outline-offset: 2px; + border-radius: 4px; + } + } } diff --git a/client/src/components/form-items-formatted/phone-form-item.component.jsx b/client/src/components/form-items-formatted/phone-form-item.component.jsx index ede4ec66c..cdf3f4fde 100644 --- a/client/src/components/form-items-formatted/phone-form-item.component.jsx +++ b/client/src/components/form-items-formatted/phone-form-item.component.jsx @@ -1,11 +1,88 @@ -import { Input } from "antd"; +import { PhoneFilled } from "@ant-design/icons"; +import { Button, Input, Space } from "antd"; import i18n from "i18next"; import parsePhoneNumber from "libphonenumber-js"; +import { forwardRef, useMemo, useState } from "react"; import "./phone-form-item.styles.scss"; -function FormItemPhone({ ref, ...props }) { - return ; -} +/** + * Formats a phone number for display purposes. If the input value is a valid phone number, it will be formatted in a + * national format (e.g., (123) 456-7890 for US/CA). If the input is not a valid phone number, it will be returned as-is. + * @param value + * @returns {*} + */ +const formatPhoneDisplayValue = (value) => { + if (!value) return value; + + try { + const parsedPhone = parsePhoneNumber(value, "CA"); + return parsedPhone?.isValid() ? parsedPhone.formatNational() : value; + } catch { + return value; + } +}; + +/** + * Generates a "tel:" URL for a phone number if it's valid. If the input value is a valid phone number, it will return a + * URL in the format "tel:+1234567890". If the input is not a valid phone number, it will attempt to trim whitespace and + * return a "tel:" URL with the raw value, or null if the trimmed value is empty. + * @param value + * @returns {string|null} + */ +const getPhoneActionHref = (value) => { + if (!value) return null; + + try { + const parsedPhone = parsePhoneNumber(value, "CA"); + if (parsedPhone?.isValid()) return `tel:${parsedPhone.number}`; + } catch { + // Fall back to the raw value below. + } + + const trimmedValue = String(value).trim(); + return trimmedValue ? `tel:${trimmedValue}` : null; +}; + +const FormItemPhone = forwardRef(function FormItemPhone( + { formatDisplayOnly = false, showPhoneAction = false, value, onBlur, onFocus, ...props }, + ref +) { + const [isFocused, setIsFocused] = useState(false); + const displayValue = useMemo(() => { + if (!formatDisplayOnly || isFocused) return value; + return formatPhoneDisplayValue(value); + }, [formatDisplayOnly, isFocused, value]); + const phoneActionHref = useMemo(() => (showPhoneAction ? getPhoneActionHref(value) : null), [showPhoneAction, value]); + + const input = ( + { + setIsFocused(true); + onFocus?.(event); + }} + onBlur={(event) => { + setIsFocused(false); + onBlur?.(event); + }} + /> + ); + + if (!showPhoneAction) return input; + + return ( + + {input} + {phoneActionHref ? ( + +); + +export const renderConfigListOrEmpty = ({ fields, actionLabel, renderItems }) => + fields.length === 0 ? : renderItems(); + +export const buildSectionActionButton = (key, label, onClick, id) => + buildConfigListActionButton({ key, label, onClick, id }); + +export const renderListOrEmpty = (fields, actionLabel, renderItems) => + renderConfigListOrEmpty({ fields, actionLabel, renderItems }); diff --git a/client/src/components/layout-form-row/config-list-empty-state.component.jsx b/client/src/components/layout-form-row/config-list-empty-state.component.jsx new file mode 100644 index 000000000..fc0f41570 --- /dev/null +++ b/client/src/components/layout-form-row/config-list-empty-state.component.jsx @@ -0,0 +1,11 @@ +import { useTranslation } from "react-i18next"; + +export default function ConfigListEmptyState({ actionLabel, minHeight = 96 }) { + const { t } = useTranslation(); + + return ( +
+ {t("general.labels.click_to_begin", { action: actionLabel })} +
+ ); +} diff --git a/client/src/components/layout-form-row/inline-form-row-title.utils.js b/client/src/components/layout-form-row/inline-form-row-title.utils.js new file mode 100644 index 000000000..70c0e8dd2 --- /dev/null +++ b/client/src/components/layout-form-row/inline-form-row-title.utils.js @@ -0,0 +1,89 @@ +import { UnorderedListOutlined } from "@ant-design/icons"; + +export const inlineFormRowTitleStyles = Object.freeze({ + input: Object.freeze({ + background: "transparent", + border: "none", + borderRadius: 0, + boxShadow: "none", + paddingInline: 0, + paddingBlock: 0, + lineHeight: 1.35, + flex: "1 1 auto", + minWidth: 0, + width: "100%" + }), + row: Object.freeze({ + display: "flex", + gap: 6, + flexWrap: "wrap", + alignItems: "center", + width: "100%", + paddingInline: 4 + }), + group: Object.freeze({ + display: "flex", + alignItems: "center", + gap: 8, + paddingInline: 8, + paddingBlock: 4, + borderRadius: 10, + border: "1px solid var(--imex-form-title-group-border)", + background: "var(--imex-form-title-group-bg)", + minWidth: 0, + flex: "1 1 0" + }), + label: Object.freeze({ + color: "var(--ant-color-text-secondary)", + fontSize: 12, + fontWeight: 600, + lineHeight: 1, + whiteSpace: "nowrap", + paddingInline: 6, + paddingBlock: 3, + borderRadius: 999, + border: "1px solid var(--imex-form-title-label-border)", + background: "var(--imex-form-title-label-bg)" + }), + handle: Object.freeze({ + color: "var(--ant-color-text-tertiary)", + fontSize: 14, + flex: "0 0 auto", + marginRight: 2 + }), + separator: Object.freeze({ + width: 1, + height: 16, + background: "color-mix(in srgb, var(--imex-form-surface-border) 58%, transparent)", + borderRadius: 999, + flex: "0 0 auto", + marginInline: 2 + }), + text: Object.freeze({ + whiteSpace: "nowrap", + fontWeight: 500, + fontSize: "var(--ant-font-size-lg)", + lineHeight: 1.2 + }) +}); + +export const INLINE_TITLE_INPUT_STYLE = inlineFormRowTitleStyles.input; +export const INLINE_TITLE_ROW_STYLE = inlineFormRowTitleStyles.row; +export const INLINE_TITLE_GROUP_STYLE = inlineFormRowTitleStyles.group; +export const InlineTitleListIcon = UnorderedListOutlined; +export const INLINE_TITLE_SWITCH_GROUP_STYLE = Object.freeze({ + ...inlineFormRowTitleStyles.group, + flex: "0 0 auto" +}); +export const INLINE_TITLE_LABEL_STYLE = inlineFormRowTitleStyles.label; +export const INLINE_TITLE_HANDLE_STYLE = inlineFormRowTitleStyles.handle; +export const INLINE_TITLE_SEPARATOR_STYLE = inlineFormRowTitleStyles.separator; +export const INLINE_TITLE_TEXT_STYLE = inlineFormRowTitleStyles.text; + +export const INLINE_FORM_ROW_WRAP_TITLE_STYLES = Object.freeze({ + title: Object.freeze({ + whiteSpace: "normal", + overflow: "visible", + textOverflow: "unset" + }) +}); diff --git a/client/src/components/layout-form-row/inline-validated-form-row.component.jsx b/client/src/components/layout-form-row/inline-validated-form-row.component.jsx new file mode 100644 index 000000000..70910856b --- /dev/null +++ b/client/src/components/layout-form-row/inline-validated-form-row.component.jsx @@ -0,0 +1,47 @@ +import { Form } from "antd"; +import LayoutFormRow from "./layout-form-row.component"; + +export default function InlineValidatedFormRow({ actions, errorNames = [], extraErrors = [], form, ...layoutFormRowProps }) { + const normalizedErrorNames = Array.isArray(errorNames) ? errorNames : [errorNames]; + const normalizedExtraErrors = Array.isArray(extraErrors) ? extraErrors.filter(Boolean) : [extraErrors].filter(Boolean); + + return ( + + {() => { + const fieldErrors = normalizedErrorNames.flatMap((name) => form?.getFieldError?.(name) || []); + const errors = [...new Set([...fieldErrors, ...normalizedExtraErrors])]; + const resolvedClassName = [ + layoutFormRowProps.className, + errors.length > 0 ? "imex-form-row--error" : null + ] + .filter(Boolean) + .join(" "); + + const normalizedActions = Array.isArray(actions) ? actions.filter(Boolean) : [actions].filter(Boolean); + const resolvedActions = + errors.length > 0 + ? [ +
0 ? 8 : 0, + width: "100%", + textAlign: "left" + }} + > + + {normalizedActions.length > 0 ?
{normalizedActions}
: null} +
+ ] + : normalizedActions.length > 0 + ? normalizedActions + : undefined; + + return ; + }} +
+ ); +} diff --git a/client/src/components/layout-form-row/layout-form-row.component.jsx b/client/src/components/layout-form-row/layout-form-row.component.jsx index 22343a790..ade2cbaab 100644 --- a/client/src/components/layout-form-row/layout-form-row.component.jsx +++ b/client/src/components/layout-form-row/layout-form-row.component.jsx @@ -1,5 +1,6 @@ import { Card, Col, Row } from "antd"; import { Children, isValidElement } from "react"; +import { INLINE_FORM_ROW_WRAP_TITLE_STYLES } from "./inline-form-row-title.utils.js"; import "./layout-form-row.styles.scss"; export default function LayoutFormRow({ @@ -7,32 +8,45 @@ export default function LayoutFormRow({ children, grow = false, noDivider = false, - gutter = [16, 16], // Responsive gutter: horizontal, vertical + titleOnly = false, + wrapTitle = false, + gutter, rowProps, // Optional overrides if you ever need per-section customization surface = true, surfaceBg, surfaceHeaderBg, + surfaceBorderColor, ...cardProps }) { const items = Children.toArray(children).filter(Boolean); - if (items.length === 0) return null; + const isCompactRow = noDivider; const title = !noDivider && header ? header : undefined; + const resolvedTitle = cardProps.title ?? title; + const isHeaderOnly = titleOnly || items.length === 0; + const hideBody = isHeaderOnly; + + if (items.length === 0 && !resolvedTitle) return null; + const resolvedGutter = gutter ?? [16, isCompactRow ? 8 : 16]; const bg = surfaceBg ?? (surface ? "var(--imex-form-surface)" : undefined); const headBg = surfaceHeaderBg ?? (surface ? "var(--imex-form-surface-head)" : undefined); + const borderColor = surfaceBorderColor ?? (surface ? "var(--imex-form-surface-border)" : undefined); const mergedStyles = mergeSemanticStyles( { + ...(wrapTitle ? INLINE_FORM_ROW_WRAP_TITLE_STYLES : null), header: { - paddingInline: 16, - background: headBg + paddingInline: isHeaderOnly ? 8 : isCompactRow ? 12 : 16, + background: headBg, + borderBottomColor: borderColor }, body: { - padding: 16, + padding: hideBody ? 0 : isCompactRow ? 12 : 16, + display: hideBody ? "none" : undefined, background: bg } }, @@ -40,28 +54,12 @@ export default function LayoutFormRow({ ); const baseCardStyle = { - marginBottom: ".8rem", + marginBottom: isHeaderOnly ? "0" : isCompactRow ? "8px" : ".8rem", ...(bg ? { background: bg } : null), // ensures the “circled area” is tinted + ...(borderColor ? { borderColor } : null), ...cardProps.style }; - // single child => just render it - if (items.length === 1) { - return ( - - {items[0]} - - ); - } - const count = items.length; // Modern responsive strategy leveraging Ant Design 6: @@ -125,20 +123,32 @@ export default function LayoutFormRow({ return ( - - {items.map((child, idx) => ( - - {child} - + {!isHeaderOnly && + (items.length === 1 ? ( + items[0] + ) : ( + + {items.map((child, idx) => ( + + {child} + + ))} + ))} - ); } @@ -152,6 +162,7 @@ function mergeSemanticStyles(defaults, userStyles) { return { ...defaults, ...computed, + title: { ...(defaults.title || {}), ...(computed.title || {}) }, header: { ...defaults.header, ...(computed.header || {}) }, body: { ...defaults.body, ...(computed.body || {}) } }; @@ -161,6 +172,7 @@ function mergeSemanticStyles(defaults, userStyles) { return { ...defaults, ...userStyles, + title: { ...(defaults.title || {}), ...(userStyles.title || {}) }, header: { ...defaults.header, ...(userStyles.header || {}) }, body: { ...defaults.body, ...(userStyles.body || {}) } }; diff --git a/client/src/components/layout-form-row/layout-form-row.styles.scss b/client/src/components/layout-form-row/layout-form-row.styles.scss index 264ae9760..2c95c7973 100644 --- a/client/src/components/layout-form-row/layout-form-row.styles.scss +++ b/client/src/components/layout-form-row/layout-form-row.styles.scss @@ -13,6 +13,12 @@ --imex-form-surface: #fafafa; /* subtle contrast vs white page */ --imex-form-surface-head: #f5f5f5; /* header strip */ --imex-form-surface-border: #d9d9d9; /* matches AntD-ish border */ + --imex-form-title-input-bg: rgba(255, 255, 255, 0.96); + --imex-form-title-input-border: rgba(0, 0, 0, 0.08); + --imex-form-title-group-bg: rgba(255, 255, 255, 0.72); + --imex-form-title-group-border: rgba(0, 0, 0, 0.08); + --imex-form-title-label-bg: rgba(0, 0, 0, 0.04); + --imex-form-title-label-border: rgba(0, 0, 0, 0.06); } /* Pick the selector that matches your app and remove the rest */ @@ -20,6 +26,12 @@ html[data-theme="dark"] { --imex-form-surface: rgba(255, 255, 255, 0.01); /* subtle lift off page bg */ --imex-form-surface-head: rgba(255, 255, 255, 0.06); /* slightly stronger for header strip */ --imex-form-surface-border: rgba(5, 5, 5, 0.12); + --imex-form-title-input-bg: rgba(255, 255, 255, 0.12); + --imex-form-title-input-border: rgba(255, 255, 255, 0.2); + --imex-form-title-group-bg: rgba(255, 255, 255, 0.08); + --imex-form-title-group-border: rgba(255, 255, 255, 0.16); + --imex-form-title-label-bg: rgba(255, 255, 255, 0.06); + --imex-form-title-label-border: rgba(255, 255, 255, 0.12); } .imex-form-row { @@ -38,18 +50,111 @@ html[data-theme="dark"] { border-color: var(--imex-form-surface-border); } + &.imex-form-row--error.ant-card { + border-color: var(--ant-color-error); + box-shadow: 0 0 0 1px color-mix(in srgb, var(--ant-color-error) 24%, transparent); + } + .ant-card-head { background: var(--imex-form-surface-head); border-bottom-color: var(--imex-form-surface-border); } + &.imex-form-row--error { + .ant-card-head, + .ant-card-actions { + border-color: color-mix(in srgb, var(--ant-color-error) 34%, var(--imex-form-surface-border)); + } + } + + &.imex-form-row--compact { + .ant-card-head { + min-height: 40px; + } + + .ant-card-head-title, + .ant-card-extra { + padding-block: 2px; + } + + .ant-form-item { + margin-bottom: 12px; + } + } + + &.imex-form-row--title-only { + .ant-card-head { + min-height: auto; + padding-inline: 6px; + padding-block: 0; + border-radius: inherit; + } + + .ant-card-head-wrapper { + gap: 2px; + align-items: center; + } + + .ant-card-head-title, + .ant-card-extra { + padding-block: 0; + display: flex; + align-items: center; + } + + .ant-card-head-title { + white-space: normal; + overflow: visible; + text-overflow: unset; + font-size: var(--ant-font-size); + line-height: 1.1; + padding-inline: 4px; + } + + .ant-card-body { + display: none; + padding: 0; + } + + .ant-input, + .ant-input-number, + .ant-input-affix-wrapper, + .ant-select-selector, + .ant-picker { + background: var(--imex-form-title-input-bg); + border-color: var(--imex-form-title-input-border); + } + + .ant-input-number-input { + background: transparent; + } + } + .ant-card-body { background: var(--imex-form-surface); } + .ant-card-actions { + background: var(--imex-form-surface-head); + border-top-color: var(--imex-form-surface-border); + } + + .ant-card-actions > li { + margin: 10px 0; + padding-inline: 12px; + } + + .ant-card-actions .ant-btn { + width: 100%; + } + + .ant-form-item:last-child { + margin-bottom: 4px; + } + /* Optional: tighter spacing on phones for better space usage */ @media (max-width: 575px) { - .ant-card-head { + &:not(.imex-form-row--title-only) .ant-card-head { padding-inline: 12px; padding-block: 12px; } @@ -70,6 +175,14 @@ html[data-theme="dark"] { width: 100%; } + .ant-form-item:has(.imex-form-row--compact) { + margin-bottom: 8px; + } + + .ant-form-item:has(.imex-form-row--title-only) { + margin-bottom: 4px; + } + /* Better form item spacing on mobile */ @media (max-width: 575px) { .ant-form-item { @@ -77,3 +190,24 @@ html[data-theme="dark"] { } } } + +.imex-form-row-empty-state { + display: flex; + align-items: center; + justify-content: center; + padding: 24px 16px; + text-align: center; + color: var(--ant-color-text-description); + font-size: var(--ant-font-size); + line-height: 1.5; +} + +.imex-inline-form-row-errors { + color: var(--ant-color-error); + + .ant-form-item-explain, + .ant-form-item-explain-error, + .ant-form-item-additional { + color: var(--ant-color-error); + } +} diff --git a/client/src/components/parts-order-modal/parts-order-modal.component.jsx b/client/src/components/parts-order-modal/parts-order-modal.component.jsx index bfb0cb1df..3766b2e7b 100644 --- a/client/src/components/parts-order-modal/parts-order-modal.component.jsx +++ b/client/src/components/parts-order-modal/parts-order-modal.component.jsx @@ -1,12 +1,13 @@ import { DeleteFilled, DownOutlined, WarningFilled } from "@ant-design/icons"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; -import { Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd"; +import { Button, Checkbox, Divider, Dropdown, Form, Input, InputNumber, Radio, Select, Space, Tag } from "antd"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component"; import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component"; @@ -50,6 +51,7 @@ export function PartsOrderModalComponent({ }); const { t } = useTranslation(); + const partsOrderLines = Form.useWatch(["parts_order_lines", "data"], form) || []; const handleClick = ({ item }) => { form.setFieldsValue({ comments: item.props.value }); }; @@ -128,10 +130,38 @@ export function PartsOrderModalComponent({ {(fields, { remove, move }) => { return (
- {fields.map((field, index) => ( - -
- + {fields.map((field, index) => { + const partsOrderLine = partsOrderLines[field.name] || {}; + + return ( + + +
-
- ))} + + ); + })}
); }} diff --git a/client/src/components/parts-receive-modal/parts-receive-modal.component.jsx b/client/src/components/parts-receive-modal/parts-receive-modal.component.jsx index 723ccb9db..c4fdd1bd0 100644 --- a/client/src/components/parts-receive-modal/parts-receive-modal.component.jsx +++ b/client/src/components/parts-receive-modal/parts-receive-modal.component.jsx @@ -1,10 +1,11 @@ import { DeleteFilled } from "@ant-design/icons"; -import { Form, Input, InputNumber, Select, Typography } from "antd"; +import { Button, Form, Input, InputNumber, Select, Space, Typography } from "antd"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { selectBodyshop } from "../../redux/user/user.selectors"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; const mapStateToProps = createStructuredSelector({ @@ -15,6 +16,7 @@ export default connect(mapStateToProps, null)(PartsReceiveModalComponent); export function PartsReceiveModalComponent({ bodyshop, form }) { const { t } = useTranslation(); + const partsOrderLines = Form.useWatch(["partsorderlines"], form) || []; return (
@@ -42,16 +44,43 @@ export function PartsReceiveModalComponent({ bodyshop, form }) { {(fields, { remove, move }) => { return (
- {fields.map((field, index) => ( - -
+ {fields.map((field, index) => { + const partsOrderLine = partsOrderLines[field.name] || {}; + + return ( + - + + diff --git a/client/src/components/shop-employees/shop-employees-form.component.jsx b/client/src/components/shop-employees/shop-employees-form.component.jsx index e5bfbde23..5aed82b8e 100644 --- a/client/src/components/shop-employees/shop-employees-form.component.jsx +++ b/client/src/components/shop-employees/shop-employees-form.component.jsx @@ -1,11 +1,10 @@ import { DeleteFilled } from "@ant-design/icons"; import { useApolloClient, useMutation, useQuery } from "@apollo/client/react"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; -import { Button, Card, Form, Input, InputNumber, Select, Switch } from "antd"; +import { Button, Card, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd"; import ResponsiveTable from "../responsive-table/responsive-table.component"; -import { useForm } from "antd/es/form/Form"; import queryString from "query-string"; -import { useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; @@ -26,9 +25,24 @@ import { DateFormatter } from "../../utils/DateFormatter"; import dayjs from "../../utils/day"; import AlertComponent from "../alert/alert.component"; import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx"; +import FormsFieldChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; +import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { + INLINE_TITLE_GROUP_STYLE, + INLINE_TITLE_HANDLE_STYLE, + INLINE_TITLE_INPUT_STYLE, + INLINE_TITLE_LABEL_STYLE, + INLINE_TITLE_ROW_STYLE, + INLINE_TITLE_SEPARATOR_STYLE, + INLINE_TITLE_SWITCH_GROUP_STYLE, + INLINE_TITLE_TEXT_STYLE, + InlineTitleListIcon +} from "../layout-form-row/inline-form-row-title.utils.js"; import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component"; +import FormItemEmail from "../form-items-formatted/email-form-item.component.jsx"; const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop @@ -37,19 +51,37 @@ const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) }); -export function ShopEmployeesFormComponent({ bodyshop }) { +export function ShopEmployeesFormComponent({ bodyshop, form, onDirtyChange, isDirty }) { const { t } = useTranslation(); - const [form] = useForm(); + const [internalIsDirty, setInternalIsDirty] = useState(false); + const resolvedIsDirty = typeof isDirty === "boolean" ? isDirty : internalIsDirty; + const employeeNumber = Form.useWatch("employee_number", form); + const firstName = Form.useWatch("first_name", form); + const lastName = Form.useWatch("last_name", form); + const employeeOptionsColProps = { + xs: 24, + sm: 12, + md: 12, + lg: 8, + xl: 8, + xxl: 8 + }; const history = useNavigate(); const search = queryString.parse(useLocation().search); const [deleteVacation] = useMutation(DELETE_VACATION); - const { error, data } = useQuery(QUERY_EMPLOYEE_BY_ID, { + const { error, data, refetch } = useQuery(QUERY_EMPLOYEE_BY_ID, { variables: { id: search.employeeId }, skip: !search.employeeId || search.employeeId === "new", fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); const notification = useNotification(); + const isNewEmployee = search.employeeId === "new"; + const currentEmployeeData = data?.employees_by_pk?.id === search.employeeId ? data.employees_by_pk : null; + const employeeTitleName = [firstName, lastName].filter(Boolean).join(" ").trim(); + const employeeCardTitle = + [employeeNumber, employeeTitleName].filter(Boolean).join(" - ") || + (isNewEmployee ? t("employees.actions.new") : t("bodyshop.labels.employees")); const { treatments: { Enhanced_Payroll } @@ -59,13 +91,46 @@ export function ShopEmployeesFormComponent({ bodyshop }) { splitKey: bodyshop.imexshopid }); + const updateDirtyState = useCallback( + (nextDirtyState) => { + setInternalIsDirty(nextDirtyState); + onDirtyChange?.(nextDirtyState); + }, + [onDirtyChange] + ); + const client = useApolloClient(); - useEffect(() => { - if (data && data.employees_by_pk) form.setFieldsValue(data.employees_by_pk); - else { - form.resetFields(); + const clearEmployeeFormMeta = useCallback(() => { + const fieldMeta = form.getFieldsError().map(({ name }) => ({ + name, + touched: false, + validating: false, + errors: [], + warnings: [] + })); + + if (fieldMeta.length > 0) { + form.setFields(fieldMeta); } - }, [form, data, search.employeeId]); + + updateDirtyState(false); + }, [form, updateDirtyState]); + + const resetEmployeeFormToCurrentData = useCallback(() => { + form.resetFields(); + + if (currentEmployeeData) { + form.setFieldsValue(currentEmployeeData); + } + + window.requestAnimationFrame(() => { + clearEmployeeFormMeta(); + }); + }, [clearEmployeeFormMeta, currentEmployeeData, form]); + + useEffect(() => { + resetEmployeeFormToCurrentData(); + }, [resetEmployeeFormToCurrentData, search.employeeId]); const [updateEmployee] = useMutation(UPDATE_EMPLOYEE); const [insertEmployees] = useMutation(INSERT_EMPLOYEES); @@ -85,6 +150,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) { } }) .then(() => { + updateDirtyState(false); + void refetch(); notification.success({ title: t("employees.successes.save") }); @@ -104,6 +171,7 @@ export function ShopEmployeesFormComponent({ bodyshop }) { variables: { employees: [{ ...values, shopid: bodyshop.id }] }, refetchQueries: ["QUERY_EMPLOYEES"] }).then((r) => { + updateDirtyState(false); search.employeeId = r.data.insert_employees.returning[0].id; history({ search: queryString.stringify(search) }); notification.success({ @@ -141,6 +209,8 @@ export function ShopEmployeesFormComponent({ bodyshop }) { key: "actions", render: (text, record) => ( } > -
- - - - - - - - ({ - async validator(rule, value) { - if (value) { - const response = await client.query({ - query: CHECK_EMPLOYEE_NUMBER, - variables: { - employeenumber: value - } - }); - - if (response.data.employees_aggregate.aggregate.count === 0) { - return Promise.resolve(); - } else if ( - response.data.employees_aggregate.nodes.length === 1 && - response.data.employees_aggregate.nodes[0].id === form.getFieldValue("id") - ) { - return Promise.resolve(); - } - return Promise.reject(t("employees.validation.unique_employee_number")); - } else { - return Promise.resolve(); + { + updateDirtyState(form.isFieldsTouched()); + }} + > + + +
+ {t("bodyshop.labels.employee_options")} +
+
+
+
+
{t("employees.labels.active")}
+ + + +
+
+
+
{t("employees.fields.flat_rate")}
+ + + +
+
+
+ } + wrapTitle + > + + + - - - - - - - - - - - - - - - - - - - - ({ - async validator(rule, value) { - const user_email = getFieldValue("user_email"); - - if (user_email && value) { - const response = await client.query({ - query: QUERY_USERS_BY_EMAIL, - variables: { - email: user_email - } - }); - - if (response.data.users.length === 1) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.useremailmustexist")); - } else { - return Promise.resolve(); + ]} + > + + + + + - - - - - + ]} + > + + + + + ({ + async validator(rule, value) { + if (value) { + const response = await client.query({ + query: CHECK_EMPLOYEE_NUMBER, + variables: { + employeenumber: value + } + }); + + if (response.data.employees_aggregate.aggregate.count === 0) { + return Promise.resolve(); + } else if ( + response.data.employees_aggregate.nodes.length === 1 && + response.data.employees_aggregate.nodes[0].id === form.getFieldValue("id") + ) { + return Promise.resolve(); + } + return Promise.reject(t("employees.validation.unique_employee_number")); + } else { + return Promise.resolve(); + } + } + }) + ]} + > + + + + + + + + + + + + + + + + + + + + ({ + async validator(rule, value) { + const user_email = getFieldValue("user_email"); + + if (user_email && value) { + const response = await client.query({ + query: QUERY_USERS_BY_EMAIL, + variables: { + email: user_email + } + }); + + if (response.data.users.length === 1) { + return Promise.resolve(); + } + return Promise.reject(t("bodyshop.validation.useremailmustexist")); + } else { + return Promise.resolve(); + } + } + }) + ]} + > + + + + + + + + + {(fields, { add, remove, move }) => { return ( -
- {fields.map((field, index) => ( - - - - ({ + value: c.name, + label: c.name + }))) + ]} + style={{ width: "100%" }} + styles={{ + selector: INLINE_TITLE_INPUT_STYLE + }} + /> + +
+
+ } + wrapTitle + extra={ + +
+ ); }} - } - columns={columns} - mobileColumnKeys={["start", "length", "actions"]} - rowKey={"id"} - dataSource={data?.employees_by_pk?.employee_vacations ?? []} - /> + + ]} + > + {(data?.employees_by_pk?.employee_vacations ?? []).length === 0 ? ( + + ) : ( +
+ +
+ )} +
); } diff --git a/client/src/components/shop-employees/shop-employees-list.component.jsx b/client/src/components/shop-employees/shop-employees-list.component.jsx index e9082ac05..45e434b5a 100644 --- a/client/src/components/shop-employees/shop-employees-list.component.jsx +++ b/client/src/components/shop-employees/shop-employees-list.component.jsx @@ -4,9 +4,16 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useLocation, useNavigate } from "react-router-dom"; import { alphaSort } from "../../utils/sorters"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import ResponsiveTable from "../responsive-table/responsive-table.component"; -export default function ShopEmployeesListComponent({ loading, employees }) { +export default function ShopEmployeesListComponent({ + loading, + employees, + onRequestEmployeeChange, + selectedEmployeeId +}) { const { t } = useTranslation(); const history = useNavigate(); const search = queryString.parse(useLocation().search); @@ -16,13 +23,33 @@ export default function ShopEmployeesListComponent({ loading, employees }) { filteredInfo: { text: "" } }); + const navigateToEmployee = (employeeId) => { + if (onRequestEmployeeChange) { + onRequestEmployeeChange(employeeId); + return; + } + + history({ + search: queryString.stringify({ + ...search, + employeeId + }) + }); + }; + + const clearEmployeeSelection = () => { + const { employeeId, ...nextSearch } = search; + void employeeId; + history({ + search: queryString.stringify(nextSearch) + }); + }; + const handleOnRowClick = (record) => { if (record) { - search.employeeId = record.id; - history({ search: queryString.stringify(search) }); + navigateToEmployee(record.id); } else { - delete search.employeeId; - history({ search: queryString.stringify(search) }); + clearEmployeeSelection(); } }; const handleTableChange = (pagination, filters, sorter) => { @@ -30,7 +57,7 @@ export default function ShopEmployeesListComponent({ loading, employees }) { }; const columns = [ { - title: t("employees.fields.employee_number"), + title: t("employees.labels.employee_number_short"), dataIndex: "employee_number", key: "employee_number", sorter: (a, b) => alphaSort(a.employee_number, b.employee_number), @@ -89,44 +116,39 @@ export default function ShopEmployeesListComponent({ loading, employees }) { } ]; return ( -
- { - return ( - - ); - }} - loading={loading} - pagination={{ placement: "top" }} - columns={columns} - mobileColumnKeys={["employee_number", "employee_name", "active"]} - rowKey="id" - dataSource={employees} - rowSelection={{ - onSelect: (props) => { - search.employeeId = props.id; - history({ search: queryString.stringify(search) }); - }, - type: "radio", - selectedRowKeys: [search.employeeId] - }} - onChange={handleTableChange} - onRow={(record) => { - return { - onClick: () => { - handleOnRowClick(record); - } - }; - }} - /> -
+ navigateToEmployee("new")}> + {t("employees.actions.new")} + + ]} + > + {employees.length === 0 ? ( + + ) : ( + navigateToEmployee(props.id), + type: "radio", + selectedRowKeys: [selectedEmployeeId || search.employeeId] + }} + onChange={handleTableChange} + onRow={(record) => { + return { + onClick: () => { + handleOnRowClick(record); + } + }; + }} + /> + )} + ); } diff --git a/client/src/components/shop-employees/shop-employees.container.jsx b/client/src/components/shop-employees/shop-employees.container.jsx index 8b22b87a4..3582ee40c 100644 --- a/client/src/components/shop-employees/shop-employees.container.jsx +++ b/client/src/components/shop-employees/shop-employees.container.jsx @@ -1,29 +1,101 @@ +import { Drawer, Form, Grid } from "antd"; import { useQuery } from "@apollo/client/react"; +import queryString from "query-string"; import { connect } from "react-redux"; +import { useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; import { createStructuredSelector } from "reselect"; import { QUERY_EMPLOYEES } from "../../graphql/employees.queries"; +import useConfirmDirtyFormNavigation from "../../hooks/useConfirmDirtyFormNavigation.jsx"; import AlertComponent from "../alert/alert.component"; import ShopEmployeesFormComponent from "./shop-employees-form.component"; import ShopEmployeesListComponent from "./shop-employees-list.component"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; +import "./shop-employees.styles.scss"; const mapStateToProps = createStructuredSelector({}); function ShopEmployeesContainer() { + const [form] = Form.useForm(); + const [isEmployeeFormDirty, setIsEmployeeFormDirty] = useState(false); + const location = useLocation(); + const navigate = useNavigate(); + const search = queryString.parse(location.search); const { loading, error, data } = useQuery(QUERY_EMPLOYEES, { fetchPolicy: "network-only", nextFetchPolicy: "network-only" }); + const screens = Grid.useBreakpoint(); + const hasSelectedEmployee = Boolean(search.employeeId); + + const bpoints = { + xs: "100%", + sm: "100%", + md: "92%", + lg: "80%", + xl: "80%", + xxl: "80%" + }; + + let drawerPercentage = "100%"; + if (screens.xxl) drawerPercentage = bpoints.xxl; + else if (screens.xl) drawerPercentage = bpoints.xl; + else if (screens.lg) drawerPercentage = bpoints.lg; + else if (screens.md) drawerPercentage = bpoints.md; + else if (screens.sm) drawerPercentage = bpoints.sm; + else if (screens.xs) drawerPercentage = bpoints.xs; + + const hasDirtyEmployeeForm = Boolean(search.employeeId) && (isEmployeeFormDirty || form.isFieldsTouched()); + const confirmCloseDirtyEmployee = useConfirmDirtyFormNavigation(hasDirtyEmployeeForm); + + const navigateToEmployee = (employeeId) => { + if (employeeId === search.employeeId) return; + if (!confirmCloseDirtyEmployee()) return; + + const nextSearch = { ...search, employeeId }; + setIsEmployeeFormDirty(false); + navigate({ + search: queryString.stringify(nextSearch) + }); + }; + + const handleDrawerClose = () => { + if (!confirmCloseDirtyEmployee()) return; + + const nextSearch = { ...search }; + delete nextSearch.employeeId; + setIsEmployeeFormDirty(false); + navigate({ + search: queryString.stringify(nextSearch) + }); + }; if (error) return ; return ( -
- - - - -
+ +
+
+ +
+
+ + {hasSelectedEmployee ? ( + + ) : null} + +
); } diff --git a/client/src/components/shop-employees/shop-employees.styles.scss b/client/src/components/shop-employees/shop-employees.styles.scss new file mode 100644 index 000000000..69ee5c1d0 --- /dev/null +++ b/client/src/components/shop-employees/shop-employees.styles.scss @@ -0,0 +1,7 @@ +.shop-employees-layout { + min-width: 0; +} + +.shop-employees-layout__list { + min-width: 0; +} diff --git a/client/src/components/shop-info/shop-info.color.utils.js b/client/src/components/shop-info/shop-info.color.utils.js new file mode 100644 index 000000000..d577aee25 --- /dev/null +++ b/client/src/components/shop-info/shop-info.color.utils.js @@ -0,0 +1,304 @@ +/** + * Default translucent card color used for tinting card surfaces when no specific color is provided. + * @type {{r: number, g: number, b: number, a: number}} + */ +export const DEFAULT_TRANSLUCENT_CARD_COLOR = { + r: 22, + g: 119, + b: 255, + a: 0.5 +}; + +/** + * Rounds a color channel value to two decimal places. + * @param value + * @returns {number} + */ +const roundColorChannel = (value) => Math.round(value * 100) / 100; + +/** + * Rounds a tint percentage value to two decimal places. + * @param value + * @returns {number} + */ +const roundTintPercentage = (value) => Math.round(value * 100) / 100; + +/** + * Clamps an alpha value to the range [0, 1] and rounds it to two decimal places. + * @param value + * @returns {number} + */ +const clampAlpha = (value) => { + const numericValue = Number(value); + + if (!Number.isFinite(numericValue)) return 1; + if (numericValue <= 0) return 0; + if (numericValue >= 1) return 1; + + return numericValue; +}; + +/** + * Converts an RGB color object to a hexadecimal color string. + * @param param0 + * @param param0.r + * @param param0.g + * @param param0.b + * @returns {`#${string}`} + */ +const rgbToHex = ({ r, g, b }) => + `#${[r, g, b].map((channel) => Math.round(channel).toString(16).padStart(2, "0")).join("")}`; + +/** + * Converts an RGB color object to an HSL color object. + * @param param0 + * @param param0.r + * @param param0.g + * @param param0.b + * @param param0.a + * @returns {{h: number, s: number, l: number, a: number}|{h: number, s: number, l: number, a: number}} + */ +const rgbToHsl = ({ r, g, b, a = 1 }) => { + const red = r / 255; + const green = g / 255; + const blue = b / 255; + const max = Math.max(red, green, blue); + const min = Math.min(red, green, blue); + const delta = max - min; + const lightness = (max + min) / 2; + + if (delta === 0) { + return { h: 0, s: 0, l: roundColorChannel(lightness), a }; + } + + const saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min); + let hue; + + switch (max) { + case red: + hue = (green - blue) / delta + (green < blue ? 6 : 0); + break; + case green: + hue = (blue - red) / delta + 2; + break; + default: + hue = (red - green) / delta + 4; + break; + } + + return { + h: roundColorChannel(hue * 60), + s: roundColorChannel(saturation), + l: roundColorChannel(lightness), + a + }; +}; + +/** + * Converts an RGB color object to an HSV color object. + * @param param0 + * @param param0.r + * @param param0.g + * @param param0.b + * @param param0.a + * @returns {{h: number, s: number, v: number, a: number}} + */ +const rgbToHsv = ({ r, g, b, a = 1 }) => { + const red = r / 255; + const green = g / 255; + const blue = b / 255; + const max = Math.max(red, green, blue); + const min = Math.min(red, green, blue); + const delta = max - min; + const saturation = max === 0 ? 0 : delta / max; + let hue = 0; + + if (delta !== 0) { + switch (max) { + case red: + hue = (green - blue) / delta + (green < blue ? 6 : 0); + break; + case green: + hue = (blue - red) / delta + 2; + break; + default: + hue = (red - green) / delta + 4; + break; + } + } + + return { + h: roundColorChannel(hue * 60), + s: roundColorChannel(saturation), + v: roundColorChannel(max), + a + }; +}; + +/** + * Builds a comprehensive color value object for a color picker component based on an input RGB color object. + * @param rgb + * @returns {{hex: `#${string}`, rgb: *, hsl: {h: number, s: number, l: number, a: number}, hsv: {h: number, s: number, v: number, a: number}, oldHue: number, source: string}} + */ +const buildPickerColorValue = (rgb) => { + const hsl = rgbToHsl(rgb); + + return { + hex: rgbToHex(rgb), + rgb: { ...rgb }, + hsl, + hsv: rgbToHsv(rgb), + oldHue: hsl.h, + source: "rgb" + }; +}; + +/** + * Default color value object for the color picker component, derived from the default translucent card color. + * @type {{hex: `#${string}`, rgb: *, hsl: {h: number, s: number, l: number, a: number}, hsv: {h: number, s: number, v: number, a: number}, oldHue: number, source: string}} + */ +export const DEFAULT_TRANSLUCENT_PICKER_COLOR = buildPickerColorValue(DEFAULT_TRANSLUCENT_CARD_COLOR); + +/** + * Parses a color string that may be a JSON representation of a color object. If the string is valid JSON and represents + * a color, it returns the parsed object; otherwise, it returns the original string. + * @param color + * @returns {*|string} + */ +const parseJsonColorString = (color) => { + if (typeof color !== "string") return color; + + const trimmedColor = color.trim(); + if (!trimmedColor.startsWith("{") && !trimmedColor.startsWith("[")) return color; + + try { + return JSON.parse(trimmedColor); + } catch { + return color; + } +}; + +/** + * Parses a hexadecimal color string (e.g., "#RRGGBB" or "#RRGGBBAA") and returns an object containing the corresponding + * RGB color value and alpha transparency. Supports both 3/4-digit and 6/8-digit hex formats. + * @param color + * @returns {{colorCssValue: string, alpha: number}|null} + */ +const parseHexColor = (color) => { + if (typeof color !== "string") return null; + + const normalizedHex = color.trim().replace(/^#/, ""); + + if (![3, 4, 6, 8].includes(normalizedHex.length) || /[^0-9a-f]/i.test(normalizedHex)) { + return null; + } + + const expandedHex = + normalizedHex.length <= 4 + ? normalizedHex + .split("") + .map((character) => `${character}${character}`) + .join("") + : normalizedHex; + + const hasAlpha = expandedHex.length === 8; + const red = Number.parseInt(expandedHex.slice(0, 2), 16); + const green = Number.parseInt(expandedHex.slice(2, 4), 16); + const blue = Number.parseInt(expandedHex.slice(4, 6), 16); + const alpha = hasAlpha ? Number.parseInt(expandedHex.slice(6, 8), 16) / 255 : 1; + + return { + colorCssValue: `rgb(${red}, ${green}, ${blue})`, + alpha: clampAlpha(alpha) + }; +}; + +/** + * Parses an RGB or RGBA color string (e.g., "rgb(255, 0, 0)" or "rgba(255, 0, 0, 0.5)") and returns an object + * containing the corresponding RGB color value and alpha transparency. Supports both integer and percentage formats for + * color channels and alpha. + * @param color + * @returns {{colorCssValue: string, alpha: number}|null} + */ +const parseRgbColor = (color) => { + if (typeof color !== "string") return null; + + const rgbMatch = color.trim().match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i); + + if (!rgbMatch) return null; + + const [, red, green, blue, alpha = 1] = rgbMatch; + + return { + colorCssValue: `rgb(${red}, ${green}, ${blue})`, + alpha: clampAlpha(alpha) + }; +}; + +/** + * Normalizes a color input into a consistent descriptor object containing a CSS color value and an alpha transparency + * level. + * @param color + * @returns {{colorCssValue: string, alpha: number}|{colorCssValue: string, alpha: number}|*|{colorCssValue: string, alpha: number}|null} + */ +const getNormalizedColorDescriptor = (color) => { + if (!color) return null; + + const normalizedColor = parseJsonColorString(color); + + if (typeof normalizedColor === "string") { + return ( + parseHexColor(normalizedColor) || + parseRgbColor(normalizedColor) || { + colorCssValue: normalizedColor, + alpha: 1 + } + ); + } + + if (typeof normalizedColor === "object" && normalizedColor.rgb) { + return getNormalizedColorDescriptor(normalizedColor.rgb); + } + + if (typeof normalizedColor === "object" && typeof normalizedColor.hex === "string") { + return getNormalizedColorDescriptor(normalizedColor.hex); + } + + if ( + typeof normalizedColor === "object" && + normalizedColor.r !== undefined && + normalizedColor.g !== undefined && + normalizedColor.b !== undefined + ) { + return { + colorCssValue: `rgb(${normalizedColor.r}, ${normalizedColor.g}, ${normalizedColor.b})`, + alpha: clampAlpha(normalizedColor.a) + }; + } + + return null; +}; + +/** + * Generates CSS styles for tinting card surfaces based on a provided color input. The function normalizes the input + * color, + * @param color + * @returns {{surfaceBg: string, surfaceHeaderBg: string, surfaceBorderColor: string}|{}} + */ +export const getTintedCardSurfaceStyles = (color) => { + const normalizedColor = getNormalizedColorDescriptor(color); + if (!normalizedColor?.colorCssValue) return {}; + + const tintStrength = clampAlpha(normalizedColor.alpha); + if (tintStrength === 0) return {}; + + const backgroundTint = roundTintPercentage(10 * tintStrength); + const headerTint = roundTintPercentage(18 * tintStrength); + const borderTint = roundTintPercentage(30 * tintStrength); + + return { + surfaceBg: `color-mix(in srgb, ${normalizedColor.colorCssValue} ${backgroundTint}%, var(--imex-form-surface))`, + surfaceHeaderBg: `color-mix(in srgb, ${normalizedColor.colorCssValue} ${headerTint}%, var(--imex-form-surface-head))`, + surfaceBorderColor: `color-mix(in srgb, ${normalizedColor.colorCssValue} ${borderTint}%, var(--imex-form-surface-border))` + }; +}; diff --git a/client/src/components/shop-info/shop-info.color.utils.test.js b/client/src/components/shop-info/shop-info.color.utils.test.js new file mode 100644 index 000000000..d39fb810f --- /dev/null +++ b/client/src/components/shop-info/shop-info.color.utils.test.js @@ -0,0 +1,52 @@ +import { describe, expect, it } from "vitest"; +import { getTintedCardSurfaceStyles } from "./shop-info.color.utils"; + +describe("shop info color utilities", () => { + it("scales card tint intensity with alpha for plain rgba values", () => { + expect( + getTintedCardSurfaceStyles({ + r: 22, + g: 119, + b: 255, + a: 0.5 + }) + ).toEqual({ + surfaceBg: "color-mix(in srgb, rgb(22, 119, 255) 5%, var(--imex-form-surface))", + surfaceHeaderBg: "color-mix(in srgb, rgb(22, 119, 255) 9%, var(--imex-form-surface-head))", + surfaceBorderColor: "color-mix(in srgb, rgb(22, 119, 255) 15%, var(--imex-form-surface-border))" + }); + }); + + it("returns no tint when the selected color alpha is zero", () => { + expect( + getTintedCardSurfaceStyles({ + hex: "#1677ff", + rgb: { + r: 22, + g: 119, + b: 255, + a: 0 + } + }) + ).toEqual({}); + }); + + it("supports legacy JSON-stringified picker values", () => { + expect( + getTintedCardSurfaceStyles( + JSON.stringify({ + rgb: { + r: 255, + g: 0, + b: 0, + a: 0.25 + } + }) + ) + ).toEqual({ + surfaceBg: "color-mix(in srgb, rgb(255, 0, 0) 2.5%, var(--imex-form-surface))", + surfaceHeaderBg: "color-mix(in srgb, rgb(255, 0, 0) 4.5%, var(--imex-form-surface-head))", + surfaceBorderColor: "color-mix(in srgb, rgb(255, 0, 0) 7.5%, var(--imex-form-surface-border))" + }); + }); +}); diff --git a/client/src/components/shop-info/shop-info.component.jsx b/client/src/components/shop-info/shop-info.component.jsx index 8dd04843e..901a14afe 100644 --- a/client/src/components/shop-info/shop-info.component.jsx +++ b/client/src/components/shop-info/shop-info.component.jsx @@ -1,6 +1,7 @@ import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; import { Button, Card, Tabs } from "antd"; import queryString from "query-string"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; @@ -21,6 +22,7 @@ import ShopInfoResponsibilityCenterComponent from "./shop-info.responsibilitycen import ShopInfoRoGuard from "./shop-info.roguard.component"; import ShopInfoROStatusComponent from "./shop-info.rostatus.component"; import ShopInfoSchedulingComponent from "./shop-info.scheduling.component"; +import ShopInfoSectionNavigator from "./shop-info.section-navigator.component.jsx"; import ShopInfoSpeedPrint from "./shop-info.speedprint.component"; import ShopInfoTaskPresets from "./shop-info.task-presets.component"; import ShopInfoIntellipay from "./shop-intellipay-config.component"; @@ -33,7 +35,7 @@ const mapDispatchToProps = () => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoComponent); -export function ShopInfoComponent({ bodyshop, form, saveLoading }) { +export function ShopInfoComponent({ bodyshop, form, saveLoading, isDirty }) { const { treatments: { CriticalPartsScanning, Enhanced_Payroll } } = useTreatmentsWithConfig({ @@ -47,6 +49,7 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) { const history = useNavigate(); const location = useLocation(); const search = queryString.parse(location.search); + const tabsRef = useRef(null); const tabItems = [ { @@ -154,23 +157,35 @@ export function ShopInfoComponent({ bodyshop, form, saveLoading }) { ] : []) ]; + const activeTabKey = search.subtab || tabItems[0]?.key; + return ( } extra={ - } > - - history({ - search: `?tab=${search.tab}&subtab=${key}` - }) - } - items={tabItems} - /> +
+ + history({ + search: `?tab=${search.tab}&subtab=${key}` + }) + } + items={tabItems} + /> +
); } diff --git a/client/src/components/shop-info/shop-info.consent.component.jsx b/client/src/components/shop-info/shop-info.consent.component.jsx index 28992e594..621095c14 100644 --- a/client/src/components/shop-info/shop-info.consent.component.jsx +++ b/client/src/components/shop-info/shop-info.consent.component.jsx @@ -1,4 +1,4 @@ -import { Card, Typography } from "antd"; +import { Card } from "antd"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -15,9 +15,8 @@ function ShopInfoConsentComponent({ bodyshop }) { const { t } = useTranslation(); return ( - - {t("settings.title")} - {} + + ); } diff --git a/client/src/components/shop-info/shop-info.container.jsx b/client/src/components/shop-info/shop-info.container.jsx index a43403776..823091b12 100644 --- a/client/src/components/shop-info/shop-info.container.jsx +++ b/client/src/components/shop-info/shop-info.container.jsx @@ -1,6 +1,6 @@ import { useMutation, useQuery } from "@apollo/client/react"; import { Form } from "antd"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { logImEXEvent } from "../../firebase/firebase.utils"; @@ -15,6 +15,7 @@ import { FEATURE_CONFIGS, useFormDataPreservation } from "./useFormDataPreservat export default function ShopInfoContainer() { const [form] = Form.useForm(); const { t } = useTranslation(); + const [isShopInfoDirty, setIsShopInfoDirty] = useState(false); const [saveLoading, setSaveLoading] = useState(false); const [updateBodyshop] = useMutation(UPDATE_SHOP); const { loading, error, data, refetch } = useQuery(QUERY_BODYSHOP, { @@ -33,7 +34,10 @@ export default function ShopInfoContainer() { return acc; }, {}); - const combinedFeatureConfig = combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters); + const combinedFeatureConfig = useMemo( + () => combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters), + [] + ); // Use form data preservation for all shop-info features const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation( @@ -51,7 +55,10 @@ export default function ShopInfoContainer() { }) .then(() => { notification.success({ title: t("bodyshop.successes.save") }); - refetch().then(() => form.resetFields()); + refetch().then(() => { + form.resetFields(); + setIsShopInfoDirty(false); + }); }) .catch((error) => { notification.error({ @@ -66,6 +73,7 @@ export default function ShopInfoContainer() { form.resetFields(); // After reset, re-apply hidden field preservation so values aren't wiped preserveHiddenFormData(); + setIsShopInfoDirty(false); }, [data, form, preserveHiddenFormData]); if (error) return ; @@ -76,6 +84,9 @@ export default function ShopInfoContainer() { layout="vertical" autoComplete="new-password" onFinish={handleFinish} + onValuesChange={() => { + setIsShopInfoDirty(form.isFieldsTouched()); + }} initialValues={ data ? data?.bodyshops?.[0]?.accountingconfig?.ClosingPeriod @@ -99,8 +110,8 @@ export default function ShopInfoContainer() { : null } > - - + + ); } diff --git a/client/src/components/shop-info/shop-info.general.component.jsx b/client/src/components/shop-info/shop-info.general.component.jsx index 00410f0f9..fa3a2c3af 100644 --- a/client/src/components/shop-info/shop-info.general.component.jsx +++ b/client/src/components/shop-info/shop-info.general.component.jsx @@ -1,5 +1,5 @@ import { DeleteFilled } from "@ant-design/icons"; -import { Button, Form, Input, InputNumber, Select, Space, Switch } from "antd"; +import { Button, Col, Form, Input, InputNumber, Row, Select, Space, Switch } from "antd"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -7,10 +7,24 @@ import FeatureWrapper from "../feature-wrapper/feature-wrapper.component"; import CurrencyInput from "../form-items-formatted/currency-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component"; import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; +import FormItemUrl from "../form-items-formatted/url-form-item.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import { buildSectionActionButton, renderListOrEmpty } from "../layout-form-row/config-list-actions.utils.jsx"; +import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { + INLINE_TITLE_GROUP_STYLE, + INLINE_TITLE_HANDLE_STYLE, + INLINE_TITLE_INPUT_STYLE, + INLINE_TITLE_LABEL_STYLE, + INLINE_TITLE_ROW_STYLE, + INLINE_TITLE_SEPARATOR_STYLE, + INLINE_TITLE_SWITCH_GROUP_STYLE, + InlineTitleListIcon +} from "../layout-form-row/inline-form-row-title.utils.js"; const timeZonesList = Intl.supportedValuesOf("timeZone"); + const mapStateToProps = createStructuredSelector({}); const mapDispatchToProps = () => ({ //setUserLanguage: language => dispatch(setUserLanguage(language)) @@ -19,6 +33,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoGeneral); export function ShopInfoGeneral({ form }) { const { t } = useTranslation(); + const insuranceCompanies = Form.useWatch(["md_ins_cos"], form) || []; + const duplicateInsuranceCompanyIndexes = getDuplicateIndexSetByNormalizedName(insuranceCompanies, "name"); return (
@@ -81,17 +97,17 @@ export function ShopInfoGeneral({ form }) { - + PhoneItemFormatterValidation(getFieldValue, "phone")]} > - + - + - + - + - + - + - + null}> - + +
+ {t("bodyshop.labels.scoreboardsetup")} +
+
+
+
+
{t("bodyshop.fields.scoreboard_setup.ignore_blocked_days")}
+ + + +
+
+
+ } + wrapTitle + > - + - - - - +
- {[ + <> - , - - - , - - - , - - - , - - - , - - - , - ({ - validator(rule, value) { - if (!value && !getFieldValue(["md_hour_split", "paint"])) { - return Promise.resolve(); - } - if (value + getFieldValue(["md_hour_split", "paint"]) === 1) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.larsplit")); - } - }) - ]} - > - - , - ({ - validator(rule, value) { - if (!value && !getFieldValue(["md_hour_split", "paint"])) { - return Promise.resolve(); - } - if (value + getFieldValue(["md_hour_split", "prep"]) === 1) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.larsplit")); - } - }) - ]} - > - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - , - - - ]} - - - - - - - - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - - - - - - - - { - remove(field.name); - }} - /> - - - - - ))} - - - -
- ); - }} -
-
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - - - - - - - - { - remove(field.name); - }} - /> - - - - - ))} - -
+
- {t("general.actions.add")} - - -
- ); - }} -
-
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - - +
+
+
+ {t("bodyshop.fields.system_settings.local_media_server.enabled")} +
+ + - - { - remove(field.name); - }} - /> - - - - - ))} - -
+
+
+ } + wrapTitle + > + + + + + + + + + +
+ + + + + + + + + + + +
+
+ } + extra={ + +
+ + ); + }} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_note_preset"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("bodyshop.fields.noteslabel_short")}
+ + + +
+
+ } + extra={ + + -
+ ); + }) + )} - ); - }} - - + + ); + }} + {/*End Insurance Provider Row */} - - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => ( - - - - - - - - - - - - PhoneItemFormatterValidation(getFieldValue, [field.name, "est_ph"]) - ]} - > - - - - - - - { - remove(field.name); - }} - /> - - - - - ))} - - - -
- ); - }} -
-
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - - - - - - - PhoneItemFormatterValidation(getFieldValue, [field.name, "ins_ph"]) - ]} - > - - - - - - - { - remove(field.name); - }} - /> - - - - - ))} - - - -
- ); - }} -
-
- null}> - - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - + fields.map((field, index) => { + return ( + + + +
+
{t("jobs.fields.est_ct_fn_short")}
+ + + +
+
+
+
{t("jobs.fields.est_ct_ln_short")}
+ + + +
+
+
+
{t("jobs.fields.est_co_nm_short")}
+ + + +
+
+ } + wrapTitle + extra={ + +
+
+ ); + }} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_adjuster"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("jobs.fields.ins_ct_fn_short")}
+ + + +
+
+
+
{t("jobs.fields.ins_ct_ln_short")}
+ + + +
+
+ } + wrapTitle + extra={ + +
+
+ ); + }} +
+ null}> + + {(fields, { add, remove, move }) => { + return ( + { + add(); + } + ) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_courtesy_car_rate_preset"), () => + fields.map((field, index) => { + return ( + + + +
+
{t("general.labels.label")}
+ + + +
+
+ } + wrapTitle + extra={ + + -
+ ); + }) + )}
- ); - }} -
-
+ + ); + }} +
- - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => ( - - - - - - - - - - + +
+
+
+
{t("joblines.fields.line_desc")}
+ + + +
+
+ } + wrapTitle + extra={ + + - + ); + }) + )} - ); - }} -
-
- - - {(fields, { add, remove, move }) => { - return ( + + ); + }} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + } + ) + ]} + >
- {fields.map((field, index) => ( - - - + fields.map((field, index) => { + return ( + + + +
+
{t("general.labels.label")}
+ + + +
+
} - ]} - > - - - + - + ); + }) + )} - ); - }} -
- - - - {(fields, { add, remove, move }) => { - return ( + + ); + }} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => ( - - - + fields.map((field, index) => { + return ( + + + +
+
{t("general.labels.label")}
+ + + +
+
} - ]} - > - - - + - + ); + }) + )} - ); - }} -
- + + ); + }} + ); } + +function getDuplicateIndexSetByNormalizedName(list, key) { + const indexes = new Set(); + const firstIndexByValue = new Map(); + + (Array.isArray(list) ? list : []).forEach((item, index) => { + const normalizedValue = (item?.[key] ?? "").toString().trim().toLowerCase(); + if (!normalizedValue) return; + + if (firstIndexByValue.has(normalizedValue)) { + indexes.add(firstIndexByValue.get(normalizedValue)); + indexes.add(index); + return; + } + + firstIndexByValue.set(normalizedValue, index); + }); + + return indexes; +} diff --git a/client/src/components/shop-info/shop-info.intake.component.jsx b/client/src/components/shop-info/shop-info.intake.component.jsx index 174a6ddf0..9a8b6bc12 100644 --- a/client/src/components/shop-info/shop-info.intake.component.jsx +++ b/client/src/components/shop-info/shop-info.intake.component.jsx @@ -5,7 +5,19 @@ import styled from "styled-components"; import { TemplateList } from "../../utils/TemplateConstants"; import ConfigFormTypes from "../config-form-components/config-form-types"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; +import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { + INLINE_TITLE_GROUP_STYLE, + INLINE_TITLE_HANDLE_STYLE, + INLINE_TITLE_INPUT_STYLE, + INLINE_TITLE_LABEL_STYLE, + INLINE_TITLE_ROW_STYLE, + INLINE_TITLE_SEPARATOR_STYLE, + INLINE_TITLE_SWITCH_GROUP_STYLE, + InlineTitleListIcon +} from "../layout-form-row/inline-form-row-title.utils.js"; const SelectorDiv = styled.div` .ant-form-item .ant-select { @@ -19,306 +31,386 @@ export default function ShopInfoIntakeChecklistComponent({ form }) { const TemplateListGenerated = TemplateList(); return (
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - - - - - - - - - {() => { - if (form.getFieldValue(["intakechecklist", "form", index, "type"]) !== "slider") return null; - return ( - <> - - - - - - - - ); - }} - - - - - - { - remove(field.name); - }} - /> - - - - - ))} - - - -
- ); - }} -
-
- - ({ + value: TemplateListGenerated[i].key, + label: TemplateListGenerated[i].title + }))} + /> + + + + +
+
+
+
{t("jobs.fields.intake.required")}
+ + + +
+
} - ]} - > - - - - + - + ); + }) + )} - ); - }} - - - - - + + +
+
+
{t("jobs.fields.intake.required")}
+ + + +
+
+ } + wrapTitle + extra={ + + + ]} + >
- {fields.map((field, index) => ( - - - + ) : ( + fields.map((field, index) => { + return ( + + + +
+
{t("jobs.fields.labor_rate_desc")}
+ + + +
+
} - ]} - > - - - + - + ); + }) + )} - ); - }} - - + + ); + }} + ); } diff --git a/client/src/components/shop-info/shop-info.notifications-autoadd.component.jsx b/client/src/components/shop-info/shop-info.notifications-autoadd.component.jsx index 45a8ef838..3647cc95c 100644 --- a/client/src/components/shop-info/shop-info.notifications-autoadd.component.jsx +++ b/client/src/components/shop-info/shop-info.notifications-autoadd.component.jsx @@ -1,6 +1,7 @@ import { Form, Typography } from "antd"; import { useTranslation } from "react-i18next"; import EmployeeSearchSelectComponent from "../employee-search-select/employee-search-select.component.jsx"; +import LayoutFormRow from "../layout-form-row/layout-form-row.component"; const { Text, Paragraph } = Typography; @@ -11,43 +12,45 @@ export default function ShopInfoNotificationsAutoadd({ bodyshop }) { const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.user_email && e.id) || []; return ( -
- {t("bodyshop.fields.notifications.description")} - {t("bodyshop.labels.notifications.followers")} - {employeeOptions.length > 0 ? ( - (value || []).filter((id) => typeof id === "string" && id.trim() !== "")} - name="notification_followers" - rules={[ - { - type: "array", - message: t("general.validation.array") - }, - { - validator: async (_, value) => { - if (!value || value.length === 0) { - return Promise.resolve(); // Allow empty array + +
+ {t("bodyshop.fields.notifications.description")} + {t("bodyshop.labels.notifications.followers")} + {employeeOptions.length > 0 ? ( + (value || []).filter((id) => typeof id === "string" && id.trim() !== "")} + name="notification_followers" + rules={[ + { + type: "array", + message: t("general.validation.array") + }, + { + validator: async (_, value) => { + if (!value || value.length === 0) { + return Promise.resolve(); // Allow empty array + } + const hasInvalid = value.some((id) => id == null || typeof id !== "string" || id.trim() === ""); + if (hasInvalid) { + return Promise.reject(new Error(t("bodyshop.fields.notifications.invalid_followers"))); + } + return Promise.resolve(); } - const hasInvalid = value.some((id) => id == null || typeof id !== "string" || id.trim() === ""); - if (hasInvalid) { - return Promise.reject(new Error(t("bodyshop.fields.notifications.invalid_followers"))); - } - return Promise.resolve(); } - } - ]} - > - - - ) : ( - {t("bodyshop.fields.no_employees_available")} - )} -
+ ]} + > + +
+ ) : ( + {t("bodyshop.fields.no_employees_available")} + )} +
+ ); } diff --git a/client/src/components/shop-info/shop-info.parts-scan.jsx b/client/src/components/shop-info/shop-info.parts-scan.jsx index 51fdbbfe4..03c55b937 100644 --- a/client/src/components/shop-info/shop-info.parts-scan.jsx +++ b/client/src/components/shop-info/shop-info.parts-scan.jsx @@ -3,7 +3,19 @@ import { Button, Col, Form, Input, Row, Select, Space, Switch } from "antd"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import ConfigListEmptyState from "../layout-form-row/config-list-empty-state.component.jsx"; +import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { + INLINE_TITLE_GROUP_STYLE, + INLINE_TITLE_HANDLE_STYLE, + INLINE_TITLE_INPUT_STYLE, + INLINE_TITLE_LABEL_STYLE, + INLINE_TITLE_ROW_STYLE, + INLINE_TITLE_SEPARATOR_STYLE, + INLINE_TITLE_SWITCH_GROUP_STYLE, + InlineTitleListIcon +} from "../layout-form-row/inline-form-row-title.utils.js"; import i18n from "i18next"; const predefinedPartTypes = ["PAN", "PAC", "PAR", "PAL", "PAA", "PAM", "PAP", "PAS", "PASL", "PAG"]; @@ -68,195 +80,223 @@ export default function ShopInfoPartsScan({ form }) { return (
- - - {(fields, { add, remove, move }) => ( + + {(fields, { add, remove, move }) => ( + + add({ + field: "line_desc", + operation: "contains", + mark_critical: true, + caseInsensitive: true + }) + } + > + {t("bodyshop.actions.addpartsrule")} + + ]} + >
- {fields.map((field, index) => { - const selectedField = watchedFields?.[index]?.field || "line_desc"; - const fieldType = getFieldType(selectedField); + {fields.length === 0 ? ( + + ) : ( + fields.map((field, index) => { + const selectedField = watchedFields?.[index]?.field || "line_desc"; + const fieldType = getFieldType(selectedField); - return ( - - - {/* Select Field */} - - - { + form.setFields([ + { name: ["md_parts_scan", index, "operation"], value: "contains" }, + { name: ["md_parts_scan", index, "value"], value: undefined } + ]); + }} + style={{ + width: "100%" + }} + styles={{ + selector: INLINE_TITLE_INPUT_STYLE + }} + size="small" + /> + +
+ {fieldType === "string" && ( + <> +
+
+
+ {t("bodyshop.fields.md_parts_scan.caseInsensitive")} +
+ + + +
+ + )} +
+
+
+ {t("bodyshop.fields.md_parts_scan.mark_critical")} +
+ + + +
+
+ } + wrapTitle + extra={ + + - + + + + + + ); + }) + )}
- )} -
-
+ + )} +
); } diff --git a/client/src/components/shop-info/shop-info.rbac.component.jsx b/client/src/components/shop-info/shop-info.rbac.component.jsx index c0377cc17..1e24355ec 100644 --- a/client/src/components/shop-info/shop-info.rbac.component.jsx +++ b/client/src/components/shop-info/shop-info.rbac.component.jsx @@ -27,7 +27,7 @@ export function ShopInfoRbacComponent({ bodyshop }) { }); return ( - + {[ ...(HasFeatureAccess({ featureName: "export", bodyshop }) ? [ diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx index 3a9588272..b8784a4bf 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.component.jsx @@ -1,6 +1,6 @@ import { DeleteFilled } from "@ant-design/icons"; import { useTreatmentsWithConfig } from "@splitsoftware/splitio-react"; -import { Button, DatePicker, Form, Input, InputNumber, Radio, Select, Space, Switch } from "antd"; +import { Button, Col, DatePicker, Divider, Form, Input, InputNumber, Radio, Row, Select, Space, Switch } from "antd"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; @@ -11,7 +11,20 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr"; import DataLabel from "../data-label/data-label.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component"; +import { getFormListItemTitle } from "../form-list-move-arrows/form-list-item-title.utils"; +import { buildSectionActionButton, renderListOrEmpty } from "../layout-form-row/config-list-actions.utils.jsx"; +import InlineValidatedFormRow from "../layout-form-row/inline-validated-form-row.component.jsx"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { + INLINE_TITLE_GROUP_STYLE, + INLINE_TITLE_HANDLE_STYLE, + INLINE_TITLE_INPUT_STYLE, + INLINE_TITLE_LABEL_STYLE, + INLINE_TITLE_ROW_STYLE, + INLINE_TITLE_SEPARATOR_STYLE, + INLINE_TITLE_SWITCH_GROUP_STYLE, + InlineTitleListIcon +} from "../layout-form-row/inline-form-row-title.utils.js"; import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component"; import ShopInfoResponsibilitycentersTaxesComponent from "./shop-info.responsibilitycenters.taxes.component"; import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js"; @@ -32,6 +45,9 @@ export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoResponsibili export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { const { t } = useTranslation(); + const taxAccountRowCol = { xs: 24, sm: 12, md: 12, lg: 6, xl: 6, xxl: 6 }; + const taxAccountFullRowCol = { xs: 24 }; + const dmsPayers = Form.useWatch(["cdk_configuration", "payers"], form) || []; const hasDMSKey = bodyshopHasDmsKey(bodyshop); @@ -66,240 +82,21 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { }; const ReceivableCustomFieldSelect = ( - ); return (
- {[ - ...(HasFeatureAccess({ featureName: "export", bodyshop }) - ? !hasDMSKey - ? [ - - - , - InstanceRenderManager({ - imex: ( - - {() => ( - - - - )} - - ) - }), - - - , - - - 2 - 3 - - , - - {() => { - return ( - - - {t("bodyshop.labels.2tiername")} - {t("bodyshop.labels.2tiersource")} - - - ); - }} - , - - - , - - - , - - {ReceivableCustomFieldSelect} - , - - {ReceivableCustomFieldSelect} - , - - {ReceivableCustomFieldSelect} - , - { - return { - required: getFieldValue("enforce_class"), - //message: t("general.validation.required"), - type: "array" - }; - } - ]} - > - - , - - - , - InstanceRenderManager({ - imex: ( - - - - ) - }), - - - , - ...(HasFeatureAccess({ featureName: "bills", bodyshop }) - ? [ - InstanceRenderManager({ - imex: ( - - - - ) - }), - - - , - - - - ] - : []), + <> - ] - : []), - ...(HasFeatureAccess({ featureName: "bills", bodyshop }) - ? [ + + - - , - - + - ] - : []), - ...(HasFeatureAccess({ featureName: "export", bodyshop }) - ? [ - ...(ClosingPeriod.treatment === "on" - ? [ - - + + {InstanceRenderManager({ + imex: ( + + + + + + ) + })} + + + + + + {HasFeatureAccess({ featureName: "export", bodyshop }) && + ClosingPeriod.treatment === "on" && ( + + + + + + )} + {HasFeatureAccess({ featureName: "export", bodyshop }) && + ADPPayroll.treatment === "on" && ( + + + + + + )} + {HasFeatureAccess({ featureName: "export", bodyshop }) && + ADPPayroll.treatment === "on" && ( + + + + + + )} + {HasFeatureAccess({ featureName: "export", bodyshop }) && !hasDMSKey && ( + <> + + + + 2 + 3 + + + + + + {() => { + return ( + + + {t("bodyshop.labels.2tiername")} + {t("bodyshop.labels.2tiersource")} + + + ); + }} + + + + { + return { + required: getFieldValue("enforce_class"), + //message: t("general.validation.required"), + type: "array" + }; + } + ]} + > + - - ] - : []), - ...(ADPPayroll.treatment === "on" - ? [ - - - - ] - : []) - ] - : []) - ]} +
+ {InstanceRenderManager({ + imex: ( +
+ {t("bodyshop.labels.qbo_usa")} + + {() => ( + + + + )} + +
+ ) + })} +
+ } + > + + + + + {ReceivableCustomFieldSelect} + + + {ReceivableCustomFieldSelect} + + + {ReceivableCustomFieldSelect} + + + )} + + {HasFeatureAccess({ featureName: "bills", bodyshop }) && ( + + {InstanceRenderManager({ + imex: ( + + + + ) + })} + + + + + + + + )} + + {hasDMSKey && ( - <> - - {bodyshop.rr_dealerid && ( - {form.getFieldValue("rr_dealerid")} - )} - {bodyshop.cdk_dealerid && ( - - {form.getFieldValue("cdk_dealerid")} - - )} - {bodyshop.pbs_serialnumber && ( - - {form.getFieldValue("pbs_serialnumber")} - - )} - - - - - - - - - - - - - - - - - - {bodyshop.pbs_serialnumber && ( + +
+ + {bodyshop.rr_dealerid && ( + + {form.getFieldValue("rr_dealerid")} + + )} + {bodyshop.cdk_dealerid && ( + + {form.getFieldValue("cdk_dealerid")} + + )} + {bodyshop.pbs_serialnumber && ( + + {form.getFieldValue("pbs_serialnumber")} + + )} + + - + - )} - {bodyshop.pbs_serialnumber && ( - + - )} - {bodyshop.pbs_serialnumber && ( - + - )} - {bodyshop.pbs_serialnumber && ( - - )} - {bodyshop.pbs_serialnumber && ( - - + + )} + {bodyshop.pbs_serialnumber && ( + + - - - - - - + + + + + + - - - - - { - remove(field.name); - }} - /> - - - ))} - - - -
+ } + ) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_control_number"), () => + fields.map((field, index) => ( + + + + + + + + +
+
); }} -
- - )} - + + )} + + )} {HasFeatureAccess({ featureName: "export", bodyshop }) && ( <> - - - {(fields, { add, remove, move }) => { - return ( + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => ( - - - - - - - - - - - - - {hasDMSKey && !bodyshop.rr_dealerid && ( - <> + {renderListOrEmpty(fields, t("bodyshop.actions.add_cost_center"), () => + fields.map((field, index) => { + return ( + + + +
+
+ {t("bodyshop.fields.responsibilitycenter")} +
+ + + +
+
+
+
+ {t("bodyshop.fields.responsibilitycenter_accountdesc")} +
+ + + +
+
+ } + wrapTitle + extra={ + + -
-
- ); - }} -
-
- - - {(fields, { add, remove, move }) => { - return ( -
- {fields.map((field, index) => ( - - - - - - - - - {!hasDMSKey && ( - - - - )} - {hasDMSKey && !bodyshop.rr_dealerid && ( - - - - )} - {bodyshop.cdk_dealerid && ( - - - - )} - {bodyshop.rr_dealerid && ( - <> - - - - - - - - - )} - - { - remove(field.name); - }} - /> - - - - - ))} - - - -
- ); - }} -
-
- - {hasDMSKey && ( - - - {(fields, { add, remove }) => { - return ( -
- {fields.map((field, index) => ( - -
- 0 ? false : true}> + {!hasDMSKey && ( - + - - - - { - remove(field.name); - }} - /> - - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); + )} + {hasDMSKey && !bodyshop.rr_dealerid && ( + <> + + + + - + + + )} + + {bodyshop.cdk_dealerid && ( ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAA`} - name={[field.name, "costs", "LAA"]} + label={t("bodyshop.fields.dms.dms_control_override")} + key={`${index}dms_control_override`} + name={[field.name, "dms_control_override"]} > - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAB`} - name={[field.name, "costs", "LAB"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAE`} - name={[field.name, "costs", "LAE"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAG`} - name={[field.name, "costs", "LAG"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAR`} - name={[field.name, "costs", "LAR"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LAU`} - name={[field.name, "costs", "LAU"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LA2`} - name={[field.name, "costs", "LA2"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-LA4`} - name={[field.name, "costs", "LA4"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAC`} - name={[field.name, "costs", "PAC"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAL`} - name={[field.name, "costs", "PAL"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAN`} - name={[field.name, "costs", "PAN"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAP`} - name={[field.name, "costs", "PAP"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-PAS`} - name={[field.name, "costs", "PAS"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-TOW`} - name={[field.name, "costs", "TOW"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (costOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}costs-MASH`} - name={[field.name, "costs", "MASH"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAA`} - name={[field.name, "profits", "LAA"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAD`} - name={[field.name, "profits", "LAD"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAF`} - name={[field.name, "profits", "LAF"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAM`} - name={[field.name, "profits", "LAM"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LAS`} - name={[field.name, "profits", "LAS"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LA1`} - name={[field.name, "profits", "LA1"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-LA3`} - name={[field.name, "profits", "LA3"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAA`} - name={[field.name, "profits", "PAA"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAG`} - name={[field.name, "profits", "PAG"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAM`} - name={[field.name, "profits", "PAM"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAO`} - name={[field.name, "profits", "PAO"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PAR`} - name={[field.name, "profits", "PAR"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-PASL`} - name={[field.name, "profits", "PASL"]} - > - ({ value: item, label: item }))} /> - - ({ - validator(rule, value) { - if (profitOptions.includes(value)) { - return Promise.resolve(); - } - return Promise.reject(t("bodyshop.validation.centermustexist")); - } - }) - ]} - key={`${index}profits-MAPA`} - name={[field.name, "profits", "MAPA"]} - > - ({ value: item, label: item }))} /> - - -
+ )} +
- ))} - - - + ); + }) + )} +
+
+ ); + }} + + + {(fields, { add, remove, move }) => { + return ( + { + add(); + }) + ]} + > +
+ {renderListOrEmpty(fields, t("bodyshop.actions.add_profit_center"), () => + fields.map((field, index) => { + return ( + + + +
+
+ {t("bodyshop.fields.responsibilitycenter")} +
+ + + +
+
+
+
+ {t("bodyshop.fields.responsibilitycenter_accountdesc")} +
+ + + +
+
+ } + wrapTitle + extra={ + +
+
+ + ); + }) + )} - ); - }} -
- + + ); + }} + )} @@ -2921,43 +3332,110 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { )} - - - - - - - - - - {hasDMSKey && ( - + - - - )} - - - + + + + + + + + + + + + + {hasDMSKey && ( + + + + )} + + + {InstanceRenderManager({ + imex: ( + + + + + + + + + + + + + + {hasDMSKey && ( + + + + )} + + ), + rome: null + })} + {DmsAp.treatment === "on" && ( @@ -2997,75 +3475,34 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { rules={[{ required: true }]} name={["md_responsibility_centers", "taxes", "federal_itc", "rate"]} > - + )} {InstanceRenderManager({ - imex: ( - - - - - - - - - - - - {hasDMSKey && ( - - - - )} - - - - - ), + imex: null, rome: })} {HasFeatureAccess({ featureName: "export", bodyshop }) && ( <> {InstanceRenderManager({ rome: ( - <> + - + ) })} AR} id="AR"> @@ -3163,78 +3600,119 @@ export function ShopInfoResponsibilityCenterComponent({ bodyshop, form }) { )} - - - {(fields, { add, remove }) => { - return ( + + {(fields, { add, remove }) => { + return ( + { + add(); + }) + ]} + >
- {fields.map((field, index) => ( - - 0 ? false : true}> - - + {renderListOrEmpty(fields, t("bodyshop.actions.newsalestaxcode"), () => + fields.map((field, index) => { + return ( + + + +
+
+ {t("bodyshop.fields.responsibilitycenters.sales_tax_codes.description")} +
+ + + +
+
+
+
+ {t("bodyshop.fields.responsibilitycenters.sales_tax_codes.code")} +
+ + + +
+
+ } + wrapTitle + extra={ + -
+ ); + }) + )}
- ); - }} -
-
+ + ); + }} + )} diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx b/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx index a947f7d1c..d742628eb 100644 --- a/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.component.jsx @@ -1,4 +1,4 @@ -import { Collapse, Divider, Form, Input, InputNumber, Space, Switch } from "antd"; +import { Col, Collapse, Form, Input, InputNumber, Row, Switch } from "antd"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -6,6 +6,7 @@ import { selectBodyshop } from "../../redux/user/user.selectors"; import InstanceRenderManager from "../../utils/instanceRenderMgr"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js"; +import "./shop-info.responsibilitycenters.taxes.styles.scss"; const mapStateToProps = createStructuredSelector({ //currentUser: selectCurrentUser @@ -16,53 +17,102 @@ const mapDispatchToProps = () => ({ }); export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoResponsibilityCenters); +const taxRootColProps = { + xs: 24, + sm: 12, + md: 8, + lg: { flex: "0 0 280px" }, + xl: { flex: "0 0 240px" }, + xxl: { flex: "0 0 300px" } +}; + +const taxTierFieldColProps = { + xs: 24, + sm: 12, + lg: 6 +}; + export function ShopInfoResponsibilityCenters({ bodyshop, form }) { const { t } = useTranslation(); - //Iteratively build the form items. - const formItems = []; - for (let tyCounter = 1; tyCounter <= 5; tyCounter++) { - const section = []; + const profileTaxCards = []; + for (let typeNum = 1; typeNum <= 5; typeNum++) { + const rootTaxItems = getRootTaxFormItems({ typeNum, bodyshop, t }); - section.push( - TaxFormItems({ - typeNum: tyCounter, - rootElements: true, - bodyshop - }) + profileTaxCards.push( + +
+ + {rootTaxItems.map((item, index) => ( + + {item} + + ))} + + + {Array.from({ length: 5 }, (_, index) => { + const typeNumIterator = index + 1; + const tierTaxItems = getTierTaxFormItems({ + typeNum, + typeNumIterator, + t + }); + + return ( + + + + {tierTaxItems.map((item, tierIndex) => ( + + {item} + + ))} + + + + ); + })} + +
+
); - for (let iterator = 1; iterator <= 5; iterator++) { - section.push( - TaxFormItems({ - typeNum: tyCounter, - typeNumIterator: iterator, - rootElements: false - }) - ); - } - formItems.push({section}); - formItems.push(); } + return ( <> - - {t("jobs.labels.cieca_pft")} - - {formItems} + +
{profileTaxCards}
+
- + + - + - + ); }} @@ -135,7 +185,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAD", "lbr_adjp"]} > - + - + ); }} @@ -208,7 +258,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAE", "lbr_adjp"]} > - + - + ); }} @@ -281,7 +331,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAF", "lbr_adjp"]} > - + - + ); }} @@ -354,7 +404,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAG", "lbr_adjp"]} > - + - + ); }} @@ -427,7 +477,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAM", "lbr_adjp"]} > - + - + ); }} @@ -500,7 +550,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAR", "lbr_adjp"]} > - + - + ); }} @@ -573,7 +623,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["md_responsibility_centers", "cieca_pfl", "LAS", "lbr_adjp"]} > - + - + ); }} @@ -740,7 +790,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.materials.mat_adjp")} name={["md_responsibility_centers", "cieca_pfm", "MAPA", "mat_adjp"]} > - + - + ); }} @@ -825,7 +875,7 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { label={t("jobs.fields.materials.mat_adjp")} name={["md_responsibility_centers", "cieca_pfm", "MASH", "mat_adjp"]} > - + - + ); }} @@ -893,15 +943,15 @@ export function ShopInfoResponsibilityCenters({ bodyshop, form }) { - - ) - }, - { - key: "cieca_pfo", - label: t("jobs.labels.cieca_pfo"), - forceRender: true, - children: ( - <> + + ) + }, + { + key: "cieca_pfo", + label: t("jobs.labels.cieca_pfo"), + forceRender: true, + children: ( + <> - - ) - } - ]} - /> + + ) + } + ]} + /> + ); } -function TaxFormItems({ typeNum, typeNumIterator, rootElements, bodyshop }) { - const { t } = useTranslation(); - - if (rootElements) - return ( - <> - - - - - - - - - - - - - - - {bodyshopHasDmsKey(bodyshop) && ( +function getRootTaxFormItems({ typeNum, bodyshop, t }) { + return [ + + + , + + + , + + + , + + + , + ...(bodyshopHasDmsKey(bodyshop) + ? [ - )} - - ); - return ( - <> - - - - - - - - - - - - - - ); + ] + : []) + ]; +} + +function getTierTaxFormItems({ typeNum, typeNumIterator, t }) { + return [ + + + , + + + , + + + , + + + + ]; } diff --git a/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.styles.scss b/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.styles.scss new file mode 100644 index 000000000..5cbfe7f95 --- /dev/null +++ b/client/src/components/shop-info/shop-info.responsibilitycenters.taxes.styles.scss @@ -0,0 +1,25 @@ +.responsibility-centers-tax-tier-grid__col.ant-col { + flex: 0 0 100%; + max-width: 100%; +} + +@media (min-width: 992px) { + .responsibility-centers-tax-tier-grid__col.ant-col { + flex: 0 0 50%; + max-width: 50%; + } +} + +@media (min-width: 1600px) { + .responsibility-centers-tax-tier-grid__col.ant-col { + flex: 0 0 25%; + max-width: 25%; + } +} + +@media (min-width: 2400px) { + .responsibility-centers-tax-tier-grid__col.ant-col { + flex: 0 0 20%; + max-width: 20%; + } +} diff --git a/client/src/components/shop-info/shop-info.roguard.component.jsx b/client/src/components/shop-info/shop-info.roguard.component.jsx index a8b6e983e..66638a9f5 100644 --- a/client/src/components/shop-info/shop-info.roguard.component.jsx +++ b/client/src/components/shop-info/shop-info.roguard.component.jsx @@ -21,7 +21,7 @@ export default function ShopInfoRoGuard({ form }) { {() => { const disabled = !form.getFieldValue(["md_ro_guard", "enabled"]); return ( - + - + [...new Set((statuses || []).map((item) => item?.trim()).filter(Boolean))]; + +const getTranslatedDragRect = (active, delta) => { + const rect = active?.rect?.current?.initial || active?.rect?.current?.translated; + + if (!rect) return null; + + const x = delta?.x || 0; + const y = delta?.y || 0; + + return { + left: rect.left + x, + right: rect.right + x, + top: rect.top + y, + bottom: rect.bottom + y, + width: rect.width, + height: rect.height + }; +}; + +const isPointWithinRect = (point, rect) => { + if (!point || !rect) return false; + + return point.x >= rect.left && point.x <= rect.right && point.y >= rect.top && point.y <= rect.bottom; +}; + +const DraggableStatusTag = ({ label, value, closable, onClose }) => { + const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ + id: value + }); + const labelText = String(label ?? value); + + return ( + { + event.stopPropagation(); + }} + onClick={(event) => { + event.stopPropagation(); + }} + {...attributes} + {...listeners} + > + { + if (event.target.closest(".ant-select-selection-item-remove")) { + event.stopPropagation(); + return; + } + + event.preventDefault(); + event.stopPropagation(); + }} + onClick={(event) => { + if (event.target.closest(".ant-select-selection-item-remove")) { + event.stopPropagation(); + return; + } + + event.stopPropagation(); + }} + title={labelText} + > + + + + {labelText} + {closable ? ( + { + event.stopPropagation(); + onClose?.(event); + }} + onMouseDown={(event) => { + event.stopPropagation(); + }} + > + + + ) : null} + + + ); +}; + +const SortableStatusesSelect = ({ value, onChange, mode = "tags", options = [] }) => { + const statuses = normalizeStatuses(value); + const isTagsMode = mode === "tags"; + const [knownStatuses, setKnownStatuses] = useState(statuses); + const selectWrapperRef = useRef(null); + const dragRectRef = useRef(null); + const tagSensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 6 + } + }) + ); + + const handleStatusesChange = (nextValues) => { + const normalizedNextValues = normalizeStatuses(nextValues); + if (isTagsMode) { + setKnownStatuses((currentKnownStatuses) => normalizeStatuses([...currentKnownStatuses, ...normalizedNextValues])); + } + onChange?.(normalizedNextValues); + }; + + useEffect(() => { + if (isTagsMode) { + setKnownStatuses((currentKnownStatuses) => normalizeStatuses([...currentKnownStatuses, ...statuses])); + } + }, [isTagsMode, statuses]); + + const shouldMoveStatusToEnd = (activeId, dragRect) => { + const selectRect = + selectWrapperRef.current?.querySelector?.(".ant-select-selector")?.getBoundingClientRect?.() || + selectWrapperRef.current?.getBoundingClientRect?.(); + if (!dragRect || !selectRect) return false; + + const dragLeadingPoint = { + x: dragRect.left, + y: dragRect.top + }; + const dragTrailingPoint = { + x: dragRect.right, + y: dragRect.bottom + }; + + if (!isPointWithinRect(dragLeadingPoint, selectRect) && !isPointWithinRect(dragTrailingPoint, selectRect)) { + return false; + } + + const trailingStatus = statuses.filter((status) => status !== activeId).at(-1); + if (!trailingStatus) return false; + + const trailingTagNode = selectWrapperRef.current?.querySelector?.( + `.job-statuses-source-tag-wrapper[data-status-tag-value="${CSS.escape(String(trailingStatus))}"]` + ); + const trailingTagRect = trailingTagNode?.getBoundingClientRect?.(); + + if (!trailingTagRect) return false; + + const isOnTrailingRow = dragRect.bottom >= trailingTagRect.top && dragRect.top <= trailingTagRect.bottom; + if (isOnTrailingRow) { + return dragRect.left >= trailingTagRect.right - 4; + } + + return dragRect.top >= trailingTagRect.bottom - 4; + }; + + const handleStatusSortEnd = ({ active, over, delta }) => { + const oldIndex = statuses.indexOf(active.id); + const dragRect = dragRectRef.current || getTranslatedDragRect(active, delta); + dragRectRef.current = null; + + if (oldIndex < 0) return; + + if (!over) { + if (oldIndex !== statuses.length - 1 && shouldMoveStatusToEnd(active.id, dragRect)) { + onChange?.(arrayMove(statuses, oldIndex, statuses.length - 1)); + } + return; + } + + if (active.id === over.id) return; + + const newIndex = statuses.indexOf(over.id); + + if (newIndex < 0) return; + + onChange?.(arrayMove(statuses, oldIndex, newIndex)); + }; + + const renderStatusTag = ({ label, value: tagValue, closable, onClose }) => { + return ; + }; + + const statusSelectOptions = isTagsMode + ? knownStatuses.map((status) => ({ + value: status, + label: status + })) + : options; + + if (statuses.length === 0) { + return ( + + + + + ); +}; + export function ShopInfoROStatusComponent({ bodyshop, form }) { const { t } = useTranslation(); + const allStatuses = normalizeStatuses(Form.useWatch(["md_ro_statuses", "statuses"], form)); + const productionStatuses = Form.useWatch(["md_ro_statuses", "production_statuses"], form) || []; + const additionalBoardStatuses = Form.useWatch(["md_ro_statuses", "additional_board_statuses"], form) || []; + const productionColors = Form.useWatch(["md_ro_statuses", "production_colors"], form) || []; + const statusOptions = allStatuses; + const statusSelectOptions = statusOptions.map((item) => ({ value: item, label: item })); + const availableProductionStatuses = [...new Set([...productionStatuses, ...additionalBoardStatuses].filter(Boolean))]; const { treatments: { Production_List_Status_Colors } @@ -37,117 +375,119 @@ export function ShopInfoROStatusComponent({ bodyshop, form }) { splitKey: bodyshop.imexshopid }); - const [options, setOptions] = useState(form.getFieldValue(["md_ro_statuses", "statuses"]) || []); - - const [productionStatus, setProductionStatus] = useState( - (form.getFieldValue(["md_ro_statuses", "production_statuses"]) || []).concat( - form.getFieldValue(["md_ro_statuses", "additional_board_statuses"]) || [] - ) || [] - ); - - const handleBlur = () => { - setOptions(form.getFieldValue(["md_ro_statuses", "statuses"])); - setProductionStatus( - form - .getFieldValue(["md_ro_statuses", "production_statuses"]) - .concat(form.getFieldValue(["md_ro_statuses", "additional_board_statuses"])) - ); - }; - return ( - - ({ value: item, label: item }))} /> - - - ({ value: item, label: item }))} /> - - - ({ value: item, label: item }))} /> - - - ({ value: item, label: item }))} /> + ({ value: item, label: item }))} /> + ({ value: item, label: item }))} /> + ({ value: item, label: item }))} /> + ({ value: item, label: item }))} /> + ({ value: item, label: item }))} /> + ({ value: item, label: item }))} /> + ({ value: item, label: item }))} /> + ({ value: item, label: item }))} /> - - { - remove(field.name); - }} - /> - - + ) : ( + + {fields.map((field, index) => { + const productionColor = productionColors[field.name] || {}; + const productionColorSurfaceStyles = getTintedCardSurfaceStyles(productionColor.color); + const selectedProductionColorStatuses = productionColors + .map((item) => item?.status) + .filter(Boolean); + const productionColorStatusOptions = [ + ...new Set([productionColor.status, ...availableProductionStatuses]) + ] + .filter(Boolean) + .filter( + (status) => + status === productionColor.status || !selectedProductionColorStatuses.includes(status) + ); + + return ( + + - - - - - - - - - + + + + + + } + extra={ + + - - + ]} + > +
+ {fields.length === 0 ? ( + + ) : ( + fields.map((field, index) => { + const schedulingBucket = + schedulingBuckets[field.name] || form.getFieldValue(["ssbuckets", field.name]) || {}; + const schedulingBucketSurfaceStyles = getTintedCardSurfaceStyles(schedulingBucket.color); + + return ( + + +
+
{t("bodyshop.fields.ssbuckets.id")}
+ + + +
+
+
+ {t("bodyshop.fields.ssbuckets.label")} +
+ + + +
+
+ } + extra={ + + - - } - key={`${index}color`} - name={[field.name, "color"]} - > - - - - { - remove(field.name); - }} - /> - - - -
- - ))} - - - - - ); - }} - -
)} ); diff --git a/client/src/components/shop-info/shop-info.scheduling.styles.scss b/client/src/components/shop-info/shop-info.scheduling.styles.scss new file mode 100644 index 000000000..b04a92e15 --- /dev/null +++ b/client/src/components/shop-info/shop-info.scheduling.styles.scss @@ -0,0 +1,58 @@ +.shop-info-scheduling__bucket-card-body { + display: flex; + gap: 12px; + align-items: stretch; +} + +.shop-info-scheduling__bucket-card-fields { + flex: 1 1 0; + min-width: 0; + display: grid; + grid-template-columns: repeat(3, minmax(92px, 1fr)); + gap: 0 12px; +} + +.shop-info-scheduling__bucket-card-fields .ant-form-item { + margin-bottom: 10px; +} + +.shop-info-scheduling__bucket-card-color { + flex: 0 0 360px; + min-width: 360px; + max-width: 360px; + display: flex; + align-items: stretch; +} + +.shop-info-scheduling__bucket-card-color .ant-form-item { + margin-bottom: 0; + width: 100%; +} + +.shop-info-scheduling__bucket-card-color .ant-form-item-control, +.shop-info-scheduling__bucket-card-color .ant-form-item-control-input, +.shop-info-scheduling__bucket-card-color .ant-form-item-control-input-content { + height: 100%; +} + +@media (max-width: 1199px) { + .shop-info-scheduling__bucket-card-body { + flex-direction: column; + } + + .shop-info-scheduling__bucket-card-fields { + grid-template-columns: repeat(2, minmax(120px, 1fr)); + } + + .shop-info-scheduling__bucket-card-color { + flex-basis: auto; + min-width: 0; + max-width: none; + } +} + +@media (max-width: 575px) { + .shop-info-scheduling__bucket-card-fields { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/client/src/components/shop-info/shop-info.section-navigator.component.jsx b/client/src/components/shop-info/shop-info.section-navigator.component.jsx new file mode 100644 index 000000000..3d120c0e5 --- /dev/null +++ b/client/src/components/shop-info/shop-info.section-navigator.component.jsx @@ -0,0 +1,213 @@ +import { Select } from "antd"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import "./shop-info.section-navigator.styles.scss"; + +const HIGHLIGHT_CLASS = "shop-info-section-navigator__target--active"; + +export default function ShopInfoSectionNavigator({ tabsRef, activeTabKey }) { + const { t } = useTranslation(); + const targetMapRef = useRef(new Map()); + const highlightedTargetRef = useRef(null); + const [options, setOptions] = useState([]); + const [selectedSection, setSelectedSection] = useState(undefined); + + useEffect(() => { + const tabsContainer = tabsRef.current; + if (!tabsContainer) return undefined; + + let animationFrameId = 0; + + const refreshOptions = () => { + const activePane = tabsContainer.querySelector(".ant-tabs-tabpane-active"); + if (!activePane) { + targetMapRef.current = new Map(); + setOptions([]); + return; + } + + const nextTargetMap = new Map(); + const nextOptions = Array.from(activePane.querySelectorAll(".imex-form-row")) + .filter((card) => { + return shouldIncludeCardInNavigator(card, activePane); + }) + .map((card, index) => { + const { title, depth, searchLabel } = getCardNavigatorInfo(card, activePane); + const value = `${activeTabKey}-shop-info-section-${index}`; + + nextTargetMap.set(value, card); + + return { + label: renderNavigatorOptionLabel(title, depth), + labelText: title, + searchLabel, + depth, + value + }; + }); + + targetMapRef.current = nextTargetMap; + setOptions((currentOptions) => (areOptionsEqual(currentOptions, nextOptions) ? currentOptions : nextOptions)); + }; + + const scheduleRefresh = () => { + cancelAnimationFrame(animationFrameId); + animationFrameId = requestAnimationFrame(refreshOptions); + }; + + scheduleRefresh(); + + const observer = new MutationObserver(scheduleRefresh); + observer.observe(tabsContainer, { + childList: true, + subtree: true, + characterData: true, + attributes: true, + attributeFilter: ["class"] + }); + + return () => { + cancelAnimationFrame(animationFrameId); + observer.disconnect(); + }; + }, [activeTabKey, tabsRef]); + + useEffect(() => { + clearHighlightedTarget(highlightedTargetRef); + setSelectedSection(undefined); + }, [activeTabKey]); + + const handleSectionChange = (value) => { + setSelectedSection(value); + + clearHighlightedTarget(highlightedTargetRef); + if (!value) return; + + const target = targetMapRef.current.get(value); + if (target) { + target.classList.add(HIGHLIGHT_CLASS); + highlightedTargetRef.current = target; + target.scrollIntoView({ + behavior: "smooth", + block: "start" + }); + } + + window.setTimeout(() => { + setSelectedSection(undefined); + }, 0); + }; + + return ( +
+ - - - - - - - + +
+
+
+
{t("bodyshop.fields.speedprint.label")}
+ + + +
+
+ } + wrapTitle + extra={ + +