Merge branch 'jules-refactor-ui-tailwind-6294750740902394972'

This commit is contained in:
Misaka_Company
2026-03-02 11:15:17 +08:00
14 changed files with 2445 additions and 2741 deletions

View File

@@ -1,6 +1,7 @@
import { resolve } from 'path' import { resolve } from 'path'
import { defineConfig } from 'electron-vite' import { defineConfig } from 'electron-vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
main: {}, main: {},
@@ -11,6 +12,6 @@ export default defineConfig({
'@renderer': resolve('src/renderer/src') '@renderer': resolve('src/renderer/src')
} }
}, },
plugins: [react()] plugins: [react(), tailwindcss()]
} }
}) })

683
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,9 +29,11 @@
"dependencies": { "dependencies": {
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"@tailwindcss/vite": "^4.2.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"lucide-react": "^0.575.0",
"mssql": "^12.2.0", "mssql": "^12.2.0",
"mysql2": "^3.18.2", "mysql2": "^3.18.2",
"playwright": "^1.58.2", "playwright": "^1.58.2",
@@ -49,6 +51,7 @@
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
"autoprefixer": "^10.4.27",
"electron": "^39.2.6", "electron": "^39.2.6",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"electron-vite": "^5.0.0", "electron-vite": "^5.0.0",
@@ -56,9 +59,11 @@
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",
"postcss": "^8.5.6",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"react": "^19.2.1", "react": "^19.2.1",
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.2.6", "vite": "^7.2.6",
"vitest": "^4.0.18" "vitest": "^4.0.18"

View File

@@ -9,6 +9,7 @@
import { ipcMain } from 'electron' import { ipcMain } from 'electron'
import { MySqlService } from '../services/database/mysql' import { MySqlService } from '../services/database/mysql'
import { SqlServerService } from '../services/database/sql-server'
import { MaterialsToBeDeletedDAO } from '../services/database/materials-to-be-deleted-dao' import { MaterialsToBeDeletedDAO } from '../services/database/materials-to-be-deleted-dao'
import { DiscreteMaterialPlanDAO } from '../services/database/discrete-material-plan-dao' import { DiscreteMaterialPlanDAO } from '../services/database/discrete-material-plan-dao'
import type { import type {
@@ -49,9 +50,26 @@ 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> { async function getValidationDatabaseService(): Promise<MySqlService | SqlServerService> {
const dbType = process.env.DB_TYPE?.toLowerCase()
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({ const mysqlService = new MySqlService({
host: process.env.DB_MYSQL_HOST || 'localhost', host: process.env.DB_MYSQL_HOST || 'localhost',
port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10), port: parseInt(process.env.DB_MYSQL_PORT || '3306', 10),
@@ -59,10 +77,27 @@ async function getValidationMySqlService(): Promise<MySqlService> {
password: process.env.DB_PASSWORD || '', password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || '' database: process.env.DB_NAME || ''
}) })
await mysqlService.connect() await mysqlService.connect()
return mysqlService 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
}
/** /**
* Read Production IDs from file * Read Production IDs from file
@@ -96,10 +131,12 @@ function identifyInputType(input: string): 'production_id' | 'order_number' | 'u
*/ */
async function getSourceNumbersFromInputs( async function getSourceNumbersFromInputs(
inputs: string[], inputs: string[],
mysqlService: MySqlService dbService: MySqlService | SqlServerService
): Promise<string[]> { ): Promise<string[]> {
const productionIds: string[] = [] const productionIds: string[] = []
const orderNumbers: string[] = [] const orderNumbers: string[] = []
const dbType = process.env.DB_TYPE?.toLowerCase()
const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql'
for (const item of inputs) { for (const item of inputs) {
const type = identifyInputType(item) const type = identifyInputType(item)
@@ -114,18 +151,39 @@ async function getSourceNumbersFromInputs(
// Table name in MySQL: productionContractData_26年压力表合同数据 // Table name in MySQL: productionContractData_26年压力表合同数据
// Column name: 生产订单号 (SourceNumber) // Column name: 生产订单号 (SourceNumber)
if (productionIds.length > 0) { if (productionIds.length > 0) {
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 placeholders = productionIds.map(() => '?').join(',')
const contractSql = ` const contractSql = `
SELECT DISTINCT 生产订单号 SELECT DISTINCT 生产订单号
FROM productionContractData_26年压力表合同数据 FROM productionContractData_26年压力表合同数据
WHERE 总排号 IN (${placeholders}) WHERE 总排号 IN (${placeholders})
` `
const contractResult = await mysqlService.query(contractSql, productionIds) const contractResult = await (dbService as MySqlService).query(contractSql, productionIds)
const dbOrderNumbers = contractResult.rows.map( const dbOrderNumbers = contractResult.rows.map(
row => row. as string row => row. as string
) )
orderNumbers.push(...dbOrderNumbers) orderNumbers.push(...dbOrderNumbers)
} }
}
// Deduplicate // Deduplicate
return [...new Set(orderNumbers)] return [...new Set(orderNumbers)]
@@ -146,13 +204,15 @@ export function registerValidationHandlers(): void {
_event, _event,
request: ValidationRequest request: ValidationRequest
): Promise<ValidationResponse> => { ): Promise<ValidationResponse> => {
let mysqlService: MySqlService | null = null let dbService: MySqlService | SqlServerService | null = null
try { try {
console.log('[Validation] Starting validation:', request) console.log('[Validation] Starting validation:', request)
// Connect to MySQL // Connect to database
mysqlService = await getValidationMySqlService() dbService = await getValidationDatabaseService()
const dbType = process.env.DB_TYPE?.toLowerCase()
const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql'
let sourceNumbers: string[] | null = null 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( console.log(
`[Validation] Got ${sourceNumbers.length} source numbers from shared Production IDs` `[Validation] Got ${sourceNumbers.length} source numbers from shared Production IDs`
) )
@@ -185,7 +245,7 @@ export function registerValidationHandlers(): void {
console.log( console.log(
`[Validation] Read ${inputs.length} inputs from file` `[Validation] Read ${inputs.length} inputs from file`
) )
sourceNumbers = await getSourceNumbersFromInputs(inputs, mysqlService) sourceNumbers = await getSourceNumbersFromInputs(inputs, dbService)
console.log( console.log(
`[Validation] Got ${sourceNumbers.length} source numbers` `[Validation] Got ${sourceNumbers.length} source numbers`
) )
@@ -194,8 +254,6 @@ export function registerValidationHandlers(): void {
// Get material records from DiscreteMaterialPlanData // Get material records from DiscreteMaterialPlanData
const materialDao = new DiscreteMaterialPlanDAO() const materialDao = new DiscreteMaterialPlanDAO()
// Inject the mysqlService instance
;(materialDao as any).mysqlService = mysqlService
let materialRecords: any[] = [] let materialRecords: any[] = []
@@ -222,24 +280,32 @@ export function registerValidationHandlers(): void {
} }
// Get type keywords from MaterialsTypeToBeDeleted // Get type keywords from MaterialsTypeToBeDeleted
const typeKeywordTableName = getTableName('dbo_MaterialsTypeToBeDeleted')
const typeKeywordSql = ` const typeKeywordSql = `
SELECT MaterialName, ManagerName SELECT MaterialName, ManagerName
FROM dbo_MaterialsTypeToBeDeleted FROM ${typeKeywordTableName}
WHERE MaterialName IS NOT NULL 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 => ({ const typeKeywords = typeKeywordResult.rows.map(row => ({
materialName: row.MaterialName as string, materialName: row.MaterialName as string,
managerName: row.ManagerName as string managerName: row.ManagerName as string
})) }))
// Get marked material codes from MaterialsToBeDeleted // Get marked material codes from MaterialsToBeDeleted
const markedTableName = getTableName('dbo_MaterialsToBeDeleted')
const markedSql = ` const markedSql = `
SELECT MaterialCode, ManagerName SELECT MaterialCode, ManagerName
FROM dbo_MaterialsToBeDeleted FROM ${markedTableName}
WHERE MaterialCode IS NOT NULL AND ManagerName IS NOT NULL 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>() const markedCodesDict = new Map<string, string>()
for (const row of markedResult.rows) { for (const row of markedResult.rows) {
markedCodesDict.set( markedCodesDict.set(
@@ -306,11 +372,11 @@ export function registerValidationHandlers(): void {
error: `Validation failed: ${message}` error: `Validation failed: ${message}`
} }
} finally { } finally {
if (mysqlService) { if (dbService) {
try { try {
await mysqlService.disconnect() await dbService.disconnect()
} catch (closeError) { } 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, _event,
request: MaterialUpsertBatchRequest request: MaterialUpsertBatchRequest
): Promise<MaterialOperationResponse> => { ): Promise<MaterialOperationResponse> => {
let mysqlService: MySqlService | null = null
try { try {
mysqlService = await getValidationMySqlService()
const dao = new MaterialsToBeDeletedDAO() const dao = new MaterialsToBeDeletedDAO()
;(dao as any).mysqlService = mysqlService
const stats = await dao.upsertBatch(request.materials) const stats = await dao.upsertBatch(request.materials)
return { return {
@@ -348,14 +409,6 @@ export function registerValidationHandlers(): void {
success: false, success: false,
error: `Upsert failed: ${message}` 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, _event,
request: MaterialDeleteRequest request: MaterialDeleteRequest
): Promise<MaterialOperationResponse> => { ): Promise<MaterialOperationResponse> => {
let mysqlService: MySqlService | null = null
try { try {
mysqlService = await getValidationMySqlService()
const dao = new MaterialsToBeDeletedDAO() const dao = new MaterialsToBeDeletedDAO()
;(dao as any).mysqlService = mysqlService
const count = await dao.deleteByMaterialCodes(request.materialCodes) const count = await dao.deleteByMaterialCodes(request.materialCodes)
return { return {
@@ -389,14 +437,6 @@ export function registerValidationHandlers(): void {
success: false, success: false,
error: `Delete failed: ${message}` 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( ipcMain.handle(
'materials:getManagers', 'materials:getManagers',
async (_event): Promise<{ managers: string[] }> => { async (_event): Promise<{ managers: string[] }> => {
let mysqlService: MySqlService | null = null
try { try {
mysqlService = await getValidationMySqlService()
const dao = new MaterialsToBeDeletedDAO() const dao = new MaterialsToBeDeletedDAO()
;(dao as any).mysqlService = mysqlService
const managers = await dao.getManagers() const managers = await dao.getManagers()
return { managers } return { managers }
} catch (error) { } catch (error) {
console.error('[Materials] Get managers error:', error) console.error('[Materials] Get managers error:', error)
return { managers: [] } 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, _event,
managerName: string managerName: string
): Promise<{ materials: MaterialRecordSummary[] }> => { ): Promise<{ materials: MaterialRecordSummary[] }> => {
let mysqlService: MySqlService | null = null let dbService: MySqlService | SqlServerService | null = null
try { try {
mysqlService = await getValidationMySqlService() dbService = await getValidationDatabaseService()
const dao = new MaterialsToBeDeletedDAO() const dbType = process.env.DB_TYPE?.toLowerCase()
;(dao as any).mysqlService = mysqlService const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql'
const dao = new MaterialsToBeDeletedDAO()
const materials = await dao.getMaterialsByManager(managerName) const materials = await dao.getMaterialsByManager(managerName)
// Get material codes set for quick lookup // Get material codes set for quick lookup
@@ -454,17 +482,32 @@ export function registerValidationHandlers(): void {
// Enrich with material details from DiscreteMaterialPlanData // Enrich with material details from DiscreteMaterialPlanData
const enrichedMaterials: MaterialRecordSummary[] = [] const enrichedMaterials: MaterialRecordSummary[] = []
const detailTableName = getTableName('dbo_DiscreteMaterialPlanData')
for (const mat of materials) { for (const mat of materials) {
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 = ` const detailSql = `
SELECT MaterialName, Specification, Model SELECT MaterialName, Specification, Model
FROM dbo_DiscreteMaterialPlanData FROM ${detailTableName}
WHERE MaterialCode = ? WHERE MaterialCode = ?
LIMIT 1 LIMIT 1
` `
const detailResult = await mysqlService.query(detailSql, [ detailResult = await (dbService as MySqlService).query(detailSql, [
mat.materialCode mat.materialCode
]) ])
}
enrichedMaterials.push({ enrichedMaterials.push({
materialCode: mat.materialCode, materialCode: mat.materialCode,
@@ -490,11 +533,11 @@ export function registerValidationHandlers(): void {
console.error('[Materials] Get by manager error:', error) console.error('[Materials] Get by manager error:', error)
return { materials: [] } return { materials: [] }
} finally { } finally {
if (mysqlService) { if (dbService) {
try { try {
await mysqlService.disconnect() await dbService.disconnect()
} catch (closeError) { } 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( ipcMain.handle(
'materials:getAll', 'materials:getAll',
async (_event): Promise<{ materials: MaterialRecordSummary[] }> => { async (_event): Promise<{ materials: MaterialRecordSummary[] }> => {
let mysqlService: MySqlService | null = null let dbService: MySqlService | SqlServerService | null = null
try { try {
mysqlService = await getValidationMySqlService() dbService = await getValidationDatabaseService()
const dao = new MaterialsToBeDeletedDAO() const dbType = process.env.DB_TYPE?.toLowerCase()
;(dao as any).mysqlService = mysqlService const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql'
const dao = new MaterialsToBeDeletedDAO()
const materials = await dao.getAllRecords() const materials = await dao.getAllRecords()
const markedCodes = await dao.getAllMaterialCodes() const markedCodes = await dao.getAllMaterialCodes()
const enrichedMaterials: MaterialRecordSummary[] = [] const enrichedMaterials: MaterialRecordSummary[] = []
const detailTableName = getTableName('dbo_DiscreteMaterialPlanData')
for (const mat of materials) { for (const mat of materials) {
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 = ` const detailSql = `
SELECT MaterialName, Specification, Model SELECT MaterialName, Specification, Model
FROM dbo_DiscreteMaterialPlanData FROM ${detailTableName}
WHERE MaterialCode = ? WHERE MaterialCode = ?
LIMIT 1 LIMIT 1
` `
const detailResult = await mysqlService.query(detailSql, [ detailResult = await (dbService as MySqlService).query(detailSql, [
mat.materialCode mat.materialCode
]) ])
}
enrichedMaterials.push({ enrichedMaterials.push({
materialCode: mat.materialCode, materialCode: mat.materialCode,
@@ -554,11 +613,11 @@ export function registerValidationHandlers(): void {
console.error('[Materials] Get all error:', error) console.error('[Materials] Get all error:', error)
return { materials: [] } return { materials: [] }
} finally { } finally {
if (mysqlService) { if (dbService) {
try { try {
await mysqlService.disconnect() await dbService.disconnect()
} catch (closeError) { } 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( ipcMain.handle(
'materials:getStatistics', 'materials:getStatistics',
async (_event): Promise<{ stats: any }> => { async (_event): Promise<{ stats: any }> => {
let mysqlService: MySqlService | null = null
try { try {
mysqlService = await getValidationMySqlService()
const dao = new MaterialsToBeDeletedDAO() const dao = new MaterialsToBeDeletedDAO()
;(dao as any).mysqlService = mysqlService
const stats = await dao.getStatistics() const stats = await dao.getStatistics()
return { stats } return { stats }
} catch (error) { } catch (error) {
console.error('[Materials] Get statistics error:', error) console.error('[Materials] Get statistics error:', error)
return { stats: null } 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[] materialCodes?: string[]
error?: string error?: string
}> => { }> => {
let mysqlService: MySqlService | null = null let dbService: MySqlService | SqlServerService | null = null
const sessionManager = (await import('../services/user/session-manager')).SessionManager.getInstance() const sessionManager = (await import('../services/user/session-manager')).SessionManager.getInstance()
try { try {
@@ -643,11 +689,13 @@ export function registerValidationHandlers(): void {
const isAdmin = userInfo.userType === 'Admin' const isAdmin = userInfo.userType === 'Admin'
const username = userInfo.username const username = userInfo.username
const dbType = process.env.DB_TYPE?.toLowerCase()
const isSqlServer = dbType === 'sqlserver' || dbType === 'mssql'
console.log(`[CleanerData] User: ${username}, isAdmin: ${isAdmin}`) console.log(`[CleanerData] User: ${username}, isAdmin: ${isAdmin}`)
// Connect to MySQL // Connect to database
mysqlService = await getValidationMySqlService() dbService = await getValidationDatabaseService()
// 1. Get order numbers from shared Production IDs // 1. Get order numbers from shared Production IDs
const sharedIds = getSharedProductionIds() const sharedIds = getSharedProductionIds()
@@ -655,36 +703,55 @@ export function registerValidationHandlers(): void {
if (sharedIds.length > 0) { if (sharedIds.length > 0) {
console.log(`[CleanerData] Using ${sharedIds.length} shared Production IDs`) 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`) console.log(`[CleanerData] Got ${orderNumbers.length} order numbers`)
} }
// 2. Get material codes from MaterialsToBeDeleted table // 2. Get material codes from MaterialsToBeDeleted table
let materialCodes: string[] = [] let materialCodes: string[] = []
const markedTableName = getTableName('dbo_MaterialsToBeDeleted')
if (isAdmin) { if (isAdmin) {
// Admin sees all materials // Admin sees all materials
const allCodesSql = ` const allCodesSql = `
SELECT MaterialCode SELECT MaterialCode
FROM dbo_MaterialsToBeDeleted FROM ${markedTableName}
WHERE MaterialCode IS NOT NULL 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 materialCodes = result.rows
.map(row => row.MaterialCode as string) .map(row => row.MaterialCode as string)
.filter(Boolean) .filter(Boolean)
console.log(`[CleanerData] Admin user: got ${materialCodes.length} materials`) console.log(`[CleanerData] Admin user: got ${materialCodes.length} materials`)
} else { } else {
// Regular users only see their own materials // Regular users only see their own materials
if (isSqlServer) {
const sql = require('mssql')
const userMaterialsSql = ` const userMaterialsSql = `
SELECT MaterialCode SELECT MaterialCode
FROM dbo_MaterialsToBeDeleted FROM ${markedTableName}
WHERE ManagerName = ? AND MaterialCode IS NOT NULL WHERE ManagerName = @username AND MaterialCode IS NOT NULL
` `
const result = await mysqlService.query(userMaterialsSql, [username]) const result = await (dbService as SqlServerService).queryWithParams(userMaterialsSql, {
username: { value: username, type: sql.NVarChar }
})
materialCodes = result.rows materialCodes = result.rows
.map(row => row.MaterialCode as string) .map(row => row.MaterialCode as string)
.filter(Boolean) .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`) console.log(`[CleanerData] Regular user: got ${materialCodes.length} materials`)
} }
@@ -701,11 +768,11 @@ export function registerValidationHandlers(): void {
error: `获取清理数据失败:${message}` error: `获取清理数据失败:${message}`
} }
} finally { } finally {
if (mysqlService) { if (dbService) {
try { try {
await mysqlService.disconnect() await dbService.disconnect()
} catch (closeError) { } catch (closeError) {
console.warn('[CleanerData] Error disconnecting MySQL:', closeError) console.warn('[CleanerData] Error disconnecting database:', closeError)
} }
} }
} }

View File

@@ -9,6 +9,8 @@
*/ */
import { MySqlService } from './mysql' import { MySqlService } from './mysql'
import { SqlServerService } from './sql-server'
import sql from 'mssql'
/** /**
* Material plan record interface * Material plan record interface
@@ -50,6 +52,7 @@ export interface MaterialPlanRecord {
* Configuration for DiscreteMaterialPlanData table * Configuration for DiscreteMaterialPlanData table
*/ */
export const DISCRETE_MATERIAL_PLAN_CONFIG = { export const DISCRETE_MATERIAL_PLAN_CONFIG = {
TABLE_NAME_SQLSERVER: '[dbo].[DiscreteMaterialPlanData]',
TABLE_NAME_MYSQL: 'dbo_DiscreteMaterialPlanData', TABLE_NAME_MYSQL: 'dbo_DiscreteMaterialPlanData',
COLUMNS: { COLUMNS: {
ID: 'ID', ID: 'ID',
@@ -90,11 +93,54 @@ export const DISCRETE_MATERIAL_PLAN_CONFIG = {
*/ */
export class DiscreteMaterialPlanDAO { export class DiscreteMaterialPlanDAO {
private mysqlService: MySqlService | null = null 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> { 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()) { if (this.mysqlService && this.mysqlService.isConnected()) {
return this.mysqlService return this.mysqlService
} }
@@ -110,6 +156,7 @@ export class DiscreteMaterialPlanDAO {
await this.mysqlService.connect() await this.mysqlService.connect()
return this.mysqlService return this.mysqlService
} }
}
// ==================== QUERY ALL ==================== // ==================== QUERY ALL ====================
@@ -119,11 +166,15 @@ export class DiscreteMaterialPlanDAO {
*/ */
async queryAll(): Promise<any[]> { async queryAll(): Promise<any[]> {
try { 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 return result.rows
} catch (error) { } catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query all error:', error) console.error('[DiscreteMaterialPlanDAO] Query all error:', error)
@@ -139,7 +190,8 @@ export class DiscreteMaterialPlanDAO {
*/ */
async queryAllDistinctByMaterialCode(): Promise<any[]> { async queryAllDistinctByMaterialCode(): Promise<any[]> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const sql = ` const sql = `
WITH RankedRecords AS ( WITH RankedRecords AS (
@@ -149,7 +201,7 @@ export class DiscreteMaterialPlanDAO {
PARTITION BY MaterialCode PARTITION BY MaterialCode
ORDER BY CreateDate ASC, SequenceNumber ASC ORDER BY CreateDate ASC, SequenceNumber ASC
) AS rn ) AS rn
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE MaterialCode IS NOT NULL WHERE MaterialCode IS NOT NULL
) )
SELECT SELECT
@@ -163,7 +215,10 @@ export class DiscreteMaterialPlanDAO {
WHERE rn = 1 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 return result.rows
} catch (error) { } catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query all distinct by material code error:', error) console.error('[DiscreteMaterialPlanDAO] Query all distinct by material code error:', error)
@@ -184,23 +239,43 @@ export class DiscreteMaterialPlanDAO {
} }
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const batchSize = 2000 const batchSize = 2000
const allResults: any[] = [] const allResults: any[] = []
for (let i = 0; i < sourceNumbers.length; i += batchSize) { for (let i = 0; i < sourceNumbers.length; i += batchSize) {
const batch = sourceNumbers.slice(i, i + batchSize) const batch = sourceNumbers.slice(i, i + batchSize)
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 = `
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 placeholders = batch.map(() => '?').join(',')
const sql = ` const sql = `
SELECT * SELECT *
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE SourceNumber IN (${placeholders}) WHERE SourceNumber IN (${placeholders})
` `
const result = await mysqlService.query(sql, batch) const result = await (dbService as MySqlService).query(sql, batch)
allResults.push(...result.rows) allResults.push(...result.rows)
} }
}
return allResults return allResults
} catch (error) { } catch (error) {
@@ -222,13 +297,21 @@ export class DiscreteMaterialPlanDAO {
} }
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const batchSize = 2000 const batchSize = 2000
const allResults: any[] = [] const allResults: any[] = []
for (let i = 0; i < sourceNumbers.length; i += batchSize) { for (let i = 0; i < sourceNumbers.length; i += batchSize) {
const batch = sourceNumbers.slice(i, i + batchSize) const batch = sourceNumbers.slice(i, i + batchSize)
const placeholders = batch.map(() => '?').join(',')
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 = ` const sql = `
WITH RankedRecords AS ( WITH RankedRecords AS (
@@ -238,7 +321,7 @@ export class DiscreteMaterialPlanDAO {
PARTITION BY MaterialCode PARTITION BY MaterialCode
ORDER BY CreateDate ASC, SequenceNumber ASC ORDER BY CreateDate ASC, SequenceNumber ASC
) AS rn ) AS rn
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE SourceNumber IN (${placeholders}) WHERE SourceNumber IN (${placeholders})
AND MaterialCode IS NOT NULL AND MaterialCode IS NOT NULL
) )
@@ -253,8 +336,37 @@ export class DiscreteMaterialPlanDAO {
WHERE rn = 1 WHERE rn = 1
` `
const result = await mysqlService.query(sql, batch) const result = await (dbService as SqlServerService).queryWithParams(sql, params)
allResults.push(...result.rows) 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 return allResults
@@ -271,16 +383,32 @@ export class DiscreteMaterialPlanDAO {
*/ */
async queryBySourceNumber(sourceNumber: string): Promise<any[]> { async queryBySourceNumber(sourceNumber: string): Promise<any[]> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT * SELECT *
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE SourceNumber = @sourceNumber
`
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 = ? WHERE SourceNumber = ?
` `
const result = await mysqlService.query(sql, [sourceNumber]) const result = await (dbService as MySqlService).query(sql, [sourceNumber])
return result.rows return result.rows
}
} catch (error) { } catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query by source number error:', error) console.error('[DiscreteMaterialPlanDAO] Query by source number error:', error)
return [] return []
@@ -296,16 +424,32 @@ export class DiscreteMaterialPlanDAO {
*/ */
async queryByPlanNumber(planNumber: string): Promise<any[]> { async queryByPlanNumber(planNumber: string): Promise<any[]> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT * SELECT *
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE PlanNumber = @planNumber
`
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 = ? WHERE PlanNumber = ?
` `
const result = await mysqlService.query(sql, [planNumber]) const result = await (dbService as MySqlService).query(sql, [planNumber])
return result.rows return result.rows
}
} catch (error) { } catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query by plan number error:', error) console.error('[DiscreteMaterialPlanDAO] Query by plan number error:', error)
return [] return []
@@ -323,17 +467,37 @@ export class DiscreteMaterialPlanDAO {
} }
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const placeholders = planNumbers.map((_, idx) => `@p${idx}`).join(',')
const params: Record<string, { value: string; type: sql.ISqlType }> = {}
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 placeholders = planNumbers.map(() => '?').join(',')
const sql = ` const sql = `
SELECT * SELECT *
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE PlanNumber IN (${placeholders}) WHERE PlanNumber IN (${placeholders})
` `
const result = await mysqlService.query(sql, planNumbers) const result = await (dbService as MySqlService).query(sql, planNumbers)
return result.rows return result.rows
}
} catch (error) { } catch (error) {
console.error('[DiscreteMaterialPlanDAO] Query by plan numbers error:', error) console.error('[DiscreteMaterialPlanDAO] Query by plan numbers error:', error)
return [] return []
@@ -348,11 +512,15 @@ export class DiscreteMaterialPlanDAO {
*/ */
async countAll(): Promise<number> { async countAll(): Promise<number> {
try { 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 return result.rows.length > 0 ? (result.rows[0].count as number) : 0
} catch (error) { } catch (error) {
console.error('[DiscreteMaterialPlanDAO] Count all error:', error) console.error('[DiscreteMaterialPlanDAO] Count all error:', error)
@@ -367,16 +535,32 @@ export class DiscreteMaterialPlanDAO {
*/ */
async countByPlanNumber(planNumber: string): Promise<number> { async countByPlanNumber(planNumber: string): Promise<number> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE PlanNumber = @planNumber
`
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 = ? WHERE PlanNumber = ?
` `
const result = await mysqlService.query(sql, [planNumber]) const result = await (dbService as MySqlService).query(sql, [planNumber])
return result.rows.length > 0 ? (result.rows[0].count as number) : 0 return result.rows.length > 0 ? (result.rows[0].count as number) : 0
}
} catch (error) { } catch (error) {
console.error('[DiscreteMaterialPlanDAO] Count by plan number error:', error) console.error('[DiscreteMaterialPlanDAO] Count by plan number error:', error)
return 0 return 0
@@ -390,32 +574,59 @@ export class DiscreteMaterialPlanDAO {
*/ */
async getUniqueMaterialNames(sourceNumbers?: string[]): Promise<string[]> { async getUniqueMaterialNames(sourceNumbers?: string[]): Promise<string[]> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
let sql: string
let params: any[] = []
if (sourceNumbers && sourceNumbers.length > 0) { if (sourceNumbers && sourceNumbers.length > 0) {
const placeholders = sourceNumbers.map(() => '?').join(',') if (this.dbType === 'sqlserver') {
sql = ` 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 SELECT DISTINCT MaterialName
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE SourceNumber IN (${placeholders}) WHERE SourceNumber IN (${placeholders})
AND MaterialName IS NOT NULL AND MaterialName IS NOT NULL
` `
params = sourceNumbers
} else {
sql = `
SELECT DISTINCT MaterialName
FROM ${DISCRETE_MATERIAL_PLAN_CONFIG.TABLE_NAME_MYSQL}
WHERE MaterialName IS NOT NULL
`
}
const result = await mysqlService.query(sql, params) const result = await (dbService as SqlServerService).queryWithParams(sql, params)
return result.rows return result.rows
.map(row => row.MaterialName as string) .map(row => row.MaterialName as string)
.filter(Boolean) .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 {
const sql = `
SELECT DISTINCT MaterialName
FROM ${tableName}
WHERE MaterialName IS NOT NULL
`
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) { } catch (error) {
console.error('[DiscreteMaterialPlanDAO] Get unique material names error:', error) console.error('[DiscreteMaterialPlanDAO] Get unique material names error:', error)
return [] return []
@@ -428,7 +639,8 @@ export class DiscreteMaterialPlanDAO {
*/ */
async getStatistics(): Promise<any> { async getStatistics(): Promise<any> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const sql = ` const sql = `
SELECT SELECT
@@ -437,10 +649,13 @@ export class DiscreteMaterialPlanDAO {
COUNT(DISTINCT SourceNumber) as uniqueOrders, COUNT(DISTINCT SourceNumber) as uniqueOrders,
MIN(CreateDate) as earliestRecord, MIN(CreateDate) as earliestRecord,
MAX(CreateDate) as latestRecord 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] : {} return result.rows.length > 0 ? result.rows[0] : {}
} catch (error) { } catch (error) {
console.error('[DiscreteMaterialPlanDAO] Get statistics error:', error) console.error('[DiscreteMaterialPlanDAO] Get statistics error:', error)
@@ -456,5 +671,9 @@ export class DiscreteMaterialPlanDAO {
await this.mysqlService.disconnect() await this.mysqlService.disconnect()
this.mysqlService = null this.mysqlService = null
} }
if (this.sqlServerService) {
await this.sqlServerService.disconnect()
this.sqlServerService = null
}
} }
} }

View File

@@ -9,6 +9,8 @@
*/ */
import { MySqlService } from './mysql' import { MySqlService } from './mysql'
import { SqlServerService } from './sql-server'
import sql from 'mssql'
/** /**
* Material record interface * Material record interface
@@ -41,6 +43,7 @@ export interface MaterialStats {
* Configuration for MaterialsToBeDeleted table * Configuration for MaterialsToBeDeleted table
*/ */
export const MATERIALS_TO_BE_DELETED_CONFIG = { export const MATERIALS_TO_BE_DELETED_CONFIG = {
TABLE_NAME_SQLSERVER: '[dbo].[MaterialsToBeDeleted]',
TABLE_NAME_MYSQL: 'dbo_MaterialsToBeDeleted', TABLE_NAME_MYSQL: 'dbo_MaterialsToBeDeleted',
COLUMNS: { COLUMNS: {
ID: 'ID', ID: 'ID',
@@ -54,11 +57,54 @@ export const MATERIALS_TO_BE_DELETED_CONFIG = {
*/ */
export class MaterialsToBeDeletedDAO { export class MaterialsToBeDeletedDAO {
private mysqlService: MySqlService | null = null 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> { 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()) { if (this.mysqlService && this.mysqlService.isConnected()) {
return this.mysqlService return this.mysqlService
} }
@@ -74,6 +120,7 @@ export class MaterialsToBeDeletedDAO {
await this.mysqlService.connect() await this.mysqlService.connect()
return this.mysqlService return this.mysqlService
} }
}
// ==================== UPSERT (MERGE) ==================== // ==================== UPSERT (MERGE) ====================
@@ -90,15 +137,34 @@ export class MaterialsToBeDeletedDAO {
} }
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const code = materialCode.trim()
const manager = managerName?.trim() || null
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
INSERT INTO ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} (MaterialCode, ManagerName) 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 (?, ?) VALUES (?, ?)
ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName)
` `
await mysqlService.query(sql, [materialCode.trim(), managerName?.trim() || null]) await (dbService as MySqlService).query(sql, [code, manager])
}
return true return true
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Upsert material error:', error) console.error('[MaterialsToBeDeletedDAO] Upsert material error:', error)
@@ -123,7 +189,8 @@ export class MaterialsToBeDeletedDAO {
} }
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
for (const material of materials) { for (const material of materials) {
const materialCode = material.materialCode?.trim() const materialCode = material.materialCode?.trim()
@@ -135,13 +202,29 @@ export class MaterialsToBeDeletedDAO {
} }
try { try {
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
INSERT INTO ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} (MaterialCode, ManagerName) 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 (?, ?) VALUES (?, ?)
ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName)
` `
await mysqlService.query(sql, [materialCode, managerName || null]) await (dbService as MySqlService).query(sql, [materialCode, managerName || null])
}
stats.success++ stats.success++
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Error upserting material:', materialCode, error) console.error('[MaterialsToBeDeletedDAO] Error upserting material:', materialCode, error)
@@ -164,15 +247,19 @@ export class MaterialsToBeDeletedDAO {
*/ */
async getAllMaterialCodes(): Promise<Set<string>> { async getAllMaterialCodes(): Promise<Set<string>> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const sql = ` const sql = `
SELECT MaterialCode SELECT MaterialCode
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE MaterialCode IS NOT NULL 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)) return new Set(result.rows.map(row => row.MaterialCode as string).filter(Boolean))
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get all material codes error:', error) console.error('[MaterialsToBeDeletedDAO] Get all material codes error:', error)
@@ -186,16 +273,20 @@ export class MaterialsToBeDeletedDAO {
*/ */
async getAllRecords(): Promise<MaterialRecord[]> { async getAllRecords(): Promise<MaterialRecord[]> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const sql = ` const sql = `
SELECT ID, MaterialCode, ManagerName SELECT ID, MaterialCode, ManagerName
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE MaterialCode IS NOT NULL WHERE MaterialCode IS NOT NULL
ORDER BY ManagerName, MaterialCode 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 => ({ return result.rows.map(row => ({
id: row.ID as number, id: row.ID as number,
materialCode: row.MaterialCode as string, materialCode: row.MaterialCode as string,
@@ -214,21 +305,42 @@ export class MaterialsToBeDeletedDAO {
*/ */
async getMaterialsByManager(managerName: string): Promise<MaterialRecord[]> { async getMaterialsByManager(managerName: string): Promise<MaterialRecord[]> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT ID, MaterialCode, ManagerName SELECT ID, MaterialCode, ManagerName
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE ManagerName = ? AND MaterialCode IS NOT NULL WHERE ManagerName = @managerName AND MaterialCode IS NOT NULL
ORDER BY MaterialCode ORDER BY MaterialCode
` `
const result = await mysqlService.query(sql, [managerName]) const result = await (dbService as SqlServerService).queryWithParams(sql, {
managerName: { value: managerName, type: sql.NVarChar }
})
return result.rows.map(row => ({ return result.rows.map(row => ({
id: row.ID as number, id: row.ID as number,
materialCode: row.MaterialCode as string, materialCode: row.MaterialCode as string,
managerName: row.ManagerName 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) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get materials by manager error:', error) console.error('[MaterialsToBeDeletedDAO] Get materials by manager error:', error)
return [] return []
@@ -241,16 +353,20 @@ export class MaterialsToBeDeletedDAO {
*/ */
async getManagers(): Promise<string[]> { async getManagers(): Promise<string[]> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const sql = ` const sql = `
SELECT DISTINCT ManagerName SELECT DISTINCT ManagerName
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE ManagerName IS NOT NULL WHERE ManagerName IS NOT NULL
ORDER BY ManagerName 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) return result.rows.map(row => row.ManagerName as string).filter(Boolean)
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get managers error:', error) console.error('[MaterialsToBeDeletedDAO] Get managers error:', error)
@@ -265,15 +381,21 @@ export class MaterialsToBeDeletedDAO {
*/ */
async getRecordByMaterialCode(materialCode: string): Promise<MaterialRecord | null> { async getRecordByMaterialCode(materialCode: string): Promise<MaterialRecord | null> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const code = materialCode.trim()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT ID, MaterialCode, ManagerName SELECT ID, MaterialCode, ManagerName
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE MaterialCode = ? WHERE MaterialCode = @materialCode
` `
const result = await mysqlService.query(sql, [materialCode.trim()]) const result = await (dbService as SqlServerService).queryWithParams(sql, {
materialCode: { value: code, type: sql.NVarChar }
})
if (result.rows.length === 0) { if (result.rows.length === 0) {
return null return null
} }
@@ -284,6 +406,26 @@ export class MaterialsToBeDeletedDAO {
materialCode: row.MaterialCode as string, materialCode: row.MaterialCode as string,
managerName: row.ManagerName 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) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Get record by material code error:', error) console.error('[MaterialsToBeDeletedDAO] Get record by material code error:', error)
return null return null
@@ -299,15 +441,31 @@ export class MaterialsToBeDeletedDAO {
*/ */
async deleteByMaterialCode(materialCode: string): Promise<boolean> { async deleteByMaterialCode(materialCode: string): Promise<boolean> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const code = materialCode.trim()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} DELETE FROM ${tableName}
WHERE MaterialCode = @materialCode
`
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 = ? WHERE MaterialCode = ?
` `
const result = await mysqlService.query(sql, [materialCode.trim()]) const result = await (dbService as MySqlService).query(sql, [code])
return result.rowCount > 0 return result.rowCount > 0
}
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Delete by material code error:', error) console.error('[MaterialsToBeDeletedDAO] Delete by material code error:', error)
return false return false
@@ -321,15 +479,30 @@ export class MaterialsToBeDeletedDAO {
*/ */
async deleteByManager(managerName: string): Promise<number> { async deleteByManager(managerName: string): Promise<number> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} DELETE FROM ${tableName}
WHERE ManagerName = @managerName
`
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 = ? WHERE ManagerName = ?
` `
const result = await mysqlService.query(sql, [managerName]) const result = await (dbService as MySqlService).query(sql, [managerName])
return result.rowCount return result.rowCount
}
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Delete by manager error:', error) console.error('[MaterialsToBeDeletedDAO] Delete by manager error:', error)
return 0 return 0
@@ -342,11 +515,15 @@ export class MaterialsToBeDeletedDAO {
*/ */
async deleteAllMaterials(): Promise<number> { async deleteAllMaterials(): Promise<number> {
try { 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 return result.rowCount
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Delete all materials error:', error) console.error('[MaterialsToBeDeletedDAO] Delete all materials error:', error)
@@ -368,19 +545,38 @@ export class MaterialsToBeDeletedDAO {
const batchSize = 1000 const batchSize = 1000
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
for (let i = 0; i < materialCodes.length; i += batchSize) { for (let i = 0; i < materialCodes.length; i += batchSize) {
const batch = materialCodes.slice(i, i + batchSize) const batch = materialCodes.slice(i, i + batchSize)
const placeholders = batch.map(() => '?').join(',')
if (this.dbType === 'sqlserver') {
const placeholders = batch.map((_, idx) => `@p${idx}`).join(',')
const params: Record<string, { value: string; type: sql.ISqlType }> = {}
batch.forEach((code, idx) => {
params[`p${idx}`] = { value: code.trim(), type: sql.NVarChar }
})
const sql = ` const sql = `
DELETE FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} DELETE FROM ${tableName}
WHERE MaterialCode IN (${placeholders}) WHERE MaterialCode IN (${placeholders})
` `
const result = await mysqlService.query(sql, batch) const result = await (dbService as SqlServerService).queryWithParams(sql, params)
totalDeleted += result.rowCount 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) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Delete by material codes error:', error) console.error('[MaterialsToBeDeletedDAO] Delete by material codes error:', error)
@@ -398,16 +594,33 @@ export class MaterialsToBeDeletedDAO {
*/ */
async materialExists(materialCode: string): Promise<boolean> { async materialExists(materialCode: string): Promise<boolean> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const code = materialCode.trim()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE MaterialCode = @materialCode
`
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 = ? WHERE MaterialCode = ?
` `
const result = await mysqlService.query(sql, [materialCode.trim()]) const result = await (dbService as MySqlService).query(sql, [code])
return result.rows.length > 0 && (result.rows[0].count as number) > 0 return result.rows.length > 0 && (result.rows[0].count as number) > 0
}
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Material exists error:', error) console.error('[MaterialsToBeDeletedDAO] Material exists error:', error)
return false return false
@@ -420,11 +633,15 @@ export class MaterialsToBeDeletedDAO {
*/ */
async countAll(): Promise<number> { async countAll(): Promise<number> {
try { 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 return result.rows.length > 0 ? (result.rows[0].count as number) : 0
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Count all error:', error) console.error('[MaterialsToBeDeletedDAO] Count all error:', error)
@@ -439,16 +656,32 @@ export class MaterialsToBeDeletedDAO {
*/ */
async countByManager(managerName: string): Promise<number> { async countByManager(managerName: string): Promise<number> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE ManagerName = @managerName
`
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 = ? WHERE ManagerName = ?
` `
const result = await mysqlService.query(sql, [managerName]) const result = await (dbService as MySqlService).query(sql, [managerName])
return result.rows.length > 0 ? (result.rows[0].count as number) : 0 return result.rows.length > 0 ? (result.rows[0].count as number) : 0
}
} catch (error) { } catch (error) {
console.error('[MaterialsToBeDeletedDAO] Count by manager error:', error) console.error('[MaterialsToBeDeletedDAO] Count by manager error:', error)
return 0 return 0
@@ -461,30 +694,37 @@ export class MaterialsToBeDeletedDAO {
*/ */
async getStatistics(): Promise<MaterialStats> { async getStatistics(): Promise<MaterialStats> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
// Get total and unique managers // Get total and unique managers
const statsSql = ` const statsSql = `
SELECT SELECT
COUNT(*) as totalMaterials, COUNT(*) as totalMaterials,
COUNT(DISTINCT ManagerName) as uniqueManagers COUNT(DISTINCT ManagerName) as uniqueManagers
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE MaterialCode IS NOT NULL 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] || {} const stats = statsResult.rows[0] || {}
// Get materials per manager // Get materials per manager
const managerSql = ` const managerSql = `
SELECT ManagerName, COUNT(*) as count SELECT ManagerName, COUNT(*) as count
FROM ${MATERIALS_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE ManagerName IS NOT NULL WHERE ManagerName IS NOT NULL
GROUP BY ManagerName GROUP BY ManagerName
ORDER BY count DESC 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 => ({ const materialsPerManager = managerResult.rows.map(row => ({
[row.ManagerName as string]: row.count as number [row.ManagerName as string]: row.count as number
})) }))
@@ -512,5 +752,9 @@ export class MaterialsToBeDeletedDAO {
await this.mysqlService.disconnect() await this.mysqlService.disconnect()
this.mysqlService = null this.mysqlService = null
} }
if (this.sqlServerService) {
await this.sqlServerService.disconnect()
this.sqlServerService = null
}
} }
} }

View File

@@ -98,8 +98,9 @@ export class SqlServerService {
const result = await request.query(sqlString) const result = await request.query(sqlString)
// Convert recordset to array of objects // Convert recordset to array of objects
const columns = result.recordset.columns?.map((col) => col.name) || []
const rows = result.recordset as Record<string, unknown>[] 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 { return {
rows, rows,
@@ -137,8 +138,9 @@ export class SqlServerService {
const result = await request.query(sqlString) const result = await request.query(sqlString)
// Convert recordset to array of objects // Convert recordset to array of objects
const columns = result.recordset.columns?.map((col) => col.name) || []
const rows = result.recordset as Record<string, unknown>[] 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 { return {
rows, rows,

View File

@@ -9,6 +9,8 @@
*/ */
import { MySqlService } from '../database/mysql' import { MySqlService } from '../database/mysql'
import { SqlServerService } from '../database/sql-server'
import sql from 'mssql'
import type { UserInfo } from '../../types/user.types' import type { UserInfo } from '../../types/user.types'
/** /**
@@ -35,11 +37,54 @@ export const BIP_USERS_CONFIG = {
*/ */
export class BIPUsersDAO { export class BIPUsersDAO {
private mysqlService: MySqlService | null = null 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> { 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()) { if (this.mysqlService && this.mysqlService.isConnected()) {
return this.mysqlService return this.mysqlService
} }
@@ -55,6 +100,7 @@ export class BIPUsersDAO {
await this.mysqlService.connect() await this.mysqlService.connect()
return this.mysqlService return this.mysqlService
} }
}
/** /**
* Authenticate a user with username and password * Authenticate a user with username and password
@@ -64,15 +110,20 @@ export class BIPUsersDAO {
*/ */
async authenticate(username: string, password: string): Promise<UserInfo | null> { async authenticate(username: string, password: string): Promise<UserInfo | null> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT ID, UserName, UserType SELECT ID, UserName, UserType
FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE UserName = ? AND Password = ? 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) { if (result.rows.length > 0) {
const row = result.rows[0] const row = result.rows[0]
@@ -83,6 +134,25 @@ export class BIPUsersDAO {
} }
} }
return null 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
}
} catch (error) { } catch (error) {
console.error('[BIPUsersDAO] Authenticate error:', error) console.error('[BIPUsersDAO] Authenticate error:', error)
return null return null
@@ -96,15 +166,19 @@ export class BIPUsersDAO {
*/ */
async authenticateByComputerName(computerName: string): Promise<UserInfo | null> { async authenticateByComputerName(computerName: string): Promise<UserInfo | null> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT ID, UserName, UserType SELECT ID, UserName, UserType
FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE ComputerNmae = ? 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) { if (result.rows.length > 0) {
const row = result.rows[0] const row = result.rows[0]
@@ -115,6 +189,25 @@ export class BIPUsersDAO {
} }
} }
return null 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
}
} catch (error) { } catch (error) {
console.error('[BIPUsersDAO] Authenticate by computer name error:', error) console.error('[BIPUsersDAO] Authenticate by computer name error:', error)
return null return null
@@ -127,15 +220,18 @@ export class BIPUsersDAO {
*/ */
async getAllUsers(): Promise<UserInfo[]> { async getAllUsers(): Promise<UserInfo[]> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
const sql = ` const sql = `
SELECT ID, UserName, UserType, CreateTime SELECT ID, UserName, UserType, CreateTime
FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
ORDER BY UserName 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 => ({ return result.rows.map(row => ({
id: row.ID as number, id: row.ID as number,
@@ -164,29 +260,63 @@ export class BIPUsersDAO {
computerName: string = '' computerName: string = ''
): Promise<boolean> { ): Promise<boolean> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
let sql: string
let params: Record<string, { value: unknown; type?: sql.ISqlType }>
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 {
let sql: string let sql: string
let params: any[] let params: any[]
if (computerName) { if (computerName) {
sql = ` sql = `
INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} INSERT INTO ${tableName}
(UserName, Password, UserType, ComputerNmae) (UserName, Password, UserType, ComputerNmae)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
` `
params = [username, password, userType, computerName] params = [username, password, userType, computerName]
} else { } else {
sql = ` sql = `
INSERT INTO ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} INSERT INTO ${tableName}
(UserName, Password, UserType) (UserName, Password, UserType)
VALUES (?, ?, ?) VALUES (?, ?, ?)
` `
params = [username, password, userType] params = [username, password, userType]
} }
await mysqlService.query(sql, params) await (dbService as MySqlService).query(sql, params)
return true return true
}
} catch (error) { } catch (error) {
console.error('[BIPUsersDAO] Create user error:', error) console.error('[BIPUsersDAO] Create user error:', error)
return false return false
@@ -201,16 +331,31 @@ export class BIPUsersDAO {
*/ */
async updateUserType(username: string, userType: string): Promise<boolean> { async updateUserType(username: string, userType: string): Promise<boolean> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} UPDATE ${tableName}
SET UserType = @userType
WHERE UserName = @username
`
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 = ? SET UserType = ?
WHERE UserName = ? WHERE UserName = ?
` `
await mysqlService.query(sql, [userType, username]) await (dbService as MySqlService).query(sql, [userType, username])
return true return true
}
} catch (error) { } catch (error) {
console.error('[BIPUsersDAO] Update user type error:', error) console.error('[BIPUsersDAO] Update user type error:', error)
return false return false
@@ -225,16 +370,31 @@ export class BIPUsersDAO {
*/ */
async updatePassword(username: string, newPassword: string): Promise<boolean> { async updatePassword(username: string, newPassword: string): Promise<boolean> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
UPDATE ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} UPDATE ${tableName}
SET Password = @newPassword
WHERE UserName = @username
`
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 = ? SET Password = ?
WHERE UserName = ? WHERE UserName = ?
` `
await mysqlService.query(sql, [newPassword, username]) await (dbService as MySqlService).query(sql, [newPassword, username])
return true return true
}
} catch (error) { } catch (error) {
console.error('[BIPUsersDAO] Update password error:', error) console.error('[BIPUsersDAO] Update password error:', error)
return false return false
@@ -248,15 +408,28 @@ export class BIPUsersDAO {
*/ */
async deleteUser(username: string): Promise<boolean> { async deleteUser(username: string): Promise<boolean> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
DELETE FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} DELETE FROM ${tableName}
WHERE UserName = @username
`
await (dbService as SqlServerService).queryWithParams(sql, {
username: { value: username, type: sql.NVarChar }
})
return true
} else {
const sql = `
DELETE FROM ${tableName}
WHERE UserName = ? WHERE UserName = ?
` `
await mysqlService.query(sql, [username]) await (dbService as MySqlService).query(sql, [username])
return true return true
}
} catch (error) { } catch (error) {
console.error('[BIPUsersDAO] Delete user error:', error) console.error('[BIPUsersDAO] Delete user error:', error)
return false return false
@@ -270,16 +443,30 @@ export class BIPUsersDAO {
*/ */
async userExists(username: string): Promise<boolean> { async userExists(username: string): Promise<boolean> {
try { try {
const mysqlService = await this.getMySqlService() const dbService = await this.getDatabaseService()
const tableName = this.getTableName()
if (this.dbType === 'sqlserver') {
const sql = ` const sql = `
SELECT COUNT(*) as count SELECT COUNT(*) as count
FROM ${BIP_USERS_CONFIG.TABLE_NAME_MYSQL} FROM ${tableName}
WHERE UserName = @username
`
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 = ? WHERE UserName = ?
` `
const result = await mysqlService.query(sql, [username]) const result = await (dbService as MySqlService).query(sql, [username])
return result.rows.length > 0 && (result.rows[0].count as number) > 0 return result.rows.length > 0 && (result.rows[0].count as number) > 0
}
} catch (error) { } catch (error) {
console.error('[BIPUsersDAO] User exists error:', error) console.error('[BIPUsersDAO] User exists error:', error)
return false return false
@@ -294,5 +481,9 @@ export class BIPUsersDAO {
await this.mysqlService.disconnect() await this.mysqlService.disconnect()
this.mysqlService = null this.mysqlService = null
} }
if (this.sqlServerService) {
await this.sqlServerService.disconnect()
this.sqlServerService = null
}
} }
} }

View File

@@ -9,10 +9,19 @@
*/ */
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
import {
LayoutDashboard,
Download,
Trash2,
Settings,
Database,
User,
LogOut
} from 'lucide-react'
import LoginDialog from './components/LoginDialog' import LoginDialog from './components/LoginDialog'
import UserSelectionDialog, { type UserInfo as SelectedUserInfo } from './components/UserSelectionDialog' import UserSelectionDialog, { type UserInfo as SelectedUserInfo } from './components/UserSelectionDialog'
import { ExtractorPage } from './pages/ExtractorPage' import ExtractorPage from './pages/ExtractorPage'
import { CleanerPage } from './pages/CleanerPage' import CleanerPage from './pages/CleanerPage'
import SettingsPage from './pages/SettingsPage' import SettingsPage from './pages/SettingsPage'
type Page = 'home' | 'extractor' | 'cleaner' | 'settings' type Page = 'home' | 'extractor' | 'cleaner' | 'settings'
@@ -39,7 +48,7 @@ function App(): React.JSX.Element {
const [isSwitchedByAdmin, setIsSwitchedByAdmin] = useState(false) const [isSwitchedByAdmin, setIsSwitchedByAdmin] = useState(false)
// Navigation state // 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 // Load error message from sessionStorage
const showError = (message: string) => { const showError = (message: string) => {
@@ -286,123 +295,90 @@ function App(): React.JSX.Element {
// Show main content when authenticated // Show main content when authenticated
console.log('Render: authenticated, currentUser:', currentUser, 'currentPage:', currentPage) 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 ( return (
<div style={{ <div className="flex flex-col h-screen bg-slate-50 text-slate-800 font-sans overflow-hidden">
minHeight: '100vh',
backgroundColor: '#f5f7fa' {/* ================= 顶部导航与标题栏 ================= */}
}}> <header
{/* Header with user info, navigation and logout */} className="h-16 bg-slate-900 text-slate-300 flex items-center justify-between px-4 shadow-md z-20 flex-shrink-0"
<header style={{ style={{ WebkitAppRegion: 'drag' } as any}
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'
}}
> >
<div className="flex items-center gap-6">
</button> <div className="flex gap-2 pl-2">
<button <div className="w-3 h-3 rounded-full bg-red-500"></div>
onClick={() => setCurrentPage('extractor')} <div className="w-3 h-3 rounded-full bg-yellow-500"></div>
style={{ <div className="w-3 h-3 rounded-full bg-green-500"></div>
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> </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>
<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 && ( {shouldShowLogout && (
<button onClick={handleLogout} style={{ <button
padding: '8px 16px', onClick={handleLogout}
backgroundColor: '#ff4d4f', className="ml-2 text-slate-400 hover:text-red-400 transition-colors"
color: '#fff', title="退出登录"
border: 'none', >
borderRadius: '6px', <LogOut size={16} />
fontSize: '14px',
cursor: 'pointer'
}}>
退
</button> </button>
)} )}
</div> </div>
</div>
</header> </header>
{/* Main content */} {/* ================= 主体内容区域 ================= */}
<div style={{ padding: '24px' }}> <div className="flex flex-1 overflow-hidden relative">
<main className="flex-1 overflow-y-auto bg-slate-50 p-6">
{currentPage === 'home' && ( {currentPage === 'home' && (
<div> <div className="max-w-4xl mx-auto mt-10 text-center animate-in fade-in slide-in-from-bottom-4 duration-500">
<h1 style={{ fontSize: '24px', color: '#333', marginBottom: '24px' }}></h1> <LayoutDashboard size={48} className="mx-auto text-blue-500 mb-4" />
<p style={{ color: '#666', fontSize: '14px' }}> <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> </p>
</div> </div>
)} )}
{currentPage === 'extractor' && <ExtractorPage />} {currentPage === 'extractor' && <ExtractorPage />}
{currentPage === 'cleaner' && <CleanerPage />} {currentPage === 'cleaner' && <CleanerPage />}
{currentPage === 'settings' && <SettingsPage />} {currentPage === 'settings' && <SettingsPage />}
</main>
</div> </div>
</div> </div>
) )

View File

@@ -1,3 +1,4 @@
@import "tailwindcss";
@import './base.css'; @import './base.css';
body { body {
@@ -7,7 +8,6 @@ body {
overflow: auto; overflow: auto;
background-image: url('./wavy-lines.svg'); background-image: url('./wavy-lines.svg');
background-size: cover; background-size: cover;
user-select: none;
} }
#root { #root {

View File

@@ -78,11 +78,11 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
<div className="login-body"> <div className="login-body">
<div className="form-group"> <div className="form-group">
<label className="form-label">:</label> <label className="form-label text-slate-700">:</label>
<input <input
ref={usernameInputRef} ref={usernameInputRef}
type="text" 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} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
placeholder="请输入用户名" placeholder="请输入用户名"
@@ -90,11 +90,11 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
/> />
</div> </div>
<div className="form-group"> <div className="form-group mt-4">
<label className="form-label">:</label> <label className="form-label text-slate-700">:</label>
<input <input
type="password" 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} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="请输入密码" placeholder="请输入密码"
@@ -103,21 +103,21 @@ export const LoginDialog: React.FC<LoginDialogProps> = ({
</div> </div>
</div> </div>
<div className="login-footer"> <div className="login-footer mt-6 flex justify-end gap-3">
<button <button
className="btn btn-primary" className="btn btn-secondary px-4 py-2 rounded-md bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors"
onClick={handleLogin}
disabled={isLoggingIn}
>
{isLoggingIn ? '登录中...' : '登录'}
</button>
<button
className="btn btn-secondary"
onClick={onCancel} onClick={onCancel}
disabled={isLoggingIn} disabled={isLoggingIn}
> >
</button> </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>
<div className="login-version"> <div className="login-version">

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react' 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) // Extractor result type (matches the type from main process)
interface ExtractorResult { interface ExtractorResult {
@@ -17,7 +17,7 @@ interface ExtractorProgress {
/** /**
* ExtractorPage - Main page for ERP data extraction * ExtractorPage - Main page for ERP data extraction
*/ */
export const ExtractorPage: React.FC = () => { const ExtractorPage: React.FC = () => {
const [orderNumbers, setOrderNumbers] = useState(() => { const [orderNumbers, setOrderNumbers] = useState(() => {
// Restore from sessionStorage on mount // Restore from sessionStorage on mount
return sessionStorage.getItem('extractor_orderNumbers') || '' return sessionStorage.getItem('extractor_orderNumbers') || ''
@@ -30,7 +30,6 @@ export const ExtractorPage: React.FC = () => {
const [progress, setProgress] = useState<ExtractorProgress | null>(null) const [progress, setProgress] = useState<ExtractorProgress | null>(null)
const [result, setResult] = useState<ExtractorResult | null>(null) const [result, setResult] = useState<ExtractorResult | null>(null)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [saveStatus, setSaveStatus] = useState<'saved' | 'unsaved' | 'saving'>('unsaved')
// Save to sessionStorage when orderNumbers changes // Save to sessionStorage when orderNumbers changes
useEffect(() => { useEffect(() => {
@@ -50,29 +49,6 @@ export const ExtractorPage: React.FC = () => {
sessionStorage.setItem('extractor_batchSize', batchSize.toString()) sessionStorage.setItem('extractor_batchSize', batchSize.toString())
}, [batchSize]) }, [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 () => { const handleExtract = async () => {
if (!orderNumbers.trim()) { if (!orderNumbers.trim()) {
setError('请输入至少一个订单号') setError('请输入至少一个订单号')
@@ -121,320 +97,108 @@ export const ExtractorPage: React.FC = () => {
setProgress(null) 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 ( return (
<div className="extractor-page"> <div className="flex h-full gap-6">
<h1 className="page-title">ERP </h1> {/* 左侧:共享数据区 (仅在数据提取页面显示) */}
<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="extractor-content"> <div className="flex-1 flex flex-col p-5 space-y-3 h-full">
{/* Input Section */} <div>
<div className="input-section"> <label className="text-sm font-medium text-slate-700"></label>
<OrderNumberInput <p className="text-xs text-slate-500 leading-relaxed mt-1">
value={orderNumbers}
onChange={setOrderNumbers} </p>
label="订单号列表" </div>
placeholder="请输入订单号,每行一个&#10;例如:&#10;SC70202602120085&#10;SC70202602120120&#10;SC70202602120137"
/> <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"
<div className="batch-size-input"> style={{ userSelect: 'text', cursor: 'text' }}
<label></label> placeholder="PO-20231024-001&#10;PO-20231024-002&#10;PO-20231024-003..."
<input value={orderNumbers}
type="number" onChange={(e) => setOrderNumbers(e.target.value)}
value={batchSize} disabled={isRunning}
onChange={(e) => setBatchSize(parseInt(e.target.value) || 100)} ></textarea>
min={1}
max={1000} <div className="flex items-center justify-between text-xs text-slate-500 pt-2">
disabled={isRunning} <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>
<span className="hint"></span> </div>
</div>
</aside>
{/* 右侧:动态功能面板 */}
<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> </div>
<div className="button-group">
<button <button
className="btn btn-primary" 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} onClick={handleExtract}
disabled={isRunning || !orderNumbers.trim()} disabled={isRunning || !orderNumbers.trim()}
> >
<Play size={20} fill="currentColor" />
{isRunning ? '提取中...' : '开始提取'} {isRunning ? '提取中...' : '开始提取'}
</button> </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>
</div>
</div> </div>
{/* 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>
)}
{/* Error Section */}
{error && (
<div className="error-section">
<h3></h3>
<div className="error-message" title="双击可选中复制">
{error}
</div>
<p className="error-hint">💡 </p>
</div>
)}
{/* Result Section */}
{result && ( {result && (
<div className="result-section"> <div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex flex-col gap-4">
<h3></h3> <h3 className="text-emerald-600 font-semibold text-lg border-b pb-2"></h3>
<div className="result-stats"> <div className="grid grid-cols-3 gap-4">
<div className="stat-item"> <div className="bg-slate-50 p-4 rounded-lg border border-slate-100 flex flex-col items-center justify-center">
<span className="stat-label"></span> <span className="text-slate-500 text-sm"></span>
<span className="stat-value">{result.downloadedFiles.length}</span> <span className="text-2xl font-bold text-slate-800">{result.downloadedFiles.length}</span>
</div> </div>
<div className="stat-item"> <div className="bg-slate-50 p-4 rounded-lg border border-slate-100 flex flex-col items-center justify-center">
<span className="stat-label"></span> <span className="text-slate-500 text-sm"></span>
<span className="stat-value">{result.recordCount}</span> <span className="text-2xl font-bold text-slate-800">{result.recordCount}</span>
</div> </div>
<div className="stat-item"> <div className="bg-slate-50 p-4 rounded-lg border border-slate-100 flex flex-col items-center justify-center">
<span className="stat-label"></span> <span className="text-slate-500 text-sm"></span>
<span className="stat-value error">{result.errors.length}</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>
</div>
)}
{result.downloadedFiles.length > 0 && ( <div className="bg-slate-900 rounded-xl shadow-lg border border-slate-700 overflow-hidden flex flex-col h-[500px]">
<div className="file-list"> <div className="bg-slate-800 px-4 py-2 flex items-center justify-between border-b border-slate-700">
<h4></h4> <div className="flex items-center gap-2 text-slate-400 text-sm">
<ul> <Terminal size={16} />
{result.downloadedFiles.map((file, index) => ( <span> (Console)</span>
<li key={index}>{file}</li> </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>
))} ))}
</ul>
</div> </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>
)}
</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> </div>
) )
} }

File diff suppressed because it is too large Load Diff