diff --git a/src/main/ipc/index.ts b/src/main/ipc/index.ts index 8991a36..8631b8f 100644 --- a/src/main/ipc/index.ts +++ b/src/main/ipc/index.ts @@ -10,6 +10,7 @@ import { registerDatabaseHandlers } from './database-handler' import { registerResolverHandlers } from './resolver-handler' import { registerAuthHandlers } from './auth-handler' import { registerValidationHandlers } from './validation-handler' +import { registerSettingsHandlers } from './settings-handler' /** * Register all IPC handlers @@ -22,4 +23,5 @@ export function registerIpcHandlers(): void { registerResolverHandlers() registerAuthHandlers() registerValidationHandlers() + registerSettingsHandlers() } diff --git a/src/main/ipc/settings-handler.ts b/src/main/ipc/settings-handler.ts new file mode 100644 index 0000000..91b91d7 --- /dev/null +++ b/src/main/ipc/settings-handler.ts @@ -0,0 +1,255 @@ +/** + * Settings IPC Handler + * + * Provides IPC handlers for settings management: + * - Get/set settings + * - Reset to defaults + * - Test ERP connection + * - Test database connection + */ + +import { ipcMain } from 'electron' +import { ConfigManager } from '../services/config/config-manager' +import { SessionManager } from '../services/user/session-manager' +import { ErpAuthService } from '../services/erp/erp-auth' +import { MySqlService } from '../services/database/mysql' +import { SqlServerService } from '../services/database/sql-server' +import type { + SettingsData, + UserType, + ConnectionTestResult, + SaveSettingsResult +} from '../types/settings.types' + +/** + * Filter settings by user type + * Admin users get all settings, User users get limited settings + */ +function filterSettingsByUserType( + settings: SettingsData, + userType: UserType +): SettingsData { + if (userType === 'Admin') { + return settings // Return all settings for Admin + } + + // User users get limited settings + return { + erp: { + username: settings.erp.username, + password: settings.erp.password, + headless: settings.erp.headless, + url: settings.erp.url, + ignoreHttpsErrors: settings.erp.ignoreHttpsErrors, + autoCloseBrowser: settings.erp.autoCloseBrowser + }, + paths: settings.paths, + execution: settings.execution, + // Include minimal required fields for other sections + database: settings.database, + extraction: settings.extraction, + validation: settings.validation, + ui: settings.ui + } +} + +/** + * Register IPC handlers for settings management + */ +export function registerSettingsHandlers(): void { + const configManager = ConfigManager.getInstance() + const sessionManager = SessionManager.getInstance() + + /** + * Get current user type + */ + ipcMain.handle('settings:getUserType', async (): Promise => { + return (sessionManager.getUserType() as UserType) || 'Guest' + }) + + /** + * Get settings (filtered by user type) + */ + ipcMain.handle('settings:getSettings', async (): Promise => { + const userType = (sessionManager.getUserType() as UserType) || 'Guest' + const settings = configManager.getAllSettings() + return filterSettingsByUserType(settings, userType) + }) + + /** + * Save settings + */ + ipcMain.handle( + 'settings:saveSettings', + async (_event, settings: SettingsData): Promise => { + try { + const success = await configManager.saveAllSettings(settings) + if (success) { + return { success: true } + } else { + return { success: false, error: '保存设置失败' } + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + return { success: false, error: `保存设置失败:${message}` } + } + } + ) + + /** + * Reset to default settings (Admin only) + */ + ipcMain.handle( + 'settings:resetDefaults', + async (): Promise => { + try { + const userType = sessionManager.getUserType() + if (userType !== 'Admin') { + return { success: false, error: '只有管理员可以恢复默认设置' } + } + + configManager.resetToDefaults() + const success = await configManager.save() + if (success) { + return { success: true } + } else { + return { success: false, error: '恢复默认设置失败' } + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + return { success: false, error: `恢复默认设置失败:${message}` } + } + } + ) + + /** + * Test ERP connection + */ + ipcMain.handle( + 'settings:testErpConnection', + async (): Promise => { + try { + const settings = configManager.getAllSettings() + const erpConfig = settings.erp + + if (!erpConfig.url || !erpConfig.username || !erpConfig.password) { + return { + success: false, + message: '请先配置 ERP URL、用户名和密码' + } + } + + // Create ERP auth service and try to login + const erpAuthService = new ErpAuthService(erpConfig) + + try { + await erpAuthService.login() + // Login successful, close browser + await erpAuthService.close() + return { + success: true, + message: 'ERP 连接测试成功!' + } + } catch (loginError) { + const errorMessage = loginError instanceof Error ? loginError.message : '登录失败' + return { + success: false, + message: `ERP 连接测试失败:${errorMessage}` + } + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + return { + success: false, + message: `ERP 连接测试失败:${message}` + } + } + } + ) + + /** + * Test database connection + */ + ipcMain.handle( + 'settings:testDbConnection', + async (): Promise => { + try { + const settings = configManager.getAllSettings() + const dbConfig = settings.database + + if (dbConfig.dbType === 'mysql') { + // Test MySQL connection + if (!dbConfig.mysqlHost || !dbConfig.database || !dbConfig.username) { + return { + success: false, + message: '请先配置 MySQL 主机、数据库名和用户名' + } + } + + const mysqlService = new MySqlService({ + host: dbConfig.mysqlHost, + port: dbConfig.mysqlPort, + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database + }) + + try { + await mysqlService.connect() + await mysqlService.disconnect() + return { + success: true, + message: 'MySQL 数据库连接测试成功!' + } + } catch (connError) { + const errorMessage = connError instanceof Error ? connError.message : '连接失败' + return { + success: false, + message: `MySQL 数据库连接测试失败:${errorMessage}` + } + } + } else { + // Test SQL Server connection + if (!dbConfig.server || !dbConfig.database || !dbConfig.username) { + return { + success: false, + message: '请先配置 SQL Server 服务器、数据库名和用户名' + } + } + + const sqlServerService = new SqlServerService({ + server: dbConfig.server, + port: 1433, // Default SQL Server port + user: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database, + options: { + trustServerCertificate: true + } + }) + + try { + await sqlServerService.connect() + await sqlServerService.disconnect() + return { + success: true, + message: 'SQL Server 数据库连接测试成功!' + } + } catch (connError) { + const errorMessage = connError instanceof Error ? connError.message : '连接失败' + return { + success: false, + message: `SQL Server 数据库连接测试失败:${errorMessage}` + } + } + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + return { + success: false, + message: `数据库连接测试失败:${message}` + } + } + } + ) +} diff --git a/src/main/ipc/validation-handler.ts b/src/main/ipc/validation-handler.ts index b906dc7..bfd19f9 100644 --- a/src/main/ipc/validation-handler.ts +++ b/src/main/ipc/validation-handler.ts @@ -615,4 +615,100 @@ export function registerValidationHandlers(): void { return { productionIds: getSharedProductionIds() } } ) + + /** + * Get cleaner data (order numbers from shared Production IDs + material codes from MaterialsToBeDeleted) + * Filters materials by current user (admin sees all, regular users see only their own) + */ + ipcMain.handle( + 'validation:getCleanerData', + async (_event): Promise<{ + success: boolean + orderNumbers?: string[] + materialCodes?: string[] + error?: string + }> => { + let mysqlService: MySqlService | null = null + const sessionManager = (await import('../services/user/session-manager')).SessionManager.getInstance() + + try { + // Get current user + const userInfo = sessionManager.getUserInfo() + if (!userInfo) { + return { + success: false, + error: '用户未登录' + } + } + + const isAdmin = userInfo.userType === 'Admin' + const username = userInfo.username + + console.log(`[CleanerData] User: ${username}, isAdmin: ${isAdmin}`) + + // Connect to MySQL + mysqlService = await getValidationMySqlService() + + // 1. Get order numbers from shared Production IDs + const sharedIds = getSharedProductionIds() + let orderNumbers: string[] = [] + + if (sharedIds.length > 0) { + console.log(`[CleanerData] Using ${sharedIds.length} shared Production IDs`) + orderNumbers = await getSourceNumbersFromInputs(sharedIds, mysqlService) + console.log(`[CleanerData] Got ${orderNumbers.length} order numbers`) + } + + // 2. Get material codes from MaterialsToBeDeleted table + let materialCodes: string[] = [] + + if (isAdmin) { + // Admin sees all materials + const allCodesSql = ` + SELECT MaterialCode + FROM dbo_MaterialsToBeDeleted + WHERE MaterialCode IS NOT NULL + ` + const result = await mysqlService.query(allCodesSql) + materialCodes = result.rows + .map(row => row.MaterialCode as string) + .filter(Boolean) + console.log(`[CleanerData] Admin user: got ${materialCodes.length} materials`) + } else { + // Regular users only see their own materials + const userMaterialsSql = ` + SELECT MaterialCode + FROM dbo_MaterialsToBeDeleted + WHERE ManagerName = ? AND MaterialCode IS NOT NULL + ` + const result = await mysqlService.query(userMaterialsSql, [username]) + materialCodes = result.rows + .map(row => row.MaterialCode as string) + .filter(Boolean) + console.log(`[CleanerData] Regular user: got ${materialCodes.length} materials`) + } + + return { + success: true, + orderNumbers, + materialCodes + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error' + console.error('[CleanerData] Error:', error) + return { + success: false, + error: `获取清理数据失败:${message}` + } + } finally { + if (mysqlService) { + try { + await mysqlService.disconnect() + } catch (closeError) { + console.warn('[CleanerData] Error disconnecting MySQL:', closeError) + } + } + } + } + ) } diff --git a/src/main/services/config/config-manager.ts b/src/main/services/config/config-manager.ts new file mode 100644 index 0000000..8e09dff --- /dev/null +++ b/src/main/services/config/config-manager.ts @@ -0,0 +1,446 @@ +/** + * Configuration Manager + * + * Manages application configuration stored in .env file + * Provides methods for reading, writing, and saving configuration values + */ + +import * as fs from 'fs' +import * as path from 'path' +import { fileURLToPath } from 'url' +import { dirname } from 'path' +import type { + SettingsData, + ErpConfig, + DatabaseConfig, + PathsConfig, + ExtractionConfig, + ValidationConfig, + UiConfig, + ExecutionConfig, + DatabaseType, + MatchMode, + ValidationDataSource +} from '../../types/settings.types' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +/** + * Default settings values + */ +const DEFAULT_SETTINGS: SettingsData = { + erp: { + url: 'https://68.11.34.30:8082/', + username: '', + password: '', + headless: true, + ignoreHttpsErrors: true, + autoCloseBrowser: true + }, + database: { + dbType: 'mysql', + server: '', + mysqlHost: '192.168.31.83', + mysqlPort: 3306, + database: 'BLD_DB', + username: 'remote_user', + password: '' + }, + paths: { + dataDir: 'D:/python/playwrite/data/', + defaultOutput: '离散备料计划维护_合并.xlsx', + validationOutput: '物料状态校验结果.xlsx' + }, + extraction: { + batchSize: 100, + verbose: true, + autoConvert: true, + mergeBatches: true, + enableDbPersistence: true + }, + validation: { + dataSource: 'database_full', + batchSize: 2000, + matchMode: 'substring', + enableCrud: false, + defaultManager: '' + }, + ui: { + fontFamily: 'Microsoft YaHei UI', + fontSize: 10, + productionIdInputWidth: 20 + }, + execution: { + dryRun: false + } +} + +/** + * Configuration Manager Class + */ +export class ConfigManager { + private static instance: ConfigManager | null = null + private envPath: string + private configCache: Map = new Map() + private initialized: boolean = false + + private constructor() { + if (this.initialized) { + return + } + this.envPath = path.resolve(__dirname, '../../.env') + this.initialized = true + } + + /** + * Get the singleton instance + */ + public static getInstance(): ConfigManager { + if (ConfigManager.instance === null) { + ConfigManager.instance = new ConfigManager() + } + return ConfigManager.instance + } + + /** + * Initialize configuration from .env file + */ + public async initialize(): Promise { + await this.loadEnvFile() + } + + /** + * Load .env file into cache + */ + private async loadEnvFile(): Promise { + try { + if (fs.existsSync(this.envPath)) { + const content = fs.readFileSync(this.envPath, 'utf-8') + const lines = content.split('\n') + + for (const line of lines) { + const trimmedLine = line.trim() + // Skip empty lines and comments + if (!trimmedLine || trimmedLine.startsWith('#')) { + continue + } + + const [key, ...valueParts] = trimmedLine.split('=') + if (key && valueParts.length > 0) { + const value = valueParts.join('=').trim() + this.configCache.set(key.trim(), value) + } + } + } + } catch (error) { + console.error('[ConfigManager] Failed to load .env file:', error) + } + } + + /** + * Get a configuration value + * @param key - Configuration key + * @param defaultValue - Default value if key doesn't exist + */ + public get(key: string, defaultValue?: string): string | undefined { + return this.configCache.get(key) ?? defaultValue + } + + /** + * Get a boolean configuration value + */ + public getBoolean(key: string, defaultValue: boolean = false): boolean { + const value = this.get(key) + if (value === undefined) return defaultValue + return value.toLowerCase() === 'true' + } + + /** + * Get a number configuration value + */ + public getNumber(key: string, defaultValue: number = 0): number { + const value = this.get(key) + if (value === undefined) return defaultValue + const parsed = parseInt(value, 10) + return isNaN(parsed) ? defaultValue : parsed + } + + /** + * Set a configuration value in cache + */ + public set(key: string, value: string | number | boolean): void { + this.configCache.set(key, String(value)) + } + + /** + * Save configuration to .env file + */ + public async save(): Promise { + try { + // Build .env content from cache + const lines: string[] = [] + + // ERP Configuration + lines.push('# ===========================') + lines.push('# ERP 系统配置') + lines.push('# ===========================') + lines.push(`ERP_URL=${this.configCache.get('erp.url') || DEFAULT_SETTINGS.erp.url}`) + lines.push(`ERP_USERNAME=${this.configCache.get('erp.username') || DEFAULT_SETTINGS.erp.username}`) + lines.push(`ERP_PASSWORD=${this.configCache.get('erp.password') || DEFAULT_SETTINGS.erp.password}`) + lines.push(`ERP_HEADLESS=${this.configCache.get('erp.headless') || DEFAULT_SETTINGS.erp.headless}`) + lines.push(`ERP_IGNORE_HTTPS_ERRORS=${this.configCache.get('erp.ignoreHttpsErrors') || DEFAULT_SETTINGS.erp.ignoreHttpsErrors}`) + lines.push(`ERP_AUTO_CLOSE_BROWSER=${this.configCache.get('erp.autoCloseBrowser') || DEFAULT_SETTINGS.erp.autoCloseBrowser}`) + lines.push('') + + // Database Configuration - SQL Server + lines.push('# ===========================') + lines.push('# 数据库配置 - SQL Server') + lines.push('# ===========================') + lines.push(`# DB_TYPE=sqlserver`) + lines.push(`# DB_SERVER=${this.configCache.get('database.server') || ''}`) + lines.push(`# DB_NAME=${this.configCache.get('database.database') || ''}`) + lines.push(`# DB_USERNAME=${this.configCache.get('database.username') || ''}`) + lines.push(`# DB_PASSWORD=${this.configCache.get('database.password') || ''}`) + lines.push(`DB_SQLSERVER_DRIVER=ODBC Driver 18 for SQL Server`) + lines.push(`DB_TRUST_SERVER_CERTIFICATE=yes`) + lines.push('') + + // Database Configuration - MySQL + lines.push('# ===========================') + lines.push('# 数据库配置 - MySQL (切换时使用)') + lines.push('# ===========================') + lines.push(`DB_TYPE=${this.configCache.get('database.dbType') || DEFAULT_SETTINGS.database.dbType}`) + lines.push(`DB_NAME=${this.configCache.get('database.database') || DEFAULT_SETTINGS.database.database}`) + lines.push(`DB_USERNAME=${this.configCache.get('database.username') || DEFAULT_SETTINGS.database.username}`) + lines.push(`DB_PASSWORD=${this.configCache.get('database.password') || DEFAULT_SETTINGS.database.password}`) + lines.push(`DB_MYSQL_HOST=${this.configCache.get('database.mysqlHost') || DEFAULT_SETTINGS.database.mysqlHost}`) + lines.push(`DB_MYSQL_PORT=${this.configCache.get('database.mysqlPort') || DEFAULT_SETTINGS.database.mysqlPort}`) + lines.push(`DB_MYSQL_CHARSET=utf8mb4`) + lines.push('') + + // Order number parsing table configuration + lines.push('# 订单号解析表配置') + lines.push('# 表名:包含 productionID 和 生产订单号 映射关系的表') + lines.push(`DB_TABLE_NAME=productionContractData_26 年压力表合同数据`) + lines.push('# 字段名:总排号 (对应 productionID)') + lines.push(`DB_FIELD_PRODUCTION_ID=总排号`) + lines.push('# 字段名:生产订单号 (对应生产订单号)') + lines.push(`DB_FIELD_ORDER_NUMBER=生产订单号`) + lines.push('') + + // Path Configuration + lines.push('# ===========================') + lines.push('# 路径配置') + lines.push('# ===========================') + lines.push(`PATH_DATA_DIR=${this.configCache.get('paths.dataDir') || DEFAULT_SETTINGS.paths.dataDir}`) + lines.push(`PATH_PRODUCTION_ID_FILE=ProductionID.txt`) + lines.push(`PATH_DEFAULT_OUTPUT=${this.configCache.get('paths.defaultOutput') || DEFAULT_SETTINGS.paths.defaultOutput}`) + lines.push(`PATH_VALIDATION_OUTPUT=${this.configCache.get('paths.validationOutput') || DEFAULT_SETTINGS.paths.validationOutput}`) + lines.push('') + + // Data Extraction Configuration + lines.push('# ===========================') + lines.push('# 数据提取配置') + lines.push('# ===========================') + lines.push(`EXTRACTION_BATCH_SIZE=${this.configCache.get('extraction.batchSize') || DEFAULT_SETTINGS.extraction.batchSize}`) + lines.push(`EXTRACTION_VERBOSE=${this.configCache.get('extraction.verbose') || DEFAULT_SETTINGS.extraction.verbose}`) + lines.push(`EXTRACTION_AUTO_CONVERT=${this.configCache.get('extraction.autoConvert') || DEFAULT_SETTINGS.extraction.autoConvert}`) + lines.push(`EXTRACTION_MERGE_BATCHES=${this.configCache.get('extraction.mergeBatches') || DEFAULT_SETTINGS.extraction.mergeBatches}`) + lines.push(`EXTRACTION_ENABLE_DB_PERSISTENCE=${this.configCache.get('extraction.enableDbPersistence') || DEFAULT_SETTINGS.extraction.enableDbPersistence}`) + lines.push('') + + // Validation Configuration + lines.push('# ===========================') + lines.push('# 校验配置') + lines.push('# ===========================') + lines.push(`VALIDATION_DATA_SOURCE=${this.configCache.get('validation.dataSource') || DEFAULT_SETTINGS.validation.dataSource}`) + lines.push(`VALIDATION_USE_DATABASE=${this.configCache.get('validation.useDatabase') || true}`) + lines.push(`VALIDATION_BATCH_SIZE=${this.configCache.get('validation.batchSize') || DEFAULT_SETTINGS.validation.batchSize}`) + lines.push(`VALIDATION_ENABLE_CRUD=${this.configCache.get('validation.enableCrud') || DEFAULT_SETTINGS.validation.enableCrud}`) + lines.push(`VALIDATION_DEFAULT_MANAGER=${this.configCache.get('validation.defaultManager') || DEFAULT_SETTINGS.validation.defaultManager}`) + lines.push(`VALIDATION_MATCH_MODE=${this.configCache.get('validation.matchMode') || DEFAULT_SETTINGS.validation.matchMode}`) + lines.push('') + + // UI Configuration + lines.push('# ===========================') + lines.push('# UI 配置') + lines.push('# ===========================') + lines.push(`UI_FONT_FAMILY=${this.configCache.get('ui.fontFamily') || DEFAULT_SETTINGS.ui.fontFamily}`) + lines.push(`UI_FONT_SIZE=${this.configCache.get('ui.fontSize') || DEFAULT_SETTINGS.ui.fontSize}`) + lines.push(`UI_PRODUCTION_ID_INPUT_WIDTH=${this.configCache.get('ui.productionIdInputWidth') || DEFAULT_SETTINGS.ui.productionIdInputWidth}`) + lines.push('') + + // Execution Configuration + lines.push('# ===========================') + lines.push('# 执行配置') + lines.push('# ===========================') + lines.push(`EXECUTION_DRYRUN=${this.configCache.get('execution.dryRun') || DEFAULT_SETTINGS.execution.dryRun}`) + + const content = lines.join('\n') + fs.writeFileSync(this.envPath, content, 'utf-8') + return true + } catch (error) { + console.error('[ConfigManager] Failed to save .env file:', error) + return false + } + } + + /** + * Get all settings as SettingsData object + */ + public getAllSettings(): SettingsData { + return { + erp: { + url: this.get('ERP_URL', DEFAULT_SETTINGS.erp.url), + username: this.get('ERP_USERNAME', DEFAULT_SETTINGS.erp.username), + password: this.get('ERP_PASSWORD', DEFAULT_SETTINGS.erp.password), + headless: this.getBoolean('ERP_HEADLESS', DEFAULT_SETTINGS.erp.headless), + ignoreHttpsErrors: this.getBoolean('ERP_IGNORE_HTTPS_ERRORS', DEFAULT_SETTINGS.erp.ignoreHttpsErrors), + autoCloseBrowser: this.getBoolean('ERP_AUTO_CLOSE_BROWSER', DEFAULT_SETTINGS.erp.autoCloseBrowser) + }, + database: { + dbType: (this.get('DB_TYPE', DEFAULT_SETTINGS.database.dbType) as DatabaseType) || DEFAULT_SETTINGS.database.dbType, + server: this.get('DB_SERVER', DEFAULT_SETTINGS.database.server), + mysqlHost: this.get('DB_MYSQL_HOST', DEFAULT_SETTINGS.database.mysqlHost), + mysqlPort: this.getNumber('DB_MYSQL_PORT', DEFAULT_SETTINGS.database.mysqlPort), + database: this.get('DB_NAME', DEFAULT_SETTINGS.database.database), + username: this.get('DB_USERNAME', DEFAULT_SETTINGS.database.username), + password: this.get('DB_PASSWORD', DEFAULT_SETTINGS.database.password) + }, + paths: { + dataDir: this.get('PATH_DATA_DIR', DEFAULT_SETTINGS.paths.dataDir), + defaultOutput: this.get('PATH_DEFAULT_OUTPUT', DEFAULT_SETTINGS.paths.defaultOutput), + validationOutput: this.get('PATH_VALIDATION_OUTPUT', DEFAULT_SETTINGS.paths.validationOutput) + }, + extraction: { + batchSize: this.getNumber('EXTRACTION_BATCH_SIZE', DEFAULT_SETTINGS.extraction.batchSize), + verbose: this.getBoolean('EXTRACTION_VERBOSE', DEFAULT_SETTINGS.extraction.verbose), + autoConvert: this.getBoolean('EXTRACTION_AUTO_CONVERT', DEFAULT_SETTINGS.extraction.autoConvert), + mergeBatches: this.getBoolean('EXTRACTION_MERGE_BATCHES', DEFAULT_SETTINGS.extraction.mergeBatches), + enableDbPersistence: this.getBoolean('EXTRACTION_ENABLE_DB_PERSISTENCE', DEFAULT_SETTINGS.extraction.enableDbPersistence) + }, + validation: { + dataSource: (this.get('VALIDATION_DATA_SOURCE', DEFAULT_SETTINGS.validation.dataSource) as ValidationDataSource) || DEFAULT_SETTINGS.validation.dataSource, + batchSize: this.getNumber('VALIDATION_BATCH_SIZE', DEFAULT_SETTINGS.validation.batchSize), + matchMode: (this.get('VALIDATION_MATCH_MODE', DEFAULT_SETTINGS.validation.matchMode) as MatchMode) || DEFAULT_SETTINGS.validation.matchMode, + enableCrud: this.getBoolean('VALIDATION_ENABLE_CRUD', DEFAULT_SETTINGS.validation.enableCrud), + defaultManager: this.get('VALIDATION_DEFAULT_MANAGER', DEFAULT_SETTINGS.validation.defaultManager) + }, + ui: { + fontFamily: this.get('UI_FONT_FAMILY', DEFAULT_SETTINGS.ui.fontFamily), + fontSize: this.getNumber('UI_FONT_SIZE', DEFAULT_SETTINGS.ui.fontSize), + productionIdInputWidth: this.getNumber('UI_PRODUCTION_ID_INPUT_WIDTH', DEFAULT_SETTINGS.ui.productionIdInputWidth) + }, + execution: { + dryRun: this.getBoolean('EXECUTION_DRYRUN', DEFAULT_SETTINGS.execution.dryRun) + } + } + } + + /** + * Save settings from SettingsData object + */ + public async saveAllSettings(settings: SettingsData): Promise { + // ERP settings + this.set('erp.url', settings.erp.url) + this.set('erp.username', settings.erp.username) + this.set('erp.password', settings.erp.password) + this.set('erp.headless', settings.erp.headless) + this.set('erp.ignoreHttpsErrors', settings.erp.ignoreHttpsErrors) + this.set('erp.autoCloseBrowser', settings.erp.autoCloseBrowser) + + // Database settings + this.set('database.dbType', settings.database.dbType) + this.set('database.server', settings.database.server) + this.set('database.mysqlHost', settings.database.mysqlHost) + this.set('database.mysqlPort', settings.database.mysqlPort) + this.set('database.database', settings.database.database) + this.set('database.username', settings.database.username) + this.set('database.password', settings.database.password) + + // Path settings + this.set('paths.dataDir', settings.paths.dataDir) + this.set('paths.defaultOutput', settings.paths.defaultOutput) + this.set('paths.validationOutput', settings.paths.validationOutput) + + // Extraction settings + this.set('extraction.batchSize', settings.extraction.batchSize) + this.set('extraction.verbose', settings.extraction.verbose) + this.set('extraction.autoConvert', settings.extraction.autoConvert) + this.set('extraction.mergeBatches', settings.extraction.mergeBatches) + this.set('extraction.enableDbPersistence', settings.extraction.enableDbPersistence) + + // Validation settings + this.set('validation.dataSource', settings.validation.dataSource) + this.set('validation.batchSize', settings.validation.batchSize) + this.set('validation.matchMode', settings.validation.matchMode) + this.set('validation.enableCrud', settings.validation.enableCrud) + this.set('validation.defaultManager', settings.validation.defaultManager) + + // UI settings + this.set('ui.fontFamily', settings.ui.fontFamily) + this.set('ui.fontSize', settings.ui.fontSize) + this.set('ui.productionIdInputWidth', settings.ui.productionIdInputWidth) + + // Execution settings + this.set('execution.dryRun', settings.execution.dryRun) + + return this.save() + } + + /** + * Reset to default settings + */ + public resetToDefaults(): SettingsData { + // Clear cache and reload from defaults + this.configCache.clear() + + // Set all defaults + this.set('erp.url', DEFAULT_SETTINGS.erp.url) + this.set('erp.username', DEFAULT_SETTINGS.erp.username) + this.set('erp.password', DEFAULT_SETTINGS.erp.password) + this.set('erp.headless', DEFAULT_SETTINGS.erp.headless) + this.set('erp.ignoreHttpsErrors', DEFAULT_SETTINGS.erp.ignoreHttpsErrors) + this.set('erp.autoCloseBrowser', DEFAULT_SETTINGS.erp.autoCloseBrowser) + + this.set('database.dbType', DEFAULT_SETTINGS.database.dbType) + this.set('database.server', DEFAULT_SETTINGS.database.server) + this.set('database.mysqlHost', DEFAULT_SETTINGS.database.mysqlHost) + this.set('database.mysqlPort', DEFAULT_SETTINGS.database.mysqlPort) + this.set('database.database', DEFAULT_SETTINGS.database.database) + this.set('database.username', DEFAULT_SETTINGS.database.username) + this.set('database.password', DEFAULT_SETTINGS.database.password) + + this.set('paths.dataDir', DEFAULT_SETTINGS.paths.dataDir) + this.set('paths.defaultOutput', DEFAULT_SETTINGS.paths.defaultOutput) + this.set('paths.validationOutput', DEFAULT_SETTINGS.paths.validationOutput) + + this.set('extraction.batchSize', DEFAULT_SETTINGS.extraction.batchSize) + this.set('extraction.verbose', DEFAULT_SETTINGS.extraction.verbose) + this.set('extraction.autoConvert', DEFAULT_SETTINGS.extraction.autoConvert) + this.set('extraction.mergeBatches', DEFAULT_SETTINGS.extraction.mergeBatches) + this.set('extraction.enableDbPersistence', DEFAULT_SETTINGS.extraction.enableDbPersistence) + + this.set('validation.dataSource', DEFAULT_SETTINGS.validation.dataSource) + this.set('validation.batchSize', DEFAULT_SETTINGS.validation.batchSize) + this.set('validation.matchMode', DEFAULT_SETTINGS.validation.matchMode) + this.set('validation.enableCrud', DEFAULT_SETTINGS.validation.enableCrud) + this.set('validation.defaultManager', DEFAULT_SETTINGS.validation.defaultManager) + + this.set('ui.fontFamily', DEFAULT_SETTINGS.ui.fontFamily) + this.set('ui.fontSize', DEFAULT_SETTINGS.ui.fontSize) + this.set('ui.productionIdInputWidth', DEFAULT_SETTINGS.ui.productionIdInputWidth) + + this.set('execution.dryRun', DEFAULT_SETTINGS.execution.dryRun) + + return DEFAULT_SETTINGS + } + + /** + * Get default settings + */ + public getDefaultSettings(): SettingsData { + return DEFAULT_SETTINGS + } +} diff --git a/src/main/types/settings.types.ts b/src/main/types/settings.types.ts new file mode 100644 index 0000000..05d4933 --- /dev/null +++ b/src/main/types/settings.types.ts @@ -0,0 +1,185 @@ +/** + * Settings types and interfaces + * + * Defines configuration structures for ERPAuto settings management + */ + +/** + * User type for settings permission control + */ +export type UserType = 'Admin' | 'User' | 'Guest' + +/** + * Database type selection + */ +export type DatabaseType = 'sqlserver' | 'mysql' + +/** + * Validation match mode + */ +export type MatchMode = 'substring' | 'exact' + +/** + * Validation data source options + */ +export type ValidationDataSource = 'database_full' | 'database_filtered' | 'excel_existing' | 'excel_full' + +/** + * ERP configuration + */ +export interface ErpConfig { + /** ERP system URL */ + url: string + /** ERP username */ + username: string + /** ERP password */ + password: string + /** Headless browser mode */ + headless: boolean + /** Ignore HTTPS certificate errors */ + ignoreHttpsErrors: boolean + /** Auto close browser after operations */ + autoCloseBrowser: boolean +} + +/** + * Database configuration + */ +export interface DatabaseConfig { + /** Database type */ + dbType: DatabaseType + /** SQL Server server address */ + server: string + /** MySQL host */ + mysqlHost: string + /** MySQL port */ + mysqlPort: number + /** Database name */ + database: string + /** Database username */ + username: string + /** Database password */ + password: string +} + +/** + * Path configuration + */ +export interface PathsConfig { + /** Data directory */ + dataDir: string + /** Default output file */ + defaultOutput: string + /** Validation output file */ + validationOutput: string +} + +/** + * Data extraction configuration + */ +export interface ExtractionConfig { + /** Batch size for data extraction */ + batchSize: number + /** Enable verbose logging */ + verbose: boolean + /** Auto convert to Excel */ + autoConvert: boolean + /** Merge batches */ + mergeBatches: boolean + /** Enable database persistence */ + enableDbPersistence: boolean +} + +/** + * Material validation configuration + */ +export interface ValidationConfig { + /** Data source type */ + dataSource: ValidationDataSource + /** Batch size for validation */ + batchSize: number + /** Match mode */ + matchMode: MatchMode + /** Enable CRUD operations */ + enableCrud: boolean + /** Default manager name */ + defaultManager: string +} + +/** + * UI configuration + */ +export interface UiConfig { + /** Font family */ + fontFamily: string + /** Font size */ + fontSize: number + /** Production ID input width */ + productionIdInputWidth: number +} + +/** + * Execution configuration (User-only) + */ +export interface ExecutionConfig { + /** Dry run mode */ + dryRun: boolean +} + +/** + * Complete settings data structure + */ +export interface SettingsData { + /** ERP configuration */ + erp: ErpConfig + /** Database configuration */ + database: DatabaseConfig + /** Path configuration */ + paths: PathsConfig + /** Extraction configuration */ + extraction: ExtractionConfig + /** Validation configuration */ + validation: ValidationConfig + /** UI configuration */ + ui: UiConfig + /** Execution configuration */ + execution: ExecutionConfig +} + +/** + * Connection test result + */ +export interface ConnectionTestResult { + /** Whether connection was successful */ + success: boolean + /** Status message */ + message?: string +} + +/** + * Save settings result + */ +export interface SaveSettingsResult { + /** Whether save was successful */ + success: boolean + /** Error message if failed */ + error?: string +} + +/** + * Settings API interface for preload + */ +export interface SettingsAPI { + /** Get current user type */ + getUserType: () => Promise + /** Get settings (filtered by user type) */ + getSettings: () => Promise + /** Save settings */ + saveSettings: (settings: SettingsData) => Promise + /** Reset to defaults (Admin only) */ + resetDefaults: () => Promise + /** Test ERP connection */ + testErpConnection: () => Promise + /** Test database connection */ + testDbConnection: () => Promise +} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 15600e0..2e4ee23 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -9,6 +9,7 @@ import type { CurrentUserResponse } from '../main/ipc/auth-handler' import type { ValidationRequest, ValidationResponse } from '../main/types/validation.types' +import type { SettingsData, UserType, ConnectionTestResult, SaveSettingsResult } from '../main/types/settings.types' /** * Order number resolver API @@ -88,6 +89,16 @@ export interface ValidationAPI { * Get shared Production IDs */ getSharedProductionIds: () => Promise<{ productionIds: string[] }> + /** + * Get cleaner data (order numbers from shared Production IDs + material codes from MaterialsToBeDeleted) + * Filters materials by current user (admin sees all, regular users see only their own) + */ + getCleanerData: () => Promise<{ + success: boolean + orderNumbers?: string[] + materialCodes?: string[] + error?: string + }> } /** @@ -131,6 +142,37 @@ export interface MaterialsAPI { getStatistics: () => Promise<{ stats: unknown }> } +/** + * Settings API + */ +export interface SettingsAPI { + /** + * Get current user type + */ + getUserType: () => Promise + /** + * Get settings (filtered by user type) + */ + getSettings: () => Promise + /** + * Save settings + * @param settings - Settings data to save + */ + saveSettings: (settings: SettingsData) => Promise + /** + * Reset to defaults (Admin only) + */ + resetDefaults: () => Promise + /** + * Test ERP connection + */ + testErpConnection: () => Promise + /** + * Test database connection + */ + testDbConnection: () => Promise +} + declare global { interface Window { electron: { @@ -157,6 +199,7 @@ declare global { auth: AuthAPI validation: ValidationAPI materials: MaterialsAPI + settings: SettingsAPI } api: unknown } diff --git a/src/preload/index.ts b/src/preload/index.ts index a07f84a..211394f 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -7,6 +7,7 @@ import type { ResolverInput } from '../main/ipc/resolver-handler' import type { LoginRequest } from '../main/ipc/auth-handler' import type { UserInfo } from '../main/types/user.types' import type { ValidationRequest } from '../main/types/validation.types' +import type { SettingsData } from '../main/types/settings.types' // Custom APIs for renderer const api = { @@ -70,7 +71,9 @@ const api = { setSharedProductionIds: (productionIds: string[]) => ipcRenderer.invoke('validation:setSharedProductionIds', productionIds), getSharedProductionIds: () => - ipcRenderer.invoke('validation:getSharedProductionIds') + ipcRenderer.invoke('validation:getSharedProductionIds'), + getCleanerData: () => + ipcRenderer.invoke('validation:getCleanerData') }, // Materials service @@ -84,6 +87,16 @@ const api = { ipcRenderer.invoke('materials:getByManager', managerName), getAll: () => ipcRenderer.invoke('materials:getAll'), getStatistics: () => ipcRenderer.invoke('materials:getStatistics') + }, + + // Settings service + settings: { + getUserType: () => ipcRenderer.invoke('settings:getUserType'), + getSettings: () => ipcRenderer.invoke('settings:getSettings'), + saveSettings: (settings: SettingsData) => ipcRenderer.invoke('settings:saveSettings', settings), + resetDefaults: () => ipcRenderer.invoke('settings:resetDefaults'), + testErpConnection: () => ipcRenderer.invoke('settings:testErpConnection'), + testDbConnection: () => ipcRenderer.invoke('settings:testDbConnection') } } as const diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 5d352b5..7b5018d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -13,8 +13,9 @@ import LoginDialog from './components/LoginDialog' import UserSelectionDialog, { type UserInfo as SelectedUserInfo } from './components/UserSelectionDialog' import { ExtractorPage } from './pages/ExtractorPage' import { CleanerPage } from './pages/CleanerPage' +import SettingsPage from './pages/SettingsPage' -type Page = 'home' | 'extractor' | 'cleaner' +type Page = 'home' | 'extractor' | 'cleaner' | 'settings' interface CurrentUser { username: string @@ -354,6 +355,21 @@ function App(): React.JSX.Element { > 物料清理 +
@@ -379,13 +395,14 @@ function App(): React.JSX.Element {

主页面

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

)} {currentPage === 'extractor' && } {currentPage === 'cleaner' && } + {currentPage === 'settings' && }
) diff --git a/src/renderer/src/pages/CleanerPage.tsx b/src/renderer/src/pages/CleanerPage.tsx index 3fd8267..3876b18 100644 --- a/src/renderer/src/pages/CleanerPage.tsx +++ b/src/renderer/src/pages/CleanerPage.tsx @@ -24,13 +24,7 @@ interface ValidationResult { */ export const CleanerPage: React.FC = () => { // State with sessionStorage persistence - const [orderNumbers, setOrderNumbers] = useState(() => { - return sessionStorage.getItem('cleaner_orderNumbers') || '' - }) - const [materialCodes, setMaterialCodes] = useState(() => { - return sessionStorage.getItem('cleaner_materialCodes') || '' - }) - const [dryRun, setDryRun] = useState(() => { + const [dryRun] = useState(() => { const saved = sessionStorage.getItem('cleaner_dryRun') return saved ? saved === 'true' : true }) @@ -92,13 +86,6 @@ export const CleanerPage: React.FC = () => { results = results.filter(r => !hiddenItems.has(r.materialCode)) return results }, [validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems]) - useEffect(() => { - sessionStorage.setItem('cleaner_orderNumbers', orderNumbers) - }, [orderNumbers]) - - useEffect(() => { - sessionStorage.setItem('cleaner_materialCodes', materialCodes) - }, [materialCodes]) useEffect(() => { sessionStorage.setItem('cleaner_dryRun', dryRun.toString()) @@ -121,11 +108,11 @@ export const CleanerPage: React.FC = () => { const admin = await window.electron.auth.isAdmin() const user = await window.electron.auth.getCurrentUser() setIsAdmin(admin) - if (user) { - setCurrentUsername(user.username) + if (user && user.userInfo) { + setCurrentUsername(user.userInfo.username) if (!admin) { // Non-admin users can only see their own data - setSelectedManagers(new Set([user.username])) + setSelectedManagers(new Set([user.userInfo.username])) } } @@ -345,64 +332,8 @@ export const CleanerPage: React.FC = () => { } }, [selectedItems, validationResults, loadManagers]) - // Handle export results - const handleExportResults = useCallback(async () => { - if (validationResults.length === 0) { - setError('没有数据可导出') - return - } - - try { - // Prepare data for export - const data = validationResults.map(result => ({ - '选择': selectedItems.has(result.materialCode) ? '是' : '否', - '材料名称': result.materialName, - '材料代码': result.materialCode, - '规格': result.specification, - '型号': result.model, - '负责人': result.managerName || '' - })) - - // Create CSV content - const headers = ['选择', '材料名称', '材料代码', '规格', '型号', '负责人'] - const csvContent = [ - headers.join(','), - ...data.map(row => - headers.map(header => { - const value = row[header as keyof typeof row] - // Escape quotes and wrap in quotes if contains comma - const escaped = String(value).replace(/"/g, '""') - return escaped.includes(',') ? `"${escaped}"` : escaped - }).join(',') - ) - ].join('\n') - - // Create blob and download - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }) - const link = document.createElement('a') - const url = URL.createObjectURL(blob) - link.setAttribute('href', url) - link.setAttribute('download', `物料状态校验结果_${new Date().getTime()}.csv`) - link.style.visibility = 'hidden' - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - } catch (err) { - setError(err instanceof Error ? err.message : '导出失败') - } - }, [validationResults, selectedItems]) - - // Handle execute deletion + // Handle execute deletion (清理) const handleExecuteDeletion = useCallback(async () => { - if (!orderNumbers.trim()) { - setError('请输入至少一个订单号') - return - } - if (!materialCodes.trim()) { - setError('请输入至少一个物料代码') - return - } - if (!dryRun) { const confirmed = window.confirm('警告:正式执行将删除 ERP 系统中的物料数据,是否继续?') if (!confirmed) return @@ -414,15 +345,35 @@ export const CleanerPage: React.FC = () => { setError(null) try { - const orderNumberList = orderNumbers - .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0) + // Get cleaner data from backend (order numbers from shared Production IDs + material codes from DB) + const cleanerDataResult = await window.electron.validation.getCleanerData() - const materialCodeList = materialCodes - .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0) + if (!cleanerDataResult.success) { + setError(cleanerDataResult.error || '获取清理数据失败') + setIsRunning(false) + return + } + + const orderNumberList = cleanerDataResult.orderNumbers || [] + const materialCodeList = cleanerDataResult.materialCodes || [] + + if (orderNumberList.length === 0) { + setError('没有订单号数据。请先到数据提取页面输入 Production ID。') + setIsRunning(false) + return + } + + if (materialCodeList.length === 0) { + setError('没有物料代码数据。请确认已在物料清理界面勾选要删除的物料。') + setIsRunning(false) + return + } + + console.log('[CleanerPage] Starting cleaner with:', { + orderNumbers: orderNumberList.length, + materialCodes: materialCodeList.length, + dryRun + }) const response = await window.electron.cleaner.runCleaner({ orderNumbers: orderNumberList, @@ -441,20 +392,7 @@ export const CleanerPage: React.FC = () => { setIsRunning(false) setProgress(null) } - }, [orderNumbers, materialCodes, dryRun]) - - // Handle clean (alias for execute deletion) - const handleClean = useCallback(handleExecuteDeletion, [handleExecuteDeletion]) - - // Handle reset - const handleReset = useCallback(() => { - setOrderNumbers('') - setMaterialCodes('') - setDryRun(true) - setResult(null) - setError(null) - setProgress(null) - }, []) + }, [dryRun]) // Handle hide checked items (for user mode) const handleHideChecked = useCallback(() => { @@ -559,13 +497,6 @@ export const CleanerPage: React.FC = () => { )} - {validationStats && ( @@ -575,26 +506,6 @@ export const CleanerPage: React.FC = () => { 已标记删除:{validationStats.markedCount} )} - - {/* Additional action buttons */} -
- - {isAdmin && ( - - )} -
{/* Manager Filter (Admin only) */} @@ -631,25 +542,44 @@ export const CleanerPage: React.FC = () => {

校验结果

- {/* User mode buttons: Hide Checked / Show All */} - {!isAdmin && ( + {/* Action buttons: left group (Hide Checked, Show All, Confirm Delete) + right button (Execute Delete in red) */} +
+ {!isAdmin && ( + <> + + + + )} -
- )} + +
@@ -684,57 +614,6 @@ export const CleanerPage: React.FC = () => { )} - {/* Simple Input Section */} -
-

订单号和物料代码输入

-
-
- -