refactor(ipc): harden channels and unify IPC contracts
This commit is contained in:
@@ -24,7 +24,7 @@ function createWindow(): void {
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
sandbox: true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ import { ipcMain } from 'electron'
|
||||
import { SessionManager } from '../services/user/session-manager'
|
||||
import { createLogger } from '../services/logger'
|
||||
import type { UserInfo } from '../types/user.types'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
import { ValidationError } from '../types/errors'
|
||||
import { withErrorHandling, type IpcResult } from './index'
|
||||
|
||||
const log = createLogger('AuthHandler')
|
||||
|
||||
@@ -70,16 +73,18 @@ export function registerAuthHandlers(): void {
|
||||
/**
|
||||
* Get computer name
|
||||
*/
|
||||
ipcMain.handle('auth:getComputerName', async (): Promise<string> => {
|
||||
const os = await import('os')
|
||||
return os.hostname()
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME, async (): Promise<IpcResult<string>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const os = await import('os')
|
||||
return os.hostname()
|
||||
}, 'auth:getComputerName')
|
||||
})
|
||||
|
||||
/**
|
||||
* Silent login by computer name
|
||||
*/
|
||||
ipcMain.handle('auth:silentLogin', async (): Promise<SilentLoginResponse> => {
|
||||
try {
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_SILENT_LOGIN, async (): Promise<IpcResult<SilentLoginResponse>> => {
|
||||
return withErrorHandling(async () => {
|
||||
log.info('Attempting silent login')
|
||||
const success = await sessionManager.loginByComputerName()
|
||||
const userInfo = sessionManager.getUserInfo()
|
||||
@@ -101,34 +106,22 @@ export function registerAuthHandlers(): void {
|
||||
}
|
||||
}
|
||||
|
||||
log.warn('Silent login failed - no matching user')
|
||||
return {
|
||||
success: false,
|
||||
requiresUserSelection: false
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Silent login error', { error: message })
|
||||
return {
|
||||
success: false,
|
||||
error: `无感登录失败:${message}`
|
||||
}
|
||||
}
|
||||
throw new ValidationError('无感登录失败:未找到匹配用户', 'VAL_INVALID_INPUT')
|
||||
}, 'auth:silentLogin')
|
||||
})
|
||||
|
||||
/**
|
||||
* Login with username and password
|
||||
*/
|
||||
ipcMain.handle('auth:login', async (_event, request: LoginRequest): Promise<LoginResponse> => {
|
||||
try {
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.AUTH_LOGIN,
|
||||
async (_event, request: LoginRequest): Promise<IpcResult<LoginResponse>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const { username, password } = request
|
||||
|
||||
if (!username || !password) {
|
||||
log.warn('Login attempt with missing credentials')
|
||||
return {
|
||||
success: false,
|
||||
error: '请输入用户名和密码'
|
||||
}
|
||||
throw new ValidationError('请输入用户名和密码', 'VAL_MISSING_REQUIRED')
|
||||
}
|
||||
|
||||
log.info('Login attempt', { username })
|
||||
@@ -144,57 +137,53 @@ export function registerAuthHandlers(): void {
|
||||
}
|
||||
|
||||
log.warn('Login failed - invalid credentials', { username })
|
||||
return {
|
||||
success: false,
|
||||
error: '用户名或密码错误'
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Login error', { error: message })
|
||||
return {
|
||||
success: false,
|
||||
error: `登录失败:${message}`
|
||||
}
|
||||
throw new ValidationError('用户名或密码错误', 'VAL_INVALID_INPUT')
|
||||
}, 'auth:login')
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
ipcMain.handle('auth:logout', async (): Promise<void> => {
|
||||
const userInfo = sessionManager.getUserInfo()
|
||||
log.info('User logout', { username: userInfo?.username })
|
||||
sessionManager.logout()
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_LOGOUT, async (): Promise<IpcResult<void>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const userInfo = sessionManager.getUserInfo()
|
||||
log.info('User logout', { username: userInfo?.username })
|
||||
sessionManager.logout()
|
||||
}, 'auth:logout')
|
||||
})
|
||||
|
||||
/**
|
||||
* Get current user
|
||||
*/
|
||||
ipcMain.handle('auth:getCurrentUser', async (): Promise<CurrentUserResponse> => {
|
||||
const isAuthenticated = sessionManager.isAuthenticated()
|
||||
const userInfo = sessionManager.getUserInfo()
|
||||
|
||||
return {
|
||||
isAuthenticated,
|
||||
userInfo: userInfo ?? undefined
|
||||
}
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_GET_CURRENT_USER, async (): Promise<IpcResult<CurrentUserResponse>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const isAuthenticated = sessionManager.isAuthenticated()
|
||||
const userInfo = sessionManager.getUserInfo()
|
||||
return {
|
||||
isAuthenticated,
|
||||
userInfo: userInfo ?? undefined
|
||||
}
|
||||
}, 'auth:getCurrentUser')
|
||||
})
|
||||
|
||||
/**
|
||||
* Get all users (for admin user selection)
|
||||
*/
|
||||
ipcMain.handle('auth:getAllUsers', async (): Promise<UserInfo[]> => {
|
||||
log.debug('Fetching all users for admin selection')
|
||||
return await sessionManager.getAllUsers()
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_GET_ALL_USERS, async (): Promise<IpcResult<UserInfo[]>> => {
|
||||
return withErrorHandling(async () => {
|
||||
log.debug('Fetching all users for admin selection')
|
||||
return await sessionManager.getAllUsers()
|
||||
}, 'auth:getAllUsers')
|
||||
})
|
||||
|
||||
/**
|
||||
* Switch user (admin only)
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'auth:switchUser',
|
||||
async (_event, userInfo: UserInfo): Promise<UserSelectionResponse> => {
|
||||
try {
|
||||
IPC_CHANNELS.AUTH_SWITCH_USER,
|
||||
async (_event, userInfo: UserInfo): Promise<IpcResult<UserSelectionResponse>> => {
|
||||
return withErrorHandling(async () => {
|
||||
log.info('User switch attempt', { targetUser: userInfo.username })
|
||||
const success = sessionManager.switchUser(userInfo)
|
||||
|
||||
@@ -208,25 +197,16 @@ export function registerAuthHandlers(): void {
|
||||
}
|
||||
|
||||
log.warn('User switch failed')
|
||||
return {
|
||||
success: false,
|
||||
error: '用户切换失败'
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('User switch error', { error: message })
|
||||
return {
|
||||
success: false,
|
||||
error: `用户切换失败:${message}`
|
||||
}
|
||||
}
|
||||
throw new ValidationError('用户切换失败', 'VAL_INVALID_INPUT')
|
||||
}, 'auth:switchUser')
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if current user is admin
|
||||
*/
|
||||
ipcMain.handle('auth:isAdmin', async (): Promise<boolean> => {
|
||||
return sessionManager.isAdmin()
|
||||
ipcMain.handle(IPC_CHANNELS.AUTH_IS_ADMIN, async (): Promise<IpcResult<boolean>> => {
|
||||
return withErrorHandling(async () => sessionManager.isAdmin(), 'auth:isAdmin')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ipcMain, webContents } from 'electron'
|
||||
import { ipcMain, type WebContents } from 'electron'
|
||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
||||
import { CleanerService } from '../services/erp/cleaner'
|
||||
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
||||
@@ -19,11 +19,12 @@ import type {
|
||||
ExportResultResponse
|
||||
} from '../types/cleaner.types'
|
||||
import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
|
||||
const log = createLogger('CleanerHandler')
|
||||
|
||||
function sendProgress(
|
||||
windowId: number,
|
||||
sender: WebContents,
|
||||
message: string,
|
||||
progress: number,
|
||||
extra?: Partial<CleanerProgress>
|
||||
@@ -39,9 +40,7 @@ function sendProgress(
|
||||
currentOrderNumber: extra?.currentOrderNumber,
|
||||
phase: extra?.phase ?? 'processing'
|
||||
}
|
||||
webContents.getAllWebContents().forEach((wc) => {
|
||||
wc.send('cleaner:progress', progressData)
|
||||
})
|
||||
sender.send(IPC_CHANNELS.CLEANER_PROGRESS, progressData)
|
||||
} catch (error) {
|
||||
log.warn('Failed to send progress event', { error })
|
||||
}
|
||||
@@ -116,9 +115,9 @@ async function getErpConfig(): Promise<{
|
||||
|
||||
export function registerCleanerHandlers(): void {
|
||||
ipcMain.handle(
|
||||
'cleaner:run',
|
||||
IPC_CHANNELS.CLEANER_RUN,
|
||||
async (event, input: CleanerInput): Promise<IpcResult<CleanerResult>> => {
|
||||
const windowId = event.sender.id
|
||||
const sender = event.sender
|
||||
const startTime = Date.now()
|
||||
|
||||
return withErrorHandling(async () => {
|
||||
@@ -192,7 +191,7 @@ export function registerCleanerHandlers(): void {
|
||||
// Send login complete progress
|
||||
const totalOrders = validOrderNumbers.length
|
||||
const loginProgress = (1 / (1 + totalOrders)) * 100
|
||||
sendProgress(windowId, 'ERP 登录成功', loginProgress, {
|
||||
sendProgress(sender, 'ERP 登录成功', loginProgress, {
|
||||
phase: 'login',
|
||||
currentOrderIndex: 0,
|
||||
totalOrders,
|
||||
@@ -206,7 +205,7 @@ export function registerCleanerHandlers(): void {
|
||||
...input,
|
||||
orderNumbers: validOrderNumbers,
|
||||
onProgress: (message, progress, extra) => {
|
||||
sendProgress(windowId, message, progress ?? 0, extra)
|
||||
sendProgress(sender, message, progress ?? 0, extra)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +217,7 @@ export function registerCleanerHandlers(): void {
|
||||
}
|
||||
|
||||
// Send completion progress
|
||||
sendProgress(windowId, '清理完成', 100, {
|
||||
sendProgress(sender, '清理完成', 100, {
|
||||
phase: 'complete',
|
||||
currentOrderIndex: totalOrders,
|
||||
totalOrders,
|
||||
@@ -280,30 +279,16 @@ export function registerCleanerHandlers(): void {
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
'cleaner:exportResults',
|
||||
async (_event, items: ExportResultItem[]): Promise<ExportResultResponse> => {
|
||||
try {
|
||||
IPC_CHANNELS.CLEANER_EXPORT_RESULTS,
|
||||
async (_event, items: ExportResultItem[]): Promise<IpcResult<ExportResultResponse>> => {
|
||||
return withErrorHandling(async () => {
|
||||
log.info('Exporting validation results', { count: items.length })
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: '没有数据可导出'
|
||||
}
|
||||
throw new ValidationError('没有数据可导出', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
|
||||
const exporter = new ResultExporter()
|
||||
const result = await exporter.exportValidationResults(items)
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
log.error('Export handler failed', { error: errorMessage })
|
||||
return {
|
||||
success: false,
|
||||
error: errorMessage
|
||||
}
|
||||
}
|
||||
return await exporter.exportValidationResults(items)
|
||||
}, 'cleaner:exportResults')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ import { ipcMain } from 'electron'
|
||||
import { MySqlService } from '../services/database/mysql'
|
||||
import { SqlServerService } from '../services/database/sql-server'
|
||||
import { createLogger } from '../services/logger'
|
||||
import { DatabaseQueryError, ValidationError } from '../types/errors'
|
||||
import { ValidationError } from '../types/errors'
|
||||
import type {
|
||||
MySqlConfig,
|
||||
MySqlQueryResult,
|
||||
SqlServerConfig,
|
||||
SqlServerQueryResult
|
||||
} from '../types/ipc-api.types'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
import { withErrorHandling, type IpcResult } from './index'
|
||||
|
||||
const log = createLogger('DatabaseHandler')
|
||||
|
||||
@@ -17,6 +19,34 @@ const mysqlServices = new Map<string, MySqlService>()
|
||||
|
||||
// Store SQL Server service instances per window/connection
|
||||
const sqlServerServices = new Map<string, SqlServerService>()
|
||||
const cleanupBoundWindows = new Set<string>()
|
||||
|
||||
function bindWindowCleanup(windowId: string, sender: { once: (event: string, listener: () => void) => void }): void {
|
||||
if (cleanupBoundWindows.has(windowId)) {
|
||||
return
|
||||
}
|
||||
|
||||
sender.once('destroyed', () => {
|
||||
const mysql = getMySqlService(windowId)
|
||||
const sqlServer = getSqlServerService(windowId)
|
||||
|
||||
if (mysql) {
|
||||
mysql.disconnect().catch((error) => log.warn('MySQL disconnect on window destroy failed', { error }))
|
||||
deleteMySqlService(windowId)
|
||||
}
|
||||
|
||||
if (sqlServer) {
|
||||
sqlServer
|
||||
.disconnect()
|
||||
.catch((error) => log.warn('SQL Server disconnect on window destroy failed', { error }))
|
||||
deleteSqlServerService(windowId)
|
||||
}
|
||||
|
||||
cleanupBoundWindows.delete(windowId)
|
||||
})
|
||||
|
||||
cleanupBoundWindows.add(windowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create MySQL service for a connection ID
|
||||
@@ -65,29 +95,25 @@ function deleteSqlServerService(connectionId: string): void {
|
||||
*/
|
||||
export function registerDatabaseHandlers(): void {
|
||||
// Connect to MySQL
|
||||
ipcMain.handle('database:mysql:connect', async (event, config: MySqlConfig): Promise<void> => {
|
||||
try {
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.DATABASE_MYSQL_CONNECT,
|
||||
async (event, config: MySqlConfig): Promise<IpcResult<void>> => {
|
||||
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 })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to connect to MySQL'
|
||||
log.error('MySQL connection failed', { error: message })
|
||||
throw new DatabaseQueryError(
|
||||
message,
|
||||
'DB_CONNECTION_FAILED',
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
}, 'database:mysql:connect')
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// Disconnect from MySQL
|
||||
ipcMain.handle('database:mysql:disconnect', async (event): Promise<void> => {
|
||||
try {
|
||||
ipcMain.handle(IPC_CHANNELS.DATABASE_MYSQL_DISCONNECT, async (event): Promise<IpcResult<void>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const windowId = (event.sender as { id: number }).id.toString()
|
||||
const service = getMySqlService(windowId)
|
||||
if (service) {
|
||||
@@ -95,29 +121,23 @@ export function registerDatabaseHandlers(): void {
|
||||
deleteMySqlService(windowId)
|
||||
log.info('MySQL disconnected', { windowId })
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to disconnect from MySQL'
|
||||
log.error('MySQL disconnect failed', { error: message })
|
||||
throw new DatabaseQueryError(
|
||||
message,
|
||||
'DB_CONNECTION_FAILED',
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
}
|
||||
}, 'database:mysql:disconnect')
|
||||
})
|
||||
|
||||
// Check if MySQL is connected
|
||||
ipcMain.handle('database:mysql:isConnected', async (event): Promise<boolean> => {
|
||||
const windowId = (event.sender as { id: number }).id.toString()
|
||||
const service = getMySqlService(windowId)
|
||||
return service ? service.isConnected() : false
|
||||
ipcMain.handle(IPC_CHANNELS.DATABASE_MYSQL_IS_CONNECTED, async (event): Promise<IpcResult<boolean>> => {
|
||||
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(
|
||||
'database:mysql:query',
|
||||
async (event, sql: string, params?: unknown[]): Promise<MySqlQueryResult> => {
|
||||
try {
|
||||
IPC_CHANNELS.DATABASE_MYSQL_QUERY,
|
||||
async (event, sql: string, params?: unknown[]): Promise<IpcResult<MySqlQueryResult>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const windowId = (event.sender as { id: number }).id.toString()
|
||||
const service = getMySqlService(windowId)
|
||||
|
||||
@@ -130,44 +150,29 @@ export function registerDatabaseHandlers(): void {
|
||||
|
||||
log.debug('Executing MySQL query', { windowId, sql: sql.substring(0, 100) })
|
||||
return await service.query(sql, params)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'MySQL query failed'
|
||||
log.error('MySQL query failed', { error: message })
|
||||
throw new DatabaseQueryError(
|
||||
message,
|
||||
'DB_QUERY_FAILED',
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
}
|
||||
}, 'database:mysql:query')
|
||||
}
|
||||
)
|
||||
|
||||
// Connect to SQL Server
|
||||
ipcMain.handle(
|
||||
'database:sqlserver:connect',
|
||||
async (event, config: SqlServerConfig): Promise<void> => {
|
||||
try {
|
||||
IPC_CHANNELS.DATABASE_SQLSERVER_CONNECT,
|
||||
async (event, config: SqlServerConfig): Promise<IpcResult<void>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const windowId = (event.sender as { id: number }).id.toString()
|
||||
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()
|
||||
setSqlServerService(windowId, service)
|
||||
log.info('SQL Server connected', { windowId })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to connect to SQL Server'
|
||||
log.error('SQL Server connection failed', { error: message })
|
||||
throw new DatabaseQueryError(
|
||||
message,
|
||||
'DB_CONNECTION_FAILED',
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
}
|
||||
}, 'database:sqlserver:connect')
|
||||
}
|
||||
)
|
||||
|
||||
// Disconnect from SQL Server
|
||||
ipcMain.handle('database:sqlserver:disconnect', async (event): Promise<void> => {
|
||||
try {
|
||||
ipcMain.handle(IPC_CHANNELS.DATABASE_SQLSERVER_DISCONNECT, async (event): Promise<IpcResult<void>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const windowId = (event.sender as { id: number }).id.toString()
|
||||
const service = getSqlServerService(windowId)
|
||||
if (service) {
|
||||
@@ -175,34 +180,27 @@ export function registerDatabaseHandlers(): void {
|
||||
deleteSqlServerService(windowId)
|
||||
log.info('SQL Server disconnected', { windowId })
|
||||
}
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Failed to disconnect from SQL Server'
|
||||
log.error('SQL Server disconnect failed', { error: message })
|
||||
throw new DatabaseQueryError(
|
||||
message,
|
||||
'DB_CONNECTION_FAILED',
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
}
|
||||
}, 'database:sqlserver:disconnect')
|
||||
})
|
||||
|
||||
// Check if SQL Server is connected
|
||||
ipcMain.handle('database:sqlserver:isConnected', async (event): Promise<boolean> => {
|
||||
const windowId = (event.sender as { id: number }).id.toString()
|
||||
const service = getSqlServerService(windowId)
|
||||
return service ? service.isConnected() : false
|
||||
ipcMain.handle(IPC_CHANNELS.DATABASE_SQLSERVER_IS_CONNECTED, async (event): Promise<IpcResult<boolean>> => {
|
||||
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(
|
||||
'database:sqlserver:query',
|
||||
IPC_CHANNELS.DATABASE_SQLSERVER_QUERY,
|
||||
async (
|
||||
event,
|
||||
sqlString: string,
|
||||
params?: Record<string, unknown>
|
||||
): Promise<SqlServerQueryResult> => {
|
||||
try {
|
||||
): Promise<IpcResult<SqlServerQueryResult>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const windowId = (event.sender as { id: number }).id.toString()
|
||||
const service = getSqlServerService(windowId)
|
||||
|
||||
@@ -226,15 +224,7 @@ export function registerDatabaseHandlers(): void {
|
||||
} else {
|
||||
return await service.query(sqlString)
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'SQL Server query failed'
|
||||
log.error('SQL Server query failed', { error: message })
|
||||
throw new DatabaseQueryError(
|
||||
message,
|
||||
'DB_QUERY_FAILED',
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
}
|
||||
}, 'database:sqlserver:query')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ipcMain, webContents } from 'electron'
|
||||
import { ipcMain, type WebContents } from 'electron'
|
||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
||||
import { ExtractorService } from '../services/erp/extractor'
|
||||
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
||||
@@ -9,30 +9,27 @@ import { ErpConnectionError, ValidationError, DatabaseQueryError } from '../type
|
||||
import type { ExtractorInput, ExtractorResult, ExtractionProgress } from '../types/extractor.types'
|
||||
import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
||||
import { ConfigManager } from '../services/config/config-manager'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
|
||||
const log = createLogger('ExtractorHandler')
|
||||
|
||||
function sendProgress(
|
||||
windowId: number,
|
||||
sender: WebContents,
|
||||
message: string,
|
||||
progress: number,
|
||||
extra?: Partial<ExtractionProgress>
|
||||
): void {
|
||||
try {
|
||||
const progressData = { message, progress, ...extra }
|
||||
webContents.getAllWebContents().forEach((wc) => {
|
||||
wc.send('extractor:progress', progressData)
|
||||
})
|
||||
sender.send(IPC_CHANNELS.EXTRACTOR_PROGRESS, progressData)
|
||||
} catch (error) {
|
||||
log.warn('Failed to send progress event', { error })
|
||||
}
|
||||
}
|
||||
|
||||
function sendLog(windowId: number, level: string, message: string): void {
|
||||
function sendLog(sender: WebContents, level: string, message: string): void {
|
||||
try {
|
||||
webContents.getAllWebContents().forEach((wc) => {
|
||||
wc.send('extractor:log', { level, message })
|
||||
})
|
||||
sender.send(IPC_CHANNELS.EXTRACTOR_LOG, { level, message })
|
||||
} catch (error) {
|
||||
log.warn('Failed to send log event', { error })
|
||||
}
|
||||
@@ -76,14 +73,13 @@ async function getErpConfig(): Promise<{
|
||||
*/
|
||||
export function registerExtractorHandlers(): void {
|
||||
ipcMain.handle(
|
||||
'extractor:run',
|
||||
IPC_CHANNELS.EXTRACTOR_RUN,
|
||||
async (event, input: ExtractorInput): Promise<IpcResult<ExtractorResult>> => {
|
||||
const windowId = event.sender.id
|
||||
const sender = event.sender
|
||||
|
||||
return withErrorHandling(async () => {
|
||||
let authService: ErpAuthService | null = null
|
||||
let dbService: IDatabaseService | null = null
|
||||
let erpConfigService: UserErpConfigService | null = null
|
||||
|
||||
try {
|
||||
// Get ERP configuration from database for current user
|
||||
@@ -97,11 +93,11 @@ export function registerExtractorHandlers(): void {
|
||||
|
||||
// Create database service using factory
|
||||
log.info('Connecting to database for order resolution...')
|
||||
sendProgress(windowId, '连接数据库...', 3.33, {
|
||||
sendProgress(sender, '连接数据库...', 3.33, {
|
||||
phase: 'login',
|
||||
subProgress: { step: '连接数据库', current: 1, total: 3 }
|
||||
})
|
||||
sendLog(windowId, 'system', '正在连接数据库...')
|
||||
sendLog(sender, 'system', '正在连接数据库...')
|
||||
|
||||
try {
|
||||
dbService = await create()
|
||||
@@ -114,11 +110,11 @@ export function registerExtractorHandlers(): void {
|
||||
}
|
||||
|
||||
// Resolve order numbers (convert productionIDs to 生产订单号)
|
||||
sendProgress(windowId, '解析订单号...', 6.67, {
|
||||
sendProgress(sender, '解析订单号...', 6.67, {
|
||||
phase: 'login',
|
||||
subProgress: { step: '解析订单号', current: 2, total: 3 }
|
||||
})
|
||||
sendLog(windowId, 'info', '正在解析订单号...')
|
||||
sendLog(sender, 'info', '正在解析订单号...')
|
||||
|
||||
const resolver = new OrderNumberResolver(dbService)
|
||||
const mappings = await resolver.resolve(input.orderNumbers)
|
||||
@@ -139,7 +135,7 @@ export function registerExtractorHandlers(): void {
|
||||
}
|
||||
|
||||
log.info('Resolved order numbers', { count: validOrderNumbers.length })
|
||||
sendLog(windowId, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`)
|
||||
sendLog(sender, 'info', `已解析 ${validOrderNumbers.length} 个有效订单号`)
|
||||
|
||||
// Create auth service and login
|
||||
authService = new ErpAuthService({
|
||||
@@ -149,18 +145,18 @@ export function registerExtractorHandlers(): void {
|
||||
headless: true
|
||||
})
|
||||
|
||||
sendProgress(windowId, '登录 ERP 系统...', 9.99, {
|
||||
sendProgress(sender, '登录 ERP 系统...', 9.99, {
|
||||
phase: 'login',
|
||||
subProgress: { step: '登录 ERP 系统', current: 3, total: 3 }
|
||||
})
|
||||
sendLog(windowId, 'system', '正在登录 ERP 系统...')
|
||||
sendLog(sender, 'system', '正在登录 ERP 系统...')
|
||||
|
||||
log.info('Logging in to ERP...')
|
||||
try {
|
||||
await authService.login()
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : '未知错误'
|
||||
sendLog(windowId, 'error', `ERP 登录失败:${errorMsg}`)
|
||||
sendLog(sender, 'error', `ERP 登录失败:${errorMsg}`)
|
||||
throw new ErpConnectionError(
|
||||
'ERP 登录失败',
|
||||
'ERP_LOGIN_FAILED',
|
||||
@@ -168,7 +164,7 @@ export function registerExtractorHandlers(): void {
|
||||
)
|
||||
}
|
||||
log.info('Login successful')
|
||||
sendLog(windowId, 'success', 'ERP 登录成功')
|
||||
sendLog(sender, 'success', 'ERP 登录成功')
|
||||
|
||||
// Create extractor service and run extraction with resolved order numbers
|
||||
const extractor = new ExtractorService(authService)
|
||||
@@ -178,11 +174,11 @@ export function registerExtractorHandlers(): void {
|
||||
...input,
|
||||
orderNumbers: validOrderNumbers,
|
||||
onProgress: (message, progress, extra) => {
|
||||
sendProgress(windowId, message, progress, extra)
|
||||
sendLog(windowId, 'info', message)
|
||||
sendProgress(sender, message, progress, extra)
|
||||
sendLog(sender, 'info', message)
|
||||
},
|
||||
onLog: (level, message) => {
|
||||
sendLog(windowId, level, message)
|
||||
sendLog(sender, level, message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,67 +1,87 @@
|
||||
import { ipcMain, shell } from 'electron'
|
||||
import { app, ipcMain, shell } from 'electron'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as path from 'path'
|
||||
import { createLogger } from '../services/logger'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
import { ValidationError } from '../types/errors'
|
||||
import { withErrorHandling, type IpcResult } from './index'
|
||||
|
||||
const log = createLogger('FileHandler')
|
||||
|
||||
function getAllowedRoots(): string[] {
|
||||
return [path.resolve(app.getAppPath()), path.resolve(app.getPath('userData'))]
|
||||
}
|
||||
|
||||
export function isPathWithinAllowedRoots(inputPath: string, roots: string[]): boolean {
|
||||
const normalized = path.resolve(inputPath)
|
||||
return roots.some((root) => {
|
||||
const rel = path.relative(root, normalized)
|
||||
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeAndValidatePath(inputPath: string): string {
|
||||
const normalized = path.resolve(inputPath)
|
||||
const isAllowed = isPathWithinAllowedRoots(normalized, getAllowedRoots())
|
||||
if (!isAllowed) {
|
||||
throw new ValidationError('Path is outside allowed roots', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function registerFileHandlers(): void {
|
||||
ipcMain.handle('file:read', async (_event, filePath: string): Promise<string> => {
|
||||
try {
|
||||
log.debug('Reading file', { filePath })
|
||||
return await fs.readFile(filePath, 'utf-8')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to read file'
|
||||
log.error('Failed to read file', { filePath, error: message })
|
||||
throw new Error(message)
|
||||
}
|
||||
ipcMain.handle(IPC_CHANNELS.FILE_READ, async (_event, filePath: string): Promise<IpcResult<string>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const safePath = normalizeAndValidatePath(filePath)
|
||||
log.debug('Reading file', { filePath: safePath })
|
||||
return await fs.readFile(safePath, 'utf-8')
|
||||
}, 'file:read')
|
||||
})
|
||||
|
||||
ipcMain.handle('file:write', async (_event, filePath: string, content: string): Promise<void> => {
|
||||
try {
|
||||
log.debug('Writing file', { filePath })
|
||||
const dir = path.dirname(filePath)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await fs.writeFile(filePath, content, 'utf-8')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to write file'
|
||||
log.error('Failed to write file', { filePath, error: message })
|
||||
throw new Error(message)
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.FILE_WRITE,
|
||||
async (_event, filePath: string, content: string): Promise<IpcResult<void>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const safePath = normalizeAndValidatePath(filePath)
|
||||
log.debug('Writing file', { filePath: safePath })
|
||||
const dir = path.dirname(safePath)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await fs.writeFile(safePath, content, 'utf-8')
|
||||
}, 'file:write')
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.FILE_EXISTS, async (_event, filePath: string): Promise<IpcResult<boolean>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const safePath = normalizeAndValidatePath(filePath)
|
||||
try {
|
||||
await fs.access(safePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}, 'file:exists')
|
||||
})
|
||||
|
||||
ipcMain.handle('file:exists', async (_event, filePath: string): Promise<boolean> => {
|
||||
try {
|
||||
await fs.access(filePath)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('file:list', async (_event, dirPath: string): Promise<string[]> => {
|
||||
try {
|
||||
log.debug('Listing directory', { dirPath })
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
ipcMain.handle(IPC_CHANNELS.FILE_LIST, async (_event, dirPath: string): Promise<IpcResult<string[]>> => {
|
||||
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()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to list directory'
|
||||
log.error('Failed to list directory', { dirPath, error: message })
|
||||
throw new Error(message)
|
||||
}
|
||||
}, 'file:list')
|
||||
})
|
||||
|
||||
ipcMain.handle('file:openPath', async (_event, filePath: string): Promise<void> => {
|
||||
try {
|
||||
log.debug('Opening path in explorer', { filePath })
|
||||
await shell.openPath(filePath)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to open path'
|
||||
log.error('Failed to open path', { filePath, error: message })
|
||||
throw new Error(message)
|
||||
}
|
||||
ipcMain.handle(IPC_CHANNELS.FILE_OPEN_PATH, async (_event, filePath: string): Promise<IpcResult<void>> => {
|
||||
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')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,6 +28,14 @@ export interface IpcResult<T = unknown> {
|
||||
code?: string
|
||||
}
|
||||
|
||||
export function ok<T>(data: T): IpcResult<T> {
|
||||
return { success: true, data }
|
||||
}
|
||||
|
||||
export function fail<T = unknown>(error: string, code?: string): IpcResult<T> {
|
||||
return { success: false, error, code }
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher-order function to wrap IPC handlers with consistent error handling
|
||||
* @param handler - The async handler function to wrap
|
||||
@@ -39,9 +47,9 @@ export function withErrorHandling<T>(
|
||||
context: string
|
||||
): Promise<IpcResult<T>> {
|
||||
return handler()
|
||||
.then((data) => {
|
||||
.then((data): IpcResult<T> => {
|
||||
log.debug(`[${context}] Handler completed successfully`)
|
||||
return { success: true, data }
|
||||
return ok(data)
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
const message = getErrorMessage(error)
|
||||
@@ -58,7 +66,7 @@ export function withErrorHandling<T>(
|
||||
log.debug(`[${context}] Stack trace:`, { stack: error.stack })
|
||||
}
|
||||
|
||||
return { success: false, error: message, code }
|
||||
return fail<T>(message, code)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ import {
|
||||
type MaterialTypeBatchRequest
|
||||
} from '../services/database/materials-type-to-be-deleted-dao'
|
||||
import { createLogger } from '../services/logger'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
import { ValidationError } from '../types/errors'
|
||||
import { withErrorHandling, type IpcResult } from './index'
|
||||
|
||||
const log = createLogger('MaterialTypeHandler')
|
||||
|
||||
@@ -30,16 +33,12 @@ export function registerMaterialTypeHandlers(): void {
|
||||
* Get all material type records
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:getAll',
|
||||
async (): Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> => {
|
||||
try {
|
||||
IPC_CHANNELS.MATERIAL_TYPE_GET_ALL,
|
||||
async (): Promise<IpcResult<MaterialTypeRecord[]>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const records = await dao.getAllMaterials()
|
||||
return { success: true, data: records }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Get all material types error', { error: message })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
return records
|
||||
}, 'materialType:getAll')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -47,19 +46,15 @@ export function registerMaterialTypeHandlers(): void {
|
||||
* Get material types by manager
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:getByManager',
|
||||
IPC_CHANNELS.MATERIAL_TYPE_GET_BY_MANAGER,
|
||||
async (
|
||||
_event,
|
||||
managerName: string
|
||||
): Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> => {
|
||||
try {
|
||||
): Promise<IpcResult<MaterialTypeRecord[]>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const records = await dao.getMaterialsByManager(managerName)
|
||||
return { success: true, data: records }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Get material types by manager error', { error: message })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
return records
|
||||
}, 'materialType:getByManager')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -67,16 +62,12 @@ export function registerMaterialTypeHandlers(): void {
|
||||
* Get list of managers
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:getManagers',
|
||||
async (): Promise<{ success: boolean; data?: string[]; error?: string }> => {
|
||||
try {
|
||||
IPC_CHANNELS.MATERIAL_TYPE_GET_MANAGERS,
|
||||
async (): Promise<IpcResult<string[]>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const managers = await dao.getManagers()
|
||||
return { success: true, data: managers }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Get managers error', { error: message })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
return managers
|
||||
}, 'materialType:getManagers')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -84,22 +75,18 @@ export function registerMaterialTypeHandlers(): void {
|
||||
* Upsert (insert or update) a material type record
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:upsert',
|
||||
IPC_CHANNELS.MATERIAL_TYPE_UPSERT,
|
||||
async (
|
||||
_event,
|
||||
{ materialName, managerName }: { materialName: string; managerName: string }
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
): Promise<IpcResult<{ updated: boolean }>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const result = await dao.upsertMaterial(materialName, managerName)
|
||||
if (!result) {
|
||||
return { success: false, error: 'Failed to upsert material type' }
|
||||
throw new ValidationError('Failed to upsert material type', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Upsert material type error', { error: message })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
return { updated: true }
|
||||
}, 'materialType:upsert')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -107,22 +94,18 @@ export function registerMaterialTypeHandlers(): void {
|
||||
* Delete a material type record
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:delete',
|
||||
IPC_CHANNELS.MATERIAL_TYPE_DELETE,
|
||||
async (
|
||||
_event,
|
||||
{ materialName, managerName }: { materialName: string; managerName: string }
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
): Promise<IpcResult<{ deleted: boolean }>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const result = await dao.deleteMaterial(materialName, managerName)
|
||||
if (!result) {
|
||||
return { success: false, error: 'Failed to delete material type' }
|
||||
throw new ValidationError('Failed to delete material type', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Delete material type error', { error: message })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
return { deleted: true }
|
||||
}, 'materialType:delete')
|
||||
}
|
||||
)
|
||||
|
||||
@@ -130,25 +113,18 @@ export function registerMaterialTypeHandlers(): void {
|
||||
* Batch operation for material types (insert, update, delete)
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:upsertBatch',
|
||||
IPC_CHANNELS.MATERIAL_TYPE_UPSERT_BATCH,
|
||||
async (
|
||||
_event,
|
||||
request: MaterialTypeBatchRequest
|
||||
): Promise<{
|
||||
success: boolean
|
||||
stats?: { total: number; success: number; failed: number }
|
||||
error?: string
|
||||
}> => {
|
||||
try {
|
||||
): Promise<IpcResult<{ stats: { total: number; success: number; failed: number } }>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const stats = await dao.upsertBatch(request)
|
||||
return { success: true, stats }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Batch upsert material types error', { error: message })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
return { stats }
|
||||
}, 'materialType:upsertBatch')
|
||||
}
|
||||
)
|
||||
|
||||
log.info('Material type handlers registered')
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { create, type IDatabaseService } from '../services/database'
|
||||
import { OrderNumberResolver } from '../services/erp/order-resolver'
|
||||
import { createLogger } from '../services/logger'
|
||||
import type { OrderMapping, ResolutionStats } from '../services/erp/order-resolver'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
import { withErrorHandling, type IpcResult } from './index'
|
||||
|
||||
const log = createLogger('ResolverHandler')
|
||||
|
||||
@@ -49,11 +51,11 @@ export function registerResolverHandlers(): void {
|
||||
* Converts productionIDs and 生产订单号 to production order numbers
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'resolver:resolve',
|
||||
async (_event, input: ResolverInput): Promise<ResolverResponse> => {
|
||||
IPC_CHANNELS.RESOLVER_RESOLVE,
|
||||
async (_event, input: ResolverInput): Promise<IpcResult<ResolverResponse>> => {
|
||||
let dbService: IDatabaseService | null = null
|
||||
|
||||
try {
|
||||
return withErrorHandling(async () => {
|
||||
// Create database service using factory
|
||||
log.info('Connecting to database for resolution', { inputCount: input.inputs.length })
|
||||
dbService = await create()
|
||||
@@ -80,14 +82,7 @@ export function registerResolverHandlers(): void {
|
||||
warnings,
|
||||
stats
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Resolution failed', { error: message })
|
||||
return {
|
||||
success: false,
|
||||
error: `解析失败:${message}`
|
||||
}
|
||||
} finally {
|
||||
}, 'resolver:resolve').finally(async () => {
|
||||
// Clean up database connection
|
||||
if (dbService) {
|
||||
try {
|
||||
@@ -99,7 +94,7 @@ export function registerResolverHandlers(): void {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -107,16 +102,12 @@ export function registerResolverHandlers(): void {
|
||||
* Validate input format only (without database lookup)
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'resolver:validateFormat',
|
||||
IPC_CHANNELS.RESOLVER_VALIDATE_FORMAT,
|
||||
async (
|
||||
_event,
|
||||
inputs: string[]
|
||||
): Promise<{
|
||||
success: boolean
|
||||
results?: Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>
|
||||
error?: string
|
||||
}> => {
|
||||
try {
|
||||
): Promise<IpcResult<Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>>> => {
|
||||
return withErrorHandling(async () => {
|
||||
// Create a mock resolver without database connection
|
||||
const resolver = new OrderNumberResolver({
|
||||
isConnected: () => false,
|
||||
@@ -130,15 +121,8 @@ export function registerResolverHandlers(): void {
|
||||
|
||||
log.debug('Format validation completed', { inputCount: inputs.length })
|
||||
|
||||
return { success: true, results }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Format validation failed', { error: message })
|
||||
return {
|
||||
success: false,
|
||||
error: `验证失败:${message}`
|
||||
}
|
||||
}
|
||||
return results
|
||||
}, 'resolver:validateFormat')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,3 @@
|
||||
/**
|
||||
* Settings IPC Handler
|
||||
*
|
||||
* Provides IPC handlers for settings management:
|
||||
* - Get/set ERP credentials (stored in database per user)
|
||||
* - Reset to defaults (Admin only)
|
||||
* - Test database connection
|
||||
*
|
||||
* Note: ERP configuration is stored in database (dbo_BIPUsers table)
|
||||
* and managed per-user via UserErpConfigService.
|
||||
* Other settings (database, paths, etc.) are managed via config.yaml
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron'
|
||||
import { ConfigManager } from '../services/config/config-manager'
|
||||
import { SessionManager } from '../services/user/session-manager'
|
||||
@@ -19,156 +6,129 @@ import { MySqlService } from '../services/database/mysql'
|
||||
import { SqlServerService } from '../services/database/sql-server'
|
||||
import { createLogger } from '../services/logger'
|
||||
import type { UserType, ConnectionTestResult, SaveSettingsResult } from '../types/settings.types'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
import { ValidationError } from '../types/errors'
|
||||
import { withErrorHandling, type IpcResult } from './index'
|
||||
|
||||
const log = createLogger('SettingsHandler')
|
||||
|
||||
/**
|
||||
* Register IPC handlers for settings management
|
||||
*/
|
||||
type ErpSettingsPayload = {
|
||||
erp?: {
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
}
|
||||
|
||||
export function registerSettingsHandlers(): void {
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const sessionManager = SessionManager.getInstance()
|
||||
const erpConfigService = UserErpConfigService.getInstance()
|
||||
|
||||
/**
|
||||
* Get current user type
|
||||
*/
|
||||
ipcMain.handle('settings:getUserType', async (): Promise<UserType> => {
|
||||
return (sessionManager.getUserType() as UserType) || 'Guest'
|
||||
ipcMain.handle(IPC_CHANNELS.SETTINGS_GET_USER_TYPE, async (): Promise<IpcResult<UserType>> => {
|
||||
return withErrorHandling(
|
||||
async () => (sessionManager.getUserType() as UserType) || 'Guest',
|
||||
'settings:getUserType'
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Get ERP credentials for current user
|
||||
*/
|
||||
ipcMain.handle('settings:getSettings', async (): Promise<any> => {
|
||||
try {
|
||||
// Get ERP credentials from database for current user
|
||||
const userErpConfig = await erpConfigService.getCurrentUserErpConfig()
|
||||
|
||||
return {
|
||||
erp: {
|
||||
username: userErpConfig?.username || '',
|
||||
password: userErpConfig?.password || ''
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Failed to get ERP credentials', { error })
|
||||
return { erp: { username: '', password: '' } }
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Save ERP credentials for current user
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'settings:saveSettings',
|
||||
async (_event, settings: any): Promise<SaveSettingsResult> => {
|
||||
try {
|
||||
log.info('Saving ERP credentials')
|
||||
|
||||
if (settings.erp) {
|
||||
// Update ERP credentials in database for current user
|
||||
const currentUser = sessionManager.getUserInfo()
|
||||
if (!currentUser) {
|
||||
return { success: false, error: '未找到当前用户' }
|
||||
IPC_CHANNELS.SETTINGS_GET_SETTINGS,
|
||||
async (): Promise<IpcResult<{ erp: { username: string; password: string } }>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const userErpConfig = await erpConfigService.getCurrentUserErpConfig()
|
||||
return {
|
||||
erp: {
|
||||
username: userErpConfig?.username || '',
|
||||
password: userErpConfig?.password || ''
|
||||
}
|
||||
|
||||
// Ensure undefined values are converted to empty strings
|
||||
const erpCredentials = {
|
||||
username: settings.erp.username || '',
|
||||
password: settings.erp.password || ''
|
||||
}
|
||||
|
||||
await erpConfigService.updateCurrentUserErpConfig(erpCredentials)
|
||||
|
||||
log.info('ERP credentials saved successfully')
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Error saving ERP credentials', { error: message })
|
||||
return { success: false, error: `保存配置失败:${message}` }
|
||||
}
|
||||
}, 'settings:getSettings')
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Reset to default settings (Admin only)
|
||||
*/
|
||||
ipcMain.handle('settings:resetDefaults', async (): Promise<SaveSettingsResult> => {
|
||||
try {
|
||||
const userType = sessionManager.getUserType()
|
||||
if (userType !== 'Admin') {
|
||||
log.warn('Non-admin user attempted to reset defaults', { userType })
|
||||
return { success: false, error: '只有管理员可以恢复默认设置' }
|
||||
}
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.SETTINGS_SAVE_SETTINGS,
|
||||
async (_event, settings: ErpSettingsPayload): Promise<IpcResult<SaveSettingsResult>> => {
|
||||
return withErrorHandling(async () => {
|
||||
if (settings.erp) {
|
||||
const currentUser = sessionManager.getUserInfo()
|
||||
if (!currentUser) {
|
||||
throw new ValidationError('未找到当前用户', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
|
||||
await erpConfigService.updateCurrentUserErpConfig({
|
||||
username: settings.erp.username || '',
|
||||
password: settings.erp.password || ''
|
||||
})
|
||||
}
|
||||
|
||||
log.info('Resetting settings to defaults')
|
||||
const success = await configManager.resetToDefaults()
|
||||
if (success) {
|
||||
log.info('Settings reset to defaults successfully')
|
||||
return { success: true }
|
||||
} else {
|
||||
log.warn('Failed to reset settings to defaults')
|
||||
return { success: false, error: '恢复默认设置失败' }
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Error resetting settings', { error: message })
|
||||
return { success: false, error: `恢复默认设置失败:${message}` }
|
||||
}, 'settings:saveSettings')
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/**
|
||||
* Test database connection
|
||||
*/
|
||||
ipcMain.handle('settings:testDbConnection', async (): Promise<ConnectionTestResult> => {
|
||||
try {
|
||||
log.info('Testing database connection')
|
||||
const config = configManager.getConfig()
|
||||
const dbType = config.database.activeType
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.SETTINGS_RESET_DEFAULTS,
|
||||
async (): Promise<IpcResult<SaveSettingsResult>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const userType = sessionManager.getUserType()
|
||||
if (userType !== 'Admin') {
|
||||
throw new ValidationError('只有管理员可以恢复默认设置', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
|
||||
if (dbType === 'mysql') {
|
||||
// Test MySQL connection
|
||||
const dbConfig = config.database.mysql
|
||||
if (!dbConfig.host || !dbConfig.database || !dbConfig.username) {
|
||||
log.warn('MySQL connection test failed - missing configuration')
|
||||
return {
|
||||
success: false,
|
||||
message: '请先配置 MySQL 主机、数据库名和用户名'
|
||||
const success = await configManager.resetToDefaults()
|
||||
if (!success) {
|
||||
throw new ValidationError('恢复默认设置失败', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}, 'settings:resetDefaults')
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.SETTINGS_TEST_DB_CONNECTION,
|
||||
async (): Promise<IpcResult<ConnectionTestResult>> => {
|
||||
return withErrorHandling(async () => {
|
||||
log.info('Testing database connection')
|
||||
const config = configManager.getConfig()
|
||||
const dbType = config.database.activeType
|
||||
|
||||
if (dbType === 'mysql') {
|
||||
const dbConfig = config.database.mysql
|
||||
if (!dbConfig.host || !dbConfig.database || !dbConfig.username) {
|
||||
return {
|
||||
success: false,
|
||||
message: '请先配置 MySQL 主机、数据库名和用户名'
|
||||
}
|
||||
}
|
||||
|
||||
const mysqlService = new MySqlService({
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database
|
||||
})
|
||||
|
||||
try {
|
||||
await mysqlService.connect()
|
||||
await mysqlService.disconnect()
|
||||
return {
|
||||
success: true,
|
||||
message: 'MySQL 数据库连接测试成功!'
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '连接失败'
|
||||
return {
|
||||
success: false,
|
||||
message: `MySQL 数据库连接测试失败:${message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mysqlService = new MySqlService({
|
||||
host: dbConfig.host,
|
||||
port: dbConfig.port,
|
||||
user: dbConfig.username,
|
||||
password: dbConfig.password,
|
||||
database: dbConfig.database
|
||||
})
|
||||
|
||||
try {
|
||||
await mysqlService.connect()
|
||||
await mysqlService.disconnect()
|
||||
log.info('MySQL connection test successful')
|
||||
return {
|
||||
success: true,
|
||||
message: 'MySQL 数据库连接测试成功!'
|
||||
}
|
||||
} catch (connError) {
|
||||
const errorMessage = connError instanceof Error ? connError.message : '连接失败'
|
||||
log.error('MySQL connection failed', { error: errorMessage })
|
||||
return {
|
||||
success: false,
|
||||
message: `MySQL 数据库连接测试失败:${errorMessage}`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Test SQL Server connection
|
||||
const dbConfig = config.database.sqlserver
|
||||
if (!dbConfig.server || !dbConfig.database || !dbConfig.username) {
|
||||
log.warn('SQL Server connection test failed - missing configuration')
|
||||
return {
|
||||
success: false,
|
||||
message: '请先配置 SQL Server 服务器、数据库名和用户名'
|
||||
@@ -189,27 +149,19 @@ export function registerSettingsHandlers(): void {
|
||||
try {
|
||||
await sqlServerService.connect()
|
||||
await sqlServerService.disconnect()
|
||||
log.info('SQL Server connection test successful')
|
||||
return {
|
||||
success: true,
|
||||
message: 'SQL Server 数据库连接测试成功!'
|
||||
}
|
||||
} catch (connError) {
|
||||
const errorMessage = connError instanceof Error ? connError.message : '连接失败'
|
||||
log.error('SQL Server connection failed', { error: errorMessage })
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '连接失败'
|
||||
return {
|
||||
success: false,
|
||||
message: `SQL Server 数据库连接测试失败:${errorMessage}`
|
||||
message: `SQL Server 数据库连接测试失败:${message}`
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Database connection test error', { error: message })
|
||||
return {
|
||||
success: false,
|
||||
message: `数据库连接测试失败:${message}`
|
||||
}
|
||||
}, 'settings:testDbConnection')
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,20 @@
|
||||
/**
|
||||
* IPC handlers for User ERP Configuration
|
||||
*
|
||||
* Provides APIs for the renderer process to:
|
||||
* - Get current user's ERP credentials
|
||||
* - Update current user's ERP credentials
|
||||
* - Test ERP connection with provided credentials
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron'
|
||||
import { UserErpConfigService, type ErpCredentials } from '../services/user/user-erp-config-service'
|
||||
import { UserErpConfigService } from '../services/user/user-erp-config-service'
|
||||
import { ErpAuthService } from '../services/erp/erp-auth'
|
||||
import { ConfigManager } from '../services/config/config-manager'
|
||||
import { createLogger } from '../services/logger'
|
||||
import type { UserInfo } from '../types/user.types'
|
||||
import { SessionManager } from '../services/user/session-manager'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
import { ValidationError } from '../types/errors'
|
||||
import { withErrorHandling, type IpcResult } from './index'
|
||||
|
||||
const log = createLogger('UserErpConfigHandler')
|
||||
|
||||
/**
|
||||
* ERP Credentials request (username and password only, URL is from config.yaml)
|
||||
*/
|
||||
export interface ErpCredentialsRequest {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
/**
|
||||
* ERP Configuration response (includes URL from config.yaml)
|
||||
*/
|
||||
export interface ErpConfigResponse {
|
||||
success: boolean
|
||||
config?: {
|
||||
@@ -37,127 +25,80 @@ export interface ErpConfigResponse {
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection test result
|
||||
*/
|
||||
export interface ConnectionTestResult {
|
||||
success: boolean
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Register IPC handlers for user ERP configuration
|
||||
*/
|
||||
export function registerUserErpConfigHandlers(): void {
|
||||
const erpConfigService = UserErpConfigService.getInstance()
|
||||
const sessionManager = SessionManager.getInstance()
|
||||
|
||||
/**
|
||||
* Get current user's ERP credentials
|
||||
*/
|
||||
ipcMain.handle('user-erp-config:getCurrent', async (): Promise<ErpConfigResponse> => {
|
||||
try {
|
||||
log.info('Fetching current user ERP credentials')
|
||||
const credentials = await erpConfigService.getCurrentUserErpConfig()
|
||||
|
||||
if (!credentials) {
|
||||
return {
|
||||
success: false,
|
||||
error: '未找到 ERP 配置。请先配置 ERP 账号和密码。'
|
||||
}
|
||||
}
|
||||
|
||||
// Get ERP URL from config.yaml (fixed for all users)
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const globalConfig = configManager.getConfig()
|
||||
const erpUrl = globalConfig.erp.url
|
||||
|
||||
return {
|
||||
success: true,
|
||||
config: {
|
||||
url: erpUrl,
|
||||
username: credentials.username,
|
||||
password: credentials.password
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Get current user ERP credentials failed', { error: message })
|
||||
return {
|
||||
success: false,
|
||||
error: `获取 ERP 配置失败:${message}`
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Update current user's ERP credentials
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'user-erp-config:update',
|
||||
async (_event, credentials: ErpCredentialsRequest): Promise<ErpConfigResponse> => {
|
||||
try {
|
||||
log.info('Updating current user ERP credentials', {
|
||||
username: credentials.username
|
||||
})
|
||||
IPC_CHANNELS.USER_ERP_CONFIG_GET_CURRENT,
|
||||
async (): Promise<IpcResult<ErpConfigResponse>> => {
|
||||
return withErrorHandling(async () => {
|
||||
log.info('Fetching current user ERP credentials')
|
||||
const credentials = await erpConfigService.getCurrentUserErpConfig()
|
||||
|
||||
const success = await erpConfigService.updateCurrentUserErpConfig(credentials)
|
||||
|
||||
if (success) {
|
||||
log.info('ERP credentials updated successfully')
|
||||
// Get ERP URL from config.yaml to return full config
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const globalConfig = configManager.getConfig()
|
||||
const erpUrl = globalConfig.erp.url
|
||||
|
||||
return {
|
||||
success: true,
|
||||
config: {
|
||||
url: erpUrl,
|
||||
username: credentials.username,
|
||||
password: credentials.password
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error('Failed to update ERP credentials')
|
||||
return {
|
||||
success: false,
|
||||
error: '更新 ERP 配置失败'
|
||||
}
|
||||
if (!credentials) {
|
||||
throw new ValidationError('未找到 ERP 配置。请先配置 ERP 账号和密码。', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('Update ERP credentials failed', { error: message })
|
||||
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const globalConfig = configManager.getConfig()
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `更新 ERP 配置失败:${message}`
|
||||
success: true,
|
||||
config: {
|
||||
url: globalConfig.erp.url,
|
||||
username: credentials.username,
|
||||
password: credentials.password
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 'user-erp-config:getCurrent')
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Test ERP connection with provided credentials
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'user-erp-config:testConnection',
|
||||
async (_event, credentials: ErpCredentialsRequest): Promise<ConnectionTestResult> => {
|
||||
try {
|
||||
log.info('Testing ERP connection', { username: credentials.username })
|
||||
|
||||
if (!credentials.username || !credentials.password) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'ERP 配置不完整,请确保用户名和密码都已填写'
|
||||
}
|
||||
IPC_CHANNELS.USER_ERP_CONFIG_UPDATE,
|
||||
async (_event, credentials: ErpCredentialsRequest): Promise<IpcResult<ErpConfigResponse>> => {
|
||||
return withErrorHandling(async () => {
|
||||
const updated = await erpConfigService.updateCurrentUserErpConfig(credentials)
|
||||
if (!updated) {
|
||||
throw new ValidationError('更新 ERP 配置失败', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
|
||||
// Get ERP URL from config.yaml (fixed for all users)
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const globalConfig = configManager.getConfig()
|
||||
const erpUrl = globalConfig.erp.url
|
||||
|
||||
return {
|
||||
success: true,
|
||||
config: {
|
||||
url: globalConfig.erp.url,
|
||||
username: credentials.username,
|
||||
password: credentials.password
|
||||
}
|
||||
}
|
||||
}, 'user-erp-config:update')
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.USER_ERP_CONFIG_TEST_CONNECTION,
|
||||
async (_event, credentials: ErpCredentialsRequest): Promise<IpcResult<ConnectionTestResult>> => {
|
||||
return withErrorHandling(async () => {
|
||||
if (!credentials.username || !credentials.password) {
|
||||
throw new ValidationError(
|
||||
'ERP 配置不完整,请确保用户名和密码都已填写',
|
||||
'VAL_MISSING_REQUIRED'
|
||||
)
|
||||
}
|
||||
|
||||
const configManager = ConfigManager.getInstance()
|
||||
const globalConfig = configManager.getConfig()
|
||||
const authService = new ErpAuthService({
|
||||
url: erpUrl,
|
||||
url: globalConfig.erp.url,
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
headless: true
|
||||
@@ -165,49 +106,37 @@ export function registerUserErpConfigHandlers(): void {
|
||||
|
||||
try {
|
||||
await authService.login()
|
||||
await authService.close()
|
||||
|
||||
log.info('ERP connection test successful')
|
||||
return {
|
||||
success: true,
|
||||
message: 'ERP 连接测试成功'
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
await authService.close().catch(() => {})
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
log.error('ERP connection test failed', { error: message })
|
||||
return {
|
||||
success: false,
|
||||
message: `ERP 连接测试失败:${message}`
|
||||
}
|
||||
}
|
||||
}, 'user-erp-config:testConnection')
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Get all users' ERP configurations (admin only)
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'user-erp-config:getAll',
|
||||
IPC_CHANNELS.USER_ERP_CONFIG_GET_ALL,
|
||||
async (): Promise<
|
||||
Array<{
|
||||
username: string
|
||||
erpUrl: string
|
||||
erpUsername: string
|
||||
}>
|
||||
IpcResult<
|
||||
Array<{
|
||||
username: string
|
||||
erpUrl: string
|
||||
erpUsername: string
|
||||
}>
|
||||
>
|
||||
> => {
|
||||
try {
|
||||
log.info('Fetching all users ERP config')
|
||||
return withErrorHandling(async () => {
|
||||
if (!sessionManager.isAdmin()) {
|
||||
throw new ValidationError('只有管理员可以查看全部用户 ERP 配置', 'VAL_INVALID_INPUT')
|
||||
}
|
||||
|
||||
const configs = await erpConfigService.getAllUsersErpConfig()
|
||||
log.info('Retrieved ERP configs for all users', { count: configs.length })
|
||||
return configs
|
||||
} catch (error) {
|
||||
log.error('Get all users ERP config failed', { error })
|
||||
return []
|
||||
}
|
||||
}, 'user-erp-config:getAll')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
ValidationResult,
|
||||
MaterialRecordSummary
|
||||
} from '../types/validation.types'
|
||||
import { IPC_CHANNELS } from '../../shared/ipc-channels'
|
||||
|
||||
const log = createLogger('ValidationHandler')
|
||||
|
||||
@@ -30,28 +31,28 @@ const log = createLogger('ValidationHandler')
|
||||
* Shared state for Production IDs from extractor page
|
||||
* This is a simple in-memory store for sharing Production IDs between pages
|
||||
*/
|
||||
const sharedProductionIds = new Set<string>()
|
||||
const sharedProductionIdsBySender = new Map<number, Set<string>>()
|
||||
|
||||
/**
|
||||
* Set shared Production IDs
|
||||
*/
|
||||
export function setSharedProductionIds(ids: string[]): void {
|
||||
sharedProductionIds.clear()
|
||||
ids.forEach((id) => sharedProductionIds.add(id))
|
||||
export function setSharedProductionIds(senderId: number, ids: string[]): void {
|
||||
sharedProductionIdsBySender.set(senderId, new Set(ids))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shared Production IDs
|
||||
*/
|
||||
export function getSharedProductionIds(): string[] {
|
||||
return [...sharedProductionIds]
|
||||
export function getSharedProductionIds(senderId: number): string[] {
|
||||
const senderSet = sharedProductionIdsBySender.get(senderId)
|
||||
return senderSet ? [...senderSet] : []
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear shared Production IDs
|
||||
*/
|
||||
export function clearSharedProductionIds(): void {
|
||||
sharedProductionIds.clear()
|
||||
export function clearSharedProductionIds(senderId: number): void {
|
||||
sharedProductionIdsBySender.delete(senderId)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,8 +234,8 @@ export function registerValidationHandlers(): void {
|
||||
* Run material validation from database
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'validation:validate',
|
||||
async (_event, request: ValidationRequest): Promise<ValidationResponse> => {
|
||||
IPC_CHANNELS.VALIDATION_VALIDATE,
|
||||
async (event, request: ValidationRequest): Promise<ValidationResponse> => {
|
||||
let dbService: MySqlService | SqlServerService | null = null
|
||||
|
||||
try {
|
||||
@@ -273,7 +274,7 @@ export function registerValidationHandlers(): void {
|
||||
if (request.mode === 'database_filtered') {
|
||||
if (request.useSharedProductionIds) {
|
||||
// Use shared Production IDs from extractor page
|
||||
const sharedIds = getSharedProductionIds()
|
||||
const sharedIds = getSharedProductionIds(event.sender.id)
|
||||
log.info(`Using ${sharedIds.length} shared Production IDs`)
|
||||
|
||||
if (sharedIds.length === 0) {
|
||||
@@ -445,7 +446,7 @@ export function registerValidationHandlers(): void {
|
||||
* Upsert batch materials to MaterialsToBeDeleted
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:upsertBatch',
|
||||
IPC_CHANNELS.MATERIALS_UPSERT_BATCH,
|
||||
async (_event, request: MaterialUpsertBatchRequest): Promise<MaterialOperationResponse> => {
|
||||
try {
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
@@ -472,7 +473,7 @@ export function registerValidationHandlers(): void {
|
||||
* Delete materials by material codes
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:delete',
|
||||
IPC_CHANNELS.MATERIALS_DELETE,
|
||||
async (_event, request: MaterialDeleteRequest): Promise<MaterialOperationResponse> => {
|
||||
try {
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
@@ -496,7 +497,7 @@ export function registerValidationHandlers(): void {
|
||||
/**
|
||||
* Get unique manager names
|
||||
*/
|
||||
ipcMain.handle('materials:getManagers', async (_event): Promise<{ managers: string[] }> => {
|
||||
ipcMain.handle(IPC_CHANNELS.MATERIALS_GET_MANAGERS, async (_event): Promise<{ managers: string[] }> => {
|
||||
try {
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
const managers = await dao.getManagers()
|
||||
@@ -513,7 +514,7 @@ export function registerValidationHandlers(): void {
|
||||
* Update manager for a single material
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:updateManager',
|
||||
IPC_CHANNELS.MATERIALS_UPDATE_MANAGER,
|
||||
async (
|
||||
_event,
|
||||
request: { materialCode: string; managerName: string }
|
||||
@@ -537,7 +538,7 @@ export function registerValidationHandlers(): void {
|
||||
* Get materials by manager
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:getByManager',
|
||||
IPC_CHANNELS.MATERIALS_GET_BY_MANAGER,
|
||||
async (_event, managerName: string): Promise<{ materials: MaterialRecordSummary[] }> => {
|
||||
let dbService: MySqlService | SqlServerService | null = null
|
||||
|
||||
@@ -616,7 +617,7 @@ export function registerValidationHandlers(): void {
|
||||
* Get all material records
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materials:getAll',
|
||||
IPC_CHANNELS.MATERIALS_GET_ALL,
|
||||
async (_event): Promise<{ materials: MaterialRecordSummary[] }> => {
|
||||
let dbService: MySqlService | SqlServerService | null = null
|
||||
|
||||
@@ -691,7 +692,7 @@ export function registerValidationHandlers(): void {
|
||||
/**
|
||||
* Get statistics
|
||||
*/
|
||||
ipcMain.handle('materials:getStatistics', async (_event): Promise<{ stats: any }> => {
|
||||
ipcMain.handle(IPC_CHANNELS.MATERIALS_GET_STATISTICS, async (_event): Promise<{ stats: any }> => {
|
||||
try {
|
||||
const dao = new MaterialsToBeDeletedDAO()
|
||||
const stats = await dao.getStatistics()
|
||||
@@ -708,10 +709,10 @@ export function registerValidationHandlers(): void {
|
||||
* Set shared Production IDs from extractor page
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'validation:setSharedProductionIds',
|
||||
async (_event, productionIds: string[]): Promise<void> => {
|
||||
IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS,
|
||||
async (event, productionIds: string[]): Promise<void> => {
|
||||
log.info(`Received ${productionIds.length} shared Production IDs`)
|
||||
setSharedProductionIds(productionIds)
|
||||
setSharedProductionIds(event.sender.id, productionIds)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -719,9 +720,9 @@ export function registerValidationHandlers(): void {
|
||||
* Get shared Production IDs
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'validation:getSharedProductionIds',
|
||||
async (): Promise<{ productionIds: string[] }> => {
|
||||
return { productionIds: getSharedProductionIds() }
|
||||
IPC_CHANNELS.VALIDATION_GET_SHARED_PRODUCTION_IDS,
|
||||
async (event): Promise<{ productionIds: string[] }> => {
|
||||
return { productionIds: getSharedProductionIds(event.sender.id) }
|
||||
}
|
||||
)
|
||||
|
||||
@@ -730,7 +731,7 @@ export function registerValidationHandlers(): void {
|
||||
* Filters materials by current user (admin sees all, regular users see only their own)
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'validation:getCleanerData',
|
||||
IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA,
|
||||
async (
|
||||
_event
|
||||
): Promise<{
|
||||
@@ -766,7 +767,7 @@ export function registerValidationHandlers(): void {
|
||||
dbService = await getValidationDatabaseService()
|
||||
|
||||
// 1. Get order numbers from shared Production IDs
|
||||
const sharedIds = getSharedProductionIds()
|
||||
const sharedIds = getSharedProductionIds(_event.sender.id)
|
||||
let orderNumbers: string[] = []
|
||||
|
||||
if (sharedIds.length > 0) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
ExportResultItem,
|
||||
ExportResultResponse
|
||||
} from './cleaner.types'
|
||||
import type { IpcResult } from '../ipc'
|
||||
|
||||
/**
|
||||
* MySQL connection configuration
|
||||
@@ -60,11 +61,11 @@ export interface SqlServerQueryResult {
|
||||
* File operation APIs
|
||||
*/
|
||||
export interface FileAPI {
|
||||
readFile: (filePath: string) => Promise<string>
|
||||
writeFile: (filePath: string, content: string) => Promise<void>
|
||||
fileExists: (filePath: string) => Promise<boolean>
|
||||
listFiles: (dirPath: string) => Promise<string[]>
|
||||
openPath: (filePath: string) => Promise<void>
|
||||
readFile: (filePath: string) => Promise<IpcResult<string>>
|
||||
writeFile: (filePath: string, content: string) => Promise<IpcResult<void>>
|
||||
fileExists: (filePath: string) => Promise<IpcResult<boolean>>
|
||||
listFiles: (dirPath: string) => Promise<IpcResult<string[]>>
|
||||
openPath: (filePath: string) => Promise<IpcResult<void>>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +78,7 @@ export interface ExtractorAPI {
|
||||
*/
|
||||
runExtractor: (
|
||||
input: ExtractorInput
|
||||
) => Promise<{ success: boolean; data?: ExtractorResult; error?: string }>
|
||||
) => Promise<IpcResult<ExtractorResult>>
|
||||
/**
|
||||
* Subscribe to progress updates
|
||||
* @param callback - Callback function receiving progress data
|
||||
@@ -102,13 +103,13 @@ export interface CleanerAPI {
|
||||
*/
|
||||
runCleaner: (
|
||||
input: CleanerInput
|
||||
) => Promise<{ success: boolean; data?: CleanerResult; error?: string }>
|
||||
) => Promise<IpcResult<CleanerResult>>
|
||||
|
||||
/**
|
||||
* Export validation results to Excel
|
||||
* @param items - Validation result items to export
|
||||
*/
|
||||
exportResults: (items: ExportResultItem[]) => Promise<ExportResultResponse>
|
||||
exportResults: (items: ExportResultItem[]) => Promise<IpcResult<ExportResultResponse>>
|
||||
|
||||
/**
|
||||
* Subscribe to progress updates
|
||||
@@ -126,45 +127,48 @@ export interface DatabaseAPI {
|
||||
* Connect to MySQL database
|
||||
* @param config - MySQL connection config
|
||||
*/
|
||||
connectMySql: (config: MySqlConfig) => Promise<void>
|
||||
connectMySql: (config: MySqlConfig) => Promise<IpcResult<void>>
|
||||
|
||||
/**
|
||||
* Disconnect from MySQL database
|
||||
*/
|
||||
disconnectMySql: () => Promise<void>
|
||||
disconnectMySql: () => Promise<IpcResult<void>>
|
||||
|
||||
/**
|
||||
* Check if MySQL is connected
|
||||
*/
|
||||
isMySqlConnected: () => Promise<boolean>
|
||||
isMySqlConnected: () => Promise<IpcResult<boolean>>
|
||||
|
||||
/**
|
||||
* Execute MySQL query
|
||||
* @param sql - SQL query
|
||||
* @param params - Query parameters
|
||||
*/
|
||||
queryMySql: (sql: string, params?: unknown[]) => Promise<MySqlQueryResult>
|
||||
queryMySql: (sql: string, params?: unknown[]) => Promise<IpcResult<MySqlQueryResult>>
|
||||
|
||||
/**
|
||||
* Connect to SQL Server database
|
||||
* @param config - SQL Server connection config
|
||||
*/
|
||||
connectSqlServer: (config: SqlServerConfig) => Promise<void>
|
||||
connectSqlServer: (config: SqlServerConfig) => Promise<IpcResult<void>>
|
||||
|
||||
/**
|
||||
* Disconnect from SQL Server database
|
||||
*/
|
||||
disconnectSqlServer: () => Promise<void>
|
||||
disconnectSqlServer: () => Promise<IpcResult<void>>
|
||||
|
||||
/**
|
||||
* Check if SQL Server is connected
|
||||
*/
|
||||
isSqlServerConnected: () => Promise<boolean>
|
||||
isSqlServerConnected: () => Promise<IpcResult<boolean>>
|
||||
|
||||
/**
|
||||
* Execute SQL Server query
|
||||
* @param sql - SQL query
|
||||
* @param params - Query parameters
|
||||
*/
|
||||
querySqlServer: (sql: string, params?: Record<string, unknown>) => Promise<SqlServerQueryResult>
|
||||
querySqlServer: (
|
||||
sql: string,
|
||||
params?: Record<string, unknown>
|
||||
) => Promise<IpcResult<SqlServerQueryResult>>
|
||||
}
|
||||
|
||||
306
src/preload/index.d.ts
vendored
306
src/preload/index.d.ts
vendored
@@ -15,277 +15,105 @@ import type {
|
||||
MaterialTypeBatchRequest
|
||||
} from '../main/types/validation.types'
|
||||
import type {
|
||||
SettingsData,
|
||||
UserType,
|
||||
ConnectionTestResult,
|
||||
SaveSettingsResult
|
||||
} from '../main/types/settings.types'
|
||||
import type { IpcResult } from '../main/ipc'
|
||||
|
||||
/**
|
||||
* Order number resolver API
|
||||
*/
|
||||
export interface ResolverAPI {
|
||||
/**
|
||||
* Resolve productionIDs and 生产订单号 to production order numbers
|
||||
* @param input - Resolver input with list of inputs
|
||||
*/
|
||||
resolve: (input: ResolverInput) => Promise<ResolverResponse>
|
||||
/**
|
||||
* Validate input format only (without database lookup)
|
||||
* @param inputs - List of inputs to validate
|
||||
*/
|
||||
validateFormat: (inputs: string[]) => Promise<{
|
||||
success: boolean
|
||||
results?: Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>
|
||||
error?: string
|
||||
}>
|
||||
resolve: (input: ResolverInput) => Promise<IpcResult<ResolverResponse>>
|
||||
validateFormat: (inputs: string[]) => Promise<
|
||||
IpcResult<Array<{ input: string; type: 'productionId' | 'orderNumber' | 'unknown' }>>
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication API
|
||||
*/
|
||||
export interface AuthAPI {
|
||||
/**
|
||||
* Get computer name
|
||||
*/
|
||||
getComputerName: () => Promise<string>
|
||||
/**
|
||||
* Silent login by computer name
|
||||
*/
|
||||
silentLogin: () => Promise<SilentLoginResponse>
|
||||
/**
|
||||
* Login with username and password
|
||||
* @param request - Login request with username and password
|
||||
*/
|
||||
login: (request: LoginRequest) => Promise<LoginResponse>
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
logout: () => Promise<void>
|
||||
/**
|
||||
* Get current user
|
||||
*/
|
||||
getCurrentUser: () => Promise<CurrentUserResponse>
|
||||
/**
|
||||
* Get all users (for admin user selection)
|
||||
*/
|
||||
getAllUsers: () => Promise<UserInfo[]>
|
||||
/**
|
||||
* Switch user (admin only)
|
||||
* @param userInfo - User info to switch to
|
||||
*/
|
||||
switchUser: (userInfo: UserInfo) => Promise<UserSelectionResponse>
|
||||
/**
|
||||
* Check if current user is admin
|
||||
*/
|
||||
isAdmin: () => Promise<boolean>
|
||||
getComputerName: () => Promise<IpcResult<string>>
|
||||
silentLogin: () => Promise<IpcResult<SilentLoginResponse>>
|
||||
login: (request: LoginRequest) => Promise<IpcResult<LoginResponse>>
|
||||
logout: () => Promise<IpcResult<void>>
|
||||
getCurrentUser: () => Promise<IpcResult<CurrentUserResponse>>
|
||||
getAllUsers: () => Promise<IpcResult<UserInfo[]>>
|
||||
switchUser: (userInfo: UserInfo) => Promise<IpcResult<UserSelectionResponse>>
|
||||
isAdmin: () => Promise<IpcResult<boolean>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation API
|
||||
*/
|
||||
export interface ValidationAPI {
|
||||
/**
|
||||
* Run material validation
|
||||
* @param request - Validation request
|
||||
*/
|
||||
validate: (request: ValidationRequest) => Promise<ValidationResponse>
|
||||
/**
|
||||
* Set shared Production IDs from extractor page
|
||||
* @param productionIds - List of Production IDs
|
||||
*/
|
||||
setSharedProductionIds: (productionIds: string[]) => Promise<void>
|
||||
/**
|
||||
* 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
|
||||
}>
|
||||
validate: (request: ValidationRequest) => Promise<IpcResult<ValidationResponse>>
|
||||
setSharedProductionIds: (productionIds: string[]) => Promise<IpcResult<void>>
|
||||
getSharedProductionIds: () => Promise<IpcResult<{ productionIds: string[] }>>
|
||||
getCleanerData: () => Promise<
|
||||
IpcResult<{
|
||||
orderNumbers: string[]
|
||||
materialCodes: string[]
|
||||
}>
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Materials API
|
||||
*/
|
||||
export interface MaterialsAPI {
|
||||
/**
|
||||
* Upsert batch materials to MaterialsToBeDeleted
|
||||
* @param materials - List of materials with materialCode and managerName
|
||||
*/
|
||||
upsertBatch: (materials: { materialCode: string; managerName: string }[]) => Promise<{
|
||||
success: boolean
|
||||
stats?: { total: number; success: number; failed: number }
|
||||
error?: string
|
||||
}>
|
||||
/**
|
||||
* Delete materials by material codes
|
||||
* @param materialCodes - List of material codes to delete
|
||||
*/
|
||||
delete: (materialCodes: string[]) => Promise<{
|
||||
success: boolean
|
||||
count?: number
|
||||
error?: string
|
||||
}>
|
||||
/**
|
||||
* Get unique manager names
|
||||
*/
|
||||
getManagers: () => Promise<{ managers: string[] }>
|
||||
/**
|
||||
* Get materials by manager
|
||||
* @param managerName - Manager name
|
||||
*/
|
||||
getByManager: (managerName: string) => Promise<{ materials: unknown[] }>
|
||||
/**
|
||||
* Get all material records
|
||||
*/
|
||||
getAll: () => Promise<{ materials: unknown[] }>
|
||||
/**
|
||||
* Get statistics
|
||||
*/
|
||||
getStatistics: () => Promise<{ stats: unknown }>
|
||||
/**
|
||||
* Update manager for a single material
|
||||
* @param materialCode - Material code
|
||||
* @param managerName - New manager name
|
||||
*/
|
||||
upsertBatch: (materials: { materialCode: string; managerName: string }[]) => Promise<
|
||||
IpcResult<{ stats: { total: number; success: number; failed: number } }>
|
||||
>
|
||||
delete: (materialCodes: string[]) => Promise<IpcResult<{ count: number }>>
|
||||
getManagers: () => Promise<IpcResult<{ managers: string[] }>>
|
||||
getByManager: (managerName: string) => Promise<IpcResult<{ materials: unknown[] }>>
|
||||
getAll: () => Promise<IpcResult<{ materials: unknown[] }>>
|
||||
getStatistics: () => Promise<IpcResult<{ stats: unknown }>>
|
||||
updateManager: (
|
||||
materialCode: string,
|
||||
managerName: string
|
||||
) => Promise<{ success: boolean; error?: string }>
|
||||
) => Promise<IpcResult<{ updated: boolean }>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings API
|
||||
*/
|
||||
export interface SettingsAPI {
|
||||
/**
|
||||
* Get current user type
|
||||
*/
|
||||
getUserType: () => Promise<UserType>
|
||||
/**
|
||||
* Get settings (filtered by user type)
|
||||
*/
|
||||
getSettings: () => Promise<SettingsData>
|
||||
/**
|
||||
* Save settings
|
||||
* @param settings - Settings data to save
|
||||
*/
|
||||
saveSettings: (settings: SettingsData) => Promise<SaveSettingsResult>
|
||||
/**
|
||||
* Reset to defaults (Admin only)
|
||||
*/
|
||||
resetDefaults: () => Promise<SaveSettingsResult>
|
||||
/**
|
||||
* Test ERP connection
|
||||
*/
|
||||
testErpConnection: () => Promise<ConnectionTestResult>
|
||||
/**
|
||||
* Test database connection
|
||||
*/
|
||||
testDbConnection: () => Promise<ConnectionTestResult>
|
||||
getUserType: () => Promise<IpcResult<UserType>>
|
||||
getSettings: () => Promise<IpcResult<{ erp: { username: string; password: string } }>>
|
||||
saveSettings: (settings: { erp?: { username?: string; password?: string } }) => Promise<IpcResult<SaveSettingsResult>>
|
||||
resetDefaults: () => Promise<IpcResult<SaveSettingsResult>>
|
||||
testDbConnection: () => Promise<IpcResult<ConnectionTestResult>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Material Type API
|
||||
*/
|
||||
export interface MaterialTypeAPI {
|
||||
/**
|
||||
* Get all material type records
|
||||
*/
|
||||
getAll: () => Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }>
|
||||
/**
|
||||
* Get material types by manager
|
||||
* @param managerName - Manager name
|
||||
*/
|
||||
getByManager: (
|
||||
managerName: string
|
||||
) => Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }>
|
||||
/**
|
||||
* Get list of managers
|
||||
*/
|
||||
getManagers: () => Promise<{ success: boolean; data?: string[]; error?: string }>
|
||||
/**
|
||||
* Upsert (insert or update) a material type record
|
||||
*/
|
||||
upsert: (
|
||||
materialName: string,
|
||||
managerName: string
|
||||
) => Promise<{ success: boolean; error?: string }>
|
||||
/**
|
||||
* Delete a material type record
|
||||
*/
|
||||
delete: (
|
||||
materialName: string,
|
||||
managerName: string
|
||||
) => Promise<{ success: boolean; error?: string }>
|
||||
/**
|
||||
* Batch operation for material types
|
||||
*/
|
||||
upsertBatch: (request: MaterialTypeBatchRequest) => Promise<{
|
||||
success: boolean
|
||||
stats?: { total: number; success: number; failed: number }
|
||||
error?: string
|
||||
}>
|
||||
getAll: () => Promise<IpcResult<MaterialTypeRecord[]>>
|
||||
getByManager: (managerName: string) => Promise<IpcResult<MaterialTypeRecord[]>>
|
||||
getManagers: () => Promise<IpcResult<string[]>>
|
||||
upsert: (materialName: string, managerName: string) => Promise<IpcResult<{ updated: boolean }>>
|
||||
delete: (materialName: string, managerName: string) => Promise<IpcResult<{ deleted: boolean }>>
|
||||
upsertBatch: (request: MaterialTypeBatchRequest) => Promise<
|
||||
IpcResult<{ stats: { total: number; success: number; failed: number } }>
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* User ERP Configuration API
|
||||
*/
|
||||
export interface UserErpConfigAPI {
|
||||
/**
|
||||
* Get current user's ERP configuration
|
||||
*/
|
||||
getCurrent: () => Promise<{
|
||||
success: boolean
|
||||
config?: { url: string; username: string; password: string }
|
||||
error?: string
|
||||
}>
|
||||
/**
|
||||
* Update current user's ERP configuration
|
||||
*/
|
||||
update: (config: { url: string; username: string; password: string }) => Promise<{
|
||||
success: boolean
|
||||
config?: { url: string; username: string; password: string }
|
||||
error?: string
|
||||
}>
|
||||
/**
|
||||
* Test ERP connection with provided credentials
|
||||
*/
|
||||
testConnection: (config: { url: string; username: string; password: string }) => Promise<{
|
||||
success: boolean
|
||||
message?: string
|
||||
}>
|
||||
/**
|
||||
* Get all users' ERP configurations (admin only)
|
||||
*/
|
||||
getAll: () => Promise<Array<{ username: string; erpUrl: string; erpUsername: string }>>
|
||||
getCurrent: () => Promise<
|
||||
IpcResult<{
|
||||
config: { url: string; username: string; password: string }
|
||||
}>
|
||||
>
|
||||
update: (config: { url: string; username: string; password: string }) => Promise<
|
||||
IpcResult<{
|
||||
config: { url: string; username: string; password: string }
|
||||
}>
|
||||
>
|
||||
testConnection: (config: { url: string; username: string; password: string }) => Promise<
|
||||
IpcResult<{ message: string }>
|
||||
>
|
||||
getAll: () => Promise<IpcResult<Array<{ username: string; erpUrl: string; erpUsername: string }>>>
|
||||
}
|
||||
|
||||
export interface ProcessAPI {
|
||||
versions: {
|
||||
electron: string
|
||||
chrome: string
|
||||
node: string
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
ipcRenderer: {
|
||||
send: (channel: string, ...args: unknown[]) => void
|
||||
on: (channel: string, func: (...args: unknown[]) => void) => void
|
||||
once: (channel: string, func: (...args: unknown[]) => void) => void
|
||||
removeListener: (channel: string, func: (...args: unknown[]) => void) => void
|
||||
removeAllListeners: (channel: string) => void
|
||||
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>
|
||||
}
|
||||
process: {
|
||||
versions: {
|
||||
electron: string
|
||||
chrome: string
|
||||
node: string
|
||||
}
|
||||
}
|
||||
process: ProcessAPI
|
||||
file: FileAPI
|
||||
extractor: ExtractorAPI
|
||||
cleaner: CleanerAPI
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import type { MySqlConfig, SqlServerConfig } from '../main/types/ipc-api.types'
|
||||
import type { ExtractorInput, ExtractionProgress } from '../main/types/extractor.types'
|
||||
import type { CleanerInput, CleanerProgress, ExportResultItem } from '../main/types/cleaner.types'
|
||||
@@ -11,158 +10,202 @@ import type {
|
||||
MaterialTypeRecord,
|
||||
MaterialTypeBatchRequest
|
||||
} from '../main/types/validation.types'
|
||||
import type { SettingsData } from '../main/types/settings.types'
|
||||
import type { IpcResult } from '../main/ipc'
|
||||
import { IPC_CHANNELS } from '../shared/ipc-channels'
|
||||
|
||||
type ErpSettingsPayload = {
|
||||
erp?: {
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
}
|
||||
|
||||
const processApi = {
|
||||
versions: {
|
||||
electron: process.versions.electron,
|
||||
chrome: process.versions.chrome,
|
||||
node: process.versions.node
|
||||
}
|
||||
}
|
||||
|
||||
function invokeIpc<T = unknown>(channel: string, ...args: unknown[]): Promise<IpcResult<T>> {
|
||||
return ipcRenderer.invoke(channel, ...args).then((result: unknown) => {
|
||||
if (result && typeof result === 'object' && 'success' in (result as Record<string, unknown>)) {
|
||||
const typed = result as Record<string, unknown>
|
||||
const hasIpcShape = 'data' in typed || 'code' in typed || 'error' in typed
|
||||
|
||||
if (hasIpcShape) {
|
||||
return typed as unknown as IpcResult<T>
|
||||
}
|
||||
|
||||
if (typed.success === true) {
|
||||
return { success: true, data: result as T }
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: typeof typed.error === 'string' ? typed.error : 'IPC operation failed'
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, data: result as T }
|
||||
})
|
||||
}
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
// File operations
|
||||
process: processApi,
|
||||
|
||||
file: {
|
||||
readFile: (filePath: string) => ipcRenderer.invoke('file:read', filePath),
|
||||
readFile: (filePath: string) => invokeIpc(IPC_CHANNELS.FILE_READ, filePath),
|
||||
writeFile: (filePath: string, content: string) =>
|
||||
ipcRenderer.invoke('file:write', filePath, content),
|
||||
fileExists: (filePath: string) => ipcRenderer.invoke('file:exists', filePath),
|
||||
listFiles: (dirPath: string) => ipcRenderer.invoke('file:list', dirPath),
|
||||
openPath: (filePath: string) => ipcRenderer.invoke('file:openPath', filePath)
|
||||
invokeIpc(IPC_CHANNELS.FILE_WRITE, filePath, content),
|
||||
fileExists: (filePath: string) => invokeIpc(IPC_CHANNELS.FILE_EXISTS, filePath),
|
||||
listFiles: (dirPath: string) => invokeIpc(IPC_CHANNELS.FILE_LIST, dirPath),
|
||||
openPath: (filePath: string) => invokeIpc(IPC_CHANNELS.FILE_OPEN_PATH, filePath)
|
||||
},
|
||||
|
||||
// Extractor service
|
||||
extractor: {
|
||||
runExtractor: (input: ExtractorInput) => ipcRenderer.invoke('extractor:run', input),
|
||||
runExtractor: (input: ExtractorInput): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.EXTRACTOR_RUN, input),
|
||||
onProgress: (callback: (data: ExtractionProgress) => void) => {
|
||||
const subscription = (_event: Electron.IpcRendererEvent, data: ExtractionProgress) =>
|
||||
callback(data)
|
||||
ipcRenderer.on('extractor:progress', subscription)
|
||||
return () => ipcRenderer.removeListener('extractor:progress', subscription)
|
||||
ipcRenderer.on(IPC_CHANNELS.EXTRACTOR_PROGRESS, subscription)
|
||||
return () => ipcRenderer.removeListener(IPC_CHANNELS.EXTRACTOR_PROGRESS, subscription)
|
||||
},
|
||||
onLog: (callback: (data: { level: string; message: string }) => void) => {
|
||||
const subscription = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
data: { level: string; message: string }
|
||||
) => callback(data)
|
||||
ipcRenderer.on('extractor:log', subscription)
|
||||
return () => ipcRenderer.removeListener('extractor:log', subscription)
|
||||
ipcRenderer.on(IPC_CHANNELS.EXTRACTOR_LOG, subscription)
|
||||
return () => ipcRenderer.removeListener(IPC_CHANNELS.EXTRACTOR_LOG, subscription)
|
||||
}
|
||||
},
|
||||
|
||||
// Cleaner service
|
||||
cleaner: {
|
||||
runCleaner: (input: CleanerInput) => ipcRenderer.invoke('cleaner:run', input),
|
||||
exportResults: (items: ExportResultItem[]) =>
|
||||
ipcRenderer.invoke('cleaner:exportResults', items),
|
||||
runCleaner: (input: CleanerInput): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.CLEANER_RUN, input),
|
||||
exportResults: (items: ExportResultItem[]): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.CLEANER_EXPORT_RESULTS, items),
|
||||
onProgress: (callback: (data: CleanerProgress) => void) => {
|
||||
const subscription = (_event: Electron.IpcRendererEvent, data: CleanerProgress) =>
|
||||
callback(data)
|
||||
ipcRenderer.on('cleaner:progress', subscription)
|
||||
return () => ipcRenderer.removeListener('cleaner:progress', subscription)
|
||||
ipcRenderer.on(IPC_CHANNELS.CLEANER_PROGRESS, subscription)
|
||||
return () => ipcRenderer.removeListener(IPC_CHANNELS.CLEANER_PROGRESS, subscription)
|
||||
}
|
||||
},
|
||||
|
||||
// Order number resolver
|
||||
resolver: {
|
||||
resolve: (input: ResolverInput) => ipcRenderer.invoke('resolver:resolve', input),
|
||||
validateFormat: (inputs: string[]) => ipcRenderer.invoke('resolver:validateFormat', inputs)
|
||||
resolve: (input: ResolverInput): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.RESOLVER_RESOLVE, input),
|
||||
validateFormat: (inputs: string[]): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.RESOLVER_VALIDATE_FORMAT, inputs)
|
||||
},
|
||||
|
||||
// Authentication service
|
||||
auth: {
|
||||
getComputerName: () => ipcRenderer.invoke('auth:getComputerName'),
|
||||
silentLogin: () => ipcRenderer.invoke('auth:silentLogin'),
|
||||
login: (request: LoginRequest) => ipcRenderer.invoke('auth:login', request),
|
||||
logout: () => ipcRenderer.invoke('auth:logout'),
|
||||
getCurrentUser: () => ipcRenderer.invoke('auth:getCurrentUser'),
|
||||
getAllUsers: () => ipcRenderer.invoke('auth:getAllUsers'),
|
||||
switchUser: (userInfo: UserInfo) => ipcRenderer.invoke('auth:switchUser', userInfo),
|
||||
isAdmin: () => ipcRenderer.invoke('auth:isAdmin')
|
||||
getComputerName: (): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.AUTH_GET_COMPUTER_NAME),
|
||||
silentLogin: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_SILENT_LOGIN),
|
||||
login: (request: LoginRequest): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.AUTH_LOGIN, request),
|
||||
logout: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_LOGOUT),
|
||||
getCurrentUser: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_GET_CURRENT_USER),
|
||||
getAllUsers: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_GET_ALL_USERS),
|
||||
switchUser: (userInfo: UserInfo): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.AUTH_SWITCH_USER, userInfo),
|
||||
isAdmin: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.AUTH_IS_ADMIN)
|
||||
},
|
||||
|
||||
// Database service
|
||||
database: {
|
||||
connectMySql: (config: MySqlConfig) => ipcRenderer.invoke('database:mysql:connect', config),
|
||||
disconnectMySql: () => ipcRenderer.invoke('database:mysql:disconnect'),
|
||||
isMySqlConnected: () => ipcRenderer.invoke('database:mysql:isConnected'),
|
||||
queryMySql: (sql: string, params?: any[]) =>
|
||||
ipcRenderer.invoke('database:mysql:query', sql, params),
|
||||
|
||||
// SQL Server
|
||||
connectSqlServer: (config: SqlServerConfig) =>
|
||||
ipcRenderer.invoke('database:sqlserver:connect', config),
|
||||
disconnectSqlServer: () => ipcRenderer.invoke('database:sqlserver:disconnect'),
|
||||
isSqlServerConnected: () => ipcRenderer.invoke('database:sqlserver:isConnected'),
|
||||
querySqlServer: (sql: string, params?: Record<string, unknown>) =>
|
||||
ipcRenderer.invoke('database:sqlserver:query', sql, params)
|
||||
connectMySql: (config: MySqlConfig): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_CONNECT, config),
|
||||
disconnectMySql: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_DISCONNECT),
|
||||
isMySqlConnected: (): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_IS_CONNECTED),
|
||||
queryMySql: (sql: string, params?: any[]): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.DATABASE_MYSQL_QUERY, sql, params),
|
||||
connectSqlServer: (config: SqlServerConfig): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_CONNECT, config),
|
||||
disconnectSqlServer: (): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_DISCONNECT),
|
||||
isSqlServerConnected: (): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_IS_CONNECTED),
|
||||
querySqlServer: (sql: string, params?: Record<string, unknown>): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.DATABASE_SQLSERVER_QUERY, sql, params)
|
||||
},
|
||||
|
||||
// Validation service
|
||||
validation: {
|
||||
validate: (request: ValidationRequest) => ipcRenderer.invoke('validation:validate', request),
|
||||
setSharedProductionIds: (productionIds: string[]) =>
|
||||
ipcRenderer.invoke('validation:setSharedProductionIds', productionIds),
|
||||
getSharedProductionIds: () => ipcRenderer.invoke('validation:getSharedProductionIds'),
|
||||
getCleanerData: () => ipcRenderer.invoke('validation:getCleanerData')
|
||||
validate: (request: ValidationRequest): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.VALIDATION_VALIDATE, request),
|
||||
setSharedProductionIds: (productionIds: string[]): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS, productionIds),
|
||||
getSharedProductionIds: (): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.VALIDATION_GET_SHARED_PRODUCTION_IDS),
|
||||
getCleanerData: (): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA)
|
||||
},
|
||||
|
||||
// Materials service
|
||||
materials: {
|
||||
upsertBatch: (materials: { materialCode: string; managerName: string }[]) =>
|
||||
ipcRenderer.invoke('materials:upsertBatch', { materials }),
|
||||
delete: (materialCodes: string[]) => ipcRenderer.invoke('materials:delete', { materialCodes }),
|
||||
getManagers: () => ipcRenderer.invoke('materials:getManagers'),
|
||||
getByManager: (managerName: string) =>
|
||||
ipcRenderer.invoke('materials:getByManager', managerName),
|
||||
getAll: () => ipcRenderer.invoke('materials:getAll'),
|
||||
getStatistics: () => ipcRenderer.invoke('materials:getStatistics'),
|
||||
updateManager: (materialCode: string, managerName: string) =>
|
||||
ipcRenderer.invoke('materials:updateManager', { materialCode, managerName })
|
||||
upsertBatch: (materials: { materialCode: string; managerName: string }[]): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIALS_UPSERT_BATCH, { materials }),
|
||||
delete: (materialCodes: string[]): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIALS_DELETE, { materialCodes }),
|
||||
getManagers: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.MATERIALS_GET_MANAGERS),
|
||||
getByManager: (managerName: string): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIALS_GET_BY_MANAGER, managerName),
|
||||
getAll: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.MATERIALS_GET_ALL),
|
||||
getStatistics: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.MATERIALS_GET_STATISTICS),
|
||||
updateManager: (materialCode: string, managerName: string): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIALS_UPDATE_MANAGER, { materialCode, managerName })
|
||||
},
|
||||
|
||||
// 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')
|
||||
getUserType: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.SETTINGS_GET_USER_TYPE),
|
||||
getSettings: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.SETTINGS_GET_SETTINGS),
|
||||
saveSettings: (settings: ErpSettingsPayload): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.SETTINGS_SAVE_SETTINGS, settings),
|
||||
resetDefaults: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.SETTINGS_RESET_DEFAULTS),
|
||||
testDbConnection: (): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.SETTINGS_TEST_DB_CONNECTION)
|
||||
},
|
||||
|
||||
// Material Type service
|
||||
materialType: {
|
||||
getAll: () => ipcRenderer.invoke('materialType:getAll'),
|
||||
getByManager: (managerName: string) =>
|
||||
ipcRenderer.invoke('materialType:getByManager', managerName),
|
||||
getManagers: () => ipcRenderer.invoke('materialType:getManagers'),
|
||||
upsert: (materialName: string, managerName: string) =>
|
||||
ipcRenderer.invoke('materialType:upsert', { materialName, managerName }),
|
||||
delete: (materialName: string, managerName: string) =>
|
||||
ipcRenderer.invoke('materialType:delete', { materialName, managerName }),
|
||||
upsertBatch: (request: MaterialTypeBatchRequest) =>
|
||||
ipcRenderer.invoke('materialType:upsertBatch', request)
|
||||
getAll: (): Promise<IpcResult<MaterialTypeRecord[]>> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_GET_ALL),
|
||||
getByManager: (managerName: string): Promise<IpcResult<MaterialTypeRecord[]>> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_GET_BY_MANAGER, managerName),
|
||||
getManagers: (): Promise<IpcResult<string[]>> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_GET_MANAGERS),
|
||||
upsert: (materialName: string, managerName: string): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_UPSERT, { materialName, managerName }),
|
||||
delete: (materialName: string, managerName: string): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_DELETE, { materialName, managerName }),
|
||||
upsertBatch: (request: MaterialTypeBatchRequest): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.MATERIAL_TYPE_UPSERT_BATCH, request)
|
||||
},
|
||||
|
||||
// User ERP Configuration service
|
||||
userErpConfig: {
|
||||
getCurrent: () => ipcRenderer.invoke('user-erp-config:getCurrent'),
|
||||
update: (config: { url: string; username: string; password: string }) =>
|
||||
ipcRenderer.invoke('user-erp-config:update', config),
|
||||
testConnection: (config: { url: string; username: string; password: string }) =>
|
||||
ipcRenderer.invoke('user-erp-config:testConnection', config),
|
||||
getAll: () => ipcRenderer.invoke('user-erp-config:getAll')
|
||||
getCurrent: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_GET_CURRENT),
|
||||
update: (config: { url: string; username: string; password: string }): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_UPDATE, config),
|
||||
testConnection: (config: { url: string; username: string; password: string }): Promise<IpcResult> =>
|
||||
invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_TEST_CONNECTION, config),
|
||||
getAll: (): Promise<IpcResult> => invokeIpc(IPC_CHANNELS.USER_ERP_CONFIG_GET_ALL)
|
||||
}
|
||||
} as const
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', { ...electronAPI, ...api })
|
||||
contextBridge.exposeInMainWorld('electron', api)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
} else {
|
||||
// @ts-ignore (define in dts)
|
||||
window.electron = { ...electronAPI, ...api }
|
||||
window.electron = api
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api
|
||||
}
|
||||
|
||||
|
||||
@@ -61,16 +61,18 @@ function App(): React.JSX.Element {
|
||||
try {
|
||||
// Get computer name
|
||||
console.log('Getting computer name...')
|
||||
const name = await window.electron.auth.getComputerName()
|
||||
const computerNameResult = await window.electron.auth.getComputerName()
|
||||
const name = computerNameResult.success && computerNameResult.data ? computerNameResult.data : ''
|
||||
console.log('Computer name:', name)
|
||||
setComputerName(name)
|
||||
|
||||
// Try silent login
|
||||
console.log('Trying silent login...')
|
||||
const result = await window.electron.auth.silentLogin()
|
||||
const silentLoginResult = await window.electron.auth.silentLogin()
|
||||
const result = silentLoginResult.data
|
||||
console.log('Silent login result:', result)
|
||||
|
||||
if (result.success && result.userInfo) {
|
||||
if (silentLoginResult.success && result?.success && result.userInfo) {
|
||||
console.log('Silent login success:', result.userInfo)
|
||||
setCurrentUser({
|
||||
username: result.userInfo.username,
|
||||
@@ -81,8 +83,8 @@ function App(): React.JSX.Element {
|
||||
if (result.requiresUserSelection) {
|
||||
console.log('Admin user needs to select user')
|
||||
// Load all users for selection
|
||||
const users = await window.electron.auth.getAllUsers()
|
||||
setAllUsers(users)
|
||||
const usersResult = await window.electron.auth.getAllUsers()
|
||||
setAllUsers(usersResult.success && usersResult.data ? usersResult.data : [])
|
||||
setShowUserSelection(true)
|
||||
} else {
|
||||
console.log('Setting authenticated to true')
|
||||
@@ -107,19 +109,20 @@ function App(): React.JSX.Element {
|
||||
const handleLogin = async (username: string, password: string): Promise<boolean> => {
|
||||
try {
|
||||
const result = await window.electron.auth.login({ username, password })
|
||||
const loginData = result.success ? result.data : undefined
|
||||
|
||||
if (result.success && result.userInfo) {
|
||||
if (result.success && loginData?.userInfo) {
|
||||
setCurrentUser({
|
||||
username: result.userInfo.username,
|
||||
userType: result.userInfo.userType
|
||||
username: loginData.userInfo.username,
|
||||
userType: loginData.userInfo.userType
|
||||
})
|
||||
|
||||
// Check if admin needs user selection
|
||||
if (result.userInfo.userType === 'Admin') {
|
||||
if (loginData.userInfo.userType === 'Admin') {
|
||||
setShowLoginDialog(false)
|
||||
// Load all users for selection
|
||||
const users = await window.electron.auth.getAllUsers()
|
||||
setAllUsers(users)
|
||||
const usersResult = await window.electron.auth.getAllUsers()
|
||||
setAllUsers(usersResult.success && usersResult.data ? usersResult.data : [])
|
||||
setShowUserSelection(true)
|
||||
} else {
|
||||
setIsAuthenticated(true)
|
||||
@@ -144,10 +147,11 @@ function App(): React.JSX.Element {
|
||||
const handleUserSelect = async (user: SelectedUserInfo) => {
|
||||
try {
|
||||
const result = await window.electron.auth.switchUser(user)
|
||||
if (result.success) {
|
||||
const switchData = result.success ? result.data : undefined
|
||||
if (result.success && switchData) {
|
||||
setCurrentUser({
|
||||
username: result.userInfo?.username || user.username,
|
||||
userType: result.userInfo?.userType || user.userType
|
||||
username: switchData.userInfo?.username || user.username,
|
||||
userType: switchData.userInfo?.userType || user.userType
|
||||
})
|
||||
setIsAuthenticated(true)
|
||||
setShowUserSelection(false)
|
||||
|
||||
@@ -252,10 +252,11 @@ export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialog
|
||||
toUpdate,
|
||||
toDelete
|
||||
})
|
||||
const payload = result.success ? (result.data as { stats?: { success?: number; failed?: number } } | undefined) : undefined
|
||||
|
||||
if (result.success) {
|
||||
alert(
|
||||
`保存完成!\n成功:${result.stats?.success || 0} 条\n失败:${result.stats?.failed || 0} 条`
|
||||
`保存完成!\n成功:${payload?.stats?.success || 0} 条\n失败:${payload?.stats?.failed || 0} 条`
|
||||
)
|
||||
await loadData()
|
||||
} else {
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
/**
|
||||
* IPC Hook for Authentication operations
|
||||
*
|
||||
* Provides a React-friendly interface for auth IPC calls
|
||||
* with loading state, error handling, and user data management.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
// Types based on the user types
|
||||
interface UserInfo {
|
||||
id: number
|
||||
username: string
|
||||
@@ -38,9 +30,6 @@ interface UseAuthReturn extends UseAuthState {
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for authentication operations
|
||||
*/
|
||||
export function useAuth(): UseAuthReturn {
|
||||
const [state, setState] = useState<UseAuthState>({
|
||||
loading: false,
|
||||
@@ -51,156 +40,94 @@ export function useAuth(): UseAuthReturn {
|
||||
|
||||
const login = useCallback(async (credentials: LoginCredentials): Promise<boolean> => {
|
||||
setState((prev) => ({ ...prev, loading: true, error: null }))
|
||||
const result = await window.electron.auth.login(credentials)
|
||||
|
||||
try {
|
||||
const result = (await window.electron.ipcRenderer.invoke('auth:login', credentials)) as {
|
||||
success: boolean
|
||||
userInfo?: UserInfo
|
||||
error?: string
|
||||
}
|
||||
|
||||
if (result.success && result.userInfo) {
|
||||
setState({
|
||||
loading: false,
|
||||
user: result.userInfo,
|
||||
error: null,
|
||||
isAuthenticated: true
|
||||
})
|
||||
return true
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: result.error || 'Login failed'
|
||||
}))
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
setState((prev) => ({ ...prev, loading: false, error: message }))
|
||||
if (!result.success || !result.data?.userInfo) {
|
||||
setState((prev) => ({ ...prev, loading: false, error: result.error ?? 'Login failed' }))
|
||||
return false
|
||||
}
|
||||
|
||||
setState({
|
||||
loading: false,
|
||||
user: result.data.userInfo,
|
||||
error: null,
|
||||
isAuthenticated: true
|
||||
})
|
||||
return true
|
||||
}, [])
|
||||
|
||||
const silentLogin = useCallback(async (): Promise<{
|
||||
success: boolean
|
||||
requiresUserSelection?: boolean
|
||||
}> => {
|
||||
const silentLogin = useCallback(async (): Promise<{ success: boolean; requiresUserSelection?: boolean }> => {
|
||||
setState((prev) => ({ ...prev, loading: true, error: null }))
|
||||
const result = await window.electron.auth.silentLogin()
|
||||
|
||||
try {
|
||||
const result = (await window.electron.ipcRenderer.invoke('auth:silentLogin')) as {
|
||||
success: boolean
|
||||
userInfo?: UserInfo
|
||||
error?: string
|
||||
requiresUserSelection?: boolean
|
||||
}
|
||||
|
||||
if (result.success && result.userInfo) {
|
||||
setState({
|
||||
loading: false,
|
||||
user: result.userInfo,
|
||||
error: null,
|
||||
isAuthenticated: true
|
||||
})
|
||||
return { success: true, requiresUserSelection: result.requiresUserSelection }
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: result.error || null
|
||||
}))
|
||||
return { success: false }
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
setState((prev) => ({ ...prev, loading: false, error: message }))
|
||||
if (!result.success || !result.data?.userInfo) {
|
||||
setState((prev) => ({ ...prev, loading: false, error: result.error || null }))
|
||||
return { success: false }
|
||||
}
|
||||
|
||||
setState({
|
||||
loading: false,
|
||||
user: result.data.userInfo,
|
||||
error: null,
|
||||
isAuthenticated: true
|
||||
})
|
||||
return { success: true, requiresUserSelection: result.data.requiresUserSelection }
|
||||
}, [])
|
||||
|
||||
const logout = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
await window.electron.ipcRenderer.invoke('auth:logout')
|
||||
setState({
|
||||
loading: false,
|
||||
user: null,
|
||||
error: null,
|
||||
isAuthenticated: false
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error)
|
||||
}
|
||||
await window.electron.auth.logout()
|
||||
setState({
|
||||
loading: false,
|
||||
user: null,
|
||||
error: null,
|
||||
isAuthenticated: false
|
||||
})
|
||||
}, [])
|
||||
|
||||
const getCurrentUser = useCallback(async (): Promise<UserInfo | null> => {
|
||||
try {
|
||||
const result = (await window.electron.ipcRenderer.invoke('auth:getCurrentUser')) as {
|
||||
isAuthenticated: boolean
|
||||
userInfo?: UserInfo
|
||||
}
|
||||
if (result.isAuthenticated && result.userInfo) {
|
||||
const userInfo = result.userInfo
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
user: userInfo,
|
||||
isAuthenticated: true
|
||||
}))
|
||||
return userInfo
|
||||
}
|
||||
return null
|
||||
} catch {
|
||||
const result = await window.electron.auth.getCurrentUser()
|
||||
if (!result.success || !result.data?.isAuthenticated || !result.data.userInfo) {
|
||||
return null
|
||||
}
|
||||
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
user: result.data!.userInfo!,
|
||||
isAuthenticated: true
|
||||
}))
|
||||
return result.data.userInfo
|
||||
}, [])
|
||||
|
||||
const getAllUsers = useCallback(async (): Promise<UserInfo[]> => {
|
||||
try {
|
||||
return (await window.electron.ipcRenderer.invoke('auth:getAllUsers')) as UserInfo[]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
const result = await window.electron.auth.getAllUsers()
|
||||
return result.success && result.data ? result.data : []
|
||||
}, [])
|
||||
|
||||
const switchUser = useCallback(async (userInfo: UserInfo): Promise<boolean> => {
|
||||
setState((prev) => ({ ...prev, loading: true, error: null }))
|
||||
const result = await window.electron.auth.switchUser(userInfo)
|
||||
|
||||
try {
|
||||
const result = (await window.electron.ipcRenderer.invoke('auth:switchUser', userInfo)) as {
|
||||
success: boolean
|
||||
userInfo?: UserInfo
|
||||
error?: string
|
||||
}
|
||||
|
||||
if (result.success && result.userInfo) {
|
||||
setState({
|
||||
loading: false,
|
||||
user: result.userInfo,
|
||||
error: null,
|
||||
isAuthenticated: true
|
||||
})
|
||||
return true
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: result.error || 'Switch user failed'
|
||||
}))
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
setState((prev) => ({ ...prev, loading: false, error: message }))
|
||||
if (!result.success || !result.data?.userInfo) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: result.error || 'Switch user failed'
|
||||
}))
|
||||
return false
|
||||
}
|
||||
|
||||
setState({
|
||||
loading: false,
|
||||
user: result.data.userInfo,
|
||||
error: null,
|
||||
isAuthenticated: true
|
||||
})
|
||||
return true
|
||||
}, [])
|
||||
|
||||
const isAdmin = useCallback(async (): Promise<boolean> => {
|
||||
try {
|
||||
return (await window.electron.ipcRenderer.invoke('auth:isAdmin')) as boolean
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
const result = await window.electron.auth.isAdmin()
|
||||
return result.success && Boolean(result.data)
|
||||
}, [])
|
||||
|
||||
const reset = useCallback(() => {
|
||||
|
||||
@@ -85,8 +85,10 @@ export function useCleaner() {
|
||||
useEffect(() => {
|
||||
const initializePage = async () => {
|
||||
try {
|
||||
const admin = await window.electron.auth.isAdmin()
|
||||
const user = await window.electron.auth.getCurrentUser()
|
||||
const adminResult = await window.electron.auth.isAdmin()
|
||||
const userResult = await window.electron.auth.getCurrentUser()
|
||||
const admin = adminResult.success && Boolean(adminResult.data)
|
||||
const user = userResult.success ? userResult.data : undefined
|
||||
setIsAdmin(admin)
|
||||
if (user && user.userInfo) {
|
||||
setCurrentUsername(user.userInfo.username)
|
||||
@@ -98,13 +100,16 @@ export function useCleaner() {
|
||||
// Load managers
|
||||
if (admin) {
|
||||
const resp = await window.electron.materials.getManagers()
|
||||
setManagers(resp.managers)
|
||||
setSelectedManagers(new Set(resp.managers))
|
||||
const managersPayload = resp.success ? (resp.data as { managers: string[] } | undefined) : undefined
|
||||
const managerList = managersPayload?.managers ?? []
|
||||
setManagers(managerList)
|
||||
setSelectedManagers(new Set(managerList))
|
||||
}
|
||||
|
||||
// Get shared Production IDs
|
||||
const result = await window.electron.validation.getSharedProductionIds()
|
||||
setSharedProductionIdsCount(result.productionIds.length)
|
||||
const idsPayload = result.success ? (result.data as { productionIds?: string[] } | undefined) : undefined
|
||||
setSharedProductionIdsCount(idsPayload?.productionIds?.length ?? 0)
|
||||
} catch (err) {
|
||||
console.error('Initialization failed:', err)
|
||||
}
|
||||
@@ -159,21 +164,28 @@ export function useCleaner() {
|
||||
mode: valMode === 'full' ? 'database_full' : 'database_filtered',
|
||||
useSharedProductionIds: valMode === 'filtered'
|
||||
})
|
||||
const validationData = response.success ? (response.data as any) : null
|
||||
|
||||
if (response.success && response.results) {
|
||||
setValidationResults(response.results)
|
||||
const markedCodes = new Set(
|
||||
response.results.filter((r) => r.isMarkedForDeletion).map((r) => r.materialCode)
|
||||
if (response.success && validationData?.success && validationData.results) {
|
||||
setValidationResults(validationData.results)
|
||||
const markedCodes = new Set<string>(
|
||||
validationData.results
|
||||
.filter((r: ValidationResult) => r.isMarkedForDeletion)
|
||||
.map((r: ValidationResult) => r.materialCode)
|
||||
)
|
||||
setSelectedItems(markedCodes)
|
||||
|
||||
if (isAdmin) {
|
||||
const uniqueManagers = new Set(response.results.map((r) => r.managerName).filter(Boolean))
|
||||
setManagers([...uniqueManagers])
|
||||
const uniqueManagers = new Set<string>(
|
||||
validationData.results
|
||||
.map((r: ValidationResult) => r.managerName)
|
||||
.filter((name: string) => Boolean(name))
|
||||
)
|
||||
setManagers(Array.from(uniqueManagers))
|
||||
setSelectedManagers(uniqueManagers)
|
||||
}
|
||||
} else {
|
||||
alert(response.error || '校验失败')
|
||||
alert(response.error || validationData?.error || '校验失败')
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : '校验过程中发生未知错误')
|
||||
@@ -285,14 +297,16 @@ 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
|
||||
if (!res.success) throw new Error(res.error || '写入物料失败')
|
||||
msgParts.push(`写入/更新成功:${res.stats?.success || 0} 条`)
|
||||
msgParts.push(`写入/更新成功:${payload?.stats?.success || 0} 条`)
|
||||
}
|
||||
|
||||
if (materialsToDelete.length > 0) {
|
||||
const res = await window.electron.materials.delete(materialsToDelete)
|
||||
const payload = res.success ? (res.data as { count?: number } | undefined) : undefined
|
||||
if (!res.success) throw new Error(res.error || '删除物料失败')
|
||||
msgParts.push(`删除成功:${res.count || 0} 条`)
|
||||
msgParts.push(`删除成功:${payload?.count || 0} 条`)
|
||||
}
|
||||
|
||||
alert(`操作完成!\n\n${msgParts.join('\n')}`)
|
||||
@@ -300,7 +314,8 @@ export function useCleaner() {
|
||||
// Reload managers if admin
|
||||
if (isAdmin) {
|
||||
const resp = await window.electron.materials.getManagers()
|
||||
setManagers(resp.managers)
|
||||
const payload = resp.success ? (resp.data as { managers?: string[] } | undefined) : undefined
|
||||
setManagers(payload?.managers ?? [])
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : '操作失败')
|
||||
@@ -331,12 +346,13 @@ export function useCleaner() {
|
||||
|
||||
try {
|
||||
const cleanerDataResult = await window.electron.validation.getCleanerData()
|
||||
if (!cleanerDataResult.success) {
|
||||
const cleanerData = cleanerDataResult.success ? (cleanerDataResult.data as any) : null
|
||||
if (!cleanerDataResult.success || cleanerData?.success === false) {
|
||||
throw new Error(cleanerDataResult.error || '获取清理数据失败')
|
||||
}
|
||||
|
||||
const orderNumberList = cleanerDataResult.orderNumbers || []
|
||||
const materialCodeList = cleanerDataResult.materialCodes || []
|
||||
const orderNumberList = cleanerData?.orderNumbers || []
|
||||
const materialCodeList = cleanerData?.materialCodes || []
|
||||
|
||||
if (orderNumberList.length === 0)
|
||||
throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。')
|
||||
@@ -349,13 +365,14 @@ export function useCleaner() {
|
||||
dryRun,
|
||||
headless
|
||||
})
|
||||
const cleanerRunData = response.success ? (response.data as any) : null
|
||||
|
||||
if (response.success && response.data) {
|
||||
if (response.success && cleanerRunData) {
|
||||
setReportData({
|
||||
ordersProcessed: response.data.ordersProcessed,
|
||||
materialsDeleted: response.data.materialsDeleted,
|
||||
materialsSkipped: response.data.materialsSkipped,
|
||||
errors: response.data.errors
|
||||
ordersProcessed: cleanerRunData.ordersProcessed,
|
||||
materialsDeleted: cleanerRunData.materialsDeleted,
|
||||
materialsSkipped: cleanerRunData.materialsSkipped,
|
||||
errors: cleanerRunData.errors
|
||||
})
|
||||
} else {
|
||||
throw new Error(response.error || '清理失败')
|
||||
@@ -390,11 +407,12 @@ export function useCleaner() {
|
||||
}))
|
||||
|
||||
const response = await window.electron.cleaner.exportResults(exportItems)
|
||||
const exportData = response.success ? (response.data as any) : null
|
||||
|
||||
if (response.success) {
|
||||
alert(`导出成功!\n文件已保存到:${response.filePath}`)
|
||||
if (response.success && exportData?.success !== false) {
|
||||
alert(`导出成功!\n文件已保存到:${exportData?.filePath ?? ''}`)
|
||||
} else {
|
||||
throw new Error(response.error || '导出失败')
|
||||
throw new Error(response.error || exportData?.error || '导出失败')
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err instanceof Error ? err.message : '导出过程中发生错误')
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
/**
|
||||
* IPC Hook for Validation operations
|
||||
*
|
||||
* Provides a React-friendly interface for validation IPC calls
|
||||
* with loading state, error handling, and data management.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react'
|
||||
|
||||
// Types based on validation types
|
||||
interface ValidationRequest {
|
||||
mode: 'database_full' | 'database_filtered'
|
||||
productionIdFile?: string
|
||||
@@ -52,9 +44,6 @@ interface UseValidationReturn extends UseValidationState {
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for validation operations
|
||||
*/
|
||||
export function useValidation(): UseValidationReturn {
|
||||
const [state, setState] = useState<UseValidationState>({
|
||||
loading: false,
|
||||
@@ -63,82 +52,62 @@ export function useValidation(): UseValidationReturn {
|
||||
error: null
|
||||
})
|
||||
|
||||
const validate = useCallback(
|
||||
async (request: ValidationRequest): Promise<ValidationResponse | null> => {
|
||||
setState((prev) => ({ ...prev, loading: true, error: null }))
|
||||
const validate = useCallback(async (request: ValidationRequest): Promise<ValidationResponse | null> => {
|
||||
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 }
|
||||
}
|
||||
|
||||
try {
|
||||
const result = (await window.electron.ipcRenderer.invoke(
|
||||
'validation:validate',
|
||||
request
|
||||
)) as ValidationResponse
|
||||
const result = response.data as ValidationResponse
|
||||
if (!result.success) {
|
||||
setState((prev) => ({ ...prev, loading: false, error: result.error || 'Validation failed' }))
|
||||
return result
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
const results = result.results || null
|
||||
const stats = result.stats || null
|
||||
setState({
|
||||
loading: false,
|
||||
data: results,
|
||||
stats: stats,
|
||||
error: null
|
||||
})
|
||||
return result
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: result.error || 'Validation failed'
|
||||
}))
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
setState((prev) => ({ ...prev, loading: false, error: message }))
|
||||
return null
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
setState({
|
||||
loading: false,
|
||||
data: result.results || null,
|
||||
stats: result.stats || null,
|
||||
error: null
|
||||
})
|
||||
return result
|
||||
}, [])
|
||||
|
||||
const setSharedProductionIds = useCallback(async (ids: string[]): Promise<void> => {
|
||||
try {
|
||||
await window.electron.ipcRenderer.invoke('validation:setSharedProductionIds', ids)
|
||||
} catch (error) {
|
||||
console.error('Failed to set shared production IDs:', error)
|
||||
}
|
||||
await window.electron.validation.setSharedProductionIds(ids)
|
||||
}, [])
|
||||
|
||||
const getSharedProductionIds = useCallback(async (): Promise<string[]> => {
|
||||
try {
|
||||
const result = (await window.electron.ipcRenderer.invoke(
|
||||
'validation:getSharedProductionIds'
|
||||
)) as { productionIds?: string[] }
|
||||
return result?.productionIds || []
|
||||
} catch {
|
||||
const result = await window.electron.validation.getSharedProductionIds()
|
||||
if (!result.success || !result.data) {
|
||||
return []
|
||||
}
|
||||
const payload = result.data as { productionIds?: string[] }
|
||||
return payload.productionIds || []
|
||||
}, [])
|
||||
|
||||
const getCleanerData = useCallback(async (): Promise<{
|
||||
orderNumbers: string[]
|
||||
materialCodes: string[]
|
||||
} | null> => {
|
||||
try {
|
||||
const result = (await window.electron.ipcRenderer.invoke('validation:getCleanerData')) as {
|
||||
success: boolean
|
||||
orderNumbers?: string[]
|
||||
materialCodes?: string[]
|
||||
}
|
||||
if (result.success) {
|
||||
return {
|
||||
orderNumbers: result.orderNumbers || [],
|
||||
materialCodes: result.materialCodes || []
|
||||
}
|
||||
}
|
||||
const getCleanerData = useCallback(async (): Promise<{ orderNumbers: string[]; materialCodes: string[] } | null> => {
|
||||
const result = await window.electron.validation.getCleanerData()
|
||||
if (!result.success || !result.data) {
|
||||
return null
|
||||
} catch {
|
||||
}
|
||||
|
||||
const payload = result.data as {
|
||||
success?: boolean
|
||||
orderNumbers?: string[]
|
||||
materialCodes?: string[]
|
||||
}
|
||||
|
||||
if (payload.success === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
orderNumbers: payload.orderNumbers || [],
|
||||
materialCodes: payload.materialCodes || []
|
||||
}
|
||||
}, [])
|
||||
|
||||
const reset = useCallback(() => {
|
||||
|
||||
@@ -26,14 +26,17 @@ const SettingsPage: React.FC = () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
// ERP credentials are loaded from database (current user's config)
|
||||
const config = await window.electron.settings.getSettings()
|
||||
const response = await window.electron.settings.getSettings()
|
||||
const config = response.success ? (response.data as { erp?: ErpCredentials } | undefined) : undefined
|
||||
|
||||
// Extract ERP credentials from the config
|
||||
if (config && (config as any).erp) {
|
||||
if (config?.erp) {
|
||||
setCredentials({
|
||||
username: (config as any).erp.username || '',
|
||||
password: (config as any).erp.password || ''
|
||||
username: config.erp.username || '',
|
||||
password: config.erp.password || ''
|
||||
})
|
||||
} else if (!response.success) {
|
||||
showMessage('error', response.error || '加载 ERP 配置失败')
|
||||
}
|
||||
setIsModified(false)
|
||||
} catch (error) {
|
||||
@@ -56,13 +59,14 @@ const SettingsPage: React.FC = () => {
|
||||
username: credentials.username,
|
||||
password: credentials.password
|
||||
}
|
||||
} as any)
|
||||
})
|
||||
const saveData = result.success ? (result.data as { success?: boolean; error?: string } | undefined) : undefined
|
||||
|
||||
if (result.success) {
|
||||
if (result.success && saveData?.success !== false) {
|
||||
setIsModified(false)
|
||||
showMessage('success', 'ERP 账号密码保存成功')
|
||||
} else {
|
||||
showMessage('error', result.error || '保存失败')
|
||||
showMessage('error', result.error || saveData?.error || '保存失败')
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('error', '保存配置时发生错误')
|
||||
|
||||
@@ -9,17 +9,77 @@ export const IPC_CHANNELS = {
|
||||
FILE_WRITE: 'file:write',
|
||||
FILE_EXISTS: 'file:exists',
|
||||
FILE_LIST: 'file:list',
|
||||
FILE_OPEN_PATH: 'file:openPath',
|
||||
|
||||
// Extractor service
|
||||
EXTRACTOR_RUN: 'extractor:run',
|
||||
EXTRACTOR_PROGRESS: 'extractor:progress',
|
||||
EXTRACTOR_LOG: 'extractor:log',
|
||||
|
||||
// Cleaner service
|
||||
CLEANER_RUN: 'cleaner:run',
|
||||
CLEANER_EXPORT_RESULTS: 'cleaner:exportResults',
|
||||
CLEANER_PROGRESS: 'cleaner:progress',
|
||||
|
||||
// Database service - MySQL
|
||||
DATABASE_MYSQL_CONNECT: 'database:mysql:connect',
|
||||
DATABASE_MYSQL_DISCONNECT: 'database:mysql:disconnect',
|
||||
DATABASE_MYSQL_IS_CONNECTED: 'database:mysql:isConnected',
|
||||
DATABASE_MYSQL_QUERY: 'database:mysql:query'
|
||||
DATABASE_MYSQL_QUERY: 'database:mysql:query',
|
||||
|
||||
// Database service - SQL Server
|
||||
DATABASE_SQLSERVER_CONNECT: 'database:sqlserver:connect',
|
||||
DATABASE_SQLSERVER_DISCONNECT: 'database:sqlserver:disconnect',
|
||||
DATABASE_SQLSERVER_IS_CONNECTED: 'database:sqlserver:isConnected',
|
||||
DATABASE_SQLSERVER_QUERY: 'database:sqlserver:query',
|
||||
|
||||
// Resolver
|
||||
RESOLVER_RESOLVE: 'resolver:resolve',
|
||||
RESOLVER_VALIDATE_FORMAT: 'resolver:validateFormat',
|
||||
|
||||
// Auth
|
||||
AUTH_GET_COMPUTER_NAME: 'auth:getComputerName',
|
||||
AUTH_SILENT_LOGIN: 'auth:silentLogin',
|
||||
AUTH_LOGIN: 'auth:login',
|
||||
AUTH_LOGOUT: 'auth:logout',
|
||||
AUTH_GET_CURRENT_USER: 'auth:getCurrentUser',
|
||||
AUTH_GET_ALL_USERS: 'auth:getAllUsers',
|
||||
AUTH_SWITCH_USER: 'auth:switchUser',
|
||||
AUTH_IS_ADMIN: 'auth:isAdmin',
|
||||
|
||||
// Validation
|
||||
VALIDATION_VALIDATE: 'validation:validate',
|
||||
VALIDATION_SET_SHARED_PRODUCTION_IDS: 'validation:setSharedProductionIds',
|
||||
VALIDATION_GET_SHARED_PRODUCTION_IDS: 'validation:getSharedProductionIds',
|
||||
VALIDATION_GET_CLEANER_DATA: 'validation:getCleanerData',
|
||||
|
||||
// Materials
|
||||
MATERIALS_UPSERT_BATCH: 'materials:upsertBatch',
|
||||
MATERIALS_DELETE: 'materials:delete',
|
||||
MATERIALS_GET_MANAGERS: 'materials:getManagers',
|
||||
MATERIALS_GET_BY_MANAGER: 'materials:getByManager',
|
||||
MATERIALS_GET_ALL: 'materials:getAll',
|
||||
MATERIALS_GET_STATISTICS: 'materials:getStatistics',
|
||||
MATERIALS_UPDATE_MANAGER: 'materials:updateManager',
|
||||
|
||||
// Settings
|
||||
SETTINGS_GET_USER_TYPE: 'settings:getUserType',
|
||||
SETTINGS_GET_SETTINGS: 'settings:getSettings',
|
||||
SETTINGS_SAVE_SETTINGS: 'settings:saveSettings',
|
||||
SETTINGS_RESET_DEFAULTS: 'settings:resetDefaults',
|
||||
SETTINGS_TEST_DB_CONNECTION: 'settings:testDbConnection',
|
||||
|
||||
// Material type
|
||||
MATERIAL_TYPE_GET_ALL: 'materialType:getAll',
|
||||
MATERIAL_TYPE_GET_BY_MANAGER: 'materialType:getByManager',
|
||||
MATERIAL_TYPE_GET_MANAGERS: 'materialType:getManagers',
|
||||
MATERIAL_TYPE_UPSERT: 'materialType:upsert',
|
||||
MATERIAL_TYPE_DELETE: 'materialType:delete',
|
||||
MATERIAL_TYPE_UPSERT_BATCH: 'materialType:upsertBatch',
|
||||
|
||||
// User ERP config
|
||||
USER_ERP_CONFIG_GET_CURRENT: 'user-erp-config:getCurrent',
|
||||
USER_ERP_CONFIG_UPDATE: 'user-erp-config:update',
|
||||
USER_ERP_CONFIG_TEST_CONNECTION: 'user-erp-config:testConnection',
|
||||
USER_ERP_CONFIG_GET_ALL: 'user-erp-config:getAll'
|
||||
} as const
|
||||
|
||||
17
tests/unit/file-ipc-paths.test.ts
Normal file
17
tests/unit/file-ipc-paths.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import path from 'path'
|
||||
import { isPathWithinAllowedRoots } from '../../src/main/ipc/file-handler'
|
||||
|
||||
describe('File IPC Path Guard', () => {
|
||||
it('accepts path under allowed root', () => {
|
||||
const root = path.resolve('C:/safe/root')
|
||||
const file = path.join(root, 'nested', 'file.txt')
|
||||
expect(isPathWithinAllowedRoots(file, [root])).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects path outside allowed roots', () => {
|
||||
const root = path.resolve('C:/safe/root')
|
||||
const outside = path.resolve('C:/other/location/file.txt')
|
||||
expect(isPathWithinAllowedRoots(outside, [root])).toBe(false)
|
||||
})
|
||||
})
|
||||
21
tests/unit/ipc-index.test.ts
Normal file
21
tests/unit/ipc-index.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { withErrorHandling } from '../../src/main/ipc'
|
||||
import { ValidationError } from '../../src/main/types/errors'
|
||||
|
||||
describe('IPC Result Wrapper', () => {
|
||||
it('wraps successful values into IpcResult.data', async () => {
|
||||
const result = await withErrorHandling(async () => ({ value: 42 }), 'test:success')
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).toEqual({ value: 42 })
|
||||
})
|
||||
|
||||
it('wraps exceptions into IpcResult.error/code', async () => {
|
||||
const result = await withErrorHandling(async () => {
|
||||
throw new ValidationError('Invalid input', 'VAL_INVALID_INPUT')
|
||||
}, 'test:failure')
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error).toBe('Invalid input')
|
||||
expect(result.code).toBe('VAL_INVALID_INPUT')
|
||||
})
|
||||
})
|
||||
11
tests/unit/preload-surface.test.ts
Normal file
11
tests/unit/preload-surface.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
describe('Preload API Surface', () => {
|
||||
it('does not expose ipcRenderer in window typings', () => {
|
||||
const dtsPath = path.resolve(process.cwd(), 'src/preload/index.d.ts')
|
||||
const content = fs.readFileSync(dtsPath, 'utf-8')
|
||||
expect(content.includes('ipcRenderer:')).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user