feat: allow editing manager field in cleaner table
- Add inline editing for manager column (Admin users can double-click to select from dropdown) - Auto-assign current user as manager when User checks a material - Defer database writes until 'Confirm Delete' button is clicked - Add updateManager IPC handler and DAO method - Update preload API with updateManager method
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
/**
|
||||
|
||||
9
src/preload/index.d.ts
vendored
9
src/preload/index.d.ts
vendored
@@ -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 }>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<HTMLInputElement | HTMLSelectElement>(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,
|
||||
|
||||
@@ -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 = () => {
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
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 (
|
||||
<tr
|
||||
@@ -293,13 +303,23 @@ const CleanerPage: React.FC = () => {
|
||||
<td className="px-4 py-3 text-center truncate">
|
||||
{isChecked ? (
|
||||
<CheckSquare
|
||||
onClick={() => handleCheckboxToggle(result.materialCode)}
|
||||
onClick={() => {
|
||||
handleCheckboxToggle(result.materialCode)
|
||||
if (!isAdmin) {
|
||||
handleAssignManagerOnSelect(result.materialCode)
|
||||
}
|
||||
}}
|
||||
size={16}
|
||||
className="text-blue-600 inline cursor-pointer"
|
||||
/>
|
||||
) : (
|
||||
<Square
|
||||
onClick={() => 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 = () => {
|
||||
</td>
|
||||
<td
|
||||
className={`px-4 py-3 truncate ${managerCellClass}`}
|
||||
title={result.managerName || '空(待分配)'}
|
||||
title={result.managerName || '空 (待分配)'}
|
||||
>
|
||||
{result.managerName || '空(待分配)'}
|
||||
{isAdmin && isEditingManager ? (
|
||||
<select
|
||||
ref={inputRef as React.Ref<HTMLSelectElement>}
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onBlur={saveEdit}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') saveEdit()
|
||||
if (e.key === 'Escape') cancelEdit()
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="w-full px-2 py-1 border border-blue-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 text-xs"
|
||||
>
|
||||
<option value="">选择负责人</option>
|
||||
{managers.map((m) => (
|
||||
<option key={m} value={m}>
|
||||
{m}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : isAdmin ? (
|
||||
<div
|
||||
className="min-h-[24px] cursor-text"
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation()
|
||||
startEdit(rowIndex, 'managerName')
|
||||
}}
|
||||
>
|
||||
{result.managerName || (
|
||||
<span className="text-slate-400 italic">双击编辑</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
result.managerName || '空 (待分配)'
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user