diff --git a/furniture-3d-system/package-lock.json b/furniture-3d-system/package-lock.json index dc73cda9..1ffa3b79 100644 --- a/furniture-3d-system/package-lock.json +++ b/furniture-3d-system/package-lock.json @@ -33,6 +33,7 @@ "jsonwebtoken": "^9.0.3", "jszip": "^3.10.1", "lucide-react": "^0.563.0", + "mssql": "^12.5.0", "nanoid": "^5.1.6", "next": "^16.1.6", "obj2gltf": "^3.2.0", @@ -1120,6 +1121,265 @@ "node": ">=18.0.0" } }, + "node_modules/@azure-rest/core-client": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/@azure-rest/core-client/-/core-client-2.6.0.tgz", + "integrity": "sha512-iuFKDm8XPzNxPfRjhyU5/xKZmcRDzSuEghXDHHk4MjBV/wFL34GmYVBZnn9wmuoLBeS1qAw9ceMdaeJBPcB1QQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmmirror.com/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-http-compat": { + "version": "2.4.0", + "resolved": "https://registry.npmmirror.com/@azure/core-http-compat/-/core-http-compat-2.4.0.tgz", + "integrity": "sha512-f1P96IB399YiN2ARYHP7EpZi3Bf3wH4SN2lGzrw7JVwm7bbsVYtf2iKSBwTywD2P62NOPZGHFSZi+6jjb75JuA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@azure/core-client": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.2", + "resolved": "https://registry.npmmirror.com/@azure/core-lro/-/core-lro-2.7.2.tgz", + "integrity": "sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/@azure/core-paging/-/core-paging-1.6.2.tgz", + "integrity": "sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmmirror.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmmirror.com/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.1", + "resolved": "https://registry.npmmirror.com/@azure/identity/-/identity-4.13.1.tgz", + "integrity": "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^5.5.0", + "@azure/msal-node": "^5.1.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-common": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@azure/keyvault-common/-/keyvault-common-2.1.0.tgz", + "integrity": "sha512-aCDidWuKY06LWQ4x7/8TIXK6iRqTaRWRL3t7T+LC+j1b07HtoIsOxP/tU90G4jCSBn5TAyUTCtA4MS/y5Hudaw==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-rest-pipeline": "^1.8.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.10.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/keyvault-keys": { + "version": "4.10.0", + "resolved": "https://registry.npmmirror.com/@azure/keyvault-keys/-/keyvault-keys-4.10.0.tgz", + "integrity": "sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==", + "license": "MIT", + "dependencies": { + "@azure-rest/core-client": "^2.3.3", + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.9.0", + "@azure/core-http-compat": "^2.2.0", + "@azure/core-lro": "^2.7.2", + "@azure/core-paging": "^1.6.2", + "@azure/core-rest-pipeline": "^1.19.0", + "@azure/core-tracing": "^1.2.0", + "@azure/core-util": "^1.11.0", + "@azure/keyvault-common": "^2.0.0", + "@azure/logger": "^1.1.4", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "5.9.0", + "resolved": "https://registry.npmmirror.com/@azure/msal-browser/-/msal-browser-5.9.0.tgz", + "integrity": "sha512-CzE+4PefDSJWj26zU7G1bKchlGRRHMBFreG4tAlGuzyI8hAPiYGobaJvZBgZBf6L63iphX7VH+ityL8VgEQz9Q==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.5.2" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "16.5.2", + "resolved": "https://registry.npmmirror.com/@azure/msal-common/-/msal-common-16.5.2.tgz", + "integrity": "sha512-GkDEL6TYo3HgT3UuqakdgE9PZfc1hMki6+Hwgy1uddb/EauvAKfu85vVhuofRSo22D1xTnWt8Ucwfg4vSCVwvA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "5.1.5", + "resolved": "https://registry.npmmirror.com/@azure/msal-node/-/msal-node-5.1.5.tgz", + "integrity": "sha512-ObTeMoNPmq19X3z40et9Xvs4ZoWVeJg43PZMRLG5iwVL+2nCtAerG3YTDItqPp1CfXNwmCXBbg8jn1DOx65c3g==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "16.5.2", + "jsonwebtoken": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -3811,6 +4071,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-joda/core": { + "version": "5.7.0", + "resolved": "https://registry.npmmirror.com/@js-joda/core/-/core-5.7.0.tgz", + "integrity": "sha512-WBu4ULVVxySLLzK1Ppq+OdfP+adRS4ntmDQT915rzDJ++i95gc2jZkM5B6LWEAwN3lGXpfie3yPABozdD3K3Vg==", + "license": "BSD-3-Clause" + }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmmirror.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", @@ -5595,6 +5861,12 @@ "react": "^18 || ^19" } }, + "node_modules/@tediousjs/connection-string": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/@tediousjs/connection-string/-/connection-string-1.1.0.tgz", + "integrity": "sha512-z9ZBWEG+8pIB5V1zYzlRPXx0oRJ5H7coPnMQK8EZOw03UTPI9Umn6viL36f5w+CuqkKsnCM50RVStpjZmR0Bng==", + "license": "MIT" + }, "node_modules/@testing-library/dom": { "version": "10.4.1", "resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz", @@ -5996,6 +6268,15 @@ "@types/react": "*" } }, + "node_modules/@types/readable-stream": { + "version": "4.0.23", + "resolved": "https://registry.npmmirror.com/@types/readable-stream/-/readable-stream-4.0.23.tgz", + "integrity": "sha512-wwXrtQvbMHxCbBgjHaMGEmImFTQxxpfMOR/ZoQnXxB1woqkUbdLGFDgauo00Py9IudiaqSeiBiulSV9i6XIPig==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ssh2": { "version": "1.15.5", "resolved": "https://registry.npmmirror.com/@types/ssh2/-/ssh2-1.15.5.tgz", @@ -6391,6 +6672,20 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.5.tgz", + "integrity": "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw==", + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", @@ -6702,6 +6997,18 @@ "node": ">=18.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", @@ -7736,6 +8043,21 @@ "node": ">=10.0.0" } }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -8561,6 +8883,34 @@ "node": ">=16.0.0" } }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", @@ -8579,6 +8929,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/define-properties": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", @@ -9711,6 +10073,24 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/events-universal": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/events-universal/-/events-universal-1.0.1.tgz", @@ -11147,6 +11527,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", @@ -11225,6 +11620,24 @@ "node": ">=0.10.0" } }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", @@ -11456,6 +11869,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", @@ -12561,6 +12989,12 @@ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", "license": "BSD-3-Clause" }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -13702,6 +14136,34 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mssql": { + "version": "12.5.0", + "resolved": "https://registry.npmmirror.com/mssql/-/mssql-12.5.0.tgz", + "integrity": "sha512-nTbhxS1qi5SPwuKygwfRzmp2p6e/2v37ZFzvwvMf27wRSI+09J7J2pP7zaAUzqT4znMyHYBrcUyxkjSeeNyDTg==", + "license": "MIT", + "dependencies": { + "@tediousjs/connection-string": "^1.0.0", + "commander": "^11.0.0", + "debug": "^4.3.3", + "tarn": "^3.0.2", + "tedious": "^19.0.0" + }, + "bin": { + "mssql": "bin/mssql" + }, + "engines": { + "node": ">=18.19.0" + } + }, + "node_modules/mssql/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/mysql2": { "version": "3.15.3", "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", @@ -13794,6 +14256,12 @@ "url": "https://opencollective.com/napi-postinstall" } }, + "node_modules/native-duplexpair": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/native-duplexpair/-/native-duplexpair-1.0.0.tgz", + "integrity": "sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==", + "license": "MIT" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", @@ -14177,6 +14645,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmmirror.com/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", @@ -14879,6 +15365,15 @@ } } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmmirror.com/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -15504,6 +15999,18 @@ "dev": true, "license": "MIT" }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -16580,6 +17087,86 @@ "node": ">=6" } }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/tedious": { + "version": "19.2.1", + "resolved": "https://registry.npmmirror.com/tedious/-/tedious-19.2.1.tgz", + "integrity": "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA==", + "license": "MIT", + "dependencies": { + "@azure/core-auth": "^1.7.2", + "@azure/identity": "^4.2.1", + "@azure/keyvault-keys": "^4.4.0", + "@js-joda/core": "^5.6.5", + "@types/node": ">=18", + "bl": "^6.1.4", + "iconv-lite": "^0.7.0", + "js-md4": "^0.3.2", + "native-duplexpair": "^1.0.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">=18.17" + } + }, + "node_modules/tedious/node_modules/bl": { + "version": "6.1.6", + "resolved": "https://registry.npmmirror.com/bl/-/bl-6.1.6.tgz", + "integrity": "sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==", + "license": "MIT", + "dependencies": { + "@types/readable-stream": "^4.0.0", + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^4.2.0" + } + }, + "node_modules/tedious/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/tedious/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/tedious/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/teex/-/teex-1.0.1.tgz", @@ -17704,6 +18291,21 @@ } } }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/xlsx": { "version": "0.18.5", "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", diff --git a/furniture-3d-system/package.json b/furniture-3d-system/package.json index 48cf73cb..d74e1d6a 100644 --- a/furniture-3d-system/package.json +++ b/furniture-3d-system/package.json @@ -53,6 +53,7 @@ "jsonwebtoken": "^9.0.3", "jszip": "^3.10.1", "lucide-react": "^0.563.0", + "mssql": "^12.5.0", "nanoid": "^5.1.6", "next": "^16.1.6", "obj2gltf": "^3.2.0", diff --git a/furniture-3d-system/prisma/migrations/20260429120000_add_business_roles_and_employee_lookup_cache/migration.sql b/furniture-3d-system/prisma/migrations/20260429120000_add_business_roles_and_employee_lookup_cache/migration.sql new file mode 100644 index 00000000..79f6c70d --- /dev/null +++ b/furniture-3d-system/prisma/migrations/20260429120000_add_business_roles_and_employee_lookup_cache/migration.sql @@ -0,0 +1,22 @@ +CREATE TYPE "BusinessAccountType" AS ENUM ('PLANNING', 'PURCHASING', 'WAREHOUSE', 'SUPPLIER'); + +ALTER TABLE "User" +ADD COLUMN "businessRoles" "BusinessAccountType"[] NOT NULL DEFAULT ARRAY[]::"BusinessAccountType"[]; + +CREATE TABLE "EmployeeLookupCache" ( + "id" TEXT NOT NULL, + "cacheKey" TEXT NOT NULL, + "keyType" TEXT NOT NULL, + "lookupValue" TEXT NOT NULL, + "result" JSONB, + "sourceStatus" "EmployeeSourceStatus", + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "EmployeeLookupCache_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "EmployeeLookupCache_cacheKey_key" ON "EmployeeLookupCache"("cacheKey"); +CREATE INDEX "EmployeeLookupCache_keyType_idx" ON "EmployeeLookupCache"("keyType"); +CREATE INDEX "EmployeeLookupCache_expiresAt_idx" ON "EmployeeLookupCache"("expiresAt"); diff --git a/furniture-3d-system/prisma/schema.prisma b/furniture-3d-system/prisma/schema.prisma index 05563096..66f8e42b 100644 --- a/furniture-3d-system/prisma/schema.prisma +++ b/furniture-3d-system/prisma/schema.prisma @@ -938,6 +938,13 @@ enum EmployeeSourceStatus { DIMISSIONED } +enum BusinessAccountType { + PLANNING + PURCHASING + WAREHOUSE + SUPPLIER +} + enum AssetType { BOARD COMPONENT @@ -998,6 +1005,7 @@ model User { // 角色与权限 role Role @default(USER) + businessRoles BusinessAccountType[] @default([]) // 组织架构 departmentId String? @@ -1053,6 +1061,23 @@ model User { @@index([employeeDirectoryId]) } +model EmployeeLookupCache { + id String @id @default(cuid()) + + cacheKey String @unique + keyType String + lookupValue String + result Json? + sourceStatus EmployeeSourceStatus? + expiresAt DateTime + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([keyType]) + @@index([expiresAt]) +} + // 部门 model Department { id String @id @default(cuid()) diff --git a/furniture-3d-system/src/app/api/auth/register/route.test.ts b/furniture-3d-system/src/app/api/auth/register/route.test.ts new file mode 100644 index 00000000..8b307d46 --- /dev/null +++ b/furniture-3d-system/src/app/api/auth/register/route.test.ts @@ -0,0 +1,118 @@ +export {} + +import { beforeEach, describe, expect, it, jest } from '@jest/globals' +import { AppError, ErrorCode } from '@/server/core/errors' + +const mockHandleRequest: any = jest.fn() +const mockJsonSuccess: any = jest.fn() +const mockCreateUser: any = jest.fn() +const mockCreateLog: any = jest.fn() +const mockMatchByJobNumber: any = jest.fn() +const mockResolveDepartmentIdByExternalId: any = jest.fn() + +jest.mock('@/server/core/response', () => ({ + handleRequest: (...args: unknown[]) => mockHandleRequest(...args), + jsonSuccess: (...args: unknown[]) => mockJsonSuccess(...args), +})) + +jest.mock('@/server/services/user.service', () => ({ + userService: { + create: (...args: unknown[]) => mockCreateUser(...args), + }, +})) + +jest.mock('@/server/services/log.service', () => ({ + logService: { + create: (...args: unknown[]) => mockCreateLog(...args), + }, +})) + +jest.mock('@/server/services/external-employee-directory.service', () => ({ + externalEmployeeDirectoryService: { + matchByJobNumber: (...args: unknown[]) => mockMatchByJobNumber(...args), + resolveDepartmentIdByExternalId: (...args: unknown[]) => mockResolveDepartmentIdByExternalId(...args), + }, +})) + +const { POST } = require('./route') + +function createRequest(body: unknown) { + return { + json: jest.fn(async () => body), + } as any +} + +describe('POST /api/auth/register', () => { + beforeEach(() => { + jest.clearAllMocks() + mockHandleRequest.mockImplementation(async (_request: unknown, handler: any) => + handler({ requestId: 'req-register' }), + ) + mockJsonSuccess.mockImplementation((data, status, requestId) => ({ + success: true, + data, + status, + requestId, + })) + }) + + it('工号匹配到在职员工后创建待激活账号', async () => { + mockMatchByJobNumber.mockResolvedValue({ + id: 'emp-1', + jobNumber: '1001', + name: '张三', + mobile: '13800000000', + sourceStatus: 'EMPLOYED', + dingtalkUserId: 'dt-1', + dingtalkUnionId: 'union-1', + primaryDepartmentExternalId: 'dept-ext-1', + }) + mockResolveDepartmentIdByExternalId.mockResolvedValue('dept-1') + mockCreateUser.mockResolvedValue({ + id: 'user-1', + email: 'user@example.com', + name: 'zhangsan', + role: 'USER', + employeeDirectoryId: 'emp-1', + }) + + const response = await POST(createRequest({ + email: 'user@example.com', + name: 'zhangsan', + jobNumber: '1001', + password: '123456', + })) + + expect(mockMatchByJobNumber).toHaveBeenCalledWith('1001') + expect(mockCreateUser).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'user@example.com', + name: 'zhangsan', + role: 'USER', + status: 'PENDING', + departmentId: 'dept-1', + employeeDirectoryId: 'emp-1', + dingtalkUserId: 'dt-1', + dingtalkUnionId: 'union-1', + }), + ) + expect(response).toEqual(expect.objectContaining({ success: true, status: 201 })) + }) + + it('工号未匹配时拒绝注册,不创建账号', async () => { + mockMatchByJobNumber.mockResolvedValue(null) + + await expect(POST(createRequest({ + email: 'user@example.com', + name: 'zhangsan', + jobNumber: 'not-found', + password: '123456', + }))).rejects.toEqual( + expect.objectContaining({ + code: ErrorCode.VALIDATION_FAILED, + message: '未找到在职员工信息,请核对工号或联系管理员', + }) as AppError, + ) + expect(mockCreateUser).not.toHaveBeenCalled() + }) +}) diff --git a/furniture-3d-system/src/app/api/auth/register/route.ts b/furniture-3d-system/src/app/api/auth/register/route.ts index ce9e92b2..de2b48dc 100644 --- a/furniture-3d-system/src/app/api/auth/register/route.ts +++ b/furniture-3d-system/src/app/api/auth/register/route.ts @@ -7,8 +7,10 @@ import { NextRequest } from 'next/server' import { handleRequest, jsonSuccess } from '@/server/core/response' +import { AppError, ErrorCode } from '@/server/core/errors' import { userService } from '@/server/services/user.service' import { logService } from '@/server/services/log.service' +import { externalEmployeeDirectoryService } from '@/server/services/external-employee-directory.service' /** * 注册请求体 @@ -17,6 +19,7 @@ interface RegisterRequest { email: string name: string password: string + jobNumber: string // role 参数被忽略,注册只允许创建 USER 角色 departmentId?: string teamId?: string @@ -30,7 +33,19 @@ export async function POST(request: NextRequest) { request, async (ctx) => { const body: RegisterRequest = await request.json() - const { email, name, password, departmentId, teamId } = body + const { email, name, password, jobNumber } = body + + const employee = await externalEmployeeDirectoryService.matchByJobNumber(jobNumber) + if (!employee || employee.sourceStatus !== 'EMPLOYED') { + throw new AppError( + ErrorCode.VALIDATION_FAILED, + '未找到在职员工信息,请核对工号或联系管理员' + ) + } + + const departmentId = await externalEmployeeDirectoryService.resolveDepartmentIdByExternalId( + employee.primaryDepartmentExternalId + ) // 调用服务层创建用户 // 强制使用 USER 角色,忽略用户传入的 role 参数,防止权限提升攻击 @@ -40,9 +55,13 @@ export async function POST(request: NextRequest) { password, role: REGISTER_DEFAULT_ROLE, status: 'PENDING', - departmentId, - teamId, + departmentId: departmentId ?? undefined, isFirstLogin: false, + phone: employee.mobile ?? undefined, + employeeDirectoryId: employee.id, + dingtalkUserId: employee.dingtalkUserId, + dingtalkUnionId: employee.dingtalkUnionId, + dingtalkBoundAt: employee.dingtalkUserId || employee.dingtalkUnionId ? new Date() : null, }) // 记录操作日志 @@ -54,6 +73,7 @@ export async function POST(request: NextRequest) { email: user.email, name: user.name, role: user.role, + employeeDirectoryId: user.employeeDirectoryId, }, }) diff --git a/furniture-3d-system/src/app/api/users/[id]/activate/route.ts b/furniture-3d-system/src/app/api/users/[id]/activate/route.ts index 35e4dc2b..d46aae70 100644 --- a/furniture-3d-system/src/app/api/users/[id]/activate/route.ts +++ b/furniture-3d-system/src/app/api/users/[id]/activate/route.ts @@ -3,6 +3,7 @@ import type { Role } from '@prisma/client' import { AppError } from '@/server/core/errors' import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response' import { userManagementService } from '@/server/services/user-management.service' +import { normalizeBusinessRoles } from '@/lib/business-account-types' export async function POST( request: NextRequest, @@ -12,13 +13,16 @@ export async function POST( const { id } = await params const body = await request.json().catch(() => ({} as Record)) const role = typeof body.role === 'string' ? (body.role as Role) : undefined + const businessRoles = Array.isArray(body.businessRoles) + ? normalizeBusinessRoles(body.businessRoles) + : undefined const departmentId = typeof body.departmentId === 'string' ? body.departmentId : body.departmentId === null ? null : undefined const teamId = typeof body.teamId === 'string' ? body.teamId : body.teamId === null ? null : undefined try { const user = await userManagementService.activateUser( id, - { role, departmentId, teamId }, + { role, businessRoles, departmentId, teamId }, ctx.user ) return jsonSuccess({ message: '用户激活成功', user }, 200, ctx.requestId) diff --git a/furniture-3d-system/src/app/api/users/[id]/employee-match/route.test.ts b/furniture-3d-system/src/app/api/users/[id]/employee-match/route.test.ts new file mode 100644 index 00000000..1b12116f --- /dev/null +++ b/furniture-3d-system/src/app/api/users/[id]/employee-match/route.test.ts @@ -0,0 +1,94 @@ +export {} + +import { beforeEach, describe, expect, it, jest } from '@jest/globals' +import { ErrorCode } from '@/server/core/errors' + +const mockHandleRequest: any = jest.fn() +const mockJsonSuccess: any = jest.fn() +const mockJsonError: any = jest.fn() +const mockRefreshEmployeeMatch: any = jest.fn() + +jest.mock('@/server/core/response', () => ({ + handleRequest: (...args: unknown[]) => mockHandleRequest(...args), + jsonSuccess: (...args: unknown[]) => mockJsonSuccess(...args), + jsonError: (...args: unknown[]) => mockJsonError(...args), +})) + +jest.mock('@/server/services/user-management.service', () => ({ + userManagementService: { + refreshEmployeeMatch: (...args: unknown[]) => mockRefreshEmployeeMatch(...args), + }, +})) + +const { POST } = require('./route') + +function createRequest(body?: unknown) { + return { + json: jest.fn(async () => body), + } as any +} + +describe('POST /api/users/[id]/employee-match', () => { + beforeEach(() => { + jest.clearAllMocks() + mockHandleRequest.mockImplementation(async (_request: unknown, handler: any) => + handler({ + requestId: 'req-test', + user: { + id: 'admin-1', + email: 'admin@example.com', + name: '管理员', + role: 'ADMIN', + status: 'ACTIVE', + }, + }), + ) + mockJsonSuccess.mockImplementation((data, status, requestId) => ({ + success: true, + data, + status, + requestId, + })) + mockJsonError.mockImplementation((code, message, status, requestId) => ({ + success: false, + code, + message, + status, + requestId, + })) + }) + + it('按工号触发管理员手动员工库匹配', async () => { + mockRefreshEmployeeMatch.mockResolvedValue({ + id: 'user-1', + employeeDirectoryId: 'emp-1', + }) + + const response = await POST(createRequest({ jobNumber: '1001' }), { + params: Promise.resolve({ id: 'user-1' }), + }) + + expect(mockRefreshEmployeeMatch).toHaveBeenCalledWith( + 'user-1', + { jobNumber: '1001' }, + expect.objectContaining({ id: 'admin-1' }), + ) + expect(response).toEqual(expect.objectContaining({ success: true, status: 200 })) + }) + + it('缺少工号时直接返回验证错误', async () => { + const response = await POST(createRequest({}), { + params: Promise.resolve({ id: 'user-1' }), + }) + + expect(mockRefreshEmployeeMatch).not.toHaveBeenCalled() + expect(response).toEqual( + expect.objectContaining({ + success: false, + code: ErrorCode.VALIDATION_REQUIRED_FIELD, + message: '请输入工号', + status: 400, + }), + ) + }) +}) diff --git a/furniture-3d-system/src/app/api/users/[id]/employee-match/route.ts b/furniture-3d-system/src/app/api/users/[id]/employee-match/route.ts new file mode 100644 index 00000000..aaa4c71f --- /dev/null +++ b/furniture-3d-system/src/app/api/users/[id]/employee-match/route.ts @@ -0,0 +1,34 @@ +import { NextRequest } from 'next/server' +import { AppError, ErrorCode } from '@/server/core/errors' +import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response' +import { userManagementService } from '@/server/services/user-management.service' + +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + return handleRequest(request, async (ctx) => { + const { id } = await params + const body = await request.json().catch(() => ({} as Record)) + const jobNumber = typeof body.jobNumber === 'string' ? body.jobNumber.trim() : '' + + if (!jobNumber) { + return jsonError( + ErrorCode.VALIDATION_REQUIRED_FIELD, + '请输入工号', + 400, + ctx.requestId + ) + } + + try { + const user = await userManagementService.refreshEmployeeMatch(id, { jobNumber }, ctx.user) + return jsonSuccess({ message: '员工库匹配已更新', user }, 200, ctx.requestId) + } catch (error) { + if (error instanceof AppError) { + return jsonError(error.code, error.message, error.statusCode, ctx.requestId) + } + throw error + } + }) +} diff --git a/furniture-3d-system/src/app/api/users/[id]/route.ts b/furniture-3d-system/src/app/api/users/[id]/route.ts index 70263abd..4589e4ab 100644 --- a/furniture-3d-system/src/app/api/users/[id]/route.ts +++ b/furniture-3d-system/src/app/api/users/[id]/route.ts @@ -8,9 +8,10 @@ import { NextRequest } from 'next/server' import { handleRequest, jsonSuccess, jsonError } from '@/server/core/response' -import { AppError, ErrorCode } from '@/server/core/errors' +import { AppError } from '@/server/core/errors' import { userManagementService } from '@/server/services/user-management.service' -import type { Role, UserStatus } from '@prisma/client' +import type { BusinessAccountType, Role, UserStatus } from '@prisma/client' +import { normalizeBusinessRoles } from '@/lib/business-account-types' export async function GET( request: NextRequest, @@ -42,16 +43,18 @@ export async function PUT( name?: string password?: string role?: Role + businessRoles?: BusinessAccountType[] departmentId?: string | null teamId?: string | null phone?: string | null status?: UserStatus } + const businessRoles = 'businessRoles' in body ? normalizeBusinessRoles(body.businessRoles) : undefined try { const updatedUser = await userManagementService.updateUser( id, - { email, name, password, role, departmentId, teamId, phone, status }, + { email, name, password, role, businessRoles, departmentId, teamId, phone, status }, ctx.user ) diff --git a/furniture-3d-system/src/app/api/users/organization-tree/route.ts b/furniture-3d-system/src/app/api/users/organization-tree/route.ts index b805e966..9bdcd83a 100644 --- a/furniture-3d-system/src/app/api/users/organization-tree/route.ts +++ b/furniture-3d-system/src/app/api/users/organization-tree/route.ts @@ -7,7 +7,7 @@ import { NextRequest } from 'next/server' import { handleRequest, jsonSuccess } from '@/server/core/response' import { userManagementService } from '@/server/services/user-management.service' -import type { Role, UserStatus } from '@prisma/client' +import type { BusinessAccountType, Role, UserStatus } from '@prisma/client' import type { AllowedRole } from '@/lib/role-scope' const ALLOWED_ROLES: AllowedRole[] = ['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR_DEPARTMENT', 'SUPERVISOR_TEAM'] @@ -19,10 +19,11 @@ export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const search = searchParams.get('search') || undefined const role = (searchParams.get('role') as Role | null) || undefined + const businessRole = (searchParams.get('businessRole') as BusinessAccountType | null) || undefined const status = (searchParams.get('status') as UserStatus | null) || undefined const tree = await userManagementService.getOrganizationTree( - { search, role, status }, + { search, role, businessRole, status }, ctx.user ) diff --git a/furniture-3d-system/src/app/api/users/route.ts b/furniture-3d-system/src/app/api/users/route.ts index b253f082..fe7a0615 100644 --- a/furniture-3d-system/src/app/api/users/route.ts +++ b/furniture-3d-system/src/app/api/users/route.ts @@ -9,8 +9,9 @@ import { NextRequest } from 'next/server' import { handleRequest, jsonSuccess, jsonSuccessWithPagination, jsonError } from '@/server/core/response' import { AppError, ErrorCode } from '@/server/core/errors' import { userManagementService } from '@/server/services/user-management.service' -import type { Role, UserStatus } from '@prisma/client' +import type { BusinessAccountType, Role, UserStatus } from '@prisma/client' import type { AllowedRole } from '@/lib/role-scope' +import { normalizeBusinessRoles } from '@/lib/business-account-types' const ALLOWED_ROLES: AllowedRole[] = ['SUPER_ADMIN', 'ADMIN', 'SUPERVISOR_DEPARTMENT', 'SUPERVISOR_TEAM'] @@ -24,10 +25,11 @@ export async function GET(request: NextRequest) { const search = searchParams.get('search') || undefined const role = searchParams.get('role') as Role | undefined const status = searchParams.get('status') as UserStatus | undefined + const businessRole = (searchParams.get('businessRole') as BusinessAccountType | null) || undefined const departmentId = searchParams.get('departmentId') || undefined const result = await userManagementService.listUsers( - { page, limit, search, role, status, departmentId }, + { page, limit, search, role, businessRole, status, departmentId }, ctx.user ) @@ -51,6 +53,8 @@ export async function POST(request: NextRequest) { return handleRequest(request, async (ctx) => { const body = await request.json() const { email, name, password, role, departmentId, teamId, phone } = body + const jobNumber = typeof body.jobNumber === 'string' ? body.jobNumber : undefined + const businessRoles = 'businessRoles' in body ? normalizeBusinessRoles(body.businessRoles) : undefined if (!email || !name || !password) { return jsonError( @@ -62,10 +66,13 @@ export async function POST(request: NextRequest) { } try { - const newUser = await userManagementService.createUser( - { email, name, password, role, departmentId, teamId, phone }, - ctx.user - ) + const createInput = { email, name, password, role, departmentId, teamId, phone } + const normalizedCreateInput = { + ...createInput, + ...(businessRoles ? { businessRoles } : {}), + ...(jobNumber ? { jobNumber } : {}), + } + const newUser = await userManagementService.createUser(normalizedCreateInput, ctx.user) return jsonSuccess({ message: '用户创建成功', user: newUser }, 201, ctx.requestId) } catch (error) { diff --git a/furniture-3d-system/src/app/globals.css b/furniture-3d-system/src/app/globals.css index 89868119..a25222e7 100644 --- a/furniture-3d-system/src/app/globals.css +++ b/furniture-3d-system/src/app/globals.css @@ -317,8 +317,8 @@ --color-red: #9f403d; --color-orange: #c47a2a; --color-yellow: #b8860b; - --color-green: #405054; - --color-mint: #647174; + --color-green: #286c34; + --color-mint: #4b9e7a; --color-teal: #4b626e; --color-cyan: #3d8fa3; --color-blue: #546067; @@ -362,9 +362,9 @@ --liquid-glass-blur-enhanced: blur(30px); /* 反馈色 */ - --feedback-success-bg: rgba(64, 80, 84, 0.08); - --feedback-success-border: rgba(64, 80, 84, 0.18); - --feedback-success-text: #405054; + --feedback-success-bg: rgba(40, 108, 52, 0.08); + --feedback-success-border: rgba(40, 108, 52, 0.2); + --feedback-success-text: #286c34; --feedback-error-bg: rgba(159, 64, 61, 0.08); --feedback-error-border: rgba(159, 64, 61, 0.2); --feedback-error-text: #9f403d; @@ -448,8 +448,8 @@ --color-red: #fe8983; --color-orange: #ffb366; --color-yellow: #ffd700; - --color-green: #405054; - --color-mint: #647174; + --color-green: #a6eea7; + --color-mint: #7ecbaa; --color-teal: #bfd8e5; --color-cyan: #7ec8d9; --color-blue: #c9d6de; @@ -493,9 +493,9 @@ --liquid-glass-blur-enhanced: blur(30px); /* 反馈色 */ - --feedback-success-bg: rgba(64, 80, 84, 0.08); - --feedback-success-border: rgba(64, 80, 84, 0.18); - --feedback-success-text: #405054; + --feedback-success-bg: rgba(166, 238, 167, 0.12); + --feedback-success-border: rgba(166, 238, 167, 0.25); + --feedback-success-text: #a6eea7; --feedback-error-bg: rgba(254, 137, 131, 0.12); --feedback-error-border: rgba(254, 137, 131, 0.25); --feedback-error-text: #fe8983; @@ -632,14 +632,14 @@ :root, [data-theme='light'] { - --obs-shell-bg: #f4f5f5; + --obs-shell-bg: #eef2f3; --obs-shell-text: #263033; --obs-shell-panel-bg: #ffffff; - --obs-shell-content-bg: #f7f7f6; - --obs-shell-header-bg: #fbfbfa; + --obs-shell-content-bg: #f4f7f7; + --obs-shell-header-bg: #f8faf9; --obs-shell-surface-bg: #ffffff; - --obs-shell-muted-bg: #f1f2f2; - --obs-shell-rail-bg: #f0f1f1; + --obs-shell-muted-bg: #edf2f2; + --obs-shell-rail-bg: #e5ebeb; --obs-shell-border: rgba(17, 23, 25, 0.12); --obs-shell-title: #101719; --obs-shell-muted: #5f6d70; @@ -653,66 +653,67 @@ --obs-shell-icon-text: #2d393c; --obs-shell-rail-title: #687477; --obs-shell-rail-text: #3f4a4d; - --obs-shell-rail-hover-bg: rgba(17, 23, 25, 0.055); + --obs-shell-rail-hover-bg: rgba(17, 23, 25, 0.06); --obs-shell-rail-hover-text: #101719; - --obs-shell-rail-active-bg: #ffffff; - --obs-shell-rail-active-text: #101719; + --obs-shell-rail-active-bg: #101719; + --obs-shell-rail-active-text: #f7f9f9; --obs-shell-status-text: #566265; --obs-shell-status-muted: #6d787b; --obs-shell-status-strong: #101719; --obs-shell-status-pill-bg: rgba(17, 23, 25, 0.055); - --obs-shell-action-bg: #ffffff; - --obs-shell-action-text: #101719; - --obs-shell-action-border: rgba(17, 23, 25, 0.34); - --obs-shell-action-hover-bg: #f2f3f3; - --obs-shell-rail-card-bg: rgba(17, 23, 25, 0.035); + --obs-shell-action-bg: #101719; + --obs-shell-action-text: #f7f9f9; + --obs-shell-action-border: rgba(17, 23, 25, 0.16); + --obs-shell-action-hover-bg: #263033; + --obs-shell-rail-card-bg: rgba(17, 23, 25, 0.045); --obs-shell-rail-card-text: #101719; --obs-shell-rail-card-muted: #687477; - --obs-shell-success: #405054; + --obs-shell-success: #1f7a56; --obs-shell-warning: #9a6a16; --obs-shell-danger: #b3453d; } [data-theme='dark'] { - --obs-shell-bg: #f4f5f5; - --obs-shell-text: #263033; - --obs-shell-panel-bg: #ffffff; - --obs-shell-content-bg: #f7f7f6; - --obs-shell-header-bg: #fbfbfa; - --obs-shell-surface-bg: #ffffff; - --obs-shell-muted-bg: #f1f2f2; - --obs-shell-rail-bg: #f0f1f1; - --obs-shell-border: rgba(17, 23, 25, 0.12); - --obs-shell-title: #101719; - --obs-shell-muted: #5f6d70; - --obs-shell-subtle: #7a878a; - --obs-shell-hover-bg: rgba(17, 23, 25, 0.055); - --obs-shell-control-bg: rgba(17, 23, 25, 0.045); - --obs-shell-control-hover-bg: rgba(17, 23, 25, 0.08); - --obs-shell-control-text: #4f5c5f; - --obs-shell-control-hover-text: #101719; - --obs-shell-icon-bg: rgba(17, 23, 25, 0.06); - --obs-shell-icon-text: #2d393c; - --obs-shell-rail-title: #687477; - --obs-shell-rail-text: #3f4a4d; - --obs-shell-rail-hover-bg: rgba(17, 23, 25, 0.055); - --obs-shell-rail-hover-text: #101719; + --obs-shell-bg: #111719; + --obs-shell-text: #e8edef; + --obs-shell-panel-bg: #111719; + --obs-shell-content-bg: #0f1416; + --obs-shell-header-bg: #101719; + --obs-shell-surface-bg: #151d1f; + --obs-shell-muted-bg: #202a2d; + --obs-shell-rail-bg: #111719; + --obs-shell-border: rgba(255, 255, 255, 0.10); + --obs-shell-title: #f4f7f6; + --obs-shell-muted: #aeb8bb; + --obs-shell-subtle: #8f9a9d; + --obs-shell-hover-bg: rgba(255, 255, 255, 0.055); + --obs-shell-control-bg: rgba(255, 255, 255, 0.04); + --obs-shell-control-hover-bg: rgba(255, 255, 255, 0.08); + --obs-shell-control-text: #aeb8bb; + --obs-shell-control-hover-text: #ffffff; + --obs-shell-icon-bg: rgba(255, 255, 255, 0.06); + --obs-shell-icon-text: #dfe6e8; + --obs-shell-rail-bg: #171d1f; + --obs-shell-rail-title: #748083; + --obs-shell-rail-text: #c4cccf; + --obs-shell-rail-hover-bg: rgba(255, 255, 255, 0.06); + --obs-shell-rail-hover-text: #ffffff; --obs-shell-rail-active-bg: #ffffff; - --obs-shell-rail-active-text: #101719; - --obs-shell-status-text: #566265; - --obs-shell-status-muted: #6d787b; - --obs-shell-status-strong: #101719; - --obs-shell-status-pill-bg: rgba(17, 23, 25, 0.055); + --obs-shell-rail-active-text: #111719; + --obs-shell-status-text: #aeb8bb; + --obs-shell-status-muted: #748083; + --obs-shell-status-strong: #ffffff; + --obs-shell-status-pill-bg: rgba(255, 255, 255, 0.04); --obs-shell-action-bg: #ffffff; - --obs-shell-action-text: #101719; - --obs-shell-action-border: rgba(17, 23, 25, 0.34); - --obs-shell-action-hover-bg: #f2f3f3; - --obs-shell-rail-card-bg: rgba(17, 23, 25, 0.035); - --obs-shell-rail-card-text: #101719; - --obs-shell-rail-card-muted: #687477; - --obs-shell-success: #405054; - --obs-shell-warning: #9a6a16; - --obs-shell-danger: #b3453d; + --obs-shell-action-text: #111719; + --obs-shell-action-border: rgba(255, 255, 255, 0.10); + --obs-shell-action-hover-bg: #e8edef; + --obs-shell-rail-card-bg: rgba(255, 255, 255, 0.04); + --obs-shell-rail-card-text: #ffffff; + --obs-shell-rail-card-muted: #748083; + --obs-shell-success: #9bdcc5; + --obs-shell-warning: #e5bd72; + --obs-shell-danger: #f3a29c; } .obsidian-control-grid { @@ -735,12 +736,6 @@ background: var(--obs-shell-action-hover-bg) !important; } - .obsidian-rail-active { - background: var(--obs-shell-rail-active-bg) !important; - box-shadow: inset 3px 0 0 var(--obs-shell-title); - color: var(--obs-shell-rail-active-text) !important; - } - .obsidian-control-rail-card { border-color: var(--obs-shell-border); background: var(--obs-shell-rail-card-bg); @@ -772,7 +767,8 @@ } .obsidian-control-shell > div > section > header, - .obsidian-control-shell > div > aside:first-of-type > div:first-child { + .obsidian-control-shell > div > aside > div:first-child, + .obsidian-control-shell [class*="bg-[#101719]"] { background: var(--obs-shell-header-bg) !important; border-color: var(--obs-shell-border) !important; } @@ -832,17 +828,22 @@ .obsidian-control-shell [class*="text-[#101517]"], .obsidian-control-shell [class*="text-[#101719]"] { - color: var(--obs-shell-title) !important; + color: var(--obs-shell-rail-active-text) !important; } .obsidian-control-shell [class*="text-white"] { - color: var(--obs-shell-action-text) !important; + color: var(--obs-shell-title) !important; } .obsidian-control-shell [class*="hover:text-white"]:hover { color: var(--obs-shell-rail-hover-text) !important; } + .obsidian-control-shell [class*="text-emerald-200"], + .obsidian-control-shell [class*="text-[#9bdcc5]"] { + color: var(--obs-shell-success) !important; + } + .obsidian-control-shell [class*="text-amber-200"], .obsidian-control-shell [class*="text-[#e5bd72]"] { color: var(--obs-shell-warning) !important; @@ -1350,84 +1351,9 @@ } [data-theme='dark'] .reduced-transparency { - --liquid-glass-standard: rgba(255, 255, 255, 0.95); - --liquid-glass-enhanced: rgba(255, 255, 255, 1); - --liquid-glass-subtle: rgba(255, 255, 255, 0.9); -} - -/* 白曜石已固定为浅色工作台;旧 dark 标记只按浅色 token 解释,避免旧偏好污染新平台。 */ -html[data-theme='dark'], -[data-theme='dark'] { - --color-red: #9f403d; - --color-orange: #c47a2a; - --color-yellow: #b8860b; - --color-green: #405054; - --color-mint: #5f6d70; - --color-teal: #4b626e; - --color-cyan: #3d8fa3; - --color-blue: #546067; - --color-background-primary: #f9f9fb; - --color-background-secondary: #f2f4f6; - --color-background-tertiary: #ffffff; - --color-label-primary: #2d3338; - --color-label-secondary: #596065; - --color-label-tertiary: rgba(45, 51, 56, 0.6); - --color-label-quaternary: rgba(45, 51, 56, 0.25); - --color-separator: rgba(172, 179, 184, 0.3); - --color-opaque-separator: #acb3b8; - --color-link: #4b626e; - --color-button-primary-start: #ffffff; - --color-button-primary-end: #ffffff; - --color-button-primary-text: #101719; - --feedback-success-bg: rgba(64, 80, 84, 0.08); - --feedback-success-border: rgba(64, 80, 84, 0.18); - --feedback-success-text: #405054; - --color-surface: #f9f9fb; - --color-surface-container: #ebeef2; - --color-surface-container-high: #e4e9ee; - --color-surface-container-highest: #dde3e9; - --color-surface-container-low: #f2f4f6; - --color-surface-container-lowest: #ffffff; - --color-outline: #757c81; - --color-outline-variant: #acb3b8; - --liquid-glass-standard: rgba(255, 255, 255, 0.70); - --liquid-glass-enhanced: rgba(255, 255, 255, 0.85); - --liquid-glass-subtle: rgba(255, 255, 255, 0.60); - --ml-bg: #f6f8f9; - --ml-rail: #fbfcfc; - --ml-canvas: #eef2f3; - --ml-workbench: #f7f9f9; - --ml-panel: #ffffff; - --ml-text: #263033; - --ml-muted: #708287; - --ml-accent: #48686b; - --ml-accent-bg: rgba(72, 104, 107, 0.12); - --ml-border: rgba(38, 48, 51, 0.10); - --ml-glass: rgba(255, 255, 255, 0.74); - --ml-row: rgba(255, 255, 255, 0.76); - --ml-row-hover: rgba(255, 255, 255, 0.94); - --obs-shell-bg: #f4f5f5; - --obs-shell-text: #263033; - --obs-shell-panel-bg: #ffffff; - --obs-shell-content-bg: #f7f7f6; - --obs-shell-header-bg: #fbfbfa; - --obs-shell-surface-bg: #ffffff; - --obs-shell-muted-bg: #f1f2f2; - --obs-shell-rail-bg: #f0f1f1; - --obs-shell-border: rgba(17, 23, 25, 0.12); - --obs-shell-title: #101719; - --obs-shell-muted: #5f6d70; - --obs-shell-subtle: #7a878a; - --obs-shell-hover-bg: rgba(17, 23, 25, 0.055); - --obs-shell-rail-active-bg: #ffffff; - --obs-shell-rail-active-text: #101719; - --obs-shell-action-bg: #ffffff; - --obs-shell-action-text: #101719; - --obs-shell-action-border: rgba(17, 23, 25, 0.34); - --obs-shell-action-hover-bg: #f2f3f3; - --obs-shell-success: #405054; - --obs-shell-warning: #9a6a16; - --obs-shell-danger: #b3453d; + --liquid-glass-standard: rgba(22, 22, 23, 0.95); + --liquid-glass-enhanced: rgba(22, 22, 23, 1); + --liquid-glass-subtle: rgba(22, 22, 23, 0.9); } /* 动态字体大小 */ diff --git a/furniture-3d-system/src/app/register/page.tsx b/furniture-3d-system/src/app/register/page.tsx index ad1b2c37..339f6867 100644 --- a/furniture-3d-system/src/app/register/page.tsx +++ b/furniture-3d-system/src/app/register/page.tsx @@ -27,6 +27,7 @@ export default function RegisterPage() { const abortControllerRef = useRef(null); const [name, setName] = useState(''); + const [jobNumber, setJobNumber] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); @@ -60,6 +61,7 @@ export default function RegisterPage() { body: JSON.stringify({ email, name, + jobNumber, password, }), signal: abortController.signal, @@ -134,6 +136,20 @@ export default function RegisterPage() {
+ setJobNumber(event.target.value)} + required + helperText="系统会用工号匹配员工库,匹配成功后仍需管理员激活" + variant="filled" + size="large" + className="bg-(--color-background-secondary) border border-(--color-separator) focus:border-(--color-blue) focus:bg-(--color-background-primary) transition-all" + /> + (null) const [editingUser, setEditingUser] = useState(null) const [activationUser, setActivationUser] = useState(null) @@ -128,6 +132,7 @@ function UsersPageContent() { }, []) const closeCreateModal = useCallback(() => setShowCreateModal(false), []) + const closeOrganizationModal = useCallback(() => setShowOrganizationModal(false), []) const closeEditModal = useCallback(() => setEditingUser(null), []) const closeActivateModal = useCallback(() => setActivationUser(null), []) const closeHandoverModal = useCallback(() => setHandoverUser(null), []) @@ -137,6 +142,10 @@ function UsersPageContent() { triggerRefresh() }, [closeCreateModal, triggerRefresh]) + const handleOrganizationSuccess = useCallback(() => { + triggerRefresh() + }, [triggerRefresh]) + const handleEditSuccess = useCallback(() => { closeEditModal() triggerRefresh() @@ -199,6 +208,31 @@ function UsersPageContent() { [canActivatePendingUser], ) + const handleEmployeeMatch = useCallback( + async (user: UserTreeItem) => { + const jobNumber = window.prompt(`请输入 ${user.name} 的工号`) + if (!jobNumber?.trim()) return + + try { + setActionError(null) + const response = await fetch(`/api/users/${user.id}/employee-match`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ jobNumber: jobNumber.trim() }), + }) + const payload = await response.json().catch(() => ({})) + if (!response.ok) { + throw new Error(payload.error?.message || '员工库匹配失败') + } + triggerRefresh() + } catch (error) { + setActionError(error instanceof Error ? error.message : '员工库匹配失败') + } + }, + [triggerRefresh], + ) + const selectedUserAllowedRoles = useMemo(() => { if (!editingUser || !currentUser) return [] if (editingUser.id === currentUser.id) { @@ -207,15 +241,6 @@ function UsersPageContent() { return assignableRoles }, [assignableRoles, currentUser, editingUser]) - const activeRoleLabel = useMemo( - () => ROLE_OPTIONS.find((option) => option.value === roleFilter)?.label ?? '所有权限角色', - [roleFilter], - ) - const activeStatusLabel = useMemo( - () => STATUS_OPTIONS.find((option) => option.value === statusFilter)?.label ?? '全状态', - [statusFilter], - ) - return ( setShowCreateModal(true)} - className="obsidian-control-action rounded-md border" - > - - 开设账号 - +
+ + +
} rail={
@@ -258,112 +294,100 @@ function UsersPageContent() { /> ))} + + setBusinessRoleFilter('')} + /> + {BUSINESS_ACCOUNT_TYPE_OPTIONS.map((option) => ( + setBusinessRoleFilter(option.value)} + /> + ))} +
} inspector={
{selectedUser ? ( <> -
-

身份元件

-
- -
-

{selectedUser.name}

-
- - {roleLabels[selectedUser.role] ?? selectedUser.role} - / - {statusLabels[selectedUser.status] ?? selectedUser.status} -
-
+
+
+
+

{selectedUser.name}

+

+ {roleLabels[selectedUser.role] ?? selectedUser.role} +

- -
- + - - + + - - - - - - - -
- -
- {canEditManagedUser(selectedUser) ? ( - handleEditUser(selectedUser)} /> - ) : null} - {selectedUser.status === 'PENDING' && canActivatePendingUser(selectedUser) ? ( - handleActivateUser(selectedUser)} /> - ) : null} - {selectedUser.id !== currentUser?.id && selectedUser.status === 'ACTIVE' ? ( - setHandoverUser(selectedUser)} /> - ) : null} -
+ 0 + ? selectedUser.businessRoles!.map((role) => BUSINESS_ACCOUNT_TYPE_LABELS[role as keyof typeof BUSINESS_ACCOUNT_TYPE_LABELS] ?? role).join(' / ') + : '未设置'} + /> + + + + + + + + + + + + + ) : (
-
- -
+

未选择账号

-

选择一个身份元件后查看权限、组织与审计状态

+

点击中间列表中的人员后查看权限和组织信息

)}
} > -
-
-
-
- - setSearchQuery(event.target.value)} - className="h-9 w-full rounded-[6px] border border-[#d6dee0] bg-white pl-9 pr-3 text-sm font-semibold text-[#101719] outline-none transition placeholder:text-[#8b9699] focus:border-[#101719]" - /> -
- - - +
+
+
+ + setSearchQuery(event.target.value)} + className="h-10 w-full rounded-[6px] border border-[#d6dee0] bg-white pl-10 pr-4 text-sm font-semibold text-[#152024] outline-none transition placeholder:text-[#7a878a] focus:border-[#152024] focus:bg-white" + />
-
-
-
-
- - 身份元件工作台 -
- 组织树 / 权限矩阵 -
-
- {actionError ? ( - - ) : null} +
+ {actionError ? ( + + ) : null} + +
-
-
+
{showCreateModal ? ( ) : null} + {showOrganizationModal ? ( + + ) : null} {activationUser ? ( +

{label}

@@ -422,106 +453,6 @@ function InspectorItem({ icon: Icon, label, value }: { icon: LucideIcon; label: ) } -function FilterToken({ label, value, active }: { label: string; value: string; active?: boolean }) { - return ( -
- {label} - {value} -
- ) -} - -function IdentityPlate({ name, email }: { name: string; email: string }) { - return ( -
- {getInitials(name, email)} -
- ) -} - -function StatusDot({ status }: { status: string }) { - const tone = getStatusTone(status) - return -} - -function InspectorBlock({ title, children }: { title: string; children: ReactNode }) { - return ( -
-

{title}

-
{children}
-
- ) -} - -function InspectorMetric({ - label, - value, - tone = 'default', -}: { - label: string - value: string - tone?: 'default' | 'success' | 'warning' -}) { - return ( -
- {label} - - {value} - -
- ) -} - -function InspectorAction({ - icon: Icon, - label, - onClick, -}: { - icon: LucideIcon - label: string - onClick: () => void -}) { - return ( - - ) -} - -function getInitials(name: string, email: string) { - const source = (name || email || '?').trim().replace(/\s+/g, '') - const chars = Array.from(source) - if (chars.length === 0) return '?' - if (/^[a-zA-Z]/.test(chars[0])) return chars.slice(0, 2).join('').toUpperCase() - return chars.slice(0, 2).join('') -} - -function getStatusTone(status: string) { - if (status === 'ACTIVE') return { dot: 'bg-[#405054]' } - if (status === 'PENDING') return { dot: 'bg-[#c18a22]' } - if (status === 'SUSPENDED') return { dot: 'bg-[#b3453d]' } - return { dot: 'bg-[#9aa7aa]' } -} - function formatDate(value: string) { const date = new Date(value) if (Number.isNaN(date.getTime())) return value diff --git a/furniture-3d-system/src/components/layout/SidebarUserCenter.tsx b/furniture-3d-system/src/components/layout/SidebarUserCenter.tsx index 0b12283e..1bda7583 100644 --- a/furniture-3d-system/src/components/layout/SidebarUserCenter.tsx +++ b/furniture-3d-system/src/components/layout/SidebarUserCenter.tsx @@ -110,29 +110,30 @@ export function SidebarUserCenter() { } return ( -
+
{isOpen ? (
{/* SECTION 1: Account Info */} -
+
{currentUser?.name ?? '加载中...'} {currentUser && ( - + {roleLabelMap[currentUser.role] ?? '用户'} )}
- + {currentUser?.email ?? '正在获取账号信息...'}
@@ -140,13 +141,13 @@ export function SidebarUserCenter() { {/* SECTION 2: Workspace Shortcuts */}
-

+

工作区

setIsOpen(false)} - className="group flex items-center gap-3 rounded-[6px] px-3.5 py-2 text-apple-xs font-apple-bold text-[#405054] transition-all hover:bg-[#f2f3f3] hover:text-[#101719]" + className="flex items-center gap-3 px-3.5 py-2 rounded-apple-lg text-apple-xs font-apple-bold text-apple-label-secondary hover:bg-white hover:text-apple-blue transition-all group" > 正式产品 @@ -154,7 +155,7 @@ export function SidebarUserCenter() { setIsOpen(false)} - className="group flex items-center gap-3 rounded-[6px] px-3.5 py-2 text-apple-xs font-apple-bold text-[#405054] transition-all hover:bg-[#f2f3f3] hover:text-[#101719]" + className="flex items-center gap-3 px-3.5 py-2 rounded-apple-lg text-apple-xs font-apple-bold text-apple-label-secondary hover:bg-white hover:text-apple-blue transition-all group" > 我的草稿 @@ -162,13 +163,13 @@ export function SidebarUserCenter() {
{/* SECTION 3: Admin & Actions */} -
+
{isAdmin && ( <> setIsOpen(false)} - className="group flex items-center gap-3 rounded-[6px] px-3.5 py-2 text-apple-xs font-apple-bold text-[#405054] transition-all hover:bg-[#fbfbfa] hover:text-[#101719]" + className="flex items-center gap-3 px-3.5 py-2 rounded-apple-lg text-apple-xs font-apple-bold text-apple-label-secondary hover:bg-white hover:text-apple-blue transition-all group" > 管理总控制台 @@ -179,7 +180,7 @@ export function SidebarUserCenter() { setIsOpen(false)} - className="group flex items-center gap-3 rounded-[6px] px-3.5 py-2 text-apple-xs font-apple-bold text-[#405054] transition-all hover:bg-[#fbfbfa] hover:text-[#101719]" + className="flex items-center gap-3 px-3.5 py-2 rounded-apple-lg text-apple-xs font-apple-bold text-apple-label-secondary hover:bg-white hover:text-apple-blue transition-all group" > 用户权限管理 @@ -189,7 +190,7 @@ export function SidebarUserCenter() { type="button" onClick={handleLogout} disabled={isLoggingOut} - className="group flex w-full items-center gap-3 rounded-[6px] px-3.5 py-2 text-apple-xs font-apple-bold text-[#8a3f3f] transition-all hover:bg-[#f4eded]" + className="w-full flex items-center gap-3 px-3.5 py-2 rounded-apple-lg text-apple-xs font-apple-bold text-apple-red hover:bg-apple-red/5 transition-all group" > {isLoggingOut ? '退出中...' : '退出登录'} @@ -204,15 +205,15 @@ export function SidebarUserCenter() { aria-expanded={isOpen} onClick={() => setIsOpen((current) => !current)} className={cn( - "flex w-full items-center gap-3 rounded-[8px] px-3 py-2.5 transition-all duration-300", + "flex w-full items-center gap-3 rounded-apple-lg px-3 py-2.5 transition-all duration-300", isOpen - ? "bg-white shadow-[inset_3px_0_0_#101719,0_8px_18px_rgba(16,24,28,0.08)] text-[#101719]" - : "text-[#405054] hover:bg-white hover:text-[#101719]" + ? "bg-white shadow-apple-sm text-apple-blue" + : "text-apple-label-secondary hover:bg-white/50" )} >
- {currentUser &&
} + {currentUser &&
}
用户中心 diff --git a/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx b/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx index eceaeeb7..a7492c90 100644 --- a/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx +++ b/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx @@ -37,47 +37,47 @@ export function ObsidianControlShell({ inspectorClassName, }: ObsidianControlShellProps) { return ( -
+
{rail ? ( -