feat: add material type management feature
- Add MaterialTypeManagementDialog component for managing material type keywords - Add MaterialsTypeToBeDeletedDAO for database operations - Add material-type-handler IPC handlers - Update CleanerPage with type management button - Add database fix scripts for AUTO_INCREMENT - Update documentation for settings partial save and validation flow Co-Authored-By: Claude (glm-5) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { registerResolverHandlers } from './resolver-handler'
|
||||
import { registerAuthHandlers } from './auth-handler'
|
||||
import { registerValidationHandlers } from './validation-handler'
|
||||
import { registerSettingsHandlers } from './settings-handler'
|
||||
import { registerMaterialTypeHandlers } from './material-type-handler'
|
||||
import { createLogger } from '../services/logger'
|
||||
import { getErrorMessage, getErrorCode, isBaseError } from '../types/errors'
|
||||
|
||||
@@ -73,5 +74,6 @@ export function registerIpcHandlers(): void {
|
||||
registerAuthHandlers()
|
||||
registerValidationHandlers()
|
||||
registerSettingsHandlers()
|
||||
registerMaterialTypeHandlers()
|
||||
log.info('All IPC handlers registered')
|
||||
}
|
||||
|
||||
154
src/main/ipc/material-type-handler.ts
Normal file
154
src/main/ipc/material-type-handler.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* IPC handlers for material type management operations
|
||||
*
|
||||
* Provides endpoints for:
|
||||
* - Getting all material type records
|
||||
* - Getting records by manager
|
||||
* - Getting list of managers
|
||||
* - Upserting (insert/update) records
|
||||
* - Deleting records
|
||||
* - Batch operations
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron'
|
||||
import {
|
||||
MaterialsTypeToBeDeletedDAO,
|
||||
type MaterialTypeRecord,
|
||||
type MaterialTypeBatchRequest
|
||||
} from '../services/database/materials-type-to-be-deleted-dao'
|
||||
import { createLogger } from '../services/logger'
|
||||
|
||||
const log = createLogger('MaterialTypeHandler')
|
||||
|
||||
/**
|
||||
* Register IPC handlers for material type operations
|
||||
*/
|
||||
export function registerMaterialTypeHandlers(): void {
|
||||
const dao = new MaterialsTypeToBeDeletedDAO()
|
||||
|
||||
/**
|
||||
* Get all material type records
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:getAll',
|
||||
async (): Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> => {
|
||||
try {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Get material types by manager
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:getByManager',
|
||||
async (
|
||||
_event,
|
||||
managerName: string
|
||||
): Promise<{ success: boolean; data?: MaterialTypeRecord[]; error?: string }> => {
|
||||
try {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Get list of managers
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:getManagers',
|
||||
async (): Promise<{ success: boolean; data?: string[]; error?: string }> => {
|
||||
try {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Upsert (insert or update) a material type record
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:upsert',
|
||||
async (
|
||||
_event,
|
||||
{ materialName, managerName }: { materialName: string; managerName: string }
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const result = await dao.upsertMaterial(materialName, managerName)
|
||||
if (!result) {
|
||||
return { success: false, error: 'Failed to upsert material type' }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Delete a material type record
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:delete',
|
||||
async (
|
||||
_event,
|
||||
{ materialName, managerName }: { materialName: string; managerName: string }
|
||||
): Promise<{ success: boolean; error?: string }> => {
|
||||
try {
|
||||
const result = await dao.deleteMaterial(materialName, managerName)
|
||||
if (!result) {
|
||||
return { success: false, error: 'Failed to delete material type' }
|
||||
}
|
||||
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 }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Batch operation for material types (insert, update, delete)
|
||||
*/
|
||||
ipcMain.handle(
|
||||
'materialType:upsertBatch',
|
||||
async (
|
||||
_event,
|
||||
request: MaterialTypeBatchRequest
|
||||
): Promise<{
|
||||
success: boolean
|
||||
stats?: { total: number; success: number; failed: number }
|
||||
error?: string
|
||||
}> => {
|
||||
try {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
log.info('Material type handlers registered')
|
||||
}
|
||||
@@ -291,12 +291,8 @@ export class ConfigManager {
|
||||
lines.push('# ===========================')
|
||||
lines.push('# 数据库配置 - MySQL (切换时使用)')
|
||||
lines.push('# ===========================')
|
||||
lines.push(
|
||||
`DB_TYPE=${this.configCache.get('DB_TYPE') || DEFAULT_SETTINGS.database.dbType}`
|
||||
)
|
||||
lines.push(
|
||||
`DB_NAME=${this.configCache.get('DB_NAME') || DEFAULT_SETTINGS.database.database}`
|
||||
)
|
||||
lines.push(`DB_TYPE=${this.configCache.get('DB_TYPE') || DEFAULT_SETTINGS.database.dbType}`)
|
||||
lines.push(`DB_NAME=${this.configCache.get('DB_NAME') || DEFAULT_SETTINGS.database.database}`)
|
||||
lines.push(
|
||||
`DB_USERNAME=${this.configCache.get('DB_USERNAME') || DEFAULT_SETTINGS.database.username}`
|
||||
)
|
||||
|
||||
376
src/main/services/database/materials-type-to-be-deleted-dao.ts
Normal file
376
src/main/services/database/materials-type-to-be-deleted-dao.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
/**
|
||||
* Data Access Object for MaterialsTypeToBeDeleted table
|
||||
*
|
||||
* Manages material type keywords for identifying materials to be deleted.
|
||||
* Used for matching material names against type keywords to assign managers.
|
||||
*/
|
||||
|
||||
import { create, type IDatabaseService } from './index'
|
||||
import { createLogger } from '../logger'
|
||||
|
||||
const log = createLogger('MaterialsTypeToBeDeletedDAO')
|
||||
|
||||
/**
|
||||
* Material type record interface
|
||||
*/
|
||||
export interface MaterialTypeRecord {
|
||||
id?: number
|
||||
materialName: string
|
||||
managerName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch update request
|
||||
*/
|
||||
export interface MaterialTypeBatchRequest {
|
||||
toInsert: MaterialTypeRecord[]
|
||||
toUpdate: { old: MaterialTypeRecord; new: MaterialTypeRecord }[]
|
||||
toDelete: MaterialTypeRecord[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for MaterialsTypeToBeDeleted table
|
||||
*/
|
||||
export const MATERIALS_TYPE_TO_BE_DELETED_CONFIG = {
|
||||
TABLE_NAME_SQLSERVER: '[dbo].[MaterialsTypeToBeDeleted]',
|
||||
TABLE_NAME_MYSQL: 'dbo_MaterialsTypeToBeDeleted',
|
||||
COLUMNS: {
|
||||
ID: 'ID',
|
||||
MATERIAL_NAME: 'MaterialName',
|
||||
MANAGER_NAME: 'ManagerName'
|
||||
}
|
||||
} as const
|
||||
|
||||
/**
|
||||
* MaterialsTypeToBeDeleted DAO Class
|
||||
*/
|
||||
export class MaterialsTypeToBeDeletedDAO {
|
||||
private dbService: IDatabaseService | null = null
|
||||
|
||||
/**
|
||||
* Get the appropriate table name based on database type
|
||||
*/
|
||||
private getTableName(): string {
|
||||
const isSqlServer = this.dbService?.type === 'sqlserver'
|
||||
return isSqlServer
|
||||
? MATERIALS_TYPE_TO_BE_DELETED_CONFIG.TABLE_NAME_SQLSERVER
|
||||
: MATERIALS_TYPE_TO_BE_DELETED_CONFIG.TABLE_NAME_MYSQL
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database service instance using DatabaseFactory
|
||||
*/
|
||||
private async getDatabaseService(): Promise<IDatabaseService> {
|
||||
if (this.dbService && this.dbService.isConnected()) {
|
||||
return this.dbService
|
||||
}
|
||||
|
||||
this.dbService = await create()
|
||||
return this.dbService
|
||||
}
|
||||
|
||||
// ==================== READ ====================
|
||||
|
||||
/**
|
||||
* Get all material type records
|
||||
* @returns List of all material type records
|
||||
*/
|
||||
async getAllMaterials(): Promise<MaterialTypeRecord[]> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
|
||||
const sqlString = `
|
||||
SELECT ID, MaterialName, ManagerName
|
||||
FROM ${tableName}
|
||||
WHERE MaterialName IS NOT NULL
|
||||
ORDER BY ManagerName, MaterialName
|
||||
`
|
||||
|
||||
const result = await dbService.query(sqlString)
|
||||
return result.rows.map((row) => ({
|
||||
id: row.ID as number,
|
||||
materialName: row.MaterialName as string,
|
||||
managerName: row.ManagerName as string
|
||||
}))
|
||||
} catch (error) {
|
||||
log.error('Get all materials error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all materials for a specific manager
|
||||
* @param managerName - Manager name
|
||||
* @returns List of materials for the manager
|
||||
*/
|
||||
async getMaterialsByManager(managerName: string): Promise<MaterialTypeRecord[]> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
const isSqlServer = dbService.type === 'sqlserver'
|
||||
|
||||
const placeholder = isSqlServer ? '@p0' : '?'
|
||||
const sqlString = `
|
||||
SELECT ID, MaterialName, ManagerName
|
||||
FROM ${tableName}
|
||||
WHERE ManagerName = ${placeholder} AND MaterialName IS NOT NULL
|
||||
ORDER BY MaterialName
|
||||
`
|
||||
|
||||
const result = await dbService.query(sqlString, [managerName])
|
||||
return result.rows.map((row) => ({
|
||||
id: row.ID as number,
|
||||
materialName: row.MaterialName as string,
|
||||
managerName: row.ManagerName as string
|
||||
}))
|
||||
} catch (error) {
|
||||
log.error('Get materials by manager error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of unique manager names
|
||||
* @returns List of unique manager names
|
||||
*/
|
||||
async getManagers(): Promise<string[]> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
|
||||
const sqlString = `
|
||||
SELECT DISTINCT ManagerName
|
||||
FROM ${tableName}
|
||||
WHERE ManagerName IS NOT NULL
|
||||
ORDER BY ManagerName
|
||||
`
|
||||
|
||||
const result = await dbService.query(sqlString)
|
||||
return result.rows.map((row) => row.ManagerName as string).filter(Boolean)
|
||||
} catch (error) {
|
||||
log.error('Get managers error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== UPSERT ====================
|
||||
|
||||
/**
|
||||
* Insert or update a material type record
|
||||
* @param materialName - Material name (type keyword)
|
||||
* @param managerName - Manager name
|
||||
* @returns True if successful
|
||||
*/
|
||||
async upsertMaterial(materialName: string, managerName: string): Promise<boolean> {
|
||||
if (!materialName || !materialName.trim()) {
|
||||
log.error('MaterialName cannot be empty')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
const name = materialName.trim()
|
||||
const manager = managerName?.trim() || null
|
||||
const isSqlServer = dbService.type === 'sqlserver'
|
||||
|
||||
if (isSqlServer) {
|
||||
// SQL Server MERGE statement
|
||||
const sqlString = `
|
||||
MERGE ${tableName} AS target
|
||||
USING (VALUES (@p0, @p1)) AS source (MaterialName, ManagerName)
|
||||
ON target.MaterialName = source.MaterialName
|
||||
WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName
|
||||
WHEN NOT MATCHED THEN INSERT (MaterialName, ManagerName) VALUES (source.MaterialName, source.ManagerName);
|
||||
`
|
||||
|
||||
await dbService.query(sqlString, [name, manager])
|
||||
} else {
|
||||
// MySQL ON DUPLICATE KEY UPDATE
|
||||
const sqlString = `
|
||||
INSERT INTO ${tableName} (MaterialName, ManagerName)
|
||||
VALUES (?, ?)
|
||||
ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName)
|
||||
`
|
||||
|
||||
await dbService.query(sqlString, [name, manager])
|
||||
}
|
||||
|
||||
return true
|
||||
} catch (error) {
|
||||
log.error('Upsert material error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DELETE ====================
|
||||
|
||||
/**
|
||||
* Delete a specific material type record
|
||||
* @param materialName - Material name
|
||||
* @param managerName - Manager name (optional, for verification)
|
||||
* @returns True if successful
|
||||
*/
|
||||
async deleteMaterial(materialName: string, managerName?: string): Promise<boolean> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
const name = materialName.trim()
|
||||
const isSqlServer = dbService.type === 'sqlserver'
|
||||
|
||||
let sqlString: string
|
||||
let params: (string | null)[]
|
||||
|
||||
if (managerName) {
|
||||
const placeholder1 = isSqlServer ? '@p0' : '?'
|
||||
const placeholder2 = isSqlServer ? '@p1' : '?'
|
||||
sqlString = `
|
||||
DELETE FROM ${tableName}
|
||||
WHERE MaterialName = ${placeholder1} AND ManagerName = ${placeholder2}
|
||||
`
|
||||
params = [name, managerName.trim()]
|
||||
} else {
|
||||
const placeholder = isSqlServer ? '@p0' : '?'
|
||||
sqlString = `
|
||||
DELETE FROM ${tableName}
|
||||
WHERE MaterialName = ${placeholder}
|
||||
`
|
||||
params = [name]
|
||||
}
|
||||
|
||||
const result = await dbService.query(sqlString, params)
|
||||
return result.rowCount > 0
|
||||
} catch (error) {
|
||||
log.error('Delete material error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== UPDATE ====================
|
||||
|
||||
/**
|
||||
* Update a material type record (change name and/or manager)
|
||||
* @param oldName - Current material name
|
||||
* @param oldManager - Current manager name
|
||||
* @param newName - New material name
|
||||
* @param newManager - New manager name
|
||||
* @returns True if successful
|
||||
*/
|
||||
async updateMaterial(
|
||||
oldName: string,
|
||||
oldManager: string,
|
||||
newName: string,
|
||||
newManager: string
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
const isSqlServer = dbService.type === 'sqlserver'
|
||||
|
||||
if (isSqlServer) {
|
||||
const sqlString = `
|
||||
UPDATE ${tableName}
|
||||
SET MaterialName = @p0, ManagerName = @p1
|
||||
WHERE MaterialName = @p2 AND ManagerName = @p3
|
||||
`
|
||||
const result = await dbService.query(sqlString, [
|
||||
newName.trim(),
|
||||
newManager.trim(),
|
||||
oldName.trim(),
|
||||
oldManager.trim()
|
||||
])
|
||||
return result.rowCount > 0
|
||||
} else {
|
||||
const sqlString = `
|
||||
UPDATE ${tableName}
|
||||
SET MaterialName = ?, ManagerName = ?
|
||||
WHERE MaterialName = ? AND ManagerName = ?
|
||||
`
|
||||
const result = await dbService.query(sqlString, [
|
||||
newName.trim(),
|
||||
newManager.trim(),
|
||||
oldName.trim(),
|
||||
oldManager.trim()
|
||||
])
|
||||
return result.rowCount > 0
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Update material error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== BATCH OPERATIONS ====================
|
||||
|
||||
/**
|
||||
* Process batch changes (insert, update, delete)
|
||||
* @param request - Batch request with toInsert, toUpdate, toDelete arrays
|
||||
* @returns Statistics object
|
||||
*/
|
||||
async upsertBatch(
|
||||
request: MaterialTypeBatchRequest
|
||||
): Promise<{ total: number; success: number; failed: number }> {
|
||||
const stats = { total: 0, success: 0, failed: 0 }
|
||||
|
||||
try {
|
||||
// Process inserts
|
||||
for (const record of request.toInsert) {
|
||||
stats.total++
|
||||
const success = await this.upsertMaterial(record.materialName, record.managerName)
|
||||
if (success) stats.success++
|
||||
else stats.failed++
|
||||
}
|
||||
|
||||
// Process updates
|
||||
for (const update of request.toUpdate) {
|
||||
stats.total++
|
||||
const success = await this.updateMaterial(
|
||||
update.old.materialName,
|
||||
update.old.managerName,
|
||||
update.new.materialName,
|
||||
update.new.managerName
|
||||
)
|
||||
if (success) stats.success++
|
||||
else stats.failed++
|
||||
}
|
||||
|
||||
// Process deletes
|
||||
for (const record of request.toDelete) {
|
||||
stats.total++
|
||||
const success = await this.deleteMaterial(record.materialName, record.managerName)
|
||||
if (success) stats.success++
|
||||
else stats.failed++
|
||||
}
|
||||
|
||||
return stats
|
||||
} catch (error) {
|
||||
log.error('Batch upsert error', {
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
})
|
||||
return stats
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from database
|
||||
*/
|
||||
async disconnect(): Promise<void> {
|
||||
if (this.dbService) {
|
||||
await this.dbService.disconnect()
|
||||
this.dbService = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,3 +103,21 @@ export interface MaterialRecordSummary {
|
||||
managerName: string
|
||||
isMarked: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Material type record for MaterialsTypeToBeDeleted table
|
||||
*/
|
||||
export interface MaterialTypeRecord {
|
||||
id?: number
|
||||
materialName: string
|
||||
managerName: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Material type batch request for batch operations
|
||||
*/
|
||||
export interface MaterialTypeBatchRequest {
|
||||
toInsert: MaterialTypeRecord[]
|
||||
toUpdate: { old: MaterialTypeRecord; new: MaterialTypeRecord }[]
|
||||
toDelete: MaterialTypeRecord[]
|
||||
}
|
||||
|
||||
51
src/preload/index.d.ts
vendored
51
src/preload/index.d.ts
vendored
@@ -8,7 +8,12 @@ import type {
|
||||
UserSelectionResponse,
|
||||
CurrentUserResponse
|
||||
} from '../main/ipc/auth-handler'
|
||||
import type { ValidationRequest, ValidationResponse } from '../main/types/validation.types'
|
||||
import type {
|
||||
ValidationRequest,
|
||||
ValidationResponse,
|
||||
MaterialTypeRecord,
|
||||
MaterialTypeBatchRequest
|
||||
} from '../main/types/validation.types'
|
||||
import type {
|
||||
SettingsData,
|
||||
UserType,
|
||||
@@ -178,6 +183,49 @@ export interface SettingsAPI {
|
||||
testDbConnection: () => Promise<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
|
||||
}>
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: {
|
||||
@@ -205,6 +253,7 @@ declare global {
|
||||
validation: ValidationAPI
|
||||
materials: MaterialsAPI
|
||||
settings: SettingsAPI
|
||||
materialType: MaterialTypeAPI
|
||||
}
|
||||
api: unknown
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ import type { CleanerInput } from '../main/types/cleaner.types'
|
||||
import type { ResolverInput } from '../main/ipc/resolver-handler'
|
||||
import type { LoginRequest } from '../main/ipc/auth-handler'
|
||||
import type { UserInfo } from '../main/types/user.types'
|
||||
import type { ValidationRequest } from '../main/types/validation.types'
|
||||
import type {
|
||||
ValidationRequest,
|
||||
MaterialTypeRecord,
|
||||
MaterialTypeBatchRequest
|
||||
} from '../main/types/validation.types'
|
||||
import type { SettingsData } from '../main/types/settings.types'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@@ -94,6 +98,20 @@ const api = {
|
||||
resetDefaults: () => ipcRenderer.invoke('settings:resetDefaults'),
|
||||
testErpConnection: () => ipcRenderer.invoke('settings:testErpConnection'),
|
||||
testDbConnection: () => ipcRenderer.invoke('settings:testDbConnection')
|
||||
},
|
||||
|
||||
// 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)
|
||||
}
|
||||
} as const
|
||||
|
||||
|
||||
523
src/renderer/src/components/MaterialTypeManagementDialog.tsx
Normal file
523
src/renderer/src/components/MaterialTypeManagementDialog.tsx
Normal file
@@ -0,0 +1,523 @@
|
||||
/**
|
||||
* Material Type Management Dialog
|
||||
*
|
||||
* Provides a dialog for managing material type keywords used to identify
|
||||
* materials for deletion. Admin users can see all records and filter by manager.
|
||||
* Regular users can only see and edit their own records.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { Plus, Trash2, Save, RotateCcw, Users } from 'lucide-react'
|
||||
import { Modal } from './ui/Modal'
|
||||
|
||||
interface MaterialTypeRecord {
|
||||
id?: number
|
||||
materialName: string
|
||||
managerName: string
|
||||
}
|
||||
|
||||
interface RowState {
|
||||
record: MaterialTypeRecord
|
||||
state: 'original' | 'new' | 'modified' | 'deleted'
|
||||
originalRecord?: MaterialTypeRecord
|
||||
}
|
||||
|
||||
interface MaterialTypeManagementDialogProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
isAdmin: boolean
|
||||
currentUsername: string
|
||||
}
|
||||
|
||||
export const MaterialTypeManagementDialog: React.FC<MaterialTypeManagementDialogProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
isAdmin,
|
||||
currentUsername
|
||||
}) => {
|
||||
const [rows, setRows] = useState<RowState[]>([])
|
||||
const [managers, setManagers] = useState<string[]>([])
|
||||
const [selectedManagers, setSelectedManagers] = useState<Set<string>>(new Set())
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [editingCell, setEditingCell] = useState<{ rowIndex: number; field: string } | null>(null)
|
||||
const [editValue, setEditValue] = useState('')
|
||||
const [selectedRowIndex, setSelectedRowIndex] = useState<number | null>(null)
|
||||
|
||||
const tableRef = useRef<HTMLTableElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Calculate pending changes count
|
||||
const pendingCount = rows.filter(
|
||||
(r) => r.state === 'new' || r.state === 'modified' || r.state === 'deleted'
|
||||
).length
|
||||
|
||||
// Load data when dialog opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadData()
|
||||
}
|
||||
}, [isOpen, isAdmin, currentUsername])
|
||||
|
||||
// Focus input when editing starts
|
||||
useEffect(() => {
|
||||
if (editingCell && inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
inputRef.current.select()
|
||||
}
|
||||
}, [editingCell])
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// Load managers list
|
||||
const managersResult = await window.electron.materialType.getManagers()
|
||||
if (managersResult.success && managersResult.data) {
|
||||
setManagers(managersResult.data)
|
||||
if (isAdmin) {
|
||||
setSelectedManagers(new Set(managersResult.data))
|
||||
}
|
||||
}
|
||||
|
||||
// Load records
|
||||
let records: MaterialTypeRecord[] = []
|
||||
if (isAdmin) {
|
||||
const result = await window.electron.materialType.getAll()
|
||||
if (result.success && result.data) {
|
||||
records = result.data
|
||||
}
|
||||
} else {
|
||||
const result = await window.electron.materialType.getByManager(currentUsername)
|
||||
if (result.success && result.data) {
|
||||
records = result.data
|
||||
}
|
||||
}
|
||||
|
||||
setRows(
|
||||
records.map((record) => ({
|
||||
record,
|
||||
state: 'original' as const,
|
||||
originalRecord: { ...record }
|
||||
}))
|
||||
)
|
||||
setSelectedRowIndex(null)
|
||||
} catch (error) {
|
||||
console.error('Failed to load material types:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter rows by selected managers (admin only)
|
||||
const filteredRows = React.useMemo(() => {
|
||||
if (!isAdmin) return rows
|
||||
if (selectedManagers.size === 0) return rows
|
||||
return rows.filter((row) => selectedManagers.has(row.record.managerName) || row.state === 'new')
|
||||
}, [rows, isAdmin, selectedManagers])
|
||||
|
||||
// Handle keyboard events
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
if (editingCell) {
|
||||
if (event.key === 'Enter') {
|
||||
saveEdit()
|
||||
} else if (event.key === 'Escape') {
|
||||
cancelEdit()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Insert') {
|
||||
event.preventDefault()
|
||||
insertNewRow()
|
||||
} else if (event.key === 'Delete' && selectedRowIndex !== null) {
|
||||
event.preventDefault()
|
||||
deleteRow(selectedRowIndex)
|
||||
}
|
||||
},
|
||||
[editingCell, selectedRowIndex]
|
||||
)
|
||||
|
||||
// Insert new row
|
||||
const insertNewRow = () => {
|
||||
const newRow: RowState = {
|
||||
record: {
|
||||
materialName: '',
|
||||
managerName: isAdmin ? '' : currentUsername
|
||||
},
|
||||
state: 'new'
|
||||
}
|
||||
setRows((prev) => [...prev, newRow])
|
||||
// Start editing the material name cell
|
||||
const newIndex = rows.length
|
||||
setTimeout(() => {
|
||||
setEditingCell({ rowIndex: newIndex, field: 'materialName' })
|
||||
setEditValue('')
|
||||
}, 0)
|
||||
}
|
||||
|
||||
// Delete row
|
||||
const deleteRow = (index: number) => {
|
||||
setRows((prev) => {
|
||||
const newRows = [...prev]
|
||||
const row = newRows[index]
|
||||
if (row.state === 'new') {
|
||||
// Remove new rows directly
|
||||
newRows.splice(index, 1)
|
||||
} else {
|
||||
// Mark existing rows as deleted
|
||||
newRows[index] = { ...row, state: 'deleted' }
|
||||
}
|
||||
return newRows
|
||||
})
|
||||
setSelectedRowIndex(null)
|
||||
}
|
||||
|
||||
// Start editing a cell
|
||||
const startEdit = (rowIndex: number, field: string) => {
|
||||
const row = rows[rowIndex]
|
||||
if (row.state === 'deleted') return
|
||||
|
||||
setEditingCell({ rowIndex, field })
|
||||
setEditValue(row.record[field as keyof MaterialTypeRecord] as string)
|
||||
}
|
||||
|
||||
// Save edit
|
||||
const saveEdit = () => {
|
||||
if (!editingCell) return
|
||||
|
||||
const { rowIndex, field } = editingCell
|
||||
setRows((prev) => {
|
||||
const newRows = [...prev]
|
||||
const row = newRows[rowIndex]
|
||||
const newValue = editValue.trim()
|
||||
|
||||
// Update the record
|
||||
newRows[rowIndex] = {
|
||||
...row,
|
||||
record: {
|
||||
...row.record,
|
||||
[field]: newValue
|
||||
},
|
||||
state: row.state === 'new' ? 'new' : 'modified'
|
||||
}
|
||||
return newRows
|
||||
})
|
||||
|
||||
setEditingCell(null)
|
||||
setEditValue('')
|
||||
}
|
||||
|
||||
// Cancel edit
|
||||
const cancelEdit = () => {
|
||||
setEditingCell(null)
|
||||
setEditValue('')
|
||||
}
|
||||
|
||||
// Save all changes
|
||||
const handleSave = async () => {
|
||||
const toInsert: MaterialTypeRecord[] = []
|
||||
const toUpdate: { old: MaterialTypeRecord; new: MaterialTypeRecord }[] = []
|
||||
const toDelete: MaterialTypeRecord[] = []
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.state === 'new' && row.record.materialName.trim()) {
|
||||
toInsert.push(row.record)
|
||||
} else if (row.state === 'modified' && row.originalRecord) {
|
||||
toUpdate.push({ old: row.originalRecord, new: row.record })
|
||||
} else if (row.state === 'deleted' && row.originalRecord) {
|
||||
toDelete.push(row.originalRecord)
|
||||
}
|
||||
}
|
||||
|
||||
if (toInsert.length === 0 && toUpdate.length === 0 && toDelete.length === 0) {
|
||||
alert('没有需要保存的更改')
|
||||
return
|
||||
}
|
||||
|
||||
const confirmParts: string[] = []
|
||||
if (toInsert.length > 0) confirmParts.push(`新增 ${toInsert.length} 条记录`)
|
||||
if (toUpdate.length > 0) confirmParts.push(`更新 ${toUpdate.length} 条记录`)
|
||||
if (toDelete.length > 0) confirmParts.push(`删除 ${toDelete.length} 条记录`)
|
||||
|
||||
if (!window.confirm(`确认以下操作?\n\n${confirmParts.join('\n')}`)) return
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
const result = await window.electron.materialType.upsertBatch({
|
||||
toInsert,
|
||||
toUpdate,
|
||||
toDelete
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
alert(
|
||||
`保存完成!\n成功:${result.stats?.success || 0} 条\n失败:${result.stats?.failed || 0} 条`
|
||||
)
|
||||
await loadData()
|
||||
} else {
|
||||
alert(`保存失败:${result.error || '未知错误'}`)
|
||||
}
|
||||
} catch (error) {
|
||||
alert(`保存失败:${error instanceof Error ? error.message : '未知错误'}`)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Reset changes
|
||||
const handleReset = () => {
|
||||
if (pendingCount === 0) return
|
||||
if (!window.confirm('确定要放弃所有未保存的更改吗?')) return
|
||||
loadData()
|
||||
}
|
||||
|
||||
// Handle close with unsaved changes warning
|
||||
const handleClose = () => {
|
||||
if (pendingCount > 0) {
|
||||
if (!window.confirm('有未保存的更改,确定要关闭吗?')) return
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
||||
// Get row background color
|
||||
const getRowStyle = (row: RowState): React.CSSProperties => {
|
||||
if (row.state === 'deleted') {
|
||||
return { backgroundColor: '#fee2e2', textDecoration: 'line-through', opacity: 0.6 }
|
||||
}
|
||||
if (row.state === 'new') {
|
||||
return { backgroundColor: '#dcfce7' }
|
||||
}
|
||||
if (row.state === 'modified') {
|
||||
return { backgroundColor: '#fef9c3' }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={handleClose} title="物料类型管理" size="2xl">
|
||||
<div onKeyDown={handleKeyDown}>
|
||||
{/* Manager filter (admin only) */}
|
||||
{isAdmin && (
|
||||
<div className="mb-4 p-3 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-slate-700">
|
||||
<Users size={16} />
|
||||
按负责人筛选
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedManagers(new Set(managers))}
|
||||
className="text-xs text-blue-600 hover:underline"
|
||||
>
|
||||
全选
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedManagers(new Set())}
|
||||
className="text-xs text-slate-500 hover:underline"
|
||||
>
|
||||
取消全选
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{managers.map((manager) => (
|
||||
<label
|
||||
key={manager}
|
||||
className="flex items-center gap-1.5 text-xs text-slate-600 cursor-pointer hover:bg-white px-2 py-1 rounded"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
className="rounded text-blue-600"
|
||||
checked={selectedManagers.has(manager)}
|
||||
onChange={(e) => {
|
||||
setSelectedManagers((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (e.target.checked) newSet.add(manager)
|
||||
else newSet.delete(manager)
|
||||
return newSet
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{manager}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={insertNewRow}
|
||||
className="flex items-center gap-1.5 text-xs bg-green-50 border border-green-200 text-green-700 px-3 py-1.5 rounded hover:bg-green-100"
|
||||
>
|
||||
<Plus size={14} /> 新增 (Insert)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => selectedRowIndex !== null && deleteRow(selectedRowIndex)}
|
||||
disabled={selectedRowIndex === null}
|
||||
className="flex items-center gap-1.5 text-xs bg-red-50 border border-red-200 text-red-700 px-3 py-1.5 rounded hover:bg-red-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Trash2 size={14} /> 删除 (Delete)
|
||||
</button>
|
||||
<button
|
||||
onClick={handleReset}
|
||||
disabled={pendingCount === 0}
|
||||
className="flex items-center gap-1.5 text-xs bg-slate-50 border border-slate-200 text-slate-700 px-3 py-1.5 rounded hover:bg-slate-100 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<RotateCcw size={14} /> 重置
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{pendingCount > 0 && (
|
||||
<span className="text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded">
|
||||
{pendingCount} 项待保存
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving || pendingCount === 0}
|
||||
className="flex items-center gap-1.5 text-xs bg-blue-500 text-white px-3 py-1.5 rounded hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Save size={14} /> {saving ? '保存中...' : '保存'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="border border-slate-200 rounded-lg overflow-hidden max-h-[400px] overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12 text-slate-500">加载中...</div>
|
||||
) : (
|
||||
<table ref={tableRef} className="w-full text-sm">
|
||||
<thead className="bg-slate-100 sticky top-0">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left font-medium text-slate-700 w-64">
|
||||
物料名称关键词
|
||||
</th>
|
||||
<th className="px-4 py-2 text-left font-medium text-slate-700">负责人</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{filteredRows.filter((r) => r.state !== 'deleted').length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={2} className="px-4 py-8 text-center text-slate-400">
|
||||
暂无数据,点击"新增"按钮添加物料类型关键词
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredRows
|
||||
.filter((r) => r.state !== 'deleted')
|
||||
.map((row, index) => {
|
||||
const originalIndex = rows.indexOf(row)
|
||||
const isSelected = selectedRowIndex === originalIndex
|
||||
const isEditingMaterial =
|
||||
editingCell?.rowIndex === originalIndex &&
|
||||
editingCell?.field === 'materialName'
|
||||
const isEditingManager =
|
||||
editingCell?.rowIndex === originalIndex &&
|
||||
editingCell?.field === 'managerName'
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
style={getRowStyle(row)}
|
||||
className={`${isSelected ? 'ring-2 ring-blue-300 ring-inset' : ''} hover:bg-slate-50 cursor-pointer`}
|
||||
onClick={() => setSelectedRowIndex(originalIndex)}
|
||||
>
|
||||
<td className="px-4 py-2 border-r border-slate-100">
|
||||
{isEditingMaterial ? (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onBlur={saveEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveEdit()
|
||||
if (e.key === 'Escape') cancelEdit()
|
||||
}}
|
||||
className="w-full px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="min-h-[24px] cursor-text"
|
||||
onDoubleClick={() => startEdit(originalIndex, 'materialName')}
|
||||
>
|
||||
{row.record.materialName || (
|
||||
<span className="text-slate-400 italic">双击编辑</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
{isEditingManager ? (
|
||||
isAdmin ? (
|
||||
<select
|
||||
ref={inputRef as any}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onBlur={saveEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveEdit()
|
||||
if (e.key === 'Escape') cancelEdit()
|
||||
}}
|
||||
className="w-full px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">选择负责人</option>
|
||||
{managers.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onBlur={saveEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveEdit()
|
||||
if (e.key === 'Escape') cancelEdit()
|
||||
}}
|
||||
className="w-full px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
className="min-h-[24px] cursor-text"
|
||||
onDoubleClick={() => startEdit(originalIndex, 'managerName')}
|
||||
>
|
||||
{row.record.managerName || (
|
||||
<span className="text-slate-400 italic">双击编辑</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer info */}
|
||||
<div className="mt-3 text-xs text-slate-500 flex justify-between">
|
||||
<span>
|
||||
双击单元格编辑 | Insert 新增 | Delete 删除
|
||||
{isAdmin && ' | 绿色=新增 | 黄色=已修改'}
|
||||
</span>
|
||||
<span>共 {filteredRows.filter((r) => r.state !== 'deleted').length} 条记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default MaterialTypeManagementDialog
|
||||
@@ -12,7 +12,7 @@ interface ModalProps {
|
||||
onClose: () => void
|
||||
title?: string
|
||||
children: React.ReactNode
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl'
|
||||
showCloseButton?: boolean
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ const sizeStyles: Record<string, string> = {
|
||||
sm: 'max-w-sm',
|
||||
md: 'max-w-md',
|
||||
lg: 'max-w-lg',
|
||||
xl: 'max-w-xl'
|
||||
xl: 'max-w-xl',
|
||||
'2xl': 'max-w-2xl',
|
||||
'3xl': 'max-w-3xl'
|
||||
}
|
||||
|
||||
export function Modal({
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Settings2,
|
||||
FileSpreadsheet
|
||||
} from 'lucide-react'
|
||||
import MaterialTypeManagementDialog from '../components/MaterialTypeManagementDialog'
|
||||
|
||||
/**
|
||||
* Material validation result interface
|
||||
@@ -60,6 +61,9 @@ const CleanerPage: React.FC = () => {
|
||||
const [sharedProductionIdsCount, setSharedProductionIdsCount] = useState(0)
|
||||
console.log(sharedProductionIdsCount)
|
||||
|
||||
// Material type management dialog state
|
||||
const [isTypeDialogOpen, setIsTypeDialogOpen] = useState(false)
|
||||
|
||||
// Check admin status and get shared Production IDs on mount
|
||||
React.useEffect(() => {
|
||||
const initializePage = async () => {
|
||||
@@ -470,7 +474,10 @@ const CleanerPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button className="text-xs bg-white border border-slate-300 text-slate-700 px-3 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => setIsTypeDialogOpen(true)}
|
||||
className="text-xs bg-white border border-slate-300 text-slate-700 px-3 py-1.5 rounded shadow-sm hover:bg-slate-50 flex items-center gap-1.5"
|
||||
>
|
||||
<Settings2 size={14} /> 类型管理
|
||||
</button>
|
||||
<button className="text-xs bg-blue-50 border border-blue-200 text-blue-700 px-3 py-1.5 rounded shadow-sm hover:bg-blue-100 flex items-center gap-1.5 font-medium">
|
||||
@@ -606,6 +613,14 @@ const CleanerPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Material Type Management Dialog */}
|
||||
<MaterialTypeManagementDialog
|
||||
isOpen={isTypeDialogOpen}
|
||||
onClose={() => setIsTypeDialogOpen(false)}
|
||||
isAdmin={isAdmin}
|
||||
currentUsername={currentUsername}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -201,6 +201,14 @@ const ExtractorPage: React.FC = () => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{result.mergedFile && (
|
||||
<div className="bg-slate-50 p-4 rounded-lg border border-slate-100">
|
||||
<span className="text-slate-500 text-sm block mb-1">合并文件路径</span>
|
||||
<span className="text-sm font-mono text-slate-700 select-all break-all">
|
||||
{result.mergedFile}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user