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
|
* Get materials by manager
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -216,6 +216,52 @@ export class MaterialsToBeDeletedDAO {
|
|||||||
return stats
|
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 ====================
|
// ==================== READ ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
9
src/preload/index.d.ts
vendored
9
src/preload/index.d.ts
vendored
@@ -150,6 +150,15 @@ export interface MaterialsAPI {
|
|||||||
* Get statistics
|
* Get statistics
|
||||||
*/
|
*/
|
||||||
getStatistics: () => Promise<{ stats: unknown }>
|
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) =>
|
getByManager: (managerName: string) =>
|
||||||
ipcRenderer.invoke('materials:getByManager', managerName),
|
ipcRenderer.invoke('materials:getByManager', managerName),
|
||||||
getAll: () => ipcRenderer.invoke('materials:getAll'),
|
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
|
// Settings service
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||||
|
|
||||||
export interface ValidationResult {
|
export interface ValidationResult {
|
||||||
materialName: string
|
materialName: string
|
||||||
@@ -60,6 +60,11 @@ export function useCleaner() {
|
|||||||
})
|
})
|
||||||
const [showSettingsMenu, setShowSettingsMenu] = useState(false)
|
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
|
// Check admin status and get shared Production IDs on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializePage = async () => {
|
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 handleConfirmDeletion = async () => {
|
||||||
const resultsToProcess = isAdmin ? validationResults : filteredResults
|
const resultsToProcess = isAdmin ? validationResults : filteredResults
|
||||||
|
|
||||||
@@ -334,6 +389,14 @@ export function useCleaner() {
|
|||||||
isReportDialogOpen,
|
isReportDialogOpen,
|
||||||
setIsReportDialogOpen,
|
setIsReportDialogOpen,
|
||||||
reportData,
|
reportData,
|
||||||
|
editingCell,
|
||||||
|
editValue,
|
||||||
|
setEditValue,
|
||||||
|
inputRef,
|
||||||
|
startEdit,
|
||||||
|
saveEdit,
|
||||||
|
cancelEdit,
|
||||||
|
handleAssignManagerOnSelect,
|
||||||
handleValidation,
|
handleValidation,
|
||||||
handleCheckboxToggle,
|
handleCheckboxToggle,
|
||||||
handleConfirmDeletion,
|
handleConfirmDeletion,
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ const CleanerPage: React.FC = () => {
|
|||||||
isReportDialogOpen,
|
isReportDialogOpen,
|
||||||
setIsReportDialogOpen,
|
setIsReportDialogOpen,
|
||||||
reportData,
|
reportData,
|
||||||
|
editingCell,
|
||||||
|
editValue,
|
||||||
|
setEditValue,
|
||||||
|
inputRef,
|
||||||
|
startEdit,
|
||||||
|
saveEdit,
|
||||||
|
cancelEdit,
|
||||||
|
handleAssignManagerOnSelect,
|
||||||
handleValidation,
|
handleValidation,
|
||||||
handleCheckboxToggle,
|
handleCheckboxToggle,
|
||||||
handleConfirmDeletion,
|
handleConfirmDeletion,
|
||||||
@@ -277,13 +285,15 @@ const CleanerPage: React.FC = () => {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
) : (
|
) : (
|
||||||
filteredResults.map((result) => {
|
filteredResults.map((result, rowIndex) => {
|
||||||
const isChecked = selectedItems.has(result.materialCode)
|
const isChecked = selectedItems.has(result.materialCode)
|
||||||
const trClass = isChecked ? 'bg-blue-50/30' : 'hover:bg-slate-50'
|
const trClass = isChecked ? 'bg-blue-50/30' : 'hover:bg-slate-50'
|
||||||
const noManager = !result.managerName?.trim()
|
const noManager = !result.managerName?.trim()
|
||||||
const managerCellClass = noManager
|
const managerCellClass = noManager
|
||||||
? 'text-amber-500 text-xs italic'
|
? 'text-amber-500 text-xs italic'
|
||||||
: 'text-slate-700'
|
: 'text-slate-700'
|
||||||
|
const isEditingManager =
|
||||||
|
editingCell?.rowIndex === rowIndex && editingCell?.field === 'managerName'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
@@ -293,13 +303,23 @@ const CleanerPage: React.FC = () => {
|
|||||||
<td className="px-4 py-3 text-center truncate">
|
<td className="px-4 py-3 text-center truncate">
|
||||||
{isChecked ? (
|
{isChecked ? (
|
||||||
<CheckSquare
|
<CheckSquare
|
||||||
onClick={() => handleCheckboxToggle(result.materialCode)}
|
onClick={() => {
|
||||||
|
handleCheckboxToggle(result.materialCode)
|
||||||
|
if (!isAdmin) {
|
||||||
|
handleAssignManagerOnSelect(result.materialCode)
|
||||||
|
}
|
||||||
|
}}
|
||||||
size={16}
|
size={16}
|
||||||
className="text-blue-600 inline cursor-pointer"
|
className="text-blue-600 inline cursor-pointer"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Square
|
<Square
|
||||||
onClick={() => handleCheckboxToggle(result.materialCode)}
|
onClick={() => {
|
||||||
|
handleCheckboxToggle(result.materialCode)
|
||||||
|
if (!isAdmin) {
|
||||||
|
handleAssignManagerOnSelect(result.materialCode)
|
||||||
|
}
|
||||||
|
}}
|
||||||
size={16}
|
size={16}
|
||||||
className="text-slate-300 inline cursor-pointer"
|
className="text-slate-300 inline cursor-pointer"
|
||||||
/>
|
/>
|
||||||
@@ -331,9 +351,43 @@ const CleanerPage: React.FC = () => {
|
|||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className={`px-4 py-3 truncate ${managerCellClass}`}
|
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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user