Merge branch 'jules-refactor-ui-tailwind-6294750740902394972'
This commit is contained in:
@@ -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()]
|
||||
}
|
||||
})
|
||||
|
||||
683
package-lock.json
generated
683
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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<MySqlService> {
|
||||
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<MySqlService | SqlServerService> {
|
||||
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<string[]> {
|
||||
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<string, { value: string; type: any }> = {}
|
||||
|
||||
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<ValidationResponse> => {
|
||||
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<string, string>()
|
||||
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<MaterialOperationResponse> => {
|
||||
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<MaterialOperationResponse> => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MySqlService> {
|
||||
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<MySqlService | SqlServerService> {
|
||||
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<any[]> {
|
||||
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<any[]> {
|
||||
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<string, { value: string; type: sql.ISqlType }> = {}
|
||||
|
||||
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<string, { value: string; type: sql.ISqlType }> = {}
|
||||
|
||||
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<any[]> {
|
||||
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<any[]> {
|
||||
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<string, { value: string; type: sql.ISqlType }> = {}
|
||||
|
||||
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<number> {
|
||||
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<number> {
|
||||
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<string[]> {
|
||||
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<string, { value: string; type: sql.ISqlType }> = {}
|
||||
|
||||
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<any> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MySqlService> {
|
||||
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<MySqlService | SqlServerService> {
|
||||
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<Set<string>> {
|
||||
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<MaterialRecord[]> {
|
||||
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<MaterialRecord[]> {
|
||||
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<string[]> {
|
||||
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<MaterialRecord | null> {
|
||||
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<boolean> {
|
||||
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<number> {
|
||||
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<number> {
|
||||
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<string, { value: string; type: sql.ISqlType }> = {}
|
||||
|
||||
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<boolean> {
|
||||
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<number> {
|
||||
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<number> {
|
||||
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<MaterialStats> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, unknown>[]
|
||||
// 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<string, unknown>[]
|
||||
// Extract column names from the first row if available
|
||||
const columns = rows.length > 0 ? Object.keys(rows[0]) : []
|
||||
|
||||
return {
|
||||
rows,
|
||||
|
||||
@@ -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<MySqlService> {
|
||||
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<MySqlService | SqlServerService> {
|
||||
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<UserInfo | null> {
|
||||
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<UserInfo | null> {
|
||||
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<UserInfo[]> {
|
||||
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<boolean> {
|
||||
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<string, { value: unknown; type?: sql.ISqlType }>
|
||||
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Page>('home')
|
||||
const [currentPage, setCurrentPage] = useState<Page>('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: <Download size={18} /> },
|
||||
{ id: 'cleaner', label: '物料验证与清理 (Cleaner)', icon: <Trash2 size={18} /> },
|
||||
{ id: 'settings', label: '系统设置 (Settings)', icon: <Settings size={18} /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: '100vh',
|
||||
backgroundColor: '#f5f7fa'
|
||||
}}>
|
||||
{/* Header with user info, navigation and logout */}
|
||||
<header style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '12px 24px',
|
||||
backgroundColor: '#fff',
|
||||
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
|
||||
marginBottom: '16px'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '24px' }}>
|
||||
<span style={{
|
||||
fontSize: '14px',
|
||||
color: '#666'
|
||||
}}>
|
||||
欢迎,{currentUser?.username} ({currentUser?.userType})
|
||||
</span>
|
||||
{/* Navigation tabs */}
|
||||
<nav style={{ display: 'flex', gap: '8px' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('home')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: currentPage === 'home' ? '#1890ff' : '#f5f5f5',
|
||||
color: currentPage === 'home' ? '#fff' : '#666',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
>
|
||||
主页
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('extractor')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: currentPage === 'extractor' ? '#1890ff' : '#f5f5f5',
|
||||
color: currentPage === 'extractor' ? '#fff' : '#666',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
>
|
||||
数据提取
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('cleaner')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: currentPage === 'cleaner' ? '#1890ff' : '#f5f5f5',
|
||||
color: currentPage === 'cleaner' ? '#fff' : '#666',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
>
|
||||
物料清理
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('settings')}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: currentPage === 'settings' ? '#1890ff' : '#f5f5f5',
|
||||
color: currentPage === 'settings' ? '#fff' : '#666',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.3s'
|
||||
}}
|
||||
>
|
||||
系统设置
|
||||
</button>
|
||||
</nav>
|
||||
<div className="flex flex-col h-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
|
||||
|
||||
{/* ================= 顶部导航与标题栏 ================= */}
|
||||
<header
|
||||
className="h-16 bg-slate-900 text-slate-300 flex items-center justify-between px-4 shadow-md z-20 flex-shrink-0"
|
||||
style={{ WebkitAppRegion: 'drag' } as any}
|
||||
>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex gap-2 pl-2">
|
||||
<div className="w-3 h-3 rounded-full bg-red-500"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-yellow-500"></div>
|
||||
<div className="w-3 h-3 rounded-full bg-green-500"></div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-white font-bold text-lg cursor-pointer" onClick={() => setCurrentPage('home')} style={{ WebkitAppRegion: 'no-drag' } as any}>
|
||||
<LayoutDashboard size={22} className="text-blue-500" />
|
||||
<span>ERP Auto</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{shouldShowLogout && (
|
||||
<button onClick={handleLogout} style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#ff4d4f',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
退出登录
|
||||
|
||||
<nav className="flex items-center gap-2 bg-slate-800 p-1 rounded-lg" style={{ WebkitAppRegion: 'no-drag' } as any}>
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setCurrentPage(item.id as Page)}
|
||||
className={`flex items-center gap-2 px-4 py-1.5 rounded-md text-sm font-medium transition-all ${
|
||||
currentPage === item.id
|
||||
? 'bg-blue-600 text-white shadow'
|
||||
: 'text-slate-400 hover:text-white hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
{item.label}
|
||||
</button>
|
||||
)}
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="flex items-center gap-4 text-sm" style={{ WebkitAppRegion: 'no-drag' } as any}>
|
||||
<div className="flex items-center gap-2 text-xs bg-slate-800 px-3 py-1.5 rounded-full border border-slate-700">
|
||||
<Database size={14} className="text-green-500" />
|
||||
<span className="text-slate-300">数据库已连接</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-slate-800 px-3 py-1.5 rounded-full">
|
||||
<User size={16} className="text-slate-400" />
|
||||
<span className="font-medium text-slate-200" title={`User Type: ${currentUser?.userType}`}>
|
||||
{currentUser?.username}
|
||||
</span>
|
||||
{shouldShowLogout && (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="ml-2 text-slate-400 hover:text-red-400 transition-colors"
|
||||
title="退出登录"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content */}
|
||||
<div style={{ padding: '24px' }}>
|
||||
{currentPage === 'home' && (
|
||||
<div>
|
||||
<h1 style={{ fontSize: '24px', color: '#333', marginBottom: '24px' }}>主页面</h1>
|
||||
<p style={{ color: '#666', fontSize: '14px' }}>
|
||||
请使用顶部导航栏切换到 数据提取、物料清理 或 系统设置 页面
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentPage === 'extractor' && <ExtractorPage />}
|
||||
{currentPage === 'cleaner' && <CleanerPage />}
|
||||
{currentPage === 'settings' && <SettingsPage />}
|
||||
{/* ================= 主体内容区域 ================= */}
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
<main className="flex-1 overflow-y-auto bg-slate-50 p-6">
|
||||
{currentPage === 'home' && (
|
||||
<div className="max-w-4xl mx-auto mt-10 text-center animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<LayoutDashboard size={48} className="mx-auto text-blue-500 mb-4" />
|
||||
<h1 className="text-3xl font-bold text-slate-800 mb-4">欢迎使用 ERP Auto</h1>
|
||||
<p className="text-slate-500 text-lg max-w-2xl mx-auto">
|
||||
自动化处理 ERP 系统中的数据提取和清理任务。请使用上方导航栏选择您需要的功能模块。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{currentPage === 'extractor' && <ExtractorPage />}
|
||||
{currentPage === 'cleaner' && <CleanerPage />}
|
||||
{currentPage === 'settings' && <SettingsPage />}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -78,11 +78,11 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
|
||||
|
||||
<div className="login-body">
|
||||
<div className="form-group">
|
||||
<label className="form-label">用户名:</label>
|
||||
<label className="form-label text-slate-700">用户名:</label>
|
||||
<input
|
||||
ref={usernameInputRef}
|
||||
type="text"
|
||||
className="form-input"
|
||||
className="form-input border border-slate-300 rounded-md p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
placeholder="请输入用户名"
|
||||
@@ -90,11 +90,11 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">密码:</label>
|
||||
<div className="form-group mt-4">
|
||||
<label className="form-label text-slate-700">密码:</label>
|
||||
<input
|
||||
type="password"
|
||||
className="form-input"
|
||||
className="form-input border border-slate-300 rounded-md p-2 w-full focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="请输入密码"
|
||||
@@ -103,21 +103,21 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-footer">
|
||||
<div className="login-footer mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleLogin}
|
||||
disabled={isLoggingIn}
|
||||
>
|
||||
{isLoggingIn ? '登录中...' : '登录'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
className="btn btn-secondary px-4 py-2 rounded-md bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors"
|
||||
onClick={onCancel}
|
||||
disabled={isLoggingIn}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary px-4 py-2 rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50"
|
||||
onClick={handleLogin}
|
||||
disabled={isLoggingIn}
|
||||
>
|
||||
{isLoggingIn ? '登录中...' : '登录'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="login-version">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { OrderNumberInput } from '../components/OrderNumberInput'
|
||||
import { Download, Play, Terminal } from 'lucide-react'
|
||||
|
||||
// Extractor result type (matches the type from main process)
|
||||
interface ExtractorResult {
|
||||
@@ -17,7 +17,7 @@ interface ExtractorProgress {
|
||||
/**
|
||||
* ExtractorPage - Main page for ERP data extraction
|
||||
*/
|
||||
export const ExtractorPage: React.FC = () => {
|
||||
const ExtractorPage: React.FC = () => {
|
||||
const [orderNumbers, setOrderNumbers] = useState(() => {
|
||||
// Restore from sessionStorage on mount
|
||||
return sessionStorage.getItem('extractor_orderNumbers') || ''
|
||||
@@ -30,7 +30,6 @@ export const ExtractorPage: React.FC = () => {
|
||||
const [progress, setProgress] = useState<ExtractorProgress | null>(null)
|
||||
const [result, setResult] = useState<ExtractorResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [saveStatus, setSaveStatus] = useState<'saved' | 'unsaved' | 'saving'>('unsaved')
|
||||
|
||||
// Save to sessionStorage when orderNumbers changes
|
||||
useEffect(() => {
|
||||
@@ -50,29 +49,6 @@ export const ExtractorPage: React.FC = () => {
|
||||
sessionStorage.setItem('extractor_batchSize', batchSize.toString())
|
||||
}, [batchSize])
|
||||
|
||||
const saveSharedProductionIds = async () => {
|
||||
if (!orderNumbers.trim()) {
|
||||
setError('请输入至少一个订单号')
|
||||
return
|
||||
}
|
||||
|
||||
setSaveStatus('saving')
|
||||
try {
|
||||
const orderNumberList = orderNumbers
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
|
||||
await window.electron.validation.setSharedProductionIds(orderNumberList)
|
||||
console.log(`[Extractor] Stored ${orderNumberList.length} Production IDs for sharing`)
|
||||
setSaveStatus('saved')
|
||||
setError(null)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '保存失败')
|
||||
setSaveStatus('unsaved')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExtract = async () => {
|
||||
if (!orderNumbers.trim()) {
|
||||
setError('请输入至少一个订单号')
|
||||
@@ -121,320 +97,108 @@ export const ExtractorPage: React.FC = () => {
|
||||
setProgress(null)
|
||||
}
|
||||
|
||||
const [logs, setLogs] = useState<string[]>([
|
||||
'[10:00:01] [System] 提取引擎已就绪。',
|
||||
'[10:00:02] [Info] 等待读取生产订单列表...'
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (progress) {
|
||||
setLogs(prev => [...prev, `[${new Date().toLocaleTimeString()}] [Info] ${progress.message}`])
|
||||
}
|
||||
}, [progress])
|
||||
|
||||
return (
|
||||
<div className="extractor-page">
|
||||
<h1 className="page-title">ERP 数据提取</h1>
|
||||
|
||||
<div className="extractor-content">
|
||||
{/* Input Section */}
|
||||
<div className="input-section">
|
||||
<OrderNumberInput
|
||||
value={orderNumbers}
|
||||
onChange={setOrderNumbers}
|
||||
label="订单号列表"
|
||||
placeholder="请输入订单号,每行一个 例如: SC70202602120085 SC70202602120120 SC70202602120137"
|
||||
/>
|
||||
|
||||
<div className="batch-size-input">
|
||||
<label>批量大小:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={batchSize}
|
||||
onChange={(e) => setBatchSize(parseInt(e.target.value) || 100)}
|
||||
min={1}
|
||||
max={1000}
|
||||
disabled={isRunning}
|
||||
/>
|
||||
<span className="hint">每批处理的订单数量</span>
|
||||
<div className="flex h-full gap-6">
|
||||
{/* 左侧:共享数据区 (仅在数据提取页面显示) */}
|
||||
<aside className="w-80 bg-white border border-slate-200 flex flex-col shadow-sm z-10 flex-shrink-0 animate-in slide-in-from-left duration-300 rounded-xl overflow-hidden h-full">
|
||||
<div className="flex-1 flex flex-col p-5 space-y-3 h-full">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-slate-700">支持输入总排号或者生产订单号</label>
|
||||
<p className="text-xs text-slate-500 leading-relaxed mt-1">
|
||||
在此输入的数据将在“数据提取”与“物料清理”模块中自动共享,每行一个。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="button-group">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleExtract}
|
||||
disabled={isRunning || !orderNumbers.trim()}
|
||||
>
|
||||
{isRunning ? '提取中...' : '开始提取'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={saveSharedProductionIds}
|
||||
disabled={isRunning || !orderNumbers.trim() || saveStatus === 'saved'}
|
||||
title="保存订单号以便在清理页面使用"
|
||||
>
|
||||
{saveStatus === 'saving' ? '保存中...' : saveStatus === 'saved' ? '已保存' : '保存为共享 ID'}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={handleReset} disabled={isRunning}>
|
||||
重置
|
||||
</button>
|
||||
<textarea
|
||||
className="flex-1 w-full border border-slate-300 rounded-lg p-3 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none shadow-inner bg-slate-50 h-full"
|
||||
style={{ userSelect: 'text', cursor: 'text' }}
|
||||
placeholder="PO-20231024-001 PO-20231024-002 PO-20231024-003..."
|
||||
value={orderNumbers}
|
||||
onChange={(e) => setOrderNumbers(e.target.value)}
|
||||
disabled={isRunning}
|
||||
></textarea>
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-slate-500 pt-2">
|
||||
<span>共解析: <strong className="text-slate-700">{orderNumbers.split('\n').filter(l => l.trim()).length}</strong> 个订单</span>
|
||||
<button className="text-slate-400 hover:text-slate-600" onClick={handleReset} disabled={isRunning}>清空</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Progress Section */}
|
||||
{progress && (
|
||||
<div className="progress-section">
|
||||
<div className="progress-bar">
|
||||
<div className="progress-fill" style={{ width: `${progress.progress}%` }} />
|
||||
</div>
|
||||
<p className="progress-message">{progress.message}</p>
|
||||
{/* 右侧:动态功能面板 */}
|
||||
<div className="flex-1 max-w-4xl space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold flex items-center gap-2 text-slate-800 mb-1">
|
||||
<Download size={20} className="text-blue-600" />
|
||||
批量数据提取
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500">将遍历左侧列表中的所有生产订单,依次自动执行数据导出并保存。</p>
|
||||
{error && <p className="text-sm text-red-500 mt-2">{error}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Section */}
|
||||
{error && (
|
||||
<div className="error-section">
|
||||
<h3>错误</h3>
|
||||
<div className="error-message" title="双击可选中复制">
|
||||
{error}
|
||||
</div>
|
||||
<p className="error-hint">💡 提示:双击错误信息可选中复制</p>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white px-8 py-3 rounded-lg flex items-center gap-2 font-medium shadow-sm transition-colors text-base"
|
||||
onClick={handleExtract}
|
||||
disabled={isRunning || !orderNumbers.trim()}
|
||||
>
|
||||
<Play size={20} fill="currentColor" />
|
||||
{isRunning ? '提取中...' : '开始提取'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Result Section */}
|
||||
{/* 结果展示 */}
|
||||
{result && (
|
||||
<div className="result-section">
|
||||
<h3>提取结果</h3>
|
||||
<div className="result-stats">
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">下载文件数</span>
|
||||
<span className="stat-value">{result.downloadedFiles.length}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">记录数</span>
|
||||
<span className="stat-value">{result.recordCount}</span>
|
||||
</div>
|
||||
<div className="stat-item">
|
||||
<span className="stat-label">错误数</span>
|
||||
<span className="stat-value error">{result.errors.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{result.downloadedFiles.length > 0 && (
|
||||
<div className="file-list">
|
||||
<h4>下载的文件</h4>
|
||||
<ul>
|
||||
{result.downloadedFiles.map((file, index) => (
|
||||
<li key={index}>{file}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{result.errors.length > 0 && (
|
||||
<div className="error-list">
|
||||
<h4>错误列表</h4>
|
||||
<ul>
|
||||
{result.errors.map((err, index) => (
|
||||
<li key={index} className="error-item">
|
||||
{err}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex flex-col gap-4">
|
||||
<h3 className="text-emerald-600 font-semibold text-lg border-b pb-2">提取结果</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="bg-slate-50 p-4 rounded-lg border border-slate-100 flex flex-col items-center justify-center">
|
||||
<span className="text-slate-500 text-sm">下载文件数</span>
|
||||
<span className="text-2xl font-bold text-slate-800">{result.downloadedFiles.length}</span>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-4 rounded-lg border border-slate-100 flex flex-col items-center justify-center">
|
||||
<span className="text-slate-500 text-sm">记录数</span>
|
||||
<span className="text-2xl font-bold text-slate-800">{result.recordCount}</span>
|
||||
</div>
|
||||
<div className="bg-slate-50 p-4 rounded-lg border border-slate-100 flex flex-col items-center justify-center">
|
||||
<span className="text-slate-500 text-sm">错误数</span>
|
||||
<span className={`text-2xl font-bold ${result.errors.length > 0 ? 'text-red-500' : 'text-slate-800'}`}>{result.errors.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.extractor-page {
|
||||
padding: 24px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.extractor-content {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
padding: 24px;
|
||||
}
|
||||
.input-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.batch-size-input {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.batch-size-input label {
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
.batch-size-input input {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
width: 100px;
|
||||
}
|
||||
.batch-size-input input:disabled {
|
||||
background: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.batch-size-input .hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
.btn {
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background: #40a9ff;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background: #e8e8e8;
|
||||
}
|
||||
.btn-secondary[data-saved="true"] {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
.progress-section {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 8px;
|
||||
background: #e8e8e8;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #1890ff, #40a9ff);
|
||||
transition: width 0.3s;
|
||||
}
|
||||
.progress-message {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
.error-section {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #fff1f0;
|
||||
border: 1px solid #ffa39e;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.error-section h3 {
|
||||
color: #ff4d4f;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.error-message {
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
border: 1px solid #ffccc7;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: text;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.error-message:hover {
|
||||
background: #fffbfc;
|
||||
}
|
||||
.error-hint {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin: 8px 0 0 0;
|
||||
}
|
||||
.result-section {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.result-section h3 {
|
||||
color: #52c41a;
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.result-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.stat-value.error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
.file-list, .error-list {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.file-list h4, .error-list h4 {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.file-list ul, .error-list ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.file-list li, .error-list li {
|
||||
padding: 6px 12px;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
font-size: 13px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
}
|
||||
.error-list li {
|
||||
background: #fff1f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
`}</style>
|
||||
<div className="bg-slate-900 rounded-xl shadow-lg border border-slate-700 overflow-hidden flex flex-col h-[500px]">
|
||||
<div className="bg-slate-800 px-4 py-2 flex items-center justify-between border-b border-slate-700">
|
||||
<div className="flex items-center gap-2 text-slate-400 text-sm">
|
||||
<Terminal size={16} />
|
||||
<span>执行日志 (Console)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-slate-500">进度: {progress?.progress || 0}%</span>
|
||||
<button className="text-xs text-slate-400 hover:text-white transition-colors" onClick={() => setLogs([])}>清空</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 p-4 font-mono text-sm overflow-y-auto leading-relaxed">
|
||||
{logs.map((log, index) => (
|
||||
<div key={index} className={log.includes('[System]') ? 'text-emerald-500' : log.includes('error') || log.includes('失败') ? 'text-red-400' : 'text-slate-400'}>
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user