diff --git a/src/main/ipc/validation-handler.ts b/src/main/ipc/validation-handler.ts index 6db04b5..1bf84db 100644 --- a/src/main/ipc/validation-handler.ts +++ b/src/main/ipc/validation-handler.ts @@ -500,6 +500,30 @@ export function registerValidationHandlers(): void { } }) + /** + * Update manager for a single material + */ + ipcMain.handle( + 'materials:updateManager', + async ( + _event, + request: { materialCode: string; managerName: string } + ): Promise<{ success: boolean; error?: string }> => { + try { + const dao = new MaterialsToBeDeletedDAO() + return await dao.updateManager(request.materialCode, request.managerName) + } catch (error) { + log.error('Update manager error', { + error: error instanceof Error ? error.message : String(error) + }) + return { + success: false, + error: error instanceof Error ? error.message : String(error) + } + } + } + ) + /** * Get materials by manager */ diff --git a/src/main/services/database/materials-to-be-deleted-dao.ts b/src/main/services/database/materials-to-be-deleted-dao.ts index 491554e..c5825c6 100644 --- a/src/main/services/database/materials-to-be-deleted-dao.ts +++ b/src/main/services/database/materials-to-be-deleted-dao.ts @@ -216,6 +216,52 @@ export class MaterialsToBeDeletedDAO { return stats } + /** + * Update manager for a single material + * @param materialCode - Material code + * @param managerName - New manager name + * @returns Success status + */ + async updateManager( + materialCode: string, + managerName: string + ): Promise<{ success: boolean; error?: string }> { + try { + const dbService = await this.getDatabaseService() + const tableName = this.getTableName() + const isSqlServer = dbService.type === 'sqlserver' + + if (isSqlServer) { + const sqlString = ` + MERGE ${tableName} AS target + USING (VALUES (@p0, @p1)) AS source (MaterialCode, ManagerName) + ON target.MaterialCode = source.MaterialCode + WHEN MATCHED THEN UPDATE SET ManagerName = source.ManagerName + WHEN NOT MATCHED THEN INSERT (MaterialCode, ManagerName) VALUES (source.MaterialCode, source.ManagerName); + ` + await dbService.query(sqlString, [materialCode, managerName || null]) + } else { + const sqlString = ` + INSERT INTO ${tableName} (MaterialCode, ManagerName) + VALUES (?, ?) + ON DUPLICATE KEY UPDATE ManagerName = VALUES(ManagerName) + ` + await dbService.query(sqlString, [materialCode, managerName || null]) + } + + return { success: true } + } catch (error) { + log.error('Update manager error', { + materialCode, + error: error instanceof Error ? error.message : String(error) + }) + return { + success: false, + error: error instanceof Error ? error.message : String(error) + } + } + } + // ==================== READ ==================== /** diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 8e836db..af6bee8 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -150,6 +150,15 @@ export interface MaterialsAPI { * Get statistics */ getStatistics: () => Promise<{ stats: unknown }> + /** + * Update manager for a single material + * @param materialCode - Material code + * @param managerName - New manager name + */ + updateManager: ( + materialCode: string, + managerName: string + ) => Promise<{ success: boolean; error?: string }> } /** diff --git a/src/preload/index.ts b/src/preload/index.ts index 2718c08..48f8195 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -103,7 +103,9 @@ const api = { getByManager: (managerName: string) => ipcRenderer.invoke('materials:getByManager', managerName), getAll: () => ipcRenderer.invoke('materials:getAll'), - getStatistics: () => ipcRenderer.invoke('materials:getStatistics') + getStatistics: () => ipcRenderer.invoke('materials:getStatistics'), + updateManager: (materialCode: string, managerName: string) => + ipcRenderer.invoke('materials:updateManager', { materialCode, managerName }) }, // Settings service diff --git a/src/renderer/src/hooks/useCleaner.ts b/src/renderer/src/hooks/useCleaner.ts index 6748b18..341950a 100644 --- a/src/renderer/src/hooks/useCleaner.ts +++ b/src/renderer/src/hooks/useCleaner.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo } from 'react' +import { useState, useEffect, useMemo, useRef } from 'react' export interface ValidationResult { materialName: string @@ -60,6 +60,11 @@ export function useCleaner() { }) const [showSettingsMenu, setShowSettingsMenu] = useState(false) + // Inline editing state for manager field (Admin only) + const [editingCell, setEditingCell] = useState<{ rowIndex: number; field: string } | null>(null) + const [editValue, setEditValue] = useState('') + const inputRef = useRef(null) + // Check admin status and get shared Production IDs on mount useEffect(() => { const initializePage = async () => { @@ -159,6 +164,56 @@ export function useCleaner() { }) } + const startEdit = (rowIndex: number, field: string) => { + const row = filteredResults[rowIndex] + if (!row) return + setEditingCell({ rowIndex, field }) + setEditValue(row[field as keyof ValidationResult] as string) + setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus() + if (inputRef.current instanceof HTMLInputElement) { + inputRef.current.select() + } + } + }, 0) + } + + const saveEdit = () => { + if (!editingCell) return + const { rowIndex } = editingCell + const row = filteredResults[rowIndex] + if (!row) return + + const newValue = editValue.trim() + if (row.managerName === newValue) { + setEditingCell(null) + return + } + + setValidationResults((prev) => + prev.map((r) => (r.materialCode === row.materialCode ? { ...r, managerName: newValue } : r)) + ) + setEditingCell(null) + } + + const cancelEdit = () => { + setEditingCell(null) + } + + const handleAssignManagerOnSelect = (materialCode: string) => { + if (!isAdmin && currentUsername) { + const result = validationResults.find((r) => r.materialCode === materialCode) + if (result && (!result.managerName || result.managerName !== currentUsername)) { + setValidationResults((prev) => + prev.map((r) => + r.materialCode === materialCode ? { ...r, managerName: currentUsername } : r + ) + ) + } + } + } + const handleConfirmDeletion = async () => { const resultsToProcess = isAdmin ? validationResults : filteredResults @@ -334,6 +389,14 @@ export function useCleaner() { isReportDialogOpen, setIsReportDialogOpen, reportData, + editingCell, + editValue, + setEditValue, + inputRef, + startEdit, + saveEdit, + cancelEdit, + handleAssignManagerOnSelect, handleValidation, handleCheckboxToggle, handleConfirmDeletion, diff --git a/src/renderer/src/pages/CleanerPage.tsx b/src/renderer/src/pages/CleanerPage.tsx index 3304b15..6b54bf3 100644 --- a/src/renderer/src/pages/CleanerPage.tsx +++ b/src/renderer/src/pages/CleanerPage.tsx @@ -47,6 +47,14 @@ const CleanerPage: React.FC = () => { isReportDialogOpen, setIsReportDialogOpen, reportData, + editingCell, + editValue, + setEditValue, + inputRef, + startEdit, + saveEdit, + cancelEdit, + handleAssignManagerOnSelect, handleValidation, handleCheckboxToggle, handleConfirmDeletion, @@ -277,13 +285,15 @@ const CleanerPage: React.FC = () => { ) : ( - filteredResults.map((result) => { + filteredResults.map((result, rowIndex) => { const isChecked = selectedItems.has(result.materialCode) const trClass = isChecked ? 'bg-blue-50/30' : 'hover:bg-slate-50' const noManager = !result.managerName?.trim() const managerCellClass = noManager ? 'text-amber-500 text-xs italic' : 'text-slate-700' + const isEditingManager = + editingCell?.rowIndex === rowIndex && editingCell?.field === 'managerName' return ( { {isChecked ? ( handleCheckboxToggle(result.materialCode)} + onClick={() => { + handleCheckboxToggle(result.materialCode) + if (!isAdmin) { + handleAssignManagerOnSelect(result.materialCode) + } + }} size={16} className="text-blue-600 inline cursor-pointer" /> ) : ( handleCheckboxToggle(result.materialCode)} + onClick={() => { + handleCheckboxToggle(result.materialCode) + if (!isAdmin) { + handleAssignManagerOnSelect(result.materialCode) + } + }} size={16} className="text-slate-300 inline cursor-pointer" /> @@ -331,9 +351,43 @@ const CleanerPage: React.FC = () => { - {result.managerName || '空(待分配)'} + {isAdmin && isEditingManager ? ( + + ) : isAdmin ? ( +
{ + e.stopPropagation() + startEdit(rowIndex, 'managerName') + }} + > + {result.managerName || ( + 双击编辑 + )} +
+ ) : ( + result.managerName || '空 (待分配)' + )} )