refactor: extract cleaner hook helpers and api
This commit is contained in:
@@ -10,17 +10,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { CheckCircle, XCircle, SkipForward, Package, Loader2 } from 'lucide-react'
|
import { CheckCircle, XCircle, SkipForward, Package, Loader2 } from 'lucide-react'
|
||||||
import { Modal } from './ui/Modal'
|
import { Modal } from './ui/Modal'
|
||||||
|
import type { CleanerProgress } from '../hooks/cleaner/types'
|
||||||
interface CleanerProgress {
|
|
||||||
message: string
|
|
||||||
progress: number
|
|
||||||
currentOrderIndex: number
|
|
||||||
totalOrders: number
|
|
||||||
currentMaterialIndex: number
|
|
||||||
totalMaterialsInOrder: number
|
|
||||||
currentOrderNumber?: string
|
|
||||||
phase: 'login' | 'processing' | 'complete'
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExecutionReportDialogProps {
|
interface ExecutionReportDialogProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
|||||||
159
src/renderer/src/hooks/cleaner/api.ts
Normal file
159
src/renderer/src/hooks/cleaner/api.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import type {
|
||||||
|
CleanerConfigResult,
|
||||||
|
CleanerInitializationResult,
|
||||||
|
CleanerReportData,
|
||||||
|
ValidationRequest,
|
||||||
|
ValidationResponsePayload
|
||||||
|
} from './types'
|
||||||
|
import type { CleanerExportItem, MaterialBatchChange } from './helpers'
|
||||||
|
|
||||||
|
export async function initializeCleanerPage(): Promise<CleanerInitializationResult> {
|
||||||
|
const adminResult = await window.electron.auth.isAdmin()
|
||||||
|
const userResult = await window.electron.auth.getCurrentUser()
|
||||||
|
const isAdmin = adminResult.success && Boolean(adminResult.data)
|
||||||
|
const user = userResult.success ? userResult.data : undefined
|
||||||
|
const currentUsername = user?.userInfo?.username ?? ''
|
||||||
|
|
||||||
|
let managers: string[] = []
|
||||||
|
if (isAdmin) {
|
||||||
|
const response = await window.electron.materials.getManagers()
|
||||||
|
const payload = response.success
|
||||||
|
? (response.data as { managers?: string[] } | undefined)
|
||||||
|
: undefined
|
||||||
|
managers = payload?.managers ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedIdsResult = await window.electron.validation.getSharedProductionIds()
|
||||||
|
const sharedIdsPayload = sharedIdsResult.success
|
||||||
|
? (sharedIdsResult.data as { productionIds?: string[] } | undefined)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAdmin,
|
||||||
|
currentUsername,
|
||||||
|
managers,
|
||||||
|
sharedProductionIdsCount: sharedIdsPayload?.productionIds?.length ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadCleanerConfig(): Promise<CleanerConfigResult | null> {
|
||||||
|
const result = await window.electron.config.getCleaner()
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
queryBatchSize: result.data.queryBatchSize,
|
||||||
|
processConcurrency: result.data.processConcurrency
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runValidationRequest(
|
||||||
|
request: ValidationRequest
|
||||||
|
): Promise<ValidationResponsePayload | null> {
|
||||||
|
const response = await window.electron.validation.validate(request)
|
||||||
|
if (!response.success || !response.data) {
|
||||||
|
return response.success ? null : { success: false, error: response.error }
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as ValidationResponsePayload
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveDeletionPlan(params: {
|
||||||
|
materialsToUpsert: MaterialBatchChange[]
|
||||||
|
materialsToDelete: string[]
|
||||||
|
}): Promise<{ upserted: number; deleted: number }> {
|
||||||
|
const { materialsToUpsert, materialsToDelete } = params
|
||||||
|
let upserted = 0
|
||||||
|
let deleted = 0
|
||||||
|
|
||||||
|
if (materialsToUpsert.length > 0) {
|
||||||
|
const response = await window.electron.materials.upsertBatch(materialsToUpsert)
|
||||||
|
const payload = response.success
|
||||||
|
? (response.data as { stats?: { success?: number } } | undefined)
|
||||||
|
: undefined
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || '写入物料失败')
|
||||||
|
}
|
||||||
|
upserted = payload?.stats?.success || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (materialsToDelete.length > 0) {
|
||||||
|
const response = await window.electron.materials.delete(materialsToDelete)
|
||||||
|
const payload = response.success ? (response.data as { count?: number } | undefined) : undefined
|
||||||
|
if (!response.success) {
|
||||||
|
throw new Error(response.error || '删除物料失败')
|
||||||
|
}
|
||||||
|
deleted = payload?.count || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return { upserted, deleted }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reloadManagers(): Promise<string[]> {
|
||||||
|
const response = await window.electron.materials.getManagers()
|
||||||
|
const payload = response.success
|
||||||
|
? (response.data as { managers?: string[] } | undefined)
|
||||||
|
: undefined
|
||||||
|
return payload?.managers ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCleanerExecution(params: {
|
||||||
|
dryRun: boolean
|
||||||
|
headless: boolean
|
||||||
|
queryBatchSize: number
|
||||||
|
processConcurrency: number
|
||||||
|
}): Promise<CleanerReportData> {
|
||||||
|
const cleanerDataResult = await window.electron.validation.getCleanerData()
|
||||||
|
const cleanerData = cleanerDataResult.success ? (cleanerDataResult.data as any) : null
|
||||||
|
|
||||||
|
if (!cleanerDataResult.success || cleanerData?.success === false) {
|
||||||
|
throw new Error(cleanerDataResult.error || '获取清理数据失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderNumberList = cleanerData?.orderNumbers || []
|
||||||
|
const materialCodeList = cleanerData?.materialCodes || []
|
||||||
|
|
||||||
|
if (orderNumberList.length === 0) {
|
||||||
|
throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。')
|
||||||
|
}
|
||||||
|
if (materialCodeList.length === 0) {
|
||||||
|
throw new Error('没有物料代码数据。请确认已在物料清理界面确认要删除的物料。')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await window.electron.cleaner.runCleaner({
|
||||||
|
orderNumbers: orderNumberList,
|
||||||
|
materialCodes: materialCodeList,
|
||||||
|
dryRun: params.dryRun,
|
||||||
|
headless: params.headless,
|
||||||
|
queryBatchSize: params.queryBatchSize,
|
||||||
|
processConcurrency: params.processConcurrency
|
||||||
|
})
|
||||||
|
|
||||||
|
const cleanerRunData = response.success ? (response.data as any) : null
|
||||||
|
if (!response.success || !cleanerRunData) {
|
||||||
|
throw new Error(response.error || '清理失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ordersProcessed: cleanerRunData.ordersProcessed,
|
||||||
|
materialsDeleted: cleanerRunData.materialsDeleted,
|
||||||
|
materialsSkipped: cleanerRunData.materialsSkipped,
|
||||||
|
errors: cleanerRunData.errors,
|
||||||
|
retriedOrders: cleanerRunData.retriedOrders,
|
||||||
|
successfulRetries: cleanerRunData.successfulRetries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportCleanerResults(items: CleanerExportItem[]): Promise<string> {
|
||||||
|
const response = await window.electron.cleaner.exportResults(items)
|
||||||
|
const exportData = response.success
|
||||||
|
? (response.data as { success?: boolean; filePath?: string; error?: string } | null)
|
||||||
|
: null
|
||||||
|
|
||||||
|
if (!response.success || exportData?.success === false) {
|
||||||
|
throw new Error(response.error || exportData?.error || '导出失败')
|
||||||
|
}
|
||||||
|
|
||||||
|
return exportData?.filePath ?? ''
|
||||||
|
}
|
||||||
107
src/renderer/src/hooks/cleaner/helpers.ts
Normal file
107
src/renderer/src/hooks/cleaner/helpers.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import type { ValidationResult } from './types'
|
||||||
|
|
||||||
|
export interface MaterialBatchChange {
|
||||||
|
materialCode: string
|
||||||
|
managerName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeletionPlan {
|
||||||
|
materialsToUpsert: MaterialBatchChange[]
|
||||||
|
materialsToDelete: string[]
|
||||||
|
missingManager: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CleanerExportItem {
|
||||||
|
materialName: string
|
||||||
|
materialCode: string
|
||||||
|
specification: string
|
||||||
|
model: string
|
||||||
|
managerName: string
|
||||||
|
isMarkedForDeletion: boolean
|
||||||
|
isSelected: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoredBoolean(key: string, fallback: boolean): boolean {
|
||||||
|
const saved = sessionStorage.getItem(key)
|
||||||
|
return saved ? saved === 'true' : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoredValidationMode(): 'full' | 'filtered' {
|
||||||
|
const saved = sessionStorage.getItem('cleaner_validationMode')
|
||||||
|
return saved === 'full' ? 'full' : 'filtered'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterValidationResults(params: {
|
||||||
|
validationResults: ValidationResult[]
|
||||||
|
isAdmin: boolean
|
||||||
|
currentUsername: string
|
||||||
|
managers: string[]
|
||||||
|
selectedManagers: Set<string>
|
||||||
|
hiddenItems: Set<string>
|
||||||
|
}): ValidationResult[] {
|
||||||
|
const { validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems } =
|
||||||
|
params
|
||||||
|
|
||||||
|
let results = validationResults
|
||||||
|
if (!isAdmin && currentUsername) {
|
||||||
|
results = results.filter((result) => {
|
||||||
|
return result.managerName === currentUsername || !result.managerName
|
||||||
|
})
|
||||||
|
} else if (managers.length > 0 && selectedManagers.size > 0) {
|
||||||
|
results = results.filter((result) => {
|
||||||
|
return selectedManagers.has(result.managerName) || !result.managerName
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.filter((result) => !hiddenItems.has(result.materialCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDeletionPlan(
|
||||||
|
resultsToProcess: ValidationResult[],
|
||||||
|
selectedItems: Set<string>
|
||||||
|
): DeletionPlan {
|
||||||
|
const materialsToUpsert: MaterialBatchChange[] = []
|
||||||
|
const materialsToDelete: string[] = []
|
||||||
|
const missingManager: string[] = []
|
||||||
|
|
||||||
|
for (const result of resultsToProcess) {
|
||||||
|
if (!result.materialCode?.trim()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = result.materialCode.trim()
|
||||||
|
if (selectedItems.has(code)) {
|
||||||
|
if (!result.managerName?.trim()) {
|
||||||
|
missingManager.push(code)
|
||||||
|
} else {
|
||||||
|
materialsToUpsert.push({
|
||||||
|
materialCode: code,
|
||||||
|
managerName: result.managerName.trim()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
materialsToDelete.push(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
materialsToUpsert,
|
||||||
|
materialsToDelete,
|
||||||
|
missingManager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildExportItems(
|
||||||
|
filteredResults: ValidationResult[],
|
||||||
|
selectedItems: Set<string>
|
||||||
|
): CleanerExportItem[] {
|
||||||
|
return filteredResults.map((result) => ({
|
||||||
|
materialName: result.materialName,
|
||||||
|
materialCode: result.materialCode,
|
||||||
|
specification: result.specification || '',
|
||||||
|
model: result.model || '',
|
||||||
|
managerName: result.managerName || '',
|
||||||
|
isMarkedForDeletion: result.isMarkedForDeletion,
|
||||||
|
isSelected: selectedItems.has(result.materialCode)
|
||||||
|
}))
|
||||||
|
}
|
||||||
60
src/renderer/src/hooks/cleaner/types.ts
Normal file
60
src/renderer/src/hooks/cleaner/types.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
export interface ValidationRequest {
|
||||||
|
mode: 'database_full' | 'database_filtered'
|
||||||
|
productionIdFile?: string
|
||||||
|
useSharedProductionIds?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
materialName: string
|
||||||
|
materialCode: string
|
||||||
|
specification: string
|
||||||
|
model: string
|
||||||
|
managerName: string
|
||||||
|
isMarkedForDeletion: boolean
|
||||||
|
matchedTypeKeyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationStats {
|
||||||
|
totalRecords: number
|
||||||
|
matchedCount: number
|
||||||
|
markedCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationResponsePayload {
|
||||||
|
success: boolean
|
||||||
|
results?: ValidationResult[]
|
||||||
|
stats?: ValidationStats
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CleanerProgress {
|
||||||
|
message: string
|
||||||
|
progress: number
|
||||||
|
currentOrderIndex: number
|
||||||
|
totalOrders: number
|
||||||
|
currentMaterialIndex: number
|
||||||
|
totalMaterialsInOrder: number
|
||||||
|
currentOrderNumber?: string
|
||||||
|
phase: 'login' | 'processing' | 'complete'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CleanerReportData {
|
||||||
|
ordersProcessed: number
|
||||||
|
materialsDeleted: number
|
||||||
|
materialsSkipped: number
|
||||||
|
errors: string[]
|
||||||
|
retriedOrders?: number
|
||||||
|
successfulRetries?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CleanerInitializationResult {
|
||||||
|
isAdmin: boolean
|
||||||
|
currentUsername: string
|
||||||
|
managers: string[]
|
||||||
|
sharedProductionIdsCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CleanerConfigResult {
|
||||||
|
queryBatchSize: number
|
||||||
|
processConcurrency: number
|
||||||
|
}
|
||||||
@@ -1,27 +1,23 @@
|
|||||||
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
import { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
||||||
import { showSuccess, showError, showWarning, formatListMessage } from '../stores/useAppStore'
|
import { showSuccess, showError, showWarning, formatListMessage } from '../stores/useAppStore'
|
||||||
import { ConfirmDialogProps } from '../components/ui/ConfirmDialog'
|
import { ConfirmDialogProps } from '../components/ui/ConfirmDialog'
|
||||||
|
import {
|
||||||
export interface ValidationResult {
|
buildDeletionPlan,
|
||||||
materialName: string
|
buildExportItems,
|
||||||
materialCode: string
|
filterValidationResults,
|
||||||
specification: string
|
getStoredBoolean,
|
||||||
model: string
|
getStoredValidationMode
|
||||||
managerName: string
|
} from './cleaner/helpers'
|
||||||
isMarkedForDeletion: boolean
|
import {
|
||||||
matchedTypeKeyword?: string
|
exportCleanerResults,
|
||||||
}
|
initializeCleanerPage,
|
||||||
|
loadCleanerConfig as fetchCleanerConfig,
|
||||||
export interface CleanerProgress {
|
reloadManagers,
|
||||||
message: string
|
runCleanerExecution,
|
||||||
progress: number
|
runValidationRequest,
|
||||||
currentOrderIndex: number
|
saveDeletionPlan
|
||||||
totalOrders: number
|
} from './cleaner/api'
|
||||||
currentMaterialIndex: number
|
import type { CleanerProgress, CleanerReportData, ValidationResult } from './cleaner/types'
|
||||||
totalMaterialsInOrder: number
|
|
||||||
currentOrderNumber?: string
|
|
||||||
phase: 'login' | 'processing' | 'complete'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCleaner() {
|
export function useCleaner() {
|
||||||
// Authentication & permissions
|
// Authentication & permissions
|
||||||
@@ -29,15 +25,8 @@ export function useCleaner() {
|
|||||||
const [currentUsername, setCurrentUsername] = useState<string>('')
|
const [currentUsername, setCurrentUsername] = useState<string>('')
|
||||||
|
|
||||||
// State with sessionStorage persistence
|
// State with sessionStorage persistence
|
||||||
const [dryRun, setDryRun] = useState(() => {
|
const [dryRun, setDryRun] = useState(() => getStoredBoolean('cleaner_dryRun', false))
|
||||||
const saved = sessionStorage.getItem('cleaner_dryRun')
|
const [valMode, setValMode] = useState<'full' | 'filtered'>(() => getStoredValidationMode())
|
||||||
return saved ? saved === 'true' : false
|
|
||||||
})
|
|
||||||
|
|
||||||
const [valMode, setValMode] = useState<'full' | 'filtered'>(() => {
|
|
||||||
const saved = sessionStorage.getItem('cleaner_validationMode')
|
|
||||||
return saved === 'full' ? 'full' : 'filtered'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Validation state
|
// Validation state
|
||||||
const [validationResults, setValidationResults] = useState<ValidationResult[]>([])
|
const [validationResults, setValidationResults] = useState<ValidationResult[]>([])
|
||||||
@@ -60,24 +49,14 @@ export function useCleaner() {
|
|||||||
|
|
||||||
// Execution report dialog state
|
// Execution report dialog state
|
||||||
const [isReportDialogOpen, setIsReportDialogOpen] = useState(false)
|
const [isReportDialogOpen, setIsReportDialogOpen] = useState(false)
|
||||||
const [reportData, setReportData] = useState<{
|
const [reportData, setReportData] = useState<CleanerReportData | null>(null)
|
||||||
ordersProcessed: number
|
|
||||||
materialsDeleted: number
|
|
||||||
materialsSkipped: number
|
|
||||||
errors: string[]
|
|
||||||
retriedOrders?: number
|
|
||||||
successfulRetries?: number
|
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
// Progress state
|
// Progress state
|
||||||
const [progress, setProgress] = useState<CleanerProgress | null>(null)
|
const [progress, setProgress] = useState<CleanerProgress | null>(null)
|
||||||
const [startTime, setStartTime] = useState<number | null>(null)
|
const [startTime, setStartTime] = useState<number | null>(null)
|
||||||
|
|
||||||
// Execution settings state
|
// Execution settings state
|
||||||
const [headless, setHeadless] = useState(() => {
|
const [headless, setHeadless] = useState(() => getStoredBoolean('cleaner_headless', true))
|
||||||
const saved = sessionStorage.getItem('cleaner_headless')
|
|
||||||
return saved ? saved === 'true' : true
|
|
||||||
})
|
|
||||||
const [queryBatchSize, setQueryBatchSize] = useState(100)
|
const [queryBatchSize, setQueryBatchSize] = useState(100)
|
||||||
const [processConcurrency, setProcessConcurrency] = useState(1)
|
const [processConcurrency, setProcessConcurrency] = useState(1)
|
||||||
const [showSettingsMenu, setShowSettingsMenu] = useState(false)
|
const [showSettingsMenu, setShowSettingsMenu] = useState(false)
|
||||||
@@ -117,35 +96,17 @@ export function useCleaner() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializePage = async () => {
|
const initializePage = async () => {
|
||||||
try {
|
try {
|
||||||
const adminResult = await window.electron.auth.isAdmin()
|
const result = await initializeCleanerPage()
|
||||||
const userResult = await window.electron.auth.getCurrentUser()
|
setIsAdmin(result.isAdmin)
|
||||||
const admin = adminResult.success && Boolean(adminResult.data)
|
setCurrentUsername(result.currentUsername)
|
||||||
const user = userResult.success ? userResult.data : undefined
|
setManagers(result.managers)
|
||||||
setIsAdmin(admin)
|
setSharedProductionIdsCount(result.sharedProductionIdsCount)
|
||||||
if (user && user.userInfo) {
|
|
||||||
setCurrentUsername(user.userInfo.username)
|
|
||||||
if (!admin) {
|
|
||||||
setSelectedManagers(new Set([user.userInfo.username]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load managers
|
if (result.isAdmin) {
|
||||||
if (admin) {
|
setSelectedManagers(new Set(result.managers))
|
||||||
const resp = await window.electron.materials.getManagers()
|
} else if (result.currentUsername) {
|
||||||
const managersPayload = resp.success
|
setSelectedManagers(new Set([result.currentUsername]))
|
||||||
? (resp.data as { managers: string[] } | undefined)
|
|
||||||
: undefined
|
|
||||||
const managerList = managersPayload?.managers ?? []
|
|
||||||
setManagers(managerList)
|
|
||||||
setSelectedManagers(new Set(managerList))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get shared Production IDs
|
|
||||||
const result = await window.electron.validation.getSharedProductionIds()
|
|
||||||
const idsPayload = result.success
|
|
||||||
? (result.data as { productionIds?: string[] } | undefined)
|
|
||||||
: undefined
|
|
||||||
setSharedProductionIdsCount(idsPayload?.productionIds?.length ?? 0)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Initialization failed:', err)
|
console.error('Initialization failed:', err)
|
||||||
}
|
}
|
||||||
@@ -168,10 +129,10 @@ export function useCleaner() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadCleanerConfig = async () => {
|
const loadCleanerConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await window.electron.config.getCleaner()
|
const result = await fetchCleanerConfig()
|
||||||
if (result.success && result.data) {
|
if (result) {
|
||||||
setQueryBatchSize(result.data.queryBatchSize)
|
setQueryBatchSize(result.queryBatchSize)
|
||||||
setProcessConcurrency(result.data.processConcurrency)
|
setProcessConcurrency(result.processConcurrency)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load cleaner config:', err)
|
console.error('Failed to load cleaner config:', err)
|
||||||
@@ -204,14 +165,14 @@ export function useCleaner() {
|
|||||||
|
|
||||||
// Filter validation results by selected managers and hidden items
|
// Filter validation results by selected managers and hidden items
|
||||||
const filteredResults = useMemo(() => {
|
const filteredResults = useMemo(() => {
|
||||||
let results = validationResults
|
return filterValidationResults({
|
||||||
if (!isAdmin && currentUsername) {
|
validationResults,
|
||||||
results = results.filter((r) => r.managerName === currentUsername || !r.managerName)
|
isAdmin,
|
||||||
} else if (managers.length > 0 && selectedManagers.size > 0) {
|
currentUsername,
|
||||||
results = results.filter((r) => selectedManagers.has(r.managerName) || !r.managerName)
|
managers,
|
||||||
}
|
selectedManagers,
|
||||||
results = results.filter((r) => !hiddenItems.has(r.materialCode))
|
hiddenItems
|
||||||
return results
|
})
|
||||||
}, [validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems])
|
}, [validationResults, isAdmin, currentUsername, managers, selectedManagers, hiddenItems])
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
@@ -222,13 +183,12 @@ export function useCleaner() {
|
|||||||
setHiddenItems(new Set())
|
setHiddenItems(new Set())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await window.electron.validation.validate({
|
const validationData = await runValidationRequest({
|
||||||
mode: valMode === 'full' ? 'database_full' : 'database_filtered',
|
mode: valMode === 'full' ? 'database_full' : 'database_filtered',
|
||||||
useSharedProductionIds: valMode === 'filtered'
|
useSharedProductionIds: valMode === 'filtered'
|
||||||
})
|
})
|
||||||
const validationData = response.success ? (response.data as any) : null
|
|
||||||
|
|
||||||
if (response.success && validationData?.success && validationData.results) {
|
if (validationData?.success && validationData.results) {
|
||||||
setValidationResults(validationData.results)
|
setValidationResults(validationData.results)
|
||||||
const markedCodes = new Set<string>(
|
const markedCodes = new Set<string>(
|
||||||
validationData.results
|
validationData.results
|
||||||
@@ -248,7 +208,7 @@ export function useCleaner() {
|
|||||||
}
|
}
|
||||||
showSuccess('校验完成')
|
showSuccess('校验完成')
|
||||||
} else {
|
} else {
|
||||||
showError(response.error || validationData?.error || '校验失败')
|
showError(validationData?.error || '校验失败')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err instanceof Error ? err.message : '校验过程中发生未知错误')
|
showError(err instanceof Error ? err.message : '校验过程中发生未知错误')
|
||||||
@@ -324,21 +284,10 @@ export function useCleaner() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const materialsToUpsert: { materialCode: string; managerName: string }[] = []
|
const { materialsToUpsert, materialsToDelete, missingManager } = buildDeletionPlan(
|
||||||
const materialsToDelete: string[] = []
|
resultsToProcess,
|
||||||
const missingManager: string[] = []
|
selectedItems
|
||||||
|
)
|
||||||
for (const result of resultsToProcess) {
|
|
||||||
if (!result.materialCode?.trim()) continue
|
|
||||||
|
|
||||||
const code = result.materialCode.trim()
|
|
||||||
if (selectedItems.has(code)) {
|
|
||||||
if (!result.managerName?.trim()) missingManager.push(code)
|
|
||||||
else materialsToUpsert.push({ materialCode: code, managerName: result.managerName.trim() })
|
|
||||||
} else {
|
|
||||||
materialsToDelete.push(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingManager.length > 0) {
|
if (missingManager.length > 0) {
|
||||||
showError(
|
showError(
|
||||||
@@ -368,31 +317,15 @@ export function useCleaner() {
|
|||||||
setIsRunning(true)
|
setIsRunning(true)
|
||||||
const msgParts: string[] = []
|
const msgParts: string[] = []
|
||||||
|
|
||||||
if (materialsToUpsert.length > 0) {
|
const result = await saveDeletionPlan({ materialsToUpsert, materialsToDelete })
|
||||||
const res = await window.electron.materials.upsertBatch(materialsToUpsert)
|
if (materialsToUpsert.length > 0) msgParts.push(`写入/更新成功:${result.upserted} 条`)
|
||||||
const payload = res.success
|
if (materialsToDelete.length > 0) msgParts.push(`删除成功:${result.deleted} 条`)
|
||||||
? (res.data as { stats?: { success?: number } } | undefined)
|
|
||||||
: undefined
|
|
||||||
if (!res.success) throw new Error(res.error || '写入物料失败')
|
|
||||||
msgParts.push(`写入/更新成功:${payload?.stats?.success || 0} 条`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (materialsToDelete.length > 0) {
|
|
||||||
const res = await window.electron.materials.delete(materialsToDelete)
|
|
||||||
const payload = res.success ? (res.data as { count?: number } | undefined) : undefined
|
|
||||||
if (!res.success) throw new Error(res.error || '删除物料失败')
|
|
||||||
msgParts.push(`删除成功:${payload?.count || 0} 条`)
|
|
||||||
}
|
|
||||||
|
|
||||||
showSuccess(`操作完成!\n\n${msgParts.join('\n')}`)
|
showSuccess(`操作完成!\n\n${msgParts.join('\n')}`)
|
||||||
|
|
||||||
// Reload managers if admin
|
// Reload managers if admin
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
const resp = await window.electron.materials.getManagers()
|
setManagers(await reloadManagers())
|
||||||
const payload = resp.success
|
|
||||||
? (resp.data as { managers?: string[] } | undefined)
|
|
||||||
: undefined
|
|
||||||
setManagers(payload?.managers ?? [])
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err instanceof Error ? err.message : '操作失败')
|
showError(err instanceof Error ? err.message : '操作失败')
|
||||||
@@ -429,42 +362,13 @@ export function useCleaner() {
|
|||||||
setIsReportDialogOpen(true)
|
setIsReportDialogOpen(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cleanerDataResult = await window.electron.validation.getCleanerData()
|
const result = await runCleanerExecution({
|
||||||
const cleanerData = cleanerDataResult.success ? (cleanerDataResult.data as any) : null
|
|
||||||
if (!cleanerDataResult.success || cleanerData?.success === false) {
|
|
||||||
throw new Error(cleanerDataResult.error || '获取清理数据失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderNumberList = cleanerData?.orderNumbers || []
|
|
||||||
const materialCodeList = cleanerData?.materialCodes || []
|
|
||||||
|
|
||||||
if (orderNumberList.length === 0)
|
|
||||||
throw new Error('没有订单号数据。请先到数据提取页面输入 Production ID。')
|
|
||||||
if (materialCodeList.length === 0)
|
|
||||||
throw new Error('没有物料代码数据。请确认已在物料清理界面确认要删除的物料。')
|
|
||||||
|
|
||||||
const response = await window.electron.cleaner.runCleaner({
|
|
||||||
orderNumbers: orderNumberList,
|
|
||||||
materialCodes: materialCodeList,
|
|
||||||
dryRun,
|
dryRun,
|
||||||
headless,
|
headless,
|
||||||
queryBatchSize,
|
queryBatchSize,
|
||||||
processConcurrency
|
processConcurrency
|
||||||
})
|
})
|
||||||
const cleanerRunData = response.success ? (response.data as any) : null
|
setReportData(result)
|
||||||
|
|
||||||
if (response.success && cleanerRunData) {
|
|
||||||
setReportData({
|
|
||||||
ordersProcessed: cleanerRunData.ordersProcessed,
|
|
||||||
materialsDeleted: cleanerRunData.materialsDeleted,
|
|
||||||
materialsSkipped: cleanerRunData.materialsSkipped,
|
|
||||||
errors: cleanerRunData.errors,
|
|
||||||
retriedOrders: cleanerRunData.retriedOrders,
|
|
||||||
successfulRetries: cleanerRunData.successfulRetries
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
throw new Error(response.error || '清理失败')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err instanceof Error ? err.message : '发生未知错误')
|
showError(err instanceof Error ? err.message : '发生未知错误')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -488,25 +392,8 @@ export function useCleaner() {
|
|||||||
|
|
||||||
setIsExporting(true)
|
setIsExporting(true)
|
||||||
try {
|
try {
|
||||||
// Prepare export data from filtered results
|
const filePath = await exportCleanerResults(buildExportItems(filteredResults, selectedItems))
|
||||||
const exportItems = filteredResults.map((result) => ({
|
showSuccess(`导出成功!\n文件已保存到:${filePath}`)
|
||||||
materialName: result.materialName,
|
|
||||||
materialCode: result.materialCode,
|
|
||||||
specification: result.specification || '',
|
|
||||||
model: result.model || '',
|
|
||||||
managerName: result.managerName || '',
|
|
||||||
isMarkedForDeletion: result.isMarkedForDeletion,
|
|
||||||
isSelected: selectedItems.has(result.materialCode)
|
|
||||||
}))
|
|
||||||
|
|
||||||
const response = await window.electron.cleaner.exportResults(exportItems)
|
|
||||||
const exportData = response.success ? (response.data as any) : null
|
|
||||||
|
|
||||||
if (response.success && exportData?.success !== false) {
|
|
||||||
showSuccess(`导出成功!\n文件已保存到:${exportData?.filePath ?? ''}`)
|
|
||||||
} else {
|
|
||||||
throw new Error(response.error || exportData?.error || '导出失败')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err instanceof Error ? err.message : '导出过程中发生错误')
|
showError(err instanceof Error ? err.message : '导出过程中发生错误')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,33 +1,10 @@
|
|||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
|
import type {
|
||||||
interface ValidationRequest {
|
ValidationRequest,
|
||||||
mode: 'database_full' | 'database_filtered'
|
ValidationResponsePayload as ValidationResponse,
|
||||||
productionIdFile?: string
|
ValidationResult,
|
||||||
useSharedProductionIds?: boolean
|
ValidationStats
|
||||||
}
|
} from './cleaner/types'
|
||||||
|
|
||||||
interface ValidationResult {
|
|
||||||
materialName: string
|
|
||||||
materialCode: string
|
|
||||||
specification: string
|
|
||||||
model: string
|
|
||||||
managerName: string
|
|
||||||
isMarkedForDeletion: boolean
|
|
||||||
matchedTypeKeyword?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ValidationStats {
|
|
||||||
totalRecords: number
|
|
||||||
matchedCount: number
|
|
||||||
markedCount: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ValidationResponse {
|
|
||||||
success: boolean
|
|
||||||
results?: ValidationResult[]
|
|
||||||
stats?: ValidationStats
|
|
||||||
error?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UseValidationState {
|
interface UseValidationState {
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
|||||||
73
tests/unit/cleaner-helpers.test.ts
Normal file
73
tests/unit/cleaner-helpers.test.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import {
|
||||||
|
buildDeletionPlan,
|
||||||
|
buildExportItems,
|
||||||
|
filterValidationResults
|
||||||
|
} from '../../src/renderer/src/hooks/cleaner/helpers'
|
||||||
|
import type { ValidationResult } from '../../src/renderer/src/hooks/cleaner/types'
|
||||||
|
|
||||||
|
const sampleResults: ValidationResult[] = [
|
||||||
|
{
|
||||||
|
materialName: 'A',
|
||||||
|
materialCode: 'M1',
|
||||||
|
specification: '',
|
||||||
|
model: '',
|
||||||
|
managerName: 'alice',
|
||||||
|
isMarkedForDeletion: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
materialName: 'B',
|
||||||
|
materialCode: 'M2',
|
||||||
|
specification: '',
|
||||||
|
model: '',
|
||||||
|
managerName: '',
|
||||||
|
isMarkedForDeletion: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
materialName: 'C',
|
||||||
|
materialCode: 'M3',
|
||||||
|
specification: '',
|
||||||
|
model: '',
|
||||||
|
managerName: 'bob',
|
||||||
|
isMarkedForDeletion: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
describe('cleaner helpers', () => {
|
||||||
|
it('filters results for non-admin users and hidden items', () => {
|
||||||
|
const filtered = filterValidationResults({
|
||||||
|
validationResults: sampleResults,
|
||||||
|
isAdmin: false,
|
||||||
|
currentUsername: 'alice',
|
||||||
|
managers: [],
|
||||||
|
selectedManagers: new Set(),
|
||||||
|
hiddenItems: new Set(['M2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(filtered.map((item) => item.materialCode)).toEqual(['M1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('builds deletion plan with missing managers', () => {
|
||||||
|
const plan = buildDeletionPlan(sampleResults, new Set(['M1', 'M2']))
|
||||||
|
|
||||||
|
expect(plan.materialsToUpsert).toEqual([{ materialCode: 'M1', managerName: 'alice' }])
|
||||||
|
expect(plan.materialsToDelete).toEqual(['M3'])
|
||||||
|
expect(plan.missingManager).toEqual(['M2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('builds export items with selection state', () => {
|
||||||
|
const items = buildExportItems(sampleResults.slice(0, 1), new Set(['M1']))
|
||||||
|
|
||||||
|
expect(items).toEqual([
|
||||||
|
{
|
||||||
|
materialName: 'A',
|
||||||
|
materialCode: 'M1',
|
||||||
|
specification: '',
|
||||||
|
model: '',
|
||||||
|
managerName: 'alice',
|
||||||
|
isMarkedForDeletion: true,
|
||||||
|
isSelected: true
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user