diff --git a/src/main/ipc/database-handler.ts b/src/main/ipc/database-handler.ts index 041612b..b8d0cd2 100644 --- a/src/main/ipc/database-handler.ts +++ b/src/main/ipc/database-handler.ts @@ -21,7 +21,10 @@ const mysqlServices = new Map() const sqlServerServices = new Map() const cleanupBoundWindows = new Set() -function bindWindowCleanup(windowId: string, sender: { once: (event: string, listener: () => void) => void }): void { +function bindWindowCleanup( + windowId: string, + sender: { once: (event: string, listener: () => void) => void } +): void { if (cleanupBoundWindows.has(windowId)) { return } @@ -31,7 +34,9 @@ function bindWindowCleanup(windowId: string, sender: { once: (event: string, lis const sqlServer = getSqlServerService(windowId) if (mysql) { - mysql.disconnect().catch((error) => log.warn('MySQL disconnect on window destroy failed', { error })) + mysql + .disconnect() + .catch((error) => log.warn('MySQL disconnect on window destroy failed', { error })) deleteMySqlService(windowId) } @@ -99,39 +104,48 @@ export function registerDatabaseHandlers(): void { IPC_CHANNELS.DATABASE_MYSQL_CONNECT, async (event, config: MySqlConfig): Promise> => { return withErrorHandling(async () => { - // Use window ID as connection identifier - const windowId = (event.sender as { id: number }).id.toString() - bindWindowCleanup(windowId, event.sender as { once: (event: string, listener: () => void) => void }) - log.info('Connecting to MySQL', { windowId }) - const service = new MySqlService(config) - await service.connect() - setMySqlService(windowId, service) - log.info('MySQL connected', { windowId }) + // Use window ID as connection identifier + const windowId = (event.sender as { id: number }).id.toString() + bindWindowCleanup( + windowId, + event.sender as { once: (event: string, listener: () => void) => void } + ) + log.info('Connecting to MySQL', { windowId }) + const service = new MySqlService(config) + await service.connect() + setMySqlService(windowId, service) + log.info('MySQL connected', { windowId }) }, 'database:mysql:connect') } ) // Disconnect from MySQL - ipcMain.handle(IPC_CHANNELS.DATABASE_MYSQL_DISCONNECT, async (event): Promise> => { - return withErrorHandling(async () => { - const windowId = (event.sender as { id: number }).id.toString() - const service = getMySqlService(windowId) - if (service) { - await service.disconnect() - deleteMySqlService(windowId) - log.info('MySQL disconnected', { windowId }) - } - }, 'database:mysql:disconnect') - }) + ipcMain.handle( + IPC_CHANNELS.DATABASE_MYSQL_DISCONNECT, + async (event): Promise> => { + return withErrorHandling(async () => { + const windowId = (event.sender as { id: number }).id.toString() + const service = getMySqlService(windowId) + if (service) { + await service.disconnect() + deleteMySqlService(windowId) + log.info('MySQL disconnected', { windowId }) + } + }, 'database:mysql:disconnect') + } + ) // Check if MySQL is connected - ipcMain.handle(IPC_CHANNELS.DATABASE_MYSQL_IS_CONNECTED, async (event): Promise> => { - return withErrorHandling(async () => { - const windowId = (event.sender as { id: number }).id.toString() - const service = getMySqlService(windowId) - return service ? service.isConnected() : false - }, 'database:mysql:isConnected') - }) + ipcMain.handle( + IPC_CHANNELS.DATABASE_MYSQL_IS_CONNECTED, + async (event): Promise> => { + return withErrorHandling(async () => { + const windowId = (event.sender as { id: number }).id.toString() + const service = getMySqlService(windowId) + return service ? service.isConnected() : false + }, 'database:mysql:isConnected') + } + ) // Execute MySQL query ipcMain.handle( @@ -160,7 +174,10 @@ export function registerDatabaseHandlers(): void { async (event, config: SqlServerConfig): Promise> => { return withErrorHandling(async () => { const windowId = (event.sender as { id: number }).id.toString() - bindWindowCleanup(windowId, event.sender as { once: (event: string, listener: () => void) => void }) + bindWindowCleanup( + windowId, + event.sender as { once: (event: string, listener: () => void) => void } + ) log.info('Connecting to SQL Server', { windowId }) const service = new SqlServerService(config) await service.connect() @@ -171,26 +188,32 @@ export function registerDatabaseHandlers(): void { ) // Disconnect from SQL Server - ipcMain.handle(IPC_CHANNELS.DATABASE_SQLSERVER_DISCONNECT, async (event): Promise> => { - return withErrorHandling(async () => { - const windowId = (event.sender as { id: number }).id.toString() - const service = getSqlServerService(windowId) - if (service) { - await service.disconnect() - deleteSqlServerService(windowId) - log.info('SQL Server disconnected', { windowId }) - } - }, 'database:sqlserver:disconnect') - }) + ipcMain.handle( + IPC_CHANNELS.DATABASE_SQLSERVER_DISCONNECT, + async (event): Promise> => { + return withErrorHandling(async () => { + const windowId = (event.sender as { id: number }).id.toString() + const service = getSqlServerService(windowId) + if (service) { + await service.disconnect() + deleteSqlServerService(windowId) + log.info('SQL Server disconnected', { windowId }) + } + }, 'database:sqlserver:disconnect') + } + ) // Check if SQL Server is connected - ipcMain.handle(IPC_CHANNELS.DATABASE_SQLSERVER_IS_CONNECTED, async (event): Promise> => { - return withErrorHandling(async () => { - const windowId = (event.sender as { id: number }).id.toString() - const service = getSqlServerService(windowId) - return service ? service.isConnected() : false - }, 'database:sqlserver:isConnected') - }) + ipcMain.handle( + IPC_CHANNELS.DATABASE_SQLSERVER_IS_CONNECTED, + async (event): Promise> => { + return withErrorHandling(async () => { + const windowId = (event.sender as { id: number }).id.toString() + const service = getSqlServerService(windowId) + return service ? service.isConnected() : false + }, 'database:sqlserver:isConnected') + } + ) // Execute SQL Server query ipcMain.handle( diff --git a/src/main/ipc/file-handler.ts b/src/main/ipc/file-handler.ts index 09bf646..22017a6 100644 --- a/src/main/ipc/file-handler.ts +++ b/src/main/ipc/file-handler.ts @@ -31,13 +31,16 @@ function normalizeAndValidatePath(inputPath: string): string { } export function registerFileHandlers(): void { - ipcMain.handle(IPC_CHANNELS.FILE_READ, async (_event, filePath: string): Promise> => { - return withErrorHandling(async () => { - const safePath = normalizeAndValidatePath(filePath) - log.debug('Reading file', { filePath: safePath }) - return await fs.readFile(safePath, 'utf-8') - }, 'file:read') - }) + ipcMain.handle( + IPC_CHANNELS.FILE_READ, + async (_event, filePath: string): Promise> => { + return withErrorHandling(async () => { + const safePath = normalizeAndValidatePath(filePath) + log.debug('Reading file', { filePath: safePath }) + return await fs.readFile(safePath, 'utf-8') + }, 'file:read') + } + ) ipcMain.handle( IPC_CHANNELS.FILE_WRITE, @@ -52,36 +55,45 @@ export function registerFileHandlers(): void { } ) - ipcMain.handle(IPC_CHANNELS.FILE_EXISTS, async (_event, filePath: string): Promise> => { - return withErrorHandling(async () => { - const safePath = normalizeAndValidatePath(filePath) - try { + ipcMain.handle( + IPC_CHANNELS.FILE_EXISTS, + async (_event, filePath: string): Promise> => { + return withErrorHandling(async () => { + const safePath = normalizeAndValidatePath(filePath) + try { + await fs.access(safePath) + return true + } catch { + return false + } + }, 'file:exists') + } + ) + + ipcMain.handle( + IPC_CHANNELS.FILE_LIST, + async (_event, dirPath: string): Promise> => { + return withErrorHandling(async () => { + const safePath = normalizeAndValidatePath(dirPath) + log.debug('Listing directory', { dirPath: safePath }) + const entries = await fs.readdir(safePath, { withFileTypes: true }) + return entries + .filter((entry) => entry.isFile()) + .map((entry) => entry.name) + .sort() + }, 'file:list') + } + ) + + ipcMain.handle( + IPC_CHANNELS.FILE_OPEN_PATH, + async (_event, filePath: string): Promise> => { + return withErrorHandling(async () => { + const safePath = normalizeAndValidatePath(filePath) + log.debug('Opening path in explorer', { filePath: safePath }) await fs.access(safePath) - return true - } catch { - return false - } - }, 'file:exists') - }) - - ipcMain.handle(IPC_CHANNELS.FILE_LIST, async (_event, dirPath: string): Promise> => { - return withErrorHandling(async () => { - const safePath = normalizeAndValidatePath(dirPath) - log.debug('Listing directory', { dirPath: safePath }) - const entries = await fs.readdir(safePath, { withFileTypes: true }) - return entries - .filter((entry) => entry.isFile()) - .map((entry) => entry.name) - .sort() - }, 'file:list') - }) - - ipcMain.handle(IPC_CHANNELS.FILE_OPEN_PATH, async (_event, filePath: string): Promise> => { - return withErrorHandling(async () => { - const safePath = normalizeAndValidatePath(filePath) - log.debug('Opening path in explorer', { filePath: safePath }) - await fs.access(safePath) - await shell.openPath(safePath) - }, 'file:openPath') - }) + await shell.openPath(safePath) + }, 'file:openPath') + } + ) } diff --git a/src/main/ipc/material-type-handler.ts b/src/main/ipc/material-type-handler.ts index 44a9d59..50103e2 100644 --- a/src/main/ipc/material-type-handler.ts +++ b/src/main/ipc/material-type-handler.ts @@ -47,10 +47,7 @@ export function registerMaterialTypeHandlers(): void { */ ipcMain.handle( IPC_CHANNELS.MATERIAL_TYPE_GET_BY_MANAGER, - async ( - _event, - managerName: string - ): Promise> => { + async (_event, managerName: string): Promise> => { return withErrorHandling(async () => { const records = await dao.getMaterialsByManager(managerName) return records @@ -127,4 +124,3 @@ export function registerMaterialTypeHandlers(): void { log.info('Material type handlers registered') } - diff --git a/src/main/ipc/resolver-handler.ts b/src/main/ipc/resolver-handler.ts index 367b4b9..b0f803a 100644 --- a/src/main/ipc/resolver-handler.ts +++ b/src/main/ipc/resolver-handler.ts @@ -106,7 +106,9 @@ export function registerResolverHandlers(): void { async ( _event, inputs: string[] - ): Promise>> => { + ): Promise< + IpcResult> + > => { return withErrorHandling(async () => { // Create a mock resolver without database connection const resolver = new OrderNumberResolver({ diff --git a/src/main/ipc/user-erp-config-handler.ts b/src/main/ipc/user-erp-config-handler.ts index f14b1dc..5907ef3 100644 --- a/src/main/ipc/user-erp-config-handler.ts +++ b/src/main/ipc/user-erp-config-handler.ts @@ -42,7 +42,10 @@ export function registerUserErpConfigHandlers(): void { const credentials = await erpConfigService.getCurrentUserErpConfig() if (!credentials) { - throw new ValidationError('未找到 ERP 配置。请先配置 ERP 账号和密码。', 'VAL_INVALID_INPUT') + throw new ValidationError( + '未找到 ERP 配置。请先配置 ERP 账号和密码。', + 'VAL_INVALID_INPUT' + ) } const configManager = ConfigManager.getInstance() @@ -86,7 +89,10 @@ export function registerUserErpConfigHandlers(): void { ipcMain.handle( IPC_CHANNELS.USER_ERP_CONFIG_TEST_CONNECTION, - async (_event, credentials: ErpCredentialsRequest): Promise> => { + async ( + _event, + credentials: ErpCredentialsRequest + ): Promise> => { return withErrorHandling(async () => { if (!credentials.username || !credentials.password) { throw new ValidationError( @@ -139,4 +145,3 @@ export function registerUserErpConfigHandlers(): void { } ) } - diff --git a/src/main/ipc/validation-handler.ts b/src/main/ipc/validation-handler.ts index 556716b..80d3f82 100644 --- a/src/main/ipc/validation-handler.ts +++ b/src/main/ipc/validation-handler.ts @@ -497,18 +497,21 @@ export function registerValidationHandlers(): void { /** * Get unique manager names */ - ipcMain.handle(IPC_CHANNELS.MATERIALS_GET_MANAGERS, async (_event): Promise<{ managers: string[] }> => { - try { - const dao = new MaterialsToBeDeletedDAO() - const managers = await dao.getManagers() - return { managers } - } catch (error) { - log.error('Get managers error', { - error: error instanceof Error ? error.message : String(error) - }) - return { managers: [] } + ipcMain.handle( + IPC_CHANNELS.MATERIALS_GET_MANAGERS, + async (_event): Promise<{ managers: string[] }> => { + try { + const dao = new MaterialsToBeDeletedDAO() + const managers = await dao.getManagers() + return { managers } + } catch (error) { + log.error('Get managers error', { + error: error instanceof Error ? error.message : String(error) + }) + return { managers: [] } + } } - }) + ) /** * Update manager for a single material diff --git a/src/main/types/ipc-api.types.ts b/src/main/types/ipc-api.types.ts index 2dafe1b..6167db2 100644 --- a/src/main/types/ipc-api.types.ts +++ b/src/main/types/ipc-api.types.ts @@ -76,9 +76,7 @@ export interface ExtractorAPI { * Run ERP data extractor * @param input - Extractor input parameters */ - runExtractor: ( - input: ExtractorInput - ) => Promise> + runExtractor: (input: ExtractorInput) => Promise> /** * Subscribe to progress updates * @param callback - Callback function receiving progress data @@ -101,9 +99,7 @@ export interface CleanerAPI { * Run ERP cleaner service * @param input - Cleaner input parameters */ - runCleaner: ( - input: CleanerInput - ) => Promise> + runCleaner: (input: CleanerInput) => Promise> /** * Export validation results to Excel diff --git a/src/renderer/src/components/MaterialTypeManagementDialog.tsx b/src/renderer/src/components/MaterialTypeManagementDialog.tsx index f142a4c..e7fccc4 100644 --- a/src/renderer/src/components/MaterialTypeManagementDialog.tsx +++ b/src/renderer/src/components/MaterialTypeManagementDialog.tsx @@ -154,13 +154,16 @@ export const MaterialTypeManagementDialog: React.FC { - const row = rows[rowIndex] - if (row.state === 'deleted') return + const startEdit = useCallback( + (rowIndex: number, field: string) => { + const row = rows[rowIndex] + if (row.state === 'deleted') return - setEditingCell({ rowIndex, field }) - setEditValue(row.record[field as keyof MaterialTypeRecord] as string) - }, [rows]) + setEditingCell({ rowIndex, field }) + setEditValue(row.record[field as keyof MaterialTypeRecord] as string) + }, + [rows] + ) // Save edit const saveEdit = useCallback(() => { @@ -252,7 +255,9 @@ export const MaterialTypeManagementDialog: React.FC => { + const silentLogin = useCallback(async (): Promise<{ + success: boolean + requiresUserSelection?: boolean + }> => { setState((prev) => ({ ...prev, loading: true, error: null })) const result = await window.electron.auth.silentLogin() diff --git a/src/renderer/src/hooks/useCleaner.ts b/src/renderer/src/hooks/useCleaner.ts index 5d40ff2..262b19e 100644 --- a/src/renderer/src/hooks/useCleaner.ts +++ b/src/renderer/src/hooks/useCleaner.ts @@ -100,7 +100,9 @@ export function useCleaner() { // Load managers if (admin) { const resp = await window.electron.materials.getManagers() - const managersPayload = resp.success ? (resp.data as { managers: string[] } | undefined) : undefined + const managersPayload = resp.success + ? (resp.data as { managers: string[] } | undefined) + : undefined const managerList = managersPayload?.managers ?? [] setManagers(managerList) setSelectedManagers(new Set(managerList)) @@ -108,7 +110,9 @@ export function useCleaner() { // Get shared Production IDs const result = await window.electron.validation.getSharedProductionIds() - const idsPayload = result.success ? (result.data as { productionIds?: string[] } | undefined) : undefined + const idsPayload = result.success + ? (result.data as { productionIds?: string[] } | undefined) + : undefined setSharedProductionIdsCount(idsPayload?.productionIds?.length ?? 0) } catch (err) { console.error('Initialization failed:', err) @@ -297,7 +301,9 @@ export function useCleaner() { if (materialsToUpsert.length > 0) { const res = await window.electron.materials.upsertBatch(materialsToUpsert) - const payload = res.success ? (res.data as { stats?: { success?: number } } | undefined) : undefined + const payload = res.success + ? (res.data as { stats?: { success?: number } } | undefined) + : undefined if (!res.success) throw new Error(res.error || '写入物料失败') msgParts.push(`写入/更新成功:${payload?.stats?.success || 0} 条`) } @@ -314,7 +320,9 @@ export function useCleaner() { // Reload managers if admin if (isAdmin) { const resp = await window.electron.materials.getManagers() - const payload = resp.success ? (resp.data as { managers?: string[] } | undefined) : undefined + const payload = resp.success + ? (resp.data as { managers?: string[] } | undefined) + : undefined setManagers(payload?.managers ?? []) } } catch (err) { diff --git a/src/renderer/src/hooks/useValidation.ts b/src/renderer/src/hooks/useValidation.ts index 242324f..6820347 100644 --- a/src/renderer/src/hooks/useValidation.ts +++ b/src/renderer/src/hooks/useValidation.ts @@ -52,28 +52,39 @@ export function useValidation(): UseValidationReturn { error: null }) - const validate = useCallback(async (request: ValidationRequest): Promise => { - setState((prev) => ({ ...prev, loading: true, error: null })) - const response = await window.electron.validation.validate(request) - if (!response.success || !response.data) { - setState((prev) => ({ ...prev, loading: false, error: response.error || 'Validation failed' })) - return response.success ? null : { success: false, error: response.error } - } + const validate = useCallback( + async (request: ValidationRequest): Promise => { + setState((prev) => ({ ...prev, loading: true, error: null })) + const response = await window.electron.validation.validate(request) + if (!response.success || !response.data) { + setState((prev) => ({ + ...prev, + loading: false, + error: response.error || 'Validation failed' + })) + return response.success ? null : { success: false, error: response.error } + } - const result = response.data as ValidationResponse - if (!result.success) { - setState((prev) => ({ ...prev, loading: false, error: result.error || 'Validation failed' })) + const result = response.data as ValidationResponse + if (!result.success) { + setState((prev) => ({ + ...prev, + loading: false, + error: result.error || 'Validation failed' + })) + return result + } + + setState({ + loading: false, + data: result.results || null, + stats: result.stats || null, + error: null + }) return result - } - - setState({ - loading: false, - data: result.results || null, - stats: result.stats || null, - error: null - }) - return result - }, []) + }, + [] + ) const setSharedProductionIds = useCallback(async (ids: string[]): Promise => { await window.electron.validation.setSharedProductionIds(ids) @@ -88,7 +99,10 @@ export function useValidation(): UseValidationReturn { return payload.productionIds || [] }, []) - const getCleanerData = useCallback(async (): Promise<{ orderNumbers: string[]; materialCodes: string[] } | null> => { + const getCleanerData = useCallback(async (): Promise<{ + orderNumbers: string[] + materialCodes: string[] + } | null> => { const result = await window.electron.validation.getCleanerData() if (!result.success || !result.data) { return null diff --git a/src/renderer/src/pages/SettingsPage.tsx b/src/renderer/src/pages/SettingsPage.tsx index aeb3f48..3d2f557 100644 --- a/src/renderer/src/pages/SettingsPage.tsx +++ b/src/renderer/src/pages/SettingsPage.tsx @@ -27,7 +27,9 @@ const SettingsPage: React.FC = () => { setIsLoading(true) // ERP credentials are loaded from database (current user's config) const response = await window.electron.settings.getSettings() - const config = response.success ? (response.data as { erp?: ErpCredentials } | undefined) : undefined + const config = response.success + ? (response.data as { erp?: ErpCredentials } | undefined) + : undefined // Extract ERP credentials from the config if (config?.erp) { @@ -60,7 +62,9 @@ const SettingsPage: React.FC = () => { password: credentials.password } }) - const saveData = result.success ? (result.data as { success?: boolean; error?: string } | undefined) : undefined + const saveData = result.success + ? (result.data as { success?: boolean; error?: string } | undefined) + : undefined if (result.success && saveData?.success !== false) { setIsModified(false)