refactor: extract cleaner hook helpers and api

This commit is contained in:
Misaka
2026-03-21 09:02:45 +08:00
parent b979b73ba1
commit 7b57545127
7 changed files with 463 additions and 210 deletions

View File

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

View 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 ?? ''
}

View 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)
}))
}

View 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
}

View File

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

View File

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

View 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
}
])
})
})