diff --git a/electron.vite.config.ts b/electron.vite.config.ts index cbe0735..80304cd 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -1,6 +1,7 @@ import { resolve } from 'path' import { defineConfig } from 'electron-vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' export default defineConfig({ main: {}, @@ -11,6 +12,6 @@ export default defineConfig({ '@renderer': resolve('src/renderer/src') } }, - plugins: [react()] + plugins: [react(), tailwindcss()] } }) diff --git a/package-lock.json b/package-lock.json index 4414c78..3c36757 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,11 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", + "@tailwindcss/vite": "^4.2.1", "date-fns": "^4.1.0", "dotenv": "^17.3.1", "exceljs": "^4.4.0", + "lucide-react": "^0.575.0", "mssql": "^12.2.0", "mysql2": "^3.18.2", "playwright": "^1.58.2", @@ -31,6 +33,7 @@ "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.0.18", + "autoprefixer": "^10.4.27", "electron": "^39.2.6", "electron-builder": "^26.0.12", "electron-vite": "^5.0.0", @@ -38,9 +41,11 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", + "postcss": "^8.5.6", "prettier": "^3.7.4", "react": "^19.2.1", "react-dom": "^19.2.1", + "tailwindcss": "^4.2.1", "typescript": "^5.9.3", "vite": "^7.2.6", "vitest": "^4.0.18" @@ -2026,7 +2031,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -2037,7 +2041,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -2048,7 +2051,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -2058,14 +2060,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2260,7 +2260,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2274,7 +2273,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2288,7 +2286,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2302,7 +2299,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2316,7 +2312,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2330,7 +2325,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2344,7 +2338,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2358,7 +2351,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2372,7 +2364,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2386,7 +2377,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2400,7 +2390,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2414,7 +2403,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2428,7 +2416,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2442,7 +2429,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2456,7 +2442,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2470,7 +2455,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2484,7 +2468,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2498,7 +2481,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2512,7 +2494,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2526,7 +2507,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2540,7 +2520,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2554,7 +2533,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2568,7 +2546,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2582,7 +2559,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2596,7 +2572,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2634,6 +2609,263 @@ "node": ">=10" } }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", + "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "tailwindcss": "4.2.1" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@tediousjs/connection-string": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@tediousjs/connection-string/-/connection-string-0.6.0.tgz", @@ -2729,7 +2961,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/fs-extra": { @@ -3917,6 +4148,43 @@ "node": ">= 4.0.0" } }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -5010,7 +5278,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -5545,6 +5812,19 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -6304,7 +6584,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -6472,6 +6751,20 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -7786,7 +8079,6 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -8086,6 +8378,255 @@ "immediate": "~3.0.5" } }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", @@ -8301,11 +8842,19 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/lucide-react": { + "version": "0.575.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.575.0.tgz", + "integrity": "sha512-VuXgKZrk0uiDlWjGGXmKV6MSk9Yy4l10qgVvzGn2AWBx1Ylt0iBexKOAoA6I7JO3m+M9oeovJd3yYENfkUbOeg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -8731,7 +9280,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -9321,14 +9869,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -9397,7 +9943,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -9413,6 +9958,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9422,6 +9968,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/postject": { "version": "1.0.0-alpha.6", "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", @@ -9598,7 +10151,6 @@ "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "dev": true, "license": "MIT", "peer": true, "engines": { @@ -9881,7 +10433,6 @@ "version": "4.59.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -10351,7 +10902,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -10672,6 +11222,25 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tar": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", @@ -10917,7 +11486,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -11341,7 +11909,6 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -11420,7 +11987,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11437,7 +12003,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11454,7 +12019,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11471,7 +12035,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11488,7 +12051,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11505,7 +12067,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11522,7 +12083,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11539,7 +12099,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11556,7 +12115,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11573,7 +12131,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11590,7 +12147,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11607,7 +12163,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11624,7 +12179,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11641,7 +12195,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11658,7 +12211,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11675,7 +12227,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11692,7 +12243,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11709,7 +12259,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11726,7 +12275,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11743,7 +12291,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11760,7 +12307,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11777,7 +12323,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11794,7 +12339,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11811,7 +12355,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11828,7 +12371,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11845,7 +12387,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -11859,7 +12400,6 @@ "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -11901,7 +12441,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/package.json b/package.json index bad84df..5db356e 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,11 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", + "@tailwindcss/vite": "^4.2.1", "date-fns": "^4.1.0", "dotenv": "^17.3.1", "exceljs": "^4.4.0", + "lucide-react": "^0.575.0", "mssql": "^12.2.0", "mysql2": "^3.18.2", "playwright": "^1.58.2", @@ -49,6 +51,7 @@ "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^5.1.1", "@vitest/coverage-v8": "^4.0.18", + "autoprefixer": "^10.4.27", "electron": "^39.2.6", "electron-builder": "^26.0.12", "electron-vite": "^5.0.0", @@ -56,9 +59,11 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", + "postcss": "^8.5.6", "prettier": "^3.7.4", "react": "^19.2.1", "react-dom": "^19.2.1", + "tailwindcss": "^4.2.1", "typescript": "^5.9.3", "vite": "^7.2.6", "vitest": "^4.0.18" diff --git a/src/main/ipc/validation-handler.ts b/src/main/ipc/validation-handler.ts index bfd19f9..fdd46fc 100644 --- a/src/main/ipc/validation-handler.ts +++ b/src/main/ipc/validation-handler.ts @@ -9,6 +9,7 @@ import { ipcMain } from 'electron' import { MySqlService } from '../services/database/mysql' +import { SqlServerService } from '../services/database/sql-server' import { MaterialsToBeDeletedDAO } from '../services/database/materials-to-be-deleted-dao' import { DiscreteMaterialPlanDAO } from '../services/database/discrete-material-plan-dao' import type { @@ -49,19 +50,53 @@ export function clearSharedProductionIds(): void { } /** - * Get MySQL service for validation operations + * Get database service for validation operations (MySQL or SQL Server) */ -async function getValidationMySqlService(): Promise { - const mysqlService = new MySqlService({ - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' - }) +async function getValidationDatabaseService(): Promise { + const dbType = process.env.DB_TYPE?.toLowerCase() - await mysqlService.connect() - return mysqlService + if (dbType === 'sqlserver' || dbType === 'mssql') { + const sqlServerService = new SqlServerService({ + server: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + user: process.env.DB_USERNAME || 'sa', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + options: { + encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', + trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + } + }) + await sqlServerService.connect() + return sqlServerService + } else { + const mysqlService = new MySqlService({ + host: process.env.DB_MYSQL_HOST || 'localhost', + port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), + user: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '' + }) + await mysqlService.connect() + return mysqlService + } +} + +/** + * Get table name based on database type + */ +function getTableName(mysqlTableName: string): string { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + // Convert MySQL table name to SQL Server format + // e.g., dbo_MaterialsToBeDeleted -> [dbo].[MaterialsToBeDeleted] + if (mysqlTableName.startsWith('dbo_')) { + const tableName = mysqlTableName.substring(4) + return `[dbo].[${tableName}]` + } + return `[dbo].[${mysqlTableName}]` + } + return mysqlTableName } /** @@ -96,10 +131,12 @@ function identifyInputType(input: string): 'production_id' | 'order_number' | 'u */ async function getSourceNumbersFromInputs( inputs: string[], - mysqlService: MySqlService + dbService: MySqlService | SqlServerService ): Promise { const productionIds: string[] = [] const orderNumbers: string[] = [] + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' for (const item of inputs) { const type = identifyInputType(item) @@ -114,17 +151,38 @@ async function getSourceNumbersFromInputs( // Table name in MySQL: productionContractData_26年压力表合同数据 // Column name: 生产订单号 (SourceNumber) if (productionIds.length > 0) { - const placeholders = productionIds.map(() => '?').join(',') - const contractSql = ` - SELECT DISTINCT 生产订单号 - FROM productionContractData_26年压力表合同数据 - WHERE 总排号 IN (${placeholders}) - ` - const contractResult = await mysqlService.query(contractSql, productionIds) - const dbOrderNumbers = contractResult.rows.map( - row => row.生产订单号 as string - ) - orderNumbers.push(...dbOrderNumbers) + if (isSqlServer) { + const sql = require('mssql') + const placeholders = productionIds.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} + + productionIds.forEach((id, idx) => { + params[`p${idx}`] = { value: id, type: sql.NVarChar } + }) + + const contractSql = ` + SELECT DISTINCT 生产订单号 + FROM productionContractData_26年压力表合同数据 + WHERE 总排号 IN (${placeholders}) + ` + const contractResult = await (dbService as SqlServerService).queryWithParams(contractSql, params) + const dbOrderNumbers = contractResult.rows.map( + row => row.生产订单号 as string + ) + orderNumbers.push(...dbOrderNumbers) + } else { + const placeholders = productionIds.map(() => '?').join(',') + const contractSql = ` + SELECT DISTINCT 生产订单号 + FROM productionContractData_26年压力表合同数据 + WHERE 总排号 IN (${placeholders}) + ` + const contractResult = await (dbService as MySqlService).query(contractSql, productionIds) + const dbOrderNumbers = contractResult.rows.map( + row => row.生产订单号 as string + ) + orderNumbers.push(...dbOrderNumbers) + } } // Deduplicate @@ -146,13 +204,15 @@ export function registerValidationHandlers(): void { _event, request: ValidationRequest ): Promise => { - let mysqlService: MySqlService | null = null + let dbService: MySqlService | SqlServerService | null = null try { console.log('[Validation] Starting validation:', request) - // Connect to MySQL - mysqlService = await getValidationMySqlService() + // Connect to database + dbService = await getValidationDatabaseService() + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' let sourceNumbers: string[] | null = null @@ -175,7 +235,7 @@ export function registerValidationHandlers(): void { } } - sourceNumbers = await getSourceNumbersFromInputs(sharedIds, mysqlService) + sourceNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) console.log( `[Validation] Got ${sourceNumbers.length} source numbers from shared Production IDs` ) @@ -185,7 +245,7 @@ export function registerValidationHandlers(): void { console.log( `[Validation] Read ${inputs.length} inputs from file` ) - sourceNumbers = await getSourceNumbersFromInputs(inputs, mysqlService) + sourceNumbers = await getSourceNumbersFromInputs(inputs, dbService) console.log( `[Validation] Got ${sourceNumbers.length} source numbers` ) @@ -194,8 +254,6 @@ export function registerValidationHandlers(): void { // Get material records from DiscreteMaterialPlanData const materialDao = new DiscreteMaterialPlanDAO() - // Inject the mysqlService instance - ;(materialDao as any).mysqlService = mysqlService let materialRecords: any[] = [] @@ -222,24 +280,32 @@ export function registerValidationHandlers(): void { } // Get type keywords from MaterialsTypeToBeDeleted + const typeKeywordTableName = getTableName('dbo_MaterialsTypeToBeDeleted') const typeKeywordSql = ` SELECT MaterialName, ManagerName - FROM dbo_MaterialsTypeToBeDeleted + FROM ${typeKeywordTableName} WHERE MaterialName IS NOT NULL ` - const typeKeywordResult = await mysqlService.query(typeKeywordSql) + const typeKeywordResult = isSqlServer + ? await (dbService as SqlServerService).query(typeKeywordSql) + : await (dbService as MySqlService).query(typeKeywordSql) + const typeKeywords = typeKeywordResult.rows.map(row => ({ materialName: row.MaterialName as string, managerName: row.ManagerName as string })) // Get marked material codes from MaterialsToBeDeleted + const markedTableName = getTableName('dbo_MaterialsToBeDeleted') const markedSql = ` SELECT MaterialCode, ManagerName - FROM dbo_MaterialsToBeDeleted + FROM ${markedTableName} WHERE MaterialCode IS NOT NULL AND ManagerName IS NOT NULL ` - const markedResult = await mysqlService.query(markedSql) + const markedResult = isSqlServer + ? await (dbService as SqlServerService).query(markedSql) + : await (dbService as MySqlService).query(markedSql) + const markedCodesDict = new Map() for (const row of markedResult.rows) { markedCodesDict.set( @@ -306,11 +372,11 @@ export function registerValidationHandlers(): void { error: `Validation failed: ${message}` } } finally { - if (mysqlService) { + if (dbService) { try { - await mysqlService.disconnect() + await dbService.disconnect() } catch (closeError) { - console.warn('[Validation] Error disconnecting MySQL:', closeError) + console.warn('[Validation] Error disconnecting database:', closeError) } } } @@ -328,13 +394,8 @@ export function registerValidationHandlers(): void { _event, request: MaterialUpsertBatchRequest ): Promise => { - let mysqlService: MySqlService | null = null - try { - mysqlService = await getValidationMySqlService() const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService - const stats = await dao.upsertBatch(request.materials) return { @@ -348,14 +409,6 @@ export function registerValidationHandlers(): void { success: false, error: `Upsert failed: ${message}` } - } finally { - if (mysqlService) { - try { - await mysqlService.disconnect() - } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) - } - } } } ) @@ -369,13 +422,8 @@ export function registerValidationHandlers(): void { _event, request: MaterialDeleteRequest ): Promise => { - let mysqlService: MySqlService | null = null - try { - mysqlService = await getValidationMySqlService() const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService - const count = await dao.deleteByMaterialCodes(request.materialCodes) return { @@ -389,14 +437,6 @@ export function registerValidationHandlers(): void { success: false, error: `Delete failed: ${message}` } - } finally { - if (mysqlService) { - try { - await mysqlService.disconnect() - } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) - } - } } } ) @@ -407,26 +447,13 @@ export function registerValidationHandlers(): void { ipcMain.handle( 'materials:getManagers', async (_event): Promise<{ managers: string[] }> => { - let mysqlService: MySqlService | null = null - try { - mysqlService = await getValidationMySqlService() const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService - const managers = await dao.getManagers() return { managers } } catch (error) { console.error('[Materials] Get managers error:', error) return { managers: [] } - } finally { - if (mysqlService) { - try { - await mysqlService.disconnect() - } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) - } - } } } ) @@ -440,13 +467,14 @@ export function registerValidationHandlers(): void { _event, managerName: string ): Promise<{ materials: MaterialRecordSummary[] }> => { - let mysqlService: MySqlService | null = null + let dbService: MySqlService | SqlServerService | null = null try { - mysqlService = await getValidationMySqlService() - const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService + dbService = await getValidationDatabaseService() + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' + const dao = new MaterialsToBeDeletedDAO() const materials = await dao.getMaterialsByManager(managerName) // Get material codes set for quick lookup @@ -454,17 +482,32 @@ export function registerValidationHandlers(): void { // Enrich with material details from DiscreteMaterialPlanData const enrichedMaterials: MaterialRecordSummary[] = [] + const detailTableName = getTableName('dbo_DiscreteMaterialPlanData') for (const mat of materials) { - const detailSql = ` - SELECT MaterialName, Specification, Model - FROM dbo_DiscreteMaterialPlanData - WHERE MaterialCode = ? - LIMIT 1 - ` - const detailResult = await mysqlService.query(detailSql, [ - mat.materialCode - ]) + let detailResult: any + + if (isSqlServer) { + const sql = require('mssql') + const detailSql = ` + SELECT TOP 1 MaterialName, Specification, Model + FROM ${detailTableName} + WHERE MaterialCode = @materialCode + ` + detailResult = await (dbService as SqlServerService).queryWithParams(detailSql, { + materialCode: { value: mat.materialCode, type: sql.NVarChar } + }) + } else { + const detailSql = ` + SELECT MaterialName, Specification, Model + FROM ${detailTableName} + WHERE MaterialCode = ? + LIMIT 1 + ` + detailResult = await (dbService as MySqlService).query(detailSql, [ + mat.materialCode + ]) + } enrichedMaterials.push({ materialCode: mat.materialCode, @@ -490,11 +533,11 @@ export function registerValidationHandlers(): void { console.error('[Materials] Get by manager error:', error) return { materials: [] } } finally { - if (mysqlService) { + if (dbService) { try { - await mysqlService.disconnect() + await dbService.disconnect() } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) + console.warn('[Materials] Error disconnecting database:', closeError) } } } @@ -507,28 +550,44 @@ export function registerValidationHandlers(): void { ipcMain.handle( 'materials:getAll', async (_event): Promise<{ materials: MaterialRecordSummary[] }> => { - let mysqlService: MySqlService | null = null + let dbService: MySqlService | SqlServerService | null = null try { - mysqlService = await getValidationMySqlService() - const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService + dbService = await getValidationDatabaseService() + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' + const dao = new MaterialsToBeDeletedDAO() const materials = await dao.getAllRecords() const markedCodes = await dao.getAllMaterialCodes() const enrichedMaterials: MaterialRecordSummary[] = [] + const detailTableName = getTableName('dbo_DiscreteMaterialPlanData') for (const mat of materials) { - const detailSql = ` - SELECT MaterialName, Specification, Model - FROM dbo_DiscreteMaterialPlanData - WHERE MaterialCode = ? - LIMIT 1 - ` - const detailResult = await mysqlService.query(detailSql, [ - mat.materialCode - ]) + let detailResult: any + + if (isSqlServer) { + const sql = require('mssql') + const detailSql = ` + SELECT TOP 1 MaterialName, Specification, Model + FROM ${detailTableName} + WHERE MaterialCode = @materialCode + ` + detailResult = await (dbService as SqlServerService).queryWithParams(detailSql, { + materialCode: { value: mat.materialCode, type: sql.NVarChar } + }) + } else { + const detailSql = ` + SELECT MaterialName, Specification, Model + FROM ${detailTableName} + WHERE MaterialCode = ? + LIMIT 1 + ` + detailResult = await (dbService as MySqlService).query(detailSql, [ + mat.materialCode + ]) + } enrichedMaterials.push({ materialCode: mat.materialCode, @@ -554,11 +613,11 @@ export function registerValidationHandlers(): void { console.error('[Materials] Get all error:', error) return { materials: [] } } finally { - if (mysqlService) { + if (dbService) { try { - await mysqlService.disconnect() + await dbService.disconnect() } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) + console.warn('[Materials] Error disconnecting database:', closeError) } } } @@ -571,26 +630,13 @@ export function registerValidationHandlers(): void { ipcMain.handle( 'materials:getStatistics', async (_event): Promise<{ stats: any }> => { - let mysqlService: MySqlService | null = null - try { - mysqlService = await getValidationMySqlService() const dao = new MaterialsToBeDeletedDAO() - ;(dao as any).mysqlService = mysqlService - const stats = await dao.getStatistics() return { stats } } catch (error) { console.error('[Materials] Get statistics error:', error) return { stats: null } - } finally { - if (mysqlService) { - try { - await mysqlService.disconnect() - } catch (closeError) { - console.warn('[Materials] Error disconnecting MySQL:', closeError) - } - } } } ) @@ -628,7 +674,7 @@ export function registerValidationHandlers(): void { materialCodes?: string[] error?: string }> => { - let mysqlService: MySqlService | null = null + let dbService: MySqlService | SqlServerService | null = null const sessionManager = (await import('../services/user/session-manager')).SessionManager.getInstance() try { @@ -643,11 +689,13 @@ export function registerValidationHandlers(): void { const isAdmin = userInfo.userType === 'Admin' const username = userInfo.username + const dbType = process.env.DB_TYPE?.toLowerCase() + const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql' console.log(`[CleanerData] User: ${username}, isAdmin: ${isAdmin}`) - // Connect to MySQL - mysqlService = await getValidationMySqlService() + // Connect to database + dbService = await getValidationDatabaseService() // 1. Get order numbers from shared Production IDs const sharedIds = getSharedProductionIds() @@ -655,36 +703,55 @@ export function registerValidationHandlers(): void { if (sharedIds.length > 0) { console.log(`[CleanerData] Using ${sharedIds.length} shared Production IDs`) - orderNumbers = await getSourceNumbersFromInputs(sharedIds, mysqlService) + orderNumbers = await getSourceNumbersFromInputs(sharedIds, dbService) console.log(`[CleanerData] Got ${orderNumbers.length} order numbers`) } // 2. Get material codes from MaterialsToBeDeleted table let materialCodes: string[] = [] + const markedTableName = getTableName('dbo_MaterialsToBeDeleted') if (isAdmin) { // Admin sees all materials const allCodesSql = ` SELECT MaterialCode - FROM dbo_MaterialsToBeDeleted + FROM ${markedTableName} WHERE MaterialCode IS NOT NULL ` - const result = await mysqlService.query(allCodesSql) + const result = isSqlServer + ? await (dbService as SqlServerService).query(allCodesSql) + : await (dbService as MySqlService).query(allCodesSql) + materialCodes = result.rows .map(row => row.MaterialCode as string) .filter(Boolean) console.log(`[CleanerData] Admin user: got ${materialCodes.length} materials`) } else { // Regular users only see their own materials - const userMaterialsSql = ` - SELECT MaterialCode - FROM dbo_MaterialsToBeDeleted - WHERE ManagerName = ? AND MaterialCode IS NOT NULL - ` - const result = await mysqlService.query(userMaterialsSql, [username]) - materialCodes = result.rows - .map(row => row.MaterialCode as string) - .filter(Boolean) + if (isSqlServer) { + const sql = require('mssql') + const userMaterialsSql = ` + SELECT MaterialCode + FROM ${markedTableName} + WHERE ManagerName = @username AND MaterialCode IS NOT NULL + ` + const result = await (dbService as SqlServerService).queryWithParams(userMaterialsSql, { + username: { value: username, type: sql.NVarChar } + }) + materialCodes = result.rows + .map(row => row.MaterialCode as string) + .filter(Boolean) + } else { + const userMaterialsSql = ` + SELECT MaterialCode + FROM ${markedTableName} + WHERE ManagerName = ? AND MaterialCode IS NOT NULL + ` + const result = await (dbService as MySqlService).query(userMaterialsSql, [username]) + materialCodes = result.rows + .map(row => row.MaterialCode as string) + .filter(Boolean) + } console.log(`[CleanerData] Regular user: got ${materialCodes.length} materials`) } @@ -701,11 +768,11 @@ export function registerValidationHandlers(): void { error: `获取清理数据失败:${message}` } } finally { - if (mysqlService) { + if (dbService) { try { - await mysqlService.disconnect() + await dbService.disconnect() } catch (closeError) { - console.warn('[CleanerData] Error disconnecting MySQL:', closeError) + console.warn('[CleanerData] Error disconnecting database:', closeError) } } } diff --git a/src/main/services/database/discrete-material-plan-dao.ts b/src/main/services/database/discrete-material-plan-dao.ts index 6581945..5ac26b2 100644 --- a/src/main/services/database/discrete-material-plan-dao.ts +++ b/src/main/services/database/discrete-material-plan-dao.ts @@ -9,6 +9,8 @@ */ import { MySqlService } from './mysql' +import { SqlServerService } from './sql-server' +import sql from 'mssql' /** * Material plan record interface @@ -50,6 +52,7 @@ export interface MaterialPlanRecord { * Configuration for DiscreteMaterialPlanData table */ export const DISCRETE_MATERIAL_PLAN_CONFIG = { + TABLE_NAME_SQLSERVER: '[dbo].[DiscreteMaterialPlanData]', TABLE_NAME_MYSQL: 'dbo_DiscreteMaterialPlanData', COLUMNS: { ID: 'ID', @@ -90,25 +93,69 @@ export const DISCRETE_MATERIAL_PLAN_CONFIG = { */ export class DiscreteMaterialPlanDAO { private mysqlService: MySqlService | null = null + private sqlServerService: SqlServerService | null = null + private dbType: 'mysql' | 'sqlserver' = 'mysql' /** - * Get MySQL service instance + * Constructor - determine database type from environment */ - private async getMySqlService(): Promise { - if (this.mysqlService && this.mysqlService.isConnected()) { + constructor() { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + this.dbType = 'sqlserver' + } else { + this.dbType = 'mysql' + } + } + + /** + * Get the appropriate table name based on database type + */ + private getTableName(): string { + return this.dbType === 'sqlserver' + ? DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_SQLSERVER + : DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL + } + + /** + * Get database service instance (MySQL or SQL Server) + */ + private async getDatabaseService(): Promise { + if (this.dbType === 'sqlserver') { + if (this.sqlServerService && this.sqlServerService.isConnected()) { + return this.sqlServerService + } + + this.sqlServerService = new SqlServerService({ + server: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + user: process.env.DB_USERNAME || 'sa', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + options: { + encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', + trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + } + }) + + await this.sqlServerService.connect() + return this.sqlServerService + } else { + if (this.mysqlService && this.mysqlService.isConnected()) { + return this.mysqlService + } + + this.mysqlService = new MySqlService({ + host: process.env.DB_MYSQL_HOST || 'localhost', + port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), + user: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '' + }) + + await this.mysqlService.connect() return this.mysqlService } - - this.mysqlService = new MySqlService({ - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' - }) - - await this.mysqlService.connect() - return this.mysqlService } // ==================== QUERY ALL ==================== @@ -119,11 +166,15 @@ export class DiscreteMaterialPlanDAO { */ async queryAll(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = `SELECT * FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}` + const sql = `SELECT * FROM ${tableName}` + + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) - const result = await mysqlService.query(sql) return result.rows } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query all error:', error) @@ -139,7 +190,8 @@ export class DiscreteMaterialPlanDAO { */ async queryAllDistinctByMaterialCode(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` WITH RankedRecords AS ( @@ -149,7 +201,7 @@ export class DiscreteMaterialPlanDAO { PARTITION BY MaterialCode ORDER BY CreateDate ASC, SequenceNumber ASC ) AS rn - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialCode IS NOT NULL ) SELECT @@ -163,7 +215,10 @@ export class DiscreteMaterialPlanDAO { WHERE rn = 1 ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return result.rows } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query all distinct by material code error:', error) @@ -184,22 +239,42 @@ export class DiscreteMaterialPlanDAO { } try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const batchSize = 2000 const allResults: any[] = [] for (let i = 0; i < sourceNumbers.length; i += batchSize) { const batch = sourceNumbers.slice(i, i + batchSize) - const placeholders = batch.map(() => '?').join(',') - const sql = ` - SELECT * - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE SourceNumber IN (${placeholders}) - ` + if (this.dbType === 'sqlserver') { + const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} - const result = await mysqlService.query(sql, batch) - allResults.push(...result.rows) + batch.forEach((num, idx) => { + params[`p${idx}`] = { value: num, type: sql.NVarChar } + }) + + const sql = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + ` + + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + allResults.push(...result.rows) + } else { + const placeholders = batch.map(() => '?').join(',') + + const sql = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + ` + + const result = await (dbService as MySqlService).query(sql, batch) + allResults.push(...result.rows) + } } return allResults @@ -222,39 +297,76 @@ export class DiscreteMaterialPlanDAO { } try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const batchSize = 2000 const allResults: any[] = [] for (let i = 0; i < sourceNumbers.length; i += batchSize) { const batch = sourceNumbers.slice(i, i + batchSize) - const placeholders = batch.map(() => '?').join(',') - const sql = ` - WITH RankedRecords AS ( + if (this.dbType === 'sqlserver') { + const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} + + batch.forEach((num, idx) => { + params[`p${idx}`] = { value: num, type: sql.NVarChar } + }) + + const sql = ` + WITH RankedRecords AS ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY MaterialCode + ORDER BY CreateDate ASC, SequenceNumber ASC + ) AS rn + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialCode IS NOT NULL + ) SELECT - *, - ROW_NUMBER() OVER ( - PARTITION BY MaterialCode - ORDER BY CreateDate ASC, SequenceNumber ASC - ) AS rn - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE SourceNumber IN (${placeholders}) - AND MaterialCode IS NOT NULL - ) - SELECT - Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, - ProductCode, ProductName, ProductUnit, ProductPlanQuantity, - UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, - SequenceNumber, MaterialCode, MaterialName, Specification, Model, - DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, - Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion - FROM RankedRecords - WHERE rn = 1 - ` + Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, + ProductCode, ProductName, ProductUnit, ProductPlanQuantity, + UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, + SequenceNumber, MaterialCode, MaterialName, Specification, Model, + DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, + Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion + FROM RankedRecords + WHERE rn = 1 + ` - const result = await mysqlService.query(sql, batch) - allResults.push(...result.rows) + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + allResults.push(...result.rows) + } else { + const placeholders = batch.map(() => '?').join(',') + + const sql = ` + WITH RankedRecords AS ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY MaterialCode + ORDER BY CreateDate ASC, SequenceNumber ASC + ) AS rn + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialCode IS NOT NULL + ) + SELECT + Factory, MaterialStatus, PlanNumber, SourceNumber, MaterialType, + ProductCode, ProductName, ProductUnit, ProductPlanQuantity, + UseDepartment, Remark, Creator, CreateDate, Approver, ApproveDate, + SequenceNumber, MaterialCode, MaterialName, Specification, Model, + DrawingNumber, MaterialQuality, PlanQuantity, Unit, RequiredDate, + Warehouse, UnitUsage, CumulativeOutputQuantity, BOMVersion + FROM RankedRecords + WHERE rn = 1 + ` + + const result = await (dbService as MySqlService).query(sql, batch) + allResults.push(...result.rows) + } } return allResults @@ -271,16 +383,32 @@ export class DiscreteMaterialPlanDAO { */ async queryBySourceNumber(sourceNumber: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT * - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE SourceNumber = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber = @sourceNumber + ` - const result = await mysqlService.query(sql, [sourceNumber]) - return result.rows + const result = await (dbService as SqlServerService).queryWithParams(sql, { + sourceNumber: { value: sourceNumber, type: sql.NVarChar } + }) + + return result.rows + } else { + const sql = ` + SELECT * + FROM ${tableName} + WHERE SourceNumber = ? + ` + + const result = await (dbService as MySqlService).query(sql, [sourceNumber]) + + return result.rows + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query by source number error:', error) return [] @@ -296,16 +424,32 @@ export class DiscreteMaterialPlanDAO { */ async queryByPlanNumber(planNumber: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT * - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE PlanNumber = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber = @planNumber + ` - const result = await mysqlService.query(sql, [planNumber]) - return result.rows + const result = await (dbService as SqlServerService).queryWithParams(sql, { + planNumber: { value: planNumber, type: sql.NVarChar } + }) + + return result.rows + } else { + const sql = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber = ? + ` + + const result = await (dbService as MySqlService).query(sql, [planNumber]) + + return result.rows + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query by plan number error:', error) return [] @@ -323,17 +467,37 @@ export class DiscreteMaterialPlanDAO { } try { - const mysqlService = await this.getMySqlService() - const placeholders = planNumbers.map(() => '?').join(',') + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT * - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE PlanNumber IN (${placeholders}) - ` + if (this.dbType === 'sqlserver') { + const placeholders = planNumbers.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} - const result = await mysqlService.query(sql, planNumbers) - return result.rows + planNumbers.forEach((num, idx) => { + params[`p${idx}`] = { value: num, type: sql.NVarChar } + }) + + const sql = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber IN (${placeholders}) + ` + + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + return result.rows + } else { + const placeholders = planNumbers.map(() => '?').join(',') + + const sql = ` + SELECT * + FROM ${tableName} + WHERE PlanNumber IN (${placeholders}) + ` + + const result = await (dbService as MySqlService).query(sql, planNumbers) + return result.rows + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Query by plan numbers error:', error) return [] @@ -348,11 +512,15 @@ export class DiscreteMaterialPlanDAO { */ async countAll(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = `SELECT COUNT(*) as count FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}` + const sql = `SELECT COUNT(*) as count FROM ${tableName}` + + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) - const result = await mysqlService.query(sql) return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } catch (error) { console.error('[DiscreteMaterialPlanDAO] Count all error:', error) @@ -367,16 +535,32 @@ export class DiscreteMaterialPlanDAO { */ async countByPlanNumber(planNumber: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT COUNT(*) as count - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE PlanNumber = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE PlanNumber = @planNumber + ` - const result = await mysqlService.query(sql, [planNumber]) - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + planNumber: { value: planNumber, type: sql.NVarChar } + }) + + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + } else { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE PlanNumber = ? + ` + + const result = await (dbService as MySqlService).query(sql, [planNumber]) + + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Count by plan number error:', error) return 0 @@ -390,32 +574,59 @@ export class DiscreteMaterialPlanDAO { */ async getUniqueMaterialNames(sourceNumbers?: string[]): Promise { try { - const mysqlService = await this.getMySqlService() - - let sql: string - let params: any[] = [] + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() if (sourceNumbers && sourceNumbers.length > 0) { - const placeholders = sourceNumbers.map(() => '?').join(',') - sql = ` - SELECT DISTINCT MaterialName - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} - WHERE SourceNumber IN (${placeholders}) - AND MaterialName IS NOT NULL - ` - params = sourceNumbers + if (this.dbType === 'sqlserver') { + const placeholders = sourceNumbers.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} + + sourceNumbers.forEach((num, idx) => { + params[`p${idx}`] = { value: num, type: sql.NVarChar } + }) + + const sql = ` + SELECT DISTINCT MaterialName + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialName IS NOT NULL + ` + + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + return result.rows + .map(row => row.MaterialName as string) + .filter(Boolean) + } else { + const placeholders = sourceNumbers.map(() => '?').join(',') + + const sql = ` + SELECT DISTINCT MaterialName + FROM ${tableName} + WHERE SourceNumber IN (${placeholders}) + AND MaterialName IS NOT NULL + ` + + const result = await (dbService as MySqlService).query(sql, sourceNumbers) + return result.rows + .map(row => row.MaterialName as string) + .filter(Boolean) + } } else { - sql = ` + const sql = ` SELECT DISTINCT MaterialName - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialName IS NOT NULL ` - } - const result = await mysqlService.query(sql, params) - return result.rows - .map(row => row.MaterialName as string) - .filter(Boolean) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + + return result.rows + .map(row => row.MaterialName as string) + .filter(Boolean) + } } catch (error) { console.error('[DiscreteMaterialPlanDAO] Get unique material names error:', error) return [] @@ -428,7 +639,8 @@ export class DiscreteMaterialPlanDAO { */ async getStatistics(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT @@ -437,10 +649,13 @@ export class DiscreteMaterialPlanDAO { COUNT(DISTINCT SourceNumber) as uniqueOrders, MIN(CreateDate) as earliestRecord, MAX(CreateDate) as latestRecord - FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return result.rows.length > 0 ? result.rows[0] : {} } catch (error) { console.error('[DiscreteMaterialPlanDAO] Get statistics error:', error) @@ -456,5 +671,9 @@ export class DiscreteMaterialPlanDAO { await this.mysqlService.disconnect() this.mysqlService = null } + if (this.sqlServerService) { + await this.sqlServerService.disconnect() + this.sqlServerService = null + } } } diff --git a/src/main/services/database/materials-to-be-deleted-dao.ts b/src/main/services/database/materials-to-be-deleted-dao.ts index 520aabf..509fd9b 100644 --- a/src/main/services/database/materials-to-be-deleted-dao.ts +++ b/src/main/services/database/materials-to-be-deleted-dao.ts @@ -9,6 +9,8 @@ */ import { MySqlService } from './mysql' +import { SqlServerService } from './sql-server' +import sql from 'mssql' /** * Material record interface @@ -41,6 +43,7 @@ export interface MaterialStats { * Configuration for MaterialsToBeDeleted table */ export const MATERIALS_TO_BE_DELETED_CONFIG = { + TABLE_NAME_SQLSERVER: '[dbo].[MaterialsToBeDeleted]', TABLE_NAME_MYSQL: 'dbo_MaterialsToBeDeleted', COLUMNS: { ID: 'ID', @@ -54,25 +57,69 @@ export const MATERIALS_TO_BE_DELETED_CONFIG = { */ export class MaterialsToBeDeletedDAO { private mysqlService: MySqlService | null = null + private sqlServerService: SqlServerService | null = null + private dbType: 'mysql' | 'sqlserver' = 'mysql' /** - * Get MySQL service instance + * Constructor - determine database type from environment */ - private async getMySqlService(): Promise { - if (this.mysqlService && this.mysqlService.isConnected()) { + constructor() { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + this.dbType = 'sqlserver' + } else { + this.dbType = 'mysql' + } + } + + /** + * Get the appropriate table name based on database type + */ + private getTableName(): string { + return this.dbType === 'sqlserver' + ? MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_SQLSERVER + : MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL + } + + /** + * Get database service instance (MySQL or SQL Server) + */ + private async getDatabaseService(): Promise { + if (this.dbType === 'sqlserver') { + if (this.sqlServerService && this.sqlServerService.isConnected()) { + return this.sqlServerService + } + + this.sqlServerService = new SqlServerService({ + server: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + user: process.env.DB_USERNAME || 'sa', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + options: { + encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', + trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + } + }) + + await this.sqlServerService.connect() + return this.sqlServerService + } else { + if (this.mysqlService && this.mysqlService.isConnected()) { + return this.mysqlService + } + + this.mysqlService = new MySqlService({ + host: process.env.DB_MYSQL_HOST || 'localhost', + port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), + user: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '' + }) + + await this.mysqlService.connect() return this.mysqlService } - - this.mysqlService = new MySqlService({ - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' - }) - - await this.mysqlService.connect() - return this.mysqlService } // ==================== UPSERT (MERGE) ==================== @@ -90,15 +137,34 @@ export class MaterialsToBeDeletedDAO { } try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + const code = materialCode.trim() + const manager = managerName?.trim() || null - const sql = ` - INSERT INTO ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} (MaterialCode, ManagerName) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) - ` + if (this.dbType === 'sqlserver') { + const sql = ` + MERGE ${tableName} AS target + USING (VALUES (@materialCode, @managerName)) AS source (MaterialCode, ManagerName) + ON target.MaterialCode = source.MaterialCode + WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName + WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); + ` + + await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: code, type: sql.NVarChar }, + managerName: { value: manager, type: sql.NVarChar } + }) + } else { + const sql = ` + INSERT INTO ${tableName} (MaterialCode, ManagerName) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) + ` + + await (dbService as MySqlService).query(sql, [code, manager]) + } - await mysqlService.query(sql, [materialCode.trim(), managerName?.trim() || null]) return true } catch (error) { console.error('[MaterialsToBeDeletedDAO] Upsert material error:', error) @@ -123,7 +189,8 @@ export class MaterialsToBeDeletedDAO { } try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() for (const material of materials) { const materialCode = material.materialCode?.trim() @@ -135,13 +202,29 @@ export class MaterialsToBeDeletedDAO { } try { - const sql = ` - INSERT INTO ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} (MaterialCode, ManagerName) - VALUES (?, ?) - ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) - ` + if (this.dbType === 'sqlserver') { + const sql = ` + MERGE ${tableName} AS target + USING (VALUES (@materialCode, @managerName)) AS source (MaterialCode, ManagerName) + ON target.MaterialCode = source.MaterialCode + WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName + WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); + ` + + await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: materialCode, type: sql.NVarChar }, + managerName: { value: managerName || null, type: sql.NVarChar } + }) + } else { + const sql = ` + INSERT INTO ${tableName} (MaterialCode, ManagerName) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) + ` + + await (dbService as MySqlService).query(sql, [materialCode, managerName || null]) + } - await mysqlService.query(sql, [materialCode, managerName || null]) stats.success++ } catch (error) { console.error('[MaterialsToBeDeletedDAO] Error upserting material:', materialCode, error) @@ -164,15 +247,19 @@ export class MaterialsToBeDeletedDAO { */ async getAllMaterialCodes(): Promise> { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT MaterialCode - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialCode IS NOT NULL ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return new Set(result.rows.map(row => row.MaterialCode as string).filter(Boolean)) } catch (error) { console.error('[MaterialsToBeDeletedDAO] Get all material codes error:', error) @@ -186,16 +273,20 @@ export class MaterialsToBeDeletedDAO { */ async getAllRecords(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT ID, MaterialCode, ManagerName - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialCode IS NOT NULL ORDER BY ManagerName, MaterialCode ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return result.rows.map(row => ({ id: row.ID as number, materialCode: row.MaterialCode as string, @@ -214,21 +305,42 @@ export class MaterialsToBeDeletedDAO { */ async getMaterialsByManager(managerName: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT ID, MaterialCode, ManagerName - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE ManagerName = ? AND MaterialCode IS NOT NULL - ORDER BY MaterialCode - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE ManagerName = @managerName AND MaterialCode IS NOT NULL + ORDER BY MaterialCode + ` - const result = await mysqlService.query(sql, [managerName]) - return result.rows.map(row => ({ - id: row.ID as number, - materialCode: row.MaterialCode as string, - managerName: row.ManagerName as string - })) + const result = await (dbService as SqlServerService).queryWithParams(sql, { + managerName: { value: managerName, type: sql.NVarChar } + }) + + return result.rows.map(row => ({ + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string + })) + } else { + const sql = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE ManagerName = ? AND MaterialCode IS NOT NULL + ORDER BY MaterialCode + ` + + const result = await (dbService as MySqlService).query(sql, [managerName]) + + return result.rows.map(row => ({ + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string + })) + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Get materials by manager error:', error) return [] @@ -241,16 +353,20 @@ export class MaterialsToBeDeletedDAO { */ async getManagers(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT DISTINCT ManagerName - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE ManagerName IS NOT NULL ORDER BY ManagerName ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) + return result.rows.map(row => row.ManagerName as string).filter(Boolean) } catch (error) { console.error('[MaterialsToBeDeletedDAO] Get managers error:', error) @@ -265,24 +381,50 @@ export class MaterialsToBeDeletedDAO { */ async getRecordByMaterialCode(materialCode: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + const code = materialCode.trim() - const sql = ` - SELECT ID, MaterialCode, ManagerName - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE MaterialCode = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE MaterialCode = @materialCode + ` - const result = await mysqlService.query(sql, [materialCode.trim()]) - if (result.rows.length === 0) { - return null - } + const result = await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: code, type: sql.NVarChar } + }) - const row = result.rows[0] - return { - id: row.ID as number, - materialCode: row.MaterialCode as string, - managerName: row.ManagerName as string + if (result.rows.length === 0) { + return null + } + + const row = result.rows[0] + return { + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string + } + } else { + const sql = ` + SELECT ID, MaterialCode, ManagerName + FROM ${tableName} + WHERE MaterialCode = ? + ` + + const result = await (dbService as MySqlService).query(sql, [code]) + + if (result.rows.length === 0) { + return null + } + + const row = result.rows[0] + return { + id: row.ID as number, + materialCode: row.MaterialCode as string, + managerName: row.ManagerName as string + } } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Get record by material code error:', error) @@ -299,15 +441,31 @@ export class MaterialsToBeDeletedDAO { */ async deleteByMaterialCode(materialCode: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + const code = materialCode.trim() - const sql = ` - DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE MaterialCode = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + DELETE FROM ${tableName} + WHERE MaterialCode = @materialCode + ` - const result = await mysqlService.query(sql, [materialCode.trim()]) - return result.rowCount > 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: code, type: sql.NVarChar } + }) + + return result.rowCount > 0 + } else { + const sql = ` + DELETE FROM ${tableName} + WHERE MaterialCode = ? + ` + + const result = await (dbService as MySqlService).query(sql, [code]) + + return result.rowCount > 0 + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Delete by material code error:', error) return false @@ -321,15 +479,30 @@ export class MaterialsToBeDeletedDAO { */ async deleteByManager(managerName: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE ManagerName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + DELETE FROM ${tableName} + WHERE ManagerName = @managerName + ` - const result = await mysqlService.query(sql, [managerName]) - return result.rowCount + const result = await (dbService as SqlServerService).queryWithParams(sql, { + managerName: { value: managerName, type: sql.NVarChar } + }) + + return result.rowCount + } else { + const sql = ` + DELETE FROM ${tableName} + WHERE ManagerName = ? + ` + + const result = await (dbService as MySqlService).query(sql, [managerName]) + + return result.rowCount + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Delete by manager error:', error) return 0 @@ -342,11 +515,15 @@ export class MaterialsToBeDeletedDAO { */ async deleteAllMaterials(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = `DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}` + const sql = `DELETE FROM ${tableName}` + + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) - const result = await mysqlService.query(sql) return result.rowCount } catch (error) { console.error('[MaterialsToBeDeletedDAO] Delete all materials error:', error) @@ -368,19 +545,38 @@ export class MaterialsToBeDeletedDAO { const batchSize = 1000 try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() for (let i = 0; i < materialCodes.length; i += batchSize) { const batch = materialCodes.slice(i, i + batchSize) - const placeholders = batch.map(() => '?').join(',') - const sql = ` - DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE MaterialCode IN (${placeholders}) - ` + if (this.dbType === 'sqlserver') { + const placeholders = batch.map((_, idx) => `@p${idx}`).join(',') + const params: Record = {} - const result = await mysqlService.query(sql, batch) - totalDeleted += result.rowCount + batch.forEach((code, idx) => { + params[`p${idx}`] = { value: code.trim(), type: sql.NVarChar } + }) + + const sql = ` + DELETE FROM ${tableName} + WHERE MaterialCode IN (${placeholders}) + ` + + const result = await (dbService as SqlServerService).queryWithParams(sql, params) + totalDeleted += result.rowCount + } else { + const placeholders = batch.map(() => '?').join(',') + + const sql = ` + DELETE FROM ${tableName} + WHERE MaterialCode IN (${placeholders}) + ` + + const result = await (dbService as MySqlService).query(sql, batch) + totalDeleted += result.rowCount + } } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Delete by material codes error:', error) @@ -398,16 +594,33 @@ export class MaterialsToBeDeletedDAO { */ async materialExists(materialCode: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + const code = materialCode.trim() - const sql = ` - SELECT COUNT(*) as count - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE MaterialCode = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE MaterialCode = @materialCode + ` - const result = await mysqlService.query(sql, [materialCode.trim()]) - return result.rows.length > 0 && (result.rows[0].count as number) > 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + materialCode: { value: code, type: sql.NVarChar } + }) + + return result.rows.length > 0 && (result.rows[0].count as number) > 0 + } else { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE MaterialCode = ? + ` + + const result = await (dbService as MySqlService).query(sql, [code]) + + return result.rows.length > 0 && (result.rows[0].count as number) > 0 + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Material exists error:', error) return false @@ -420,11 +633,15 @@ export class MaterialsToBeDeletedDAO { */ async countAll(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = `SELECT COUNT(*) as count FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL}` + const sql = `SELECT COUNT(*) as count FROM ${tableName}` + + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) - const result = await mysqlService.query(sql) return result.rows.length > 0 ? (result.rows[0].count as number) : 0 } catch (error) { console.error('[MaterialsToBeDeletedDAO] Count all error:', error) @@ -439,16 +656,32 @@ export class MaterialsToBeDeletedDAO { */ async countByManager(managerName: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT COUNT(*) as count - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} - WHERE ManagerName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE ManagerName = @managerName + ` - const result = await mysqlService.query(sql, [managerName]) - return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + managerName: { value: managerName, type: sql.NVarChar } + }) + + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + } else { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE ManagerName = ? + ` + + const result = await (dbService as MySqlService).query(sql, [managerName]) + + return result.rows.length > 0 ? (result.rows[0].count as number) : 0 + } } catch (error) { console.error('[MaterialsToBeDeletedDAO] Count by manager error:', error) return 0 @@ -461,30 +694,37 @@ export class MaterialsToBeDeletedDAO { */ async getStatistics(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() // Get total and unique managers const statsSql = ` SELECT COUNT(*) as totalMaterials, COUNT(DISTINCT ManagerName) as uniqueManagers - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE MaterialCode IS NOT NULL ` - const statsResult = await mysqlService.query(statsSql) + const statsResult = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(statsSql) + : await (dbService as MySqlService).query(statsSql) + const stats = statsResult.rows[0] || {} // Get materials per manager const managerSql = ` SELECT ManagerName, COUNT(*) as count - FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} WHERE ManagerName IS NOT NULL GROUP BY ManagerName ORDER BY count DESC ` - const managerResult = await mysqlService.query(managerSql) + const managerResult = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(managerSql) + : await (dbService as MySqlService).query(managerSql) + const materialsPerManager = managerResult.rows.map(row => ({ [row.ManagerName as string]: row.count as number })) @@ -512,5 +752,9 @@ export class MaterialsToBeDeletedDAO { await this.mysqlService.disconnect() this.mysqlService = null } + if (this.sqlServerService) { + await this.sqlServerService.disconnect() + this.sqlServerService = null + } } } diff --git a/src/main/services/database/sql-server.ts b/src/main/services/database/sql-server.ts index 2222a21..a2936a3 100644 --- a/src/main/services/database/sql-server.ts +++ b/src/main/services/database/sql-server.ts @@ -98,8 +98,9 @@ export class SqlServerService { const result = await request.query(sqlString) // Convert recordset to array of objects - const columns = result.recordset.columns?.map((col) => col.name) || [] const rows = result.recordset as Record[] + // Extract column names from the first row if available + const columns = rows.length > 0 ? Object.keys(rows[0]) : [] return { rows, @@ -137,8 +138,9 @@ export class SqlServerService { const result = await request.query(sqlString) // Convert recordset to array of objects - const columns = result.recordset.columns?.map((col) => col.name) || [] const rows = result.recordset as Record[] + // Extract column names from the first row if available + const columns = rows.length > 0 ? Object.keys(rows[0]) : [] return { rows, diff --git a/src/main/services/user/bip-users-dao.ts b/src/main/services/user/bip-users-dao.ts index 1a281e7..ea3ec3d 100644 --- a/src/main/services/user/bip-users-dao.ts +++ b/src/main/services/user/bip-users-dao.ts @@ -9,6 +9,8 @@ */ import { MySqlService } from '../database/mysql' +import { SqlServerService } from '../database/sql-server' +import sql from 'mssql' import type { UserInfo } from '../../types/user.types' /** @@ -35,25 +37,69 @@ export const BIP_USERS_CONFIG = { */ export class BIPUsersDAO { private mysqlService: MySqlService | null = null + private sqlServerService: SqlServerService | null = null + private dbType: 'mysql' | 'sqlserver' = 'mysql' /** - * Get MySQL service instance + * Constructor - determine database type from environment */ - private async getMySqlService(): Promise { - if (this.mysqlService && this.mysqlService.isConnected()) { + constructor() { + const dbType = process.env.DB_TYPE?.toLowerCase() + if (dbType === 'sqlserver' || dbType === 'mssql') { + this.dbType = 'sqlserver' + } else { + this.dbType = 'mysql' + } + } + + /** + * Get the appropriate table name based on database type + */ + private getTableName(): string { + return this.dbType === 'sqlserver' + ? BIP_USERS_CONFIG.TABLE_NAME_SQLSERVER + : BIP_USERS_CONFIG.TABLE_NAME_MYSQL + } + + /** + * Get database service instance (MySQL or SQL Server) + */ + private async getDatabaseService(): Promise { + if (this.dbType === 'sqlserver') { + if (this.sqlServerService && this.sqlServerService.isConnected()) { + return this.sqlServerService + } + + this.sqlServerService = new SqlServerService({ + server: process.env.DB_SERVER || 'localhost', + port: parseInt(process.env.DB_SQLSERVER_PORT || '1433', 10), + user: process.env.DB_USERNAME || 'sa', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '', + options: { + encrypt: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes', + trustServerCertificate: process.env.DB_TRUST_SERVER_CERTIFICATE === 'yes' + } + }) + + await this.sqlServerService.connect() + return this.sqlServerService + } else { + if (this.mysqlService && this.mysqlService.isConnected()) { + return this.mysqlService + } + + this.mysqlService = new MySqlService({ + host: process.env.DB_MYSQL_HOST || 'localhost', + port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), + user: process.env.DB_USERNAME || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || '' + }) + + await this.mysqlService.connect() return this.mysqlService } - - this.mysqlService = new MySqlService({ - host: process.env.DB_MYSQL_HOST || 'localhost', - port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), - user: process.env.DB_USERNAME || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || '' - }) - - await this.mysqlService.connect() - return this.mysqlService } /** @@ -64,25 +110,49 @@ export class BIPUsersDAO { */ async authenticate(username: string, password: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT ID, UserName, UserType - FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - WHERE UserName = ? AND Password = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT ID, UserName, UserType + FROM ${tableName} + WHERE UserName = @username AND Password = @password + ` - const result = await mysqlService.query(sql, [username, password]) + const result = await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar }, + password: { value: password, type: sql.NVarChar } + }) - if (result.rows.length > 0) { - const row = result.rows[0] - return { - id: row.ID as number, - username: row.UserName as string, - userType: row.UserType as 'Admin' | 'User' | 'Guest' + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } } + return null + } else { + const sql = ` + SELECT ID, UserName, UserType + FROM ${tableName} + WHERE UserName = ? AND Password = ? + ` + + const result = await (dbService as MySqlService).query(sql, [username, password]) + + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } + } + return null } - return null } catch (error) { console.error('[BIPUsersDAO] Authenticate error:', error) return null @@ -96,25 +166,48 @@ export class BIPUsersDAO { */ async authenticateByComputerName(computerName: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT ID, UserName, UserType - FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - WHERE ComputerNmae = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT ID, UserName, UserType + FROM ${tableName} + WHERE ComputerNmae = @computerName + ` - const result = await mysqlService.query(sql, [computerName]) + const result = await (dbService as SqlServerService).queryWithParams(sql, { + computerName: { value: computerName, type: sql.NVarChar } + }) - if (result.rows.length > 0) { - const row = result.rows[0] - return { - id: row.ID as number, - username: row.UserName as string, - userType: row.UserType as 'Admin' | 'User' | 'Guest' + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } } + return null + } else { + const sql = ` + SELECT ID, UserName, UserType + FROM ${tableName} + WHERE ComputerNmae = ? + ` + + const result = await (dbService as MySqlService).query(sql, [computerName]) + + if (result.rows.length > 0) { + const row = result.rows[0] + return { + id: row.ID as number, + username: row.UserName as string, + userType: row.UserType as 'Admin' | 'User' | 'Guest' + } + } + return null } - return null } catch (error) { console.error('[BIPUsersDAO] Authenticate by computer name error:', error) return null @@ -127,15 +220,18 @@ export class BIPUsersDAO { */ async getAllUsers(): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() const sql = ` SELECT ID, UserName, UserType, CreateTime - FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} + FROM ${tableName} ORDER BY UserName ` - const result = await mysqlService.query(sql) + const result = this.dbType === 'sqlserver' + ? await (dbService as SqlServerService).query(sql) + : await (dbService as MySqlService).query(sql) return result.rows.map(row => ({ id: row.ID as number, @@ -164,29 +260,63 @@ export class BIPUsersDAO { computerName: string = '' ): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - let sql: string - let params: any[] + if (this.dbType === 'sqlserver') { + let sql: string + let params: Record - if (computerName) { - sql = ` - INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - (UserName, Password, UserType, ComputerNmae) - VALUES (?, ?, ?, ?) - ` - params = [username, password, userType, computerName] + if (computerName) { + sql = ` + INSERT INTO ${tableName} + (UserName, Password, UserType, ComputerNmae) + VALUES (@username, @password, @userType, @computerName) + ` + params = { + username: { value: username, type: sql.NVarChar }, + password: { value: password, type: sql.NVarChar }, + userType: { value: userType, type: sql.NVarChar }, + computerName: { value: computerName, type: sql.NVarChar } + } + } else { + sql = ` + INSERT INTO ${tableName} + (UserName, Password, UserType) + VALUES (@username, @password, @userType) + ` + params = { + username: { value: username, type: sql.NVarChar }, + password: { value: password, type: sql.NVarChar }, + userType: { value: userType, type: sql.NVarChar } + } + } + + await (dbService as SqlServerService).queryWithParams(sql, params) + return true } else { - sql = ` - INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - (UserName, Password, UserType) - VALUES (?, ?, ?) - ` - params = [username, password, userType] - } + let sql: string + let params: any[] - await mysqlService.query(sql, params) - return true + if (computerName) { + sql = ` + INSERT INTO ${tableName} + (UserName, Password, UserType, ComputerNmae) + VALUES (?, ?, ?, ?) + ` + params = [username, password, userType, computerName] + } else { + sql = ` + INSERT INTO ${tableName} + (UserName, Password, UserType) + VALUES (?, ?, ?) + ` + params = [username, password, userType] + } + + await (dbService as MySqlService).query(sql, params) + return true + } } catch (error) { console.error('[BIPUsersDAO] Create user error:', error) return false @@ -201,16 +331,31 @@ export class BIPUsersDAO { */ async updateUserType(username: string, userType: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - SET UserType = ? - WHERE UserName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + UPDATE ${tableName} + SET UserType = @userType + WHERE UserName = @username + ` - await mysqlService.query(sql, [userType, username]) - return true + await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar }, + userType: { value: userType, type: sql.NVarChar } + }) + return true + } else { + const sql = ` + UPDATE ${tableName} + SET UserType = ? + WHERE UserName = ? + ` + + await (dbService as MySqlService).query(sql, [userType, username]) + return true + } } catch (error) { console.error('[BIPUsersDAO] Update user type error:', error) return false @@ -225,16 +370,31 @@ export class BIPUsersDAO { */ async updatePassword(username: string, newPassword: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - SET Password = ? - WHERE UserName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + UPDATE ${tableName} + SET Password = @newPassword + WHERE UserName = @username + ` - await mysqlService.query(sql, [newPassword, username]) - return true + await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar }, + newPassword: { value: newPassword, type: sql.NVarChar } + }) + return true + } else { + const sql = ` + UPDATE ${tableName} + SET Password = ? + WHERE UserName = ? + ` + + await (dbService as MySqlService).query(sql, [newPassword, username]) + return true + } } catch (error) { console.error('[BIPUsersDAO] Update password error:', error) return false @@ -248,15 +408,28 @@ export class BIPUsersDAO { */ async deleteUser(username: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - DELETE FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - WHERE UserName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + DELETE FROM ${tableName} + WHERE UserName = @username + ` - await mysqlService.query(sql, [username]) - return true + await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar } + }) + return true + } else { + const sql = ` + DELETE FROM ${tableName} + WHERE UserName = ? + ` + + await (dbService as MySqlService).query(sql, [username]) + return true + } } catch (error) { console.error('[BIPUsersDAO] Delete user error:', error) return false @@ -270,16 +443,30 @@ export class BIPUsersDAO { */ async userExists(username: string): Promise { try { - const mysqlService = await this.getMySqlService() + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() - const sql = ` - SELECT COUNT(*) as count - FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} - WHERE UserName = ? - ` + if (this.dbType === 'sqlserver') { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE UserName = @username + ` - const result = await mysqlService.query(sql, [username]) - return result.rows.length > 0 && (result.rows[0].count as number) > 0 + const result = await (dbService as SqlServerService).queryWithParams(sql, { + username: { value: username, type: sql.NVarChar } + }) + return result.rows.length > 0 && (result.rows[0].count as number) > 0 + } else { + const sql = ` + SELECT COUNT(*) as count + FROM ${tableName} + WHERE UserName = ? + ` + + const result = await (dbService as MySqlService).query(sql, [username]) + return result.rows.length > 0 && (result.rows[0].count as number) > 0 + } } catch (error) { console.error('[BIPUsersDAO] User exists error:', error) return false @@ -294,5 +481,9 @@ export class BIPUsersDAO { await this.mysqlService.disconnect() this.mysqlService = null } + if (this.sqlServerService) { + await this.sqlServerService.disconnect() + this.sqlServerService = null + } } } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 7b5018d..6489851 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -9,10 +9,19 @@ */ import React, { useState, useEffect } from 'react' +import { + LayoutDashboard, + Download, + Trash2, + Settings, + Database, + User, + LogOut +} from 'lucide-react' import LoginDialog from './components/LoginDialog' import UserSelectionDialog, { type UserInfo as SelectedUserInfo } from './components/UserSelectionDialog' -import { ExtractorPage } from './pages/ExtractorPage' -import { CleanerPage } from './pages/CleanerPage' +import ExtractorPage from './pages/ExtractorPage' +import CleanerPage from './pages/CleanerPage' import SettingsPage from './pages/SettingsPage' type Page = 'home' | 'extractor' | 'cleaner' | 'settings' @@ -39,7 +48,7 @@ function App(): React.JSX.Element { const [isSwitchedByAdmin, setIsSwitchedByAdmin] = useState(false) // Navigation state - const [currentPage, setCurrentPage] = useState('home') + const [currentPage, setCurrentPage] = useState('extractor') // Default to extractor for the new layout // Load error message from sessionStorage const showError = (message: string) => { @@ -286,123 +295,90 @@ function App(): React.JSX.Element { // Show main content when authenticated console.log('Render: authenticated, currentUser:', currentUser, 'currentPage:', currentPage) + + const navItems = [ + { id: 'extractor', label: '数据提取 (Extractor)', icon: }, + { id: 'cleaner', label: '物料验证与清理 (Cleaner)', icon: }, + { id: 'settings', label: '系统设置 (Settings)', icon: }, + ]; + return ( -
- {/* Header with user info, navigation and logout */} -
-
- - 欢迎,{currentUser?.username} ({currentUser?.userType}) - - {/* Navigation tabs */} - +
+ + {/* ================= 顶部导航与标题栏 ================= */} +
+
+
+
+
+
+
+ +
setCurrentPage('home')} style={{ WebkitAppRegion: 'no-drag' } as any}> + + ERP Auto +
-
- {shouldShowLogout && ( - - )} + ))} + + +
+
+ + 数据库已连接 +
+
+ + + {currentUser?.username} + + {shouldShowLogout && ( + + )} +
- {/* Main content */} -
- {currentPage === 'home' && ( -
-

主页面

-

- 请使用顶部导航栏切换到 数据提取、物料清理 或 系统设置 页面 -

-
- )} - - {currentPage === 'extractor' && } - {currentPage === 'cleaner' && } - {currentPage === 'settings' && } + {/* ================= 主体内容区域 ================= */} +
+
+ {currentPage === 'home' && ( +
+ +

欢迎使用 ERP Auto

+

+ 自动化处理 ERP 系统中的数据提取和清理任务。请使用上方导航栏选择您需要的功能模块。 +

+
+ )} + {currentPage === 'extractor' && } + {currentPage === 'cleaner' && } + {currentPage === 'settings' && } +
) diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index c8cfc9f..ff47106 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -1,3 +1,4 @@ +@import "tailwindcss"; @import './base.css'; body { @@ -7,7 +8,6 @@ body { overflow: auto; background-image: url('./wavy-lines.svg'); background-size: cover; - user-select: none; } #root { diff --git a/src/renderer/src/components/LoginDialog.tsx b/src/renderer/src/components/LoginDialog.tsx index 173c256..58bf303 100644 --- a/src/renderer/src/components/LoginDialog.tsx +++ b/src/renderer/src/components/LoginDialog.tsx @@ -78,11 +78,11 @@ export const LoginDialog: React.FC = ({
- + setUsername(e.target.value)} placeholder="请输入用户名" @@ -90,11 +90,11 @@ export const LoginDialog: React.FC = ({ />
-
- +
+ setPassword(e.target.value)} placeholder="请输入密码" @@ -103,21 +103,21 @@ export const LoginDialog: React.FC = ({
-
+
- +
diff --git a/src/renderer/src/pages/CleanerPage.tsx b/src/renderer/src/pages/CleanerPage.tsx index 3876b18..12e7b5b 100644 --- a/src/renderer/src/pages/CleanerPage.tsx +++ b/src/renderer/src/pages/CleanerPage.tsx @@ -1,4 +1,20 @@ -import React, { useState, useCallback, useEffect } from 'react' +import React, { useState, useEffect } from 'react' +import { + Play, + ToggleLeft, + ToggleRight, + DatabaseZap, + Layers, + Users, + Search, + CheckSquare, + Square, + EyeOff, + Eye, + HardDrive, + Settings2, + FileSpreadsheet +} from 'lucide-react' /** * Material validation result interface @@ -13,152 +29,105 @@ interface ValidationResult { matchedTypeKeyword?: string } -/** - * CleanerPage with material validation functionality - * - * This component provides the same functionality as the Python MaterialValidationTab: - * - Material validation from database (full table or filtered by ProductionID) - * - Checkbox selection for materials to delete - * - Manager-based filtering (Admin only) - * - Two-phase deletion: confirm (mark in DB) + execute (delete from ERP) - */ -export const CleanerPage: React.FC = () => { +const CleanerPage: React.FC = () => { + // Authentication & permissions + const [isAdmin, setIsAdmin] = useState(false) + const [currentUsername, setCurrentUsername] = useState('') + // State with sessionStorage persistence - const [dryRun] = useState(() => { + const [dryRun, setDryRun] = useState(() => { const saved = sessionStorage.getItem('cleaner_dryRun') return saved ? saved === 'true' : true }) - const [validationMode, setValidationMode] = useState<'database_full' | 'database_filtered'>(() => { - const saved = sessionStorage.getItem('cleaner_validationMode') as 'database_full' | 'database_filtered' - return saved || 'database_filtered' - }) - const [useSharedProductionIds, setUseSharedProductionIds] = useState(() => { - const saved = sessionStorage.getItem('cleaner_useSharedProductionIds') - return saved ? saved === 'true' : true - }) - const [isRunning, setIsRunning] = useState(false) - const [progress, setProgress] = useState<{ message: string; progress?: number } | null>(null) - const [result, setResult] = useState<{ - ordersProcessed: number - materialsDeleted: number - materialsSkipped: number - errors: string[] - details: Array<{ - orderNumber: string - materialsDeleted: number - materialsSkipped: number - errors: string[] - }> - } | null>(null) - const [error, setError] = useState(null) + const [valMode, setValMode] = useState<'full' | 'filtered'>(() => { + const saved = sessionStorage.getItem('cleaner_validationMode') + return saved === 'full' ? 'full' : 'filtered' + }) // Validation state const [validationResults, setValidationResults] = useState([]) const [selectedItems, setSelectedItems] = useState>(new Set()) + const [hiddenItems, setHiddenItems] = useState>(new Set()) const [managers, setManagers] = useState([]) const [selectedManagers, setSelectedManagers] = useState>(new Set()) - const [isAdmin, setIsAdmin] = useState(false) - const [currentUsername, setCurrentUsername] = useState('') + + // Execution state + const [isRunning, setIsRunning] = useState(false) const [isValidationRunning, setIsValidationRunning] = useState(false) - const [validationStats, setValidationStats] = useState<{ - totalRecords: number - matchedCount: number - markedCount: number - } | null>(null) - // Hidden items state (for user mode hide checked feature) - const [hiddenItems, setHiddenItems] = useState>(new Set()) + // Shared Production IDs state + const [sharedProductionIdsCount, setSharedProductionIdsCount] = useState(0) + console.log(sharedProductionIdsCount) - // Filter validation results by selected managers and hidden items - const filteredResults = React.useMemo(() => { - let results = validationResults - if (!isAdmin && currentUsername) { - // Non-admin users only see their own data - results = results.filter(r => r.managerName === currentUsername || !r.managerName) - } else if (managers.length > 0 && selectedManagers.size > 0) { - // Admin users see selected managers' data - results = results.filter( - r => selectedManagers.has(r.managerName) || !r.managerName - ) + // Check admin status and get shared Production IDs on mount + React.useEffect(() => { + const initializePage = async () => { + try { + const admin = await window.electron.auth.isAdmin() + const user = await window.electron.auth.getCurrentUser() + setIsAdmin(admin) + if (user && user.userInfo) { + setCurrentUsername(user.userInfo.username) + if (!admin) { + setSelectedManagers(new Set([user.userInfo.username])) + } + } + + // Load managers + if (admin) { + const resp = await window.electron.materials.getManagers() + setManagers(resp.managers) + setSelectedManagers(new Set(resp.managers)) + } + + // Get shared Production IDs + const result = await window.electron.validation.getSharedProductionIds() + setSharedProductionIdsCount(result.productionIds.length) + } catch (err) { + console.error('Initialization failed:', err) + } } - // Filter out hidden items - results = results.filter(r => !hiddenItems.has(r.materialCode)) - return results - }, [validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems]) + initializePage() + }, []) useEffect(() => { sessionStorage.setItem('cleaner_dryRun', dryRun.toString()) }, [dryRun]) useEffect(() => { - sessionStorage.setItem('cleaner_validationMode', validationMode) - }, [validationMode]) + sessionStorage.setItem('cleaner_validationMode', valMode) + }, [valMode]) - useEffect(() => { - sessionStorage.setItem('cleaner_useSharedProductionIds', useSharedProductionIds.toString()) - }, [useSharedProductionIds]) - - // Shared Production IDs state - const [sharedProductionIdsCount, setSharedProductionIdsCount] = useState(0) - - // Check admin status and get shared Production IDs on mount - React.useEffect(() => { - const initializePage = async () => { - const admin = await window.electron.auth.isAdmin() - const user = await window.electron.auth.getCurrentUser() - setIsAdmin(admin) - if (user && user.userInfo) { - setCurrentUsername(user.userInfo.username) - if (!admin) { - // Non-admin users can only see their own data - setSelectedManagers(new Set([user.userInfo.username])) - } - } - - // Get shared Production IDs - try { - const result = await window.electron.validation.getSharedProductionIds() - setSharedProductionIdsCount(result.productionIds.length) - } catch (err) { - console.error('Failed to get shared Production IDs:', err) - } + // Filter validation results by selected managers and hidden items + const filteredResults = React.useMemo(() => { + let results = validationResults + if (!isAdmin && currentUsername) { + results = results.filter(r => r.managerName === currentUsername || !r.managerName) + } else if (managers.length > 0 && selectedManagers.size > 0) { + results = results.filter( + r => selectedManagers.has(r.managerName) || !r.managerName + ) } - initializePage() - }, []) + results = results.filter(r => !hiddenItems.has(r.materialCode)) + return results + }, [validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems]) - // Load managers for filter (Admin only) - const loadManagers = useCallback(async () => { - if (!isAdmin) return - try { - const response = await window.electron.materials.getManagers() - setManagers(response.managers) - // Select all managers by default - setSelectedManagers(new Set(response.managers)) - } catch (err) { - console.error('Failed to load managers:', err) - } - }, [isAdmin]) - - // Handle validation - const handleValidation = useCallback(async () => { + // Handlers + const handleValidation = async () => { setIsValidationRunning(true) - setError(null) setValidationResults([]) setSelectedItems(new Set()) - setValidationStats(null) + setHiddenItems(new Set()) try { const response = await window.electron.validation.validate({ - mode: validationMode, - useSharedProductionIds: validationMode === 'database_filtered' ? useSharedProductionIds : undefined + mode: valMode === 'full' ? 'database_full' : 'database_filtered', + useSharedProductionIds: valMode === 'filtered' }) if (response.success && response.results) { setValidationResults(response.results) - setValidationStats(response.stats || null) - - // Auto-select items that are already marked for deletion const markedCodes = new Set( response.results .filter(r => r.isMarkedForDeletion) @@ -166,7 +135,6 @@ export const CleanerPage: React.FC = () => { ) setSelectedItems(markedCodes) - // Auto-select managers for admin users if (isAdmin) { const uniqueManagers = new Set( response.results @@ -177,203 +145,103 @@ export const CleanerPage: React.FC = () => { setSelectedManagers(uniqueManagers) } } else { - setError(response.error || '校验失败') + alert(response.error || '校验失败') } } catch (err) { - setError(err instanceof Error ? err.message : '校验过程中发生未知错误') + alert(err instanceof Error ? err.message : '校验过程中发生未知错误') } finally { setIsValidationRunning(false) } - }, [validationMode, useSharedProductionIds, isAdmin]) + } - // Handle checkbox toggle - const handleCheckboxToggle = useCallback((materialCode: string) => { + const handleCheckboxToggle = (materialCode: string) => { setSelectedItems(prev => { const newSet = new Set(prev) - if (newSet.has(materialCode)) { - newSet.delete(materialCode) - } else { - newSet.add(materialCode) - } + if (newSet.has(materialCode)) newSet.delete(materialCode) + else newSet.add(materialCode) return newSet }) - }, []) + } - // Handle select all - const handleSelectAll = useCallback(() => { - setSelectedItems(new Set(filteredResults.map(r => r.materialCode))) - }, [filteredResults]) + const handleConfirmDeletion = async () => { + if (validationResults.length === 0) return alert('没有可处理的数据') - // Handle deselect all - const handleDeselectAll = useCallback(() => { - setSelectedItems(new Set()) - }, []) - - // Handle confirm deletion (mark materials in database) - const handleConfirmDeletion = useCallback(async () => { - if (validationResults.length === 0) { - alert('没有可处理的数据') - return - } - - // Collect all data const materialsToUpsert: { materialCode: string; managerName: string }[] = [] const materialsToDelete: string[] = [] const missingManager: string[] = [] for (const result of validationResults) { - // Skip empty material codes - if (!result.materialCode || !result.materialCode.trim()) { - continue - } + if (!result.materialCode?.trim()) continue - const materialCode = result.materialCode.trim() - const isChecked = selectedItems.has(materialCode) - - if (isChecked) { - // Checked: needs to be upserted - if (!result.managerName || !result.managerName.trim()) { - missingManager.push(materialCode) - } else { - materialsToUpsert.push({ - materialCode, - managerName: result.managerName.trim() - }) - } + const code = result.materialCode.trim() + if (selectedItems.has(code)) { + if (!result.managerName?.trim()) missingManager.push(code) + else materialsToUpsert.push({ materialCode: code, managerName: result.managerName.trim() }) } else { - // Unchecked: needs to be deleted - materialsToDelete.push(materialCode) + materialsToDelete.push(code) } } - // Validate: checked items must have manager names if (missingManager.length > 0) { - const msg = `以下已勾选的记录缺少负责人信息,无法保存:\n\n` + - missingManager.slice(0, 10).join('\n') + - (missingManager.length > 10 ? `\n... 共 ${missingManager.length} 条` : '') - alert(msg) + alert(`以下已勾选的记录缺少负责人信息,无法保存:\n\n${missingManager.slice(0, 10).join('\n')}`) return } - // Check if there's anything to do - if (materialsToUpsert.length === 0 && materialsToDelete.length === 0) { - alert('没有需要处理的记录') - return - } + if (materialsToUpsert.length === 0 && materialsToDelete.length === 0) return alert('没有需要处理的记录') - // Build confirmation message const confirmParts: string[] = [] - if (materialsToUpsert.length > 0) { - confirmParts.push(`写入/更新 ${materialsToUpsert.length} 条记录`) - } - if (materialsToDelete.length > 0) { - confirmParts.push(`删除 ${materialsToDelete.length} 条记录`) - } + if (materialsToUpsert.length > 0) confirmParts.push(`写入/更新 ${materialsToUpsert.length} 条记录`) + if (materialsToDelete.length > 0) confirmParts.push(`删除 ${materialsToDelete.length} 条记录`) - const confirmed = window.confirm(`确认以下操作吗?\n\n${confirmParts.join('\n')}`) - if (!confirmed) { - return - } + if (!window.confirm(`确认以下操作吗?\n\n${confirmParts.join('\n')}`)) return - // Execute the operation try { setIsRunning(true) + let msgParts: string[] = [] - // Upsert selected materials - let upsertSuccess = 0 - let upsertFailed = 0 if (materialsToUpsert.length > 0) { - const upsertResult = await window.electron.materials.upsertBatch(materialsToUpsert) - if (upsertResult.success) { - upsertSuccess = upsertResult.stats?.success || 0 - upsertFailed = upsertResult.stats?.failed || 0 - } else { - throw new Error(upsertResult.error || '写入物料失败') - } + const res = await window.electron.materials.upsertBatch(materialsToUpsert) + if (!res.success) throw new Error(res.error || '写入物料失败') + msgParts.push(`写入/更新成功:${res.stats?.success || 0} 条`) } - // Delete unselected materials - let deleteCount = 0 if (materialsToDelete.length > 0) { - const deleteResult = await window.electron.materials.delete(materialsToDelete) - if (deleteResult.success) { - deleteCount = deleteResult.count || 0 - } else { - throw new Error(deleteResult.error || '删除物料失败') - } + const res = await window.electron.materials.delete(materialsToDelete) + if (!res.success) throw new Error(res.error || '删除物料失败') + msgParts.push(`删除成功:${res.count || 0} 条`) } - // Show result message - const msgParts: string[] = [] - if (upsertSuccess > 0) { - msgParts.push(`写入/更新成功:${upsertSuccess} 条`) - } - if (upsertFailed > 0) { - msgParts.push(`写入/更新失败:${upsertFailed} 条`) - } - if (deleteCount > 0) { - msgParts.push(`删除成功:${deleteCount} 条`) - } + alert(`操作完成!\n\n${msgParts.join('\n')}`) - const msg = `操作完成!\n\n${msgParts.join('\n')}` - - if (upsertFailed > 0) { - alert(`完成(部分失败)\n\n${msg}`) - } else { - alert(msg) + // Reload managers if admin + if (isAdmin) { + const resp = await window.electron.materials.getManagers() + setManagers(resp.managers) } - - // Reload managers to reflect changes - await loadManagers() } catch (err) { - setError(err instanceof Error ? err.message : '操作失败') + alert(err instanceof Error ? err.message : '操作失败') } finally { setIsRunning(false) } - }, [selectedItems, validationResults, loadManagers]) + } - // Handle execute deletion (清理) - const handleExecuteDeletion = useCallback(async () => { + const handleExecuteDeletion = async () => { if (!dryRun) { - const confirmed = window.confirm('警告:正式执行将删除 ERP 系统中的物料数据,是否继续?') - if (!confirmed) return + if (!window.confirm('警告:正式执行将删除 ERP 系统中的物料数据,是否继续?')) return } setIsRunning(true) - setProgress(null) - setResult(null) - setError(null) - try { - // Get cleaner data from backend (order numbers from shared Production IDs + material codes from DB) const cleanerDataResult = await window.electron.validation.getCleanerData() - if (!cleanerDataResult.success) { - setError(cleanerDataResult.error || '获取清理数据失败') - setIsRunning(false) - return + throw new Error(cleanerDataResult.error || '获取清理数据失败') } const orderNumberList = cleanerDataResult.orderNumbers || [] const materialCodeList = cleanerDataResult.materialCodes || [] - if (orderNumberList.length === 0) { - setError('没有订单号数据。请先到数据提取页面输入 Production ID。') - setIsRunning(false) - return - } - - if (materialCodeList.length === 0) { - setError('没有物料代码数据。请确认已在物料清理界面勾选要删除的物料。') - setIsRunning(false) - return - } - - console.log('[CleanerPage] Starting cleaner with:', { - orderNumbers: orderNumberList.length, - materialCodes: materialCodeList.length, - dryRun - }) + if (orderNumberList.length === 0) throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。') + if (materialCodeList.length === 0) throw new Error('没有物料代码数据。请确认已在物料清理界面确认要删除的物料。') const response = await window.electron.cleaner.runCleaner({ orderNumbers: orderNumberList, @@ -382,642 +250,291 @@ export const CleanerPage: React.FC = () => { }) if (response.success && response.data) { - setResult(response.data) + let msg = `清理执行完毕:\n处理订单: ${response.data.ordersProcessed}\n删除物料: ${response.data.materialsDeleted}\n跳过物料: ${response.data.materialsSkipped}` + if (response.data.errors.length) { + msg += `\n错误: ${response.data.errors.join(', ')}` + } + alert(msg) } else { - setError(response.error || '清理失败') + throw new Error(response.error || '清理失败') } } catch (err) { - setError(err instanceof Error ? err.message : '发生未知错误') + alert(err instanceof Error ? err.message : '发生未知错误') } finally { setIsRunning(false) - setProgress(null) } - }, [dryRun]) - - // Handle hide checked items (for user mode) - const handleHideChecked = useCallback(() => { - const checkedCodes = filteredResults - .filter(r => selectedItems.has(r.materialCode)) - .map(r => r.materialCode) - - if (checkedCodes.length === 0) { - setError('没有已勾选的物料') - return - } - - setHiddenItems(prev => new Set([...prev, ...checkedCodes])) - }, [filteredResults, selectedItems]) - - // Handle show all items - const handleShowAll = useCallback(() => { - setHiddenItems(new Set()) - }, []) + } return ( -
-

ERP 物料清理

+
-
- {/* Validation Section (Admin only for mode selection) */} -
-

物料校验

+ {/* 左栏:数据源与执行控制区 */} +
- {isAdmin && ( -
-