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:
Misaka_Company
2026-03-05 13:19:12 +08:00
parent 921ca15be6
commit ba64c27457
6 changed files with 205 additions and 7 deletions

View File

@@ -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
*/

View File

@@ -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 ====================
/**

View File

@@ -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 }>
}
/**

View File

@@ -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

View File

@@ -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,

View File

@@ -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>
)