diff --git a/docs/cleaner-role-based-flow.md b/docs/cleaner-role-based-flow.md
new file mode 100644
index 0000000..5a7c9a6
--- /dev/null
+++ b/docs/cleaner-role-based-flow.md
@@ -0,0 +1,308 @@
+# 清理器角色差异流程 — Admin vs User
+
+**文档版本**: 1.1
+**创建日期**: 2026-04-06
+**面向对象**: 开发人员
+
+## 概述
+
+清理器(Cleaner)在决定"哪些物料需要被清除"时,Admin 和 User 两个角色存在系统性的差异。这些差异贯穿三个阶段:**初始化 → 校验确认 → 执行清理**。
+
+本文档使用 Mermaid 图表说明每个阶段的角色分支逻辑。
+
+---
+
+## 全局流程概览
+
+```mermaid
+flowchart TB
+ subgraph init["阶段一:页面初始化"]
+ I1([页面加载]) --> I2{角色判断}
+ I2 -->|Admin| I3["管理员列表 ← 全部负责人
默认选中全部"]
+ I2 -->|User| I4["管理员列表 ← 空
默认选中仅自己"]
+ end
+
+ subgraph validate["阶段二:校验 → 勾选 → 同步数据库"]
+ V1([点击校验]) --> V2["后端查询物料
(不区分角色)"]
+ V2 --> V3["物料匹配算法
(User 有覆盖匹配)"]
+ V3 --> V4{角色判断}
+ V4 -->|Admin| V5["显示全部物料
侧边栏可按负责人筛选"]
+ V4 -->|User| V6["仅显示自己的物料
+ 无负责人的物料"]
+ V5 --> V7["用户勾选/取消勾选"]
+ V6 --> V7
+ V7 --> V8{点击同步数据库}
+ V8 --> V9{角色判断}
+ V9 -->|Admin| V10["处理范围:全部校验结果"]
+ V9 -->|User| V11["处理范围:仅筛选后结果"]
+ end
+
+ subgraph execute["阶段三:执行清理(ERP 删除)"]
+ E1([点击执行清理]) --> E2["getCleanerData(selectedManagers)
获取物料代码"]
+ E2 --> E3{角色判断}
+ E3 -->|Admin| E3a{selectedManagers
非空?}
+ E3a -->|"是"| E4["SQL WHERE ManagerName IN (选中)
从 MaterialsToBeDeleted 获取"]
+ E3a -->|"否"| E4b["从 DiscreteMaterialPlanData
按 orderNumbers 获取"]
+ E3 -->|User| E5["SQL WHERE ManagerName = 用户
仅获取自己的物料代码"]
+ E4 --> E6["传递给 runCleaner 执行"]
+ E4b --> E6
+ E5 --> E6
+ E6 --> E7([在 ERP 中删除物料])
+ end
+
+ init --> validate --> execute
+```
+
+---
+
+## 阶段一:页面初始化
+
+**源码位置**: `src/renderer/src/hooks/cleaner/api.ts:25-52` 与 `src/renderer/src/hooks/useCleaner.ts:98-112`
+
+```mermaid
+flowchart TB
+ Start([页面加载]) --> GetAdmin["调用 auth:isAdmin
判断是否管理员"]
+ GetAdmin --> GetUser["调用 auth:getCurrentUser
获取当前用户名"]
+ GetUser --> RoleCheck{isAdmin?}
+
+ RoleCheck -->|Admin| GetManagers["调用 materials:getManagers
获取全部负责人列表"]
+ GetManagers --> SelectAll["selectedManagers ← 全部负责人
(默认全选)"]
+ SelectAll --> RenderSidebar["渲染 CleanerSidebar
显示负责人复选框"]
+
+ RoleCheck -->|User| SetSelf["selectedManagers ← {currentUsername}
(仅选中自己)"]
+ SetSelf --> NoSidebar["不渲染 CleanerSidebar
无侧边栏"]
+
+ RenderSidebar --> Ready([就绪])
+ NoSidebar --> Ready
+```
+
+**差异总结**:
+
+| 维度 | Admin | User |
+|------|-------|------|
+| 侧边栏 | 有 CleanerSidebar | 无 |
+| 管理员列表 | 查询全部负责人 | 不查询 |
+| 默认选中 | 所有负责人 | 仅自己 |
+
+---
+
+## 阶段二:校验 → 勾选 → 同步数据库
+
+### 2.1 物料校验(后端,不区分角色)
+
+**源码位置**: `src/main/services/validation/validation-application-service.ts`
+
+校验阶段后端查询不区分角色,Admin 和 User 拿到相同的物料数据。区别在于**匹配算法**:
+
+```mermaid
+flowchart TB
+ Start([遍历每条物料记录]) --> P1{"优先级1
MaterialsToBeDeleted
精确匹配 MaterialCode?"}
+
+ P1 -->|"匹配"| SetManager["managerName ← 表中记录
isMarkedForDeletion = true"]
+ P1 -->|"未匹配"| P2{"优先级2
MaterialsTypeToBeDeleted
MaterialName 包含匹配?"}
+
+ P2 -->|"匹配"| SetType["managerName ← 类型关键词负责人
matchedTypeKeyword ← 匹配项"]
+ P2 -->|"未匹配"| SetNull["managerName = null"]
+
+ SetManager --> RoleCheck{角色?}
+ SetType --> RoleCheck
+ SetNull --> RoleCheck
+
+ RoleCheck -->|"Admin"| Skip["跳过覆盖
使用当前结果"]
+ RoleCheck -->|"User"| P3{"优先级3(User 覆盖)
自己的类型关键词匹配?"}
+
+ P3 -->|"匹配"| Override["强制覆盖
managerName ← 当前用户"]
+ P3 -->|"未匹配"| Keep["保持当前结果"]
+ Skip --> Next(["下一条物料"])
+ Override --> Next
+ Keep --> Next
+```
+
+**匹配优先级说明**:
+
+| 优先级 | 数据源 | 匹配方式 | 适用角色 |
+|--------|--------|----------|----------|
+| 1(最高) | `MaterialsToBeDeleted` | MaterialCode 精确匹配 | 全部 |
+| 2 | `MaterialsTypeToBeDeleted` | MaterialName 包含匹配 | 全部 |
+| 3(User 覆盖) | 当前用户的类型关键词 | MaterialName 包含匹配 | 仅 User |
+
+> **优先级 3 的作用**:当某个物料按优先级 2 被分配给其他负责人,但当前 User 有匹配的类型关键词时,会强制覆盖为自己的。这确保 User 不会为他人操作物料。
+
+### 2.2 前端显示过滤
+
+**源码位置**: `src/renderer/src/hooks/cleaner/helpers.ts:34-57`
+
+校验结果返回前端后,会根据角色进行显示过滤:
+
+```mermaid
+flowchart TB
+ Input([校验结果 validationResults]) --> RoleCheck{角色判断}
+
+ RoleCheck -->|Admin| FilterManagers["按侧边栏选中的负责人过滤
selectedManagers.has(managerName)
|| !managerName"]
+ RoleCheck -->|User| FilterSelf["仅显示自己的 + 无负责人的
managerName === currentUsername
|| !managerName"]
+
+ FilterManagers --> FilterHidden["排除已隐藏的物料
!hiddenItems.has(materialCode)"]
+ FilterSelf --> FilterHidden
+
+ FilterHidden --> Output([filteredResults
用于表格显示])
+```
+
+### 2.3 确认删除(同步数据库)
+
+**源码位置**: `src/renderer/src/hooks/useCleaner.ts:289-344`
+
+```mermaid
+flowchart TB
+ Start([点击确认删除]) --> RoleScope{角色判断}
+
+ RoleScope -->|Admin| UseAll["resultsToProcess = validationResults
处理全部校验结果"]
+ RoleScope -->|User| UseFiltered["resultsToProcess = filteredResults
仅处理筛选后结果"]
+
+ UseAll --> BuildPlan["buildDeletionPlan(resultsToProcess, selectedItems)"]
+ UseFiltered --> BuildPlan
+
+ BuildPlan --> Loop["遍历 resultsToProcess"]
+ Loop --> Check{物料是否勾选?}
+
+ Check -->|"已勾选"| HasManager{有负责人?}
+ Check -->|"未勾选"| ToDelete["加入 materialsToDelete
从数据库移除标记"]
+
+ HasManager -->|"有"| ToUpsert["加入 materialsToUpsert
写入/更新到数据库"]
+ HasManager -->|"无"| Missing["加入 missingManager
阻止操作"]
+
+ ToUpsert --> Save["调用 materials:upsertBatch"]
+ ToDelete --> Del["调用 materials:delete"]
+ Missing --> Warn(["弹窗警告:缺少负责人"])
+ Save --> Done([完成])
+ Del --> Done
+```
+
+**关键代码**:
+
+```typescript
+// Admin 处理全部结果,User 只处理筛选后的结果
+const resultsToProcess = isAdmin ? validationResults : filteredResults
+```
+
+**差异总结**:
+
+| 维度 | Admin | User |
+|------|-------|------|
+| 处理范围 | `validationResults`(全部) | `filteredResults`(自己的+无负责人的) |
+| 可操作物料 | 所有负责人的物料 | 仅自己的 + 无负责人的 |
+| 能否修改他人数据 | 是 | 否 |
+
+---
+
+## 阶段三:执行清理(ERP 删除)
+
+**源码位置**:
+- 前端调用: `src/renderer/src/hooks/cleaner/api.ts:116-166`
+- 获取数据: `src/main/services/validation/validation-application-service.ts:497-655`
+- 执行删除: `src/main/services/cleaner/cleaner-application-service.ts`
+
+```mermaid
+sequenceDiagram
+ participant UI as 前端 useCleaner
+ participant API as api.ts
+ participant Main as 主进程
+ participant DB as 数据库
+ participant ERP as ERP 系统
+
+ UI->>API: runCleanerExecution({ dryRun, selectedManagers, ... })
+ API->>Main: getCleanerData({ selectedManagers })
+
+ alt Admin + selectedManagers 非空
+ Main->>DB: SELECT MaterialCode FROM MaterialsToBeDeleted
WHERE ManagerName IN (@manager0, @manager1, ...)
+ Note over Main,DB: 按选中的负责人过滤
从 MaterialsToBeDeleted 获取
+ else Admin + selectedManagers 为空
+ Main->>DB: SELECT DISTINCT MaterialCode FROM DiscreteMaterialPlanData
WHERE SourceNumber IN (orderNumbers)
+ Note over Main,DB: 按订单号查询
从 DiscreteMaterialPlanData 获取
+ else User
+ Main->>DB: SELECT MaterialCode FROM MaterialsToBeDeleted
WHERE ManagerName = @username
+ Note over Main,DB: 按 ManagerName 过滤
仅获取自己的物料代码
+ end
+
+ DB-->>Main: materialCodes[]
+ Main-->>API: { orderNumbers, materialCodes }
+
+ Note over API: 传入角色过滤后的 materialCodes
+ API->>Main: cleaner.runCleaner({ orderNumbers, materialCodes, ... })
+
+ Main->>ERP: 按订单遍历,删除指定物料
+ ERP-->>Main: 删除结果
+ Main-->>API: CleanerResult
+ API-->>UI: 显示执行报告
+```
+
+**SQL 差异**:
+
+```mermaid
+flowchart TB
+ subgraph AdminWithMgr["Admin + selectedManagers 非空"]
+ A1["SELECT MaterialCode
FROM MaterialsToBeDeleted
WHERE ManagerName IN (@manager0, ...)
AND MaterialCode IS NOT NULL"]
+ end
+
+ subgraph AdminNoMgr["Admin + selectedManagers 为空"]
+ A2["SELECT DISTINCT MaterialCode
FROM DiscreteMaterialPlanData
WHERE SourceNumber IN (orderNumbers)"]
+ end
+
+ subgraph User["User 查询"]
+ U1["SELECT MaterialCode
FROM MaterialsToBeDeleted
WHERE ManagerName = @username
AND MaterialCode IS NOT NULL"]
+ end
+
+ AdminWithMgr --> |"按选中负责人过滤"| Result([传入 runCleaner])
+ AdminNoMgr --> |"按订单号查 DiscreteMaterialPlanData"| Result
+ User --> |"仅返回自己的物料代码"| Result
+```
+
+**差异总结**:
+
+| 维度 | Admin(有 selectedManagers) | Admin(无 selectedManagers) | User |
+|------|---------------------------|----------------------------|------|
+| 数据源 | `MaterialsToBeDeleted` | `DiscreteMaterialPlanData` | `MaterialsToBeDeleted` |
+| 查询条件 | `WHERE ManagerName IN (...)` | `WHERE SourceNumber IN (orderNumbers)` | `WHERE ManagerName = @username` |
+| 可删除物料 | 选中负责人的物料 | 订单关联的全部物料 | 仅自己标记的物料 |
+| 无订单号时 | — | 返回空数组 | — |
+
+---
+
+## 数据安全边界
+
+角色隔离在**三个层面**同时生效,形成纵深防御:
+
+```mermaid
+flowchart TB
+ subgraph layer1["第一层:前端过滤"]
+ L1["filterValidationResults()
User 仅看到自己的物料"]
+ end
+
+ subgraph layer2["第二层:同步范围"]
+ L2["handleConfirmDeletion()
User 仅同步 filteredResults"]
+ end
+
+ subgraph layer3["第三层:后端查询"]
+ L3["loadMaterialCodesForCleaner()
Admin: WHERE ManagerName IN (selectedManagers)
User: SQL WHERE ManagerName = user"]
+ end
+
+ L1 -->|"防止误操作"| L2
+ L2 -->|"缩小同步范围"| L3
+ L3 -->|"最终保证"| Safe([User 无法删除他人物料])
+```
+
+> **注意**:`runCleaner()` 本身不做角色过滤,它信任上游传入的 `materialCodes` 已经过角色过滤。安全性由 `getCleanerData()` 的 SQL 查询保证。
+
+---
+
+## 涉及文件索引
+
+| 文件 | 关键函数/逻辑 | 行号 |
+|------|---------------|------|
+| `src/renderer/src/hooks/cleaner/api.ts` | `initializeCleanerPage()`, `runCleanerExecution()` | 25-52, 116-166 |
+| `src/renderer/src/hooks/useCleaner.ts` | `handleConfirmDeletion()`, 初始化逻辑 | 98-120, 289-345 |
+| `src/renderer/src/hooks/cleaner/helpers.ts` | `filterValidationResults()`, `buildDeletionPlan()` | 34-57, 59-92 |
+| `src/main/services/validation/validation-application-service.ts` | `getCleanerData()`, `loadMaterialCodesForCleaner()`, `queryMaterialCodesByManagers()` | 232-305, 497-604, 606-655 |
+| `src/main/services/cleaner/cleaner-application-service.ts` | `runCleaner()` | 31-168 |
+| `src/main/ipc/cleaner-handler.ts` | `CLEANER_RUN` handler | 16-22 |
+| `src/main/ipc/validation-handler.ts` | `getCleanerData` handler | 194-223 |
+| `src/preload/api/validation.ts` | `getCleanerData()` IPC 桥接 | 11-12 |
+| `src/renderer/src/pages/CleanerPage.tsx` | 页面组件,条件渲染侧边栏 | 74-82 |
diff --git a/src/main/ipc/validation-handler.ts b/src/main/ipc/validation-handler.ts
index 65e3819..46327d4 100644
--- a/src/main/ipc/validation-handler.ts
+++ b/src/main/ipc/validation-handler.ts
@@ -194,7 +194,8 @@ export function registerValidationHandlers(): void {
ipcMain.handle(
IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA,
async (
- event
+ event,
+ params?: { selectedManagers?: string[] }
): Promise<{
success: boolean
orderNumbers?: string[]
@@ -213,7 +214,11 @@ export function registerValidationHandlers(): void {
}
}
- return validationApplicationService.getCleanerData(userInfo, event.sender.id)
+ return validationApplicationService.getCleanerData(
+ userInfo,
+ event.sender.id,
+ params?.selectedManagers ?? []
+ )
}
)
}
diff --git a/src/main/services/validation/validation-application-service.ts b/src/main/services/validation/validation-application-service.ts
index 99e8e3a..d64f80f 100644
--- a/src/main/services/validation/validation-application-service.ts
+++ b/src/main/services/validation/validation-application-service.ts
@@ -231,7 +231,8 @@ export class ValidationApplicationService {
async getCleanerData(
userInfo: UserInfo,
- senderId: number
+ senderId: number,
+ selectedManagers: string[] = []
): Promise<{
success: boolean
orderNumbers?: string[]
@@ -250,6 +251,7 @@ export class ValidationApplicationService {
userId: userInfo.id,
username,
isAdmin,
+ selectedManagers,
requestId
})
@@ -270,7 +272,13 @@ export class ValidationApplicationService {
})
}
- const materialCodes = await this.loadMaterialCodesForCleaner(dbService, username, isAdmin)
+ const materialCodes = await this.loadMaterialCodesForCleaner(
+ dbService,
+ username,
+ isAdmin,
+ selectedManagers,
+ orderNumbers
+ )
log.info('Cleaner data retrieved', {
userId: userInfo.id,
orderCount: orderNumbers.length,
@@ -489,27 +497,54 @@ export class ValidationApplicationService {
private async loadMaterialCodesForCleaner(
dbService: ValidationDatabaseService,
username: string,
- isAdmin: boolean
+ isAdmin: boolean,
+ selectedManagers: string[],
+ orderNumbers: string[]
): Promise {
const markedTableName = getValidationTableName('dbo_MaterialsToBeDeleted')
- if (isAdmin) {
- const result = await dbService.query(
- `
- SELECT MaterialCode
- FROM ${markedTableName}
- WHERE MaterialCode IS NOT NULL
- `
+ // Admin with selected managers: filter MaterialsToBeDeleted by ManagerName IN (selectedManagers)
+ if (isAdmin && selectedManagers && selectedManagers.length > 0) {
+ const materialCodes = await this.queryMaterialCodesByManagers(
+ dbService,
+ markedTableName,
+ selectedManagers
)
- const materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean)
- log.info(`Admin user: got ${materialCodes.length} materials`, {
+ log.info(`Admin with selected managers: got ${materialCodes.length} materials`, {
userId: username,
isAdmin: true,
+ selectedManagers,
materialCount: materialCodes.length
})
return materialCodes
}
+ // Admin without selected managers: query all materials from DiscreteMaterialPlanData by orderNumbers
+ if (isAdmin) {
+ if (orderNumbers.length === 0) {
+ log.warn('Admin without selected managers but no orderNumbers available', {
+ userId: username
+ })
+ return []
+ }
+ const materialDao = new DiscreteMaterialPlanDAO()
+ const records = await materialDao.queryBySourceNumbersDistinct(orderNumbers)
+ const materialCodes = [
+ ...new Set(records.map((r) => r.MaterialCode as string).filter(Boolean))
+ ]
+ log.info(
+ `Admin without selected managers: got ${materialCodes.length} materials from DiscreteMaterialPlanData`,
+ {
+ userId: username,
+ isAdmin: true,
+ orderCount: orderNumbers.length,
+ materialCount: materialCodes.length
+ }
+ )
+ return materialCodes
+ }
+
+ // Regular user: filter MaterialsToBeDeleted by ManagerName = username
if (dbService.type === 'sqlserver') {
const sql = await import('mssql')
const result = await (dbService as SqlServerService).queryWithParams(
@@ -568,6 +603,57 @@ export class ValidationApplicationService {
return materialCodes
}
+ private async queryMaterialCodesByManagers(
+ dbService: ValidationDatabaseService,
+ tableName: string,
+ managers: string[]
+ ): Promise {
+ if (dbService.type === 'sqlserver') {
+ const sql = await import('mssql')
+ const params: Record = {}
+ const paramNames = managers.map((m, i) => {
+ const name = `@manager${i}`
+ params[`manager${i}`] = { value: m, type: sql.default.NVarChar }
+ return name
+ })
+ const result = await (dbService as SqlServerService).queryWithParams(
+ `
+ SELECT MaterialCode
+ FROM ${tableName}
+ WHERE ManagerName IN (${paramNames.join(', ')}) AND MaterialCode IS NOT NULL
+ `,
+ params
+ )
+ return result.rows
+ .map((row: Record) => row.MaterialCode as string)
+ .filter(Boolean)
+ }
+
+ if (dbService.type === 'postgresql') {
+ const placeholders = managers.map((_, i) => `$${i + 1}`).join(', ')
+ const result = await dbService.query(
+ `
+ SELECT "MaterialCode"
+ FROM ${tableName}
+ WHERE "ManagerName" IN (${placeholders}) AND "MaterialCode" IS NOT NULL
+ `,
+ managers
+ )
+ return result.rows.map((row) => row.MaterialCode as string).filter(Boolean)
+ }
+
+ const placeholders = managers.map(() => '?').join(', ')
+ const result = await dbService.query(
+ `
+ SELECT MaterialCode
+ FROM ${tableName}
+ WHERE ManagerName IN (${placeholders}) AND MaterialCode IS NOT NULL
+ `,
+ managers
+ )
+ return result.rows.map((row) => row.MaterialCode as string).filter(Boolean)
+ }
+
private async disconnectQuietly(dbService: ValidationDatabaseService): Promise {
try {
await dbService.disconnect()
diff --git a/src/preload/api/validation.ts b/src/preload/api/validation.ts
index fda5b39..7e3301f 100644
--- a/src/preload/api/validation.ts
+++ b/src/preload/api/validation.ts
@@ -8,5 +8,6 @@ export const validationApi = {
invokeIpc(IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS, productionIds),
getSharedProductionIds: () => invokeIpc(IPC_CHANNELS.VALIDATION_GET_SHARED_PRODUCTION_IDS),
clearSharedProductionIds: () => invokeIpc(IPC_CHANNELS.VALIDATION_CLEAR_SHARED_PRODUCTION_IDS),
- getCleanerData: () => invokeIpc(IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA)
+ getCleanerData: (params?: { selectedManagers?: string[] }) =>
+ invokeIpc(IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA, params ?? { selectedManagers: [] })
} as const
diff --git a/src/renderer/src/hooks/cleaner/api.ts b/src/renderer/src/hooks/cleaner/api.ts
index 20432fb..5840423 100644
--- a/src/renderer/src/hooks/cleaner/api.ts
+++ b/src/renderer/src/hooks/cleaner/api.ts
@@ -118,8 +118,11 @@ export async function runCleanerExecution(params: {
headless: boolean
queryBatchSize: number
processConcurrency: number
+ selectedManagers: string[]
}): Promise {
- const cleanerDataResult = await window.electron.validation.getCleanerData()
+ const cleanerDataResult = await window.electron.validation.getCleanerData({
+ selectedManagers: params.selectedManagers
+ })
const cleanerData = cleanerDataResult.success
? (cleanerDataResult.data as CleanerDataPayload | null)
: null
diff --git a/src/renderer/src/hooks/useCleaner.ts b/src/renderer/src/hooks/useCleaner.ts
index 3fbdd40..3882ca9 100644
--- a/src/renderer/src/hooks/useCleaner.ts
+++ b/src/renderer/src/hooks/useCleaner.ts
@@ -376,7 +376,8 @@ export function useCleaner() {
dryRun,
headless,
queryBatchSize,
- processConcurrency
+ processConcurrency,
+ selectedManagers: Array.from(selectedManagers)
})
setReportData(result)
} catch (err) {
diff --git a/tests/unit/services/validation/validation-application-service.test.ts b/tests/unit/services/validation/validation-application-service.test.ts
index 7e7dfb5..55c1062 100644
--- a/tests/unit/services/validation/validation-application-service.test.ts
+++ b/tests/unit/services/validation/validation-application-service.test.ts
@@ -324,26 +324,56 @@ describe('ValidationApplicationService', () => {
// ─── getCleanerData ───
describe('getCleanerData', () => {
- it('should return order numbers and material codes for admin user', async () => {
+ it('Admin with selected managers should query MaterialsToBeDeleted by ManagerName IN', async () => {
mockSharedIdsGet.mockReturnValue(['PROD001'])
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
- const result = await service.getCleanerData(userInfo, 1)
+ const result = await service.getCleanerData(userInfo, 1, ['Mgr', 'Other'])
expect(result.success).toBe(true)
expect(result.orderNumbers).toBeDefined()
expect(result.orderNumbers!.length).toBeGreaterThan(0)
+ // materialCodes come from MaterialsToBeDeleted query (mock returns M1)
expect(result.materialCodes).toBeDefined()
+ expect(result.materialCodes).toContain('M1')
})
- it('should return empty order numbers when no shared IDs', async () => {
+ it('Admin without selected managers should query DiscreteMaterialPlanData by orderNumbers', async () => {
+ mockSharedIdsGet.mockReturnValue(['PROD001'])
+ const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
+
+ const result = await service.getCleanerData(userInfo, 1, [])
+
+ expect(result.success).toBe(true)
+ expect(result.orderNumbers).toBeDefined()
+ // materialCodes come from DiscreteMaterialPlanDAO.queryBySourceNumbersDistinct
+ // mock returns MF1
+ expect(result.materialCodes).toBeDefined()
+ expect(result.materialCodes).toContain('MF1')
+ })
+
+ it('Admin without selected managers and no orderNumbers should return empty material codes', async () => {
mockSharedIdsGet.mockReturnValue([])
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any
+ const result = await service.getCleanerData(userInfo, 1, [])
+
+ expect(result.success).toBe(true)
+ expect(result.orderNumbers).toEqual([])
+ expect(result.materialCodes).toEqual([])
+ })
+
+ it('regular user should filter by ManagerName = username', async () => {
+ mockSharedIdsGet.mockReturnValue(['PROD001'])
+ const userInfo = { id: 2, username: 'guest', userType: 'User' } as any
+
const result = await service.getCleanerData(userInfo, 1)
expect(result.success).toBe(true)
- expect(result.orderNumbers).toEqual([])
+ // materialCodes from MaterialsToBeDeleted WHERE ManagerName = 'guest'
+ // mock returns M1 for any MaterialsToBeDeleted query
+ expect(result.materialCodes).toBeDefined()
+ expect(result.materialCodes).toContain('M1')
})
it('should handle errors gracefully', async () => {