feat(history): add Admin-only delete button and multi-user filter with chips
- Add Admin-only delete button in operation history modal - Replace dropdown with multi-select chip filters for Admin users - Support filtering by multiple usernames using IN clause - Fix user state propagation by passing currentUser via props - Change GetBatchesOptions.username to usernames (array) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -173,10 +173,7 @@ export class ExtractorOperationHistoryDAO {
|
||||
* @param status - New status (success, failed, partial)
|
||||
* @returns Update result
|
||||
*/
|
||||
async updateBatchStatus(
|
||||
batchId: string,
|
||||
status: string
|
||||
): Promise<UpdateBatchStatusResult> {
|
||||
async updateBatchStatus(batchId: string, status: string): Promise<UpdateBatchStatusResult> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
@@ -265,7 +262,7 @@ export class ExtractorOperationHistoryDAO {
|
||||
/**
|
||||
* Get batch statistics with optional user filtering
|
||||
* @param userId - Optional user ID for filtering (Admin gets all, User gets own)
|
||||
* @param options - Query options (limit, offset)
|
||||
* @param options - Query options (limit, offset, usernames)
|
||||
* @returns Array of batch statistics
|
||||
*/
|
||||
async getBatches(userId?: number, options?: GetBatchesOptions): Promise<BatchStats[]> {
|
||||
@@ -293,6 +290,11 @@ export class ExtractorOperationHistoryDAO {
|
||||
if (userId !== undefined) {
|
||||
sqlString += isSqlServer ? ` WHERE UserId = @p0 ` : ` WHERE UserId = ? `
|
||||
params.push(userId)
|
||||
} else if (options?.usernames && options.usernames.length > 0) {
|
||||
// Admin user filtering by multiple usernames using IN clause
|
||||
const placeholders = this.buildPlaceholders(options.usernames.length, isSqlServer)
|
||||
sqlString += ` WHERE Username IN (${placeholders}) `
|
||||
params.push(...options.usernames)
|
||||
}
|
||||
|
||||
sqlString += `
|
||||
@@ -573,9 +575,10 @@ export class ExtractorOperationHistoryDAO {
|
||||
/**
|
||||
* Count total batches with optional user filtering
|
||||
* @param userId - Optional user ID for filtering
|
||||
* @param usernames - Optional usernames filter for Admin users
|
||||
* @returns Total number of batches
|
||||
*/
|
||||
async countBatches(userId?: number): Promise<number> {
|
||||
async countBatches(userId?: number, usernames?: string[]): Promise<number> {
|
||||
try {
|
||||
const dbService = await this.getDatabaseService()
|
||||
const tableName = this.getTableName()
|
||||
@@ -586,11 +589,16 @@ export class ExtractorOperationHistoryDAO {
|
||||
FROM ${tableName}
|
||||
`
|
||||
|
||||
const params: number[] = []
|
||||
const params: (number | string)[] = []
|
||||
|
||||
if (userId !== undefined) {
|
||||
sqlString += isSqlServer ? ` WHERE UserId = @p0 ` : ` WHERE UserId = ? `
|
||||
params.push(userId)
|
||||
} else if (usernames && usernames.length > 0) {
|
||||
// Admin user filtering by multiple usernames using IN clause
|
||||
const placeholders = this.buildPlaceholders(usernames.length, isSqlServer)
|
||||
sqlString += ` WHERE Username IN (${placeholders}) `
|
||||
params.push(...usernames)
|
||||
}
|
||||
|
||||
const result = await dbService.query(sqlString, params)
|
||||
|
||||
@@ -83,4 +83,6 @@ export interface GetBatchesOptions {
|
||||
limit?: number
|
||||
/** Number of batches to skip (for pagination) */
|
||||
offset?: number
|
||||
/** Optional username filter for Admin users (supports multiple) */
|
||||
usernames?: string[]
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
const [expandedBatches, setExpandedBatches] = useState<Set<string>>(new Set())
|
||||
const [batchDetails, setBatchDetails] = useState<Map<string, OperationHistoryRecord[]>>(new Map())
|
||||
const [deleting, setDeleting] = useState<Set<string>>(new Set())
|
||||
const [allUsers, setAllUsers] = useState<string[]>([])
|
||||
const [selectedUsers, setSelectedUsers] = useState<string[]>([])
|
||||
|
||||
const isAdmin = user?.userType === 'Admin'
|
||||
|
||||
@@ -87,7 +89,13 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const result = await window.electron.operationHistory.getBatches({ limit: 100 })
|
||||
// Admin user can pass usernames filter
|
||||
const options =
|
||||
isAdmin && selectedUsers.length > 0
|
||||
? { limit: 100, usernames: selectedUsers }
|
||||
: { limit: 100 }
|
||||
|
||||
const result = await window.electron.operationHistory.getBatches(options)
|
||||
if (result.success && result.data) {
|
||||
setBatches(result.data)
|
||||
} else {
|
||||
@@ -98,6 +106,18 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [isAdmin, selectedUsers])
|
||||
|
||||
const fetchAllUsers = useCallback(async () => {
|
||||
try {
|
||||
const result = await window.electron.auth.getAllUsers()
|
||||
if (result.success && result.data) {
|
||||
const usernames = result.data.map((u: UserInfo) => u.username)
|
||||
setAllUsers(usernames)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch users:', err)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const fetchBatchDetails = useCallback(
|
||||
@@ -123,8 +143,11 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
void fetchBatches()
|
||||
if (isAdmin) {
|
||||
void fetchAllUsers()
|
||||
}
|
||||
}, [isOpen, fetchBatches])
|
||||
}
|
||||
}, [isOpen, fetchBatches, fetchAllUsers, isAdmin])
|
||||
|
||||
const toggleBatchExpansion = (batchId: string) => {
|
||||
setExpandedBatches((prev) => {
|
||||
@@ -196,17 +219,63 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
}
|
||||
}
|
||||
|
||||
const toggleUserFilter = (username: string) => {
|
||||
setSelectedUsers((prev) =>
|
||||
prev.includes(username)
|
||||
? prev.filter((u) => u !== username)
|
||||
: [...prev, username]
|
||||
)
|
||||
}
|
||||
|
||||
const clearUserFilters = () => {
|
||||
setSelectedUsers([])
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="操作历史" size="3xl">
|
||||
<div className="flex flex-col h-[70vh]">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200">
|
||||
<div className="flex items-start justify-between mb-4 pb-4 border-b border-gray-200">
|
||||
<div className="flex-1">
|
||||
{isAdmin && allUsers.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<div className="text-xs text-gray-500 mb-2">筛选用户:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allUsers.map((username) => {
|
||||
const isSelected = selectedUsers.includes(username)
|
||||
return (
|
||||
<button
|
||||
key={username}
|
||||
onClick={() => toggleUserFilter(username)}
|
||||
className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm font-medium transition-all ${
|
||||
isSelected
|
||||
? 'bg-blue-600 text-white shadow-sm'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{username}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{selectedUsers.length > 0 && (
|
||||
<button
|
||||
onClick={clearUserFilters}
|
||||
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-sm font-medium bg-red-50 text-red-600 hover:bg-red-100 transition-all"
|
||||
>
|
||||
清空筛选
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-600">
|
||||
{isAdmin ? (
|
||||
<span className="text-amber-600 font-medium">管理员模式:显示所有用户记录</span>
|
||||
<span className="text-amber-600 font-medium">
|
||||
管理员模式:{selectedUsers.length > 0 ? `已选择 ${selectedUsers.length} 个用户` : '显示所有用户记录'}
|
||||
</span>
|
||||
) : (
|
||||
<span>仅显示您的操作记录</span>
|
||||
)}
|
||||
@@ -215,8 +284,9 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
<span className="text-sm text-gray-500">共 {batches.length} 条批次</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50"
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors disabled:opacity-50 flex-shrink-0"
|
||||
onClick={() => void fetchBatches()}
|
||||
disabled={loading}
|
||||
title="刷新"
|
||||
@@ -309,6 +379,7 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAdmin && (
|
||||
<button
|
||||
className="p-2 hover:bg-red-50 text-gray-400 hover:text-red-600 rounded transition-colors disabled:opacity-50"
|
||||
onClick={(e) => {
|
||||
@@ -320,6 +391,7 @@ export const ExtractorOperationHistoryModal: React.FC<ExtractorOperationHistoryM
|
||||
>
|
||||
<Trash2 size={16} className={isDeleting ? 'animate-pulse' : ''} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Batch details */}
|
||||
|
||||
@@ -185,7 +185,7 @@ export function AuthenticatedAppShell({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{currentPage === 'extractor' && <ExtractorPage />}
|
||||
{currentPage === 'extractor' && <ExtractorPage currentUser={currentUser} />}
|
||||
{currentPage === 'cleaner' && <CleanerPage />}
|
||||
{currentPage === 'settings' && <SettingsPage />}
|
||||
</main>
|
||||
|
||||
@@ -7,12 +7,28 @@ import { useSharedProductionIds } from '../hooks/useSharedProductionIds'
|
||||
import LogPanel from '../components/ui/LogPanel'
|
||||
import { SegmentedProgressBar } from '../components/ui/SegmentedProgressBar'
|
||||
import ExtractorOperationHistoryModal from '../components/ExtractorOperationHistoryModal'
|
||||
import { useUserStore } from '../stores/useUserStore'
|
||||
import type { CurrentUser } from '../hooks/useAppBootstrap'
|
||||
|
||||
const ExtractorPage: React.FC = () => {
|
||||
interface ExtractorPageProps {
|
||||
currentUser: CurrentUser | null
|
||||
}
|
||||
|
||||
const ExtractorPage: React.FC<ExtractorPageProps> = ({ currentUser }) => {
|
||||
const [orderNumbers, setOrderNumbers] = usePersistentTextState('extractor_orderNumbers')
|
||||
const [showHistoryModal, setShowHistoryModal] = React.useState(false)
|
||||
const user = useUserStore((state) => state.user)
|
||||
|
||||
// Convert currentUser to UserInfo format for the modal
|
||||
const user = React.useMemo(
|
||||
() =>
|
||||
currentUser
|
||||
? {
|
||||
id: 0, // ID is not needed for modal display logic
|
||||
username: currentUser.username,
|
||||
userType: currentUser.userType
|
||||
}
|
||||
: null,
|
||||
[currentUser]
|
||||
)
|
||||
|
||||
const {
|
||||
isRunning,
|
||||
|
||||
Reference in New Issue
Block a user