feat(cleaner): support selectedManagers filtering for admin cleaner execution

Admin can now pass selectedManagers to getCleanerData so material codes
are queried from MaterialsToBeDeleted by ManagerName IN (selectedManagers).
When no managers are selected, fallback to DiscreteMaterialPlanData by
orderNumbers. User behavior is unchanged. Includes updated tests and
role-based flow documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-04-06 19:20:22 +08:00
parent 91f29a1167
commit 3be7959067
7 changed files with 455 additions and 21 deletions

View File

@@ -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["管理员列表 ← 全部负责人<br/>默认选中全部"]
I2 -->|User| I4["管理员列表 ← 空<br/>默认选中仅自己"]
end
subgraph validate["阶段二:校验 → 勾选 → 同步数据库"]
V1([点击校验]) --> V2["后端查询物料<br/>(不区分角色)"]
V2 --> V3["物料匹配算法<br/>User 有覆盖匹配)"]
V3 --> V4{角色判断}
V4 -->|Admin| V5["显示全部物料<br/>侧边栏可按负责人筛选"]
V4 -->|User| V6["仅显示自己的物料<br/>+ 无负责人的物料"]
V5 --> V7["用户勾选/取消勾选"]
V6 --> V7
V7 --> V8{点击同步数据库}
V8 --> V9{角色判断}
V9 -->|Admin| V10["处理范围:全部校验结果"]
V9 -->|User| V11["处理范围:仅筛选后结果"]
end
subgraph execute["阶段三执行清理ERP 删除)"]
E1([点击执行清理]) --> E2["getCleanerData(selectedManagers)<br/>获取物料代码"]
E2 --> E3{角色判断}
E3 -->|Admin| E3a{selectedManagers<br/>非空?}
E3a -->|"是"| E4["SQL WHERE ManagerName IN (选中)<br/>从 MaterialsToBeDeleted 获取"]
E3a -->|"否"| E4b["从 DiscreteMaterialPlanData<br/>按 orderNumbers 获取"]
E3 -->|User| E5["SQL WHERE ManagerName = 用户<br/>仅获取自己的物料代码"]
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<br/>判断是否管理员"]
GetAdmin --> GetUser["调用 auth:getCurrentUser<br/>获取当前用户名"]
GetUser --> RoleCheck{isAdmin?}
RoleCheck -->|Admin| GetManagers["调用 materials:getManagers<br/>获取全部负责人列表"]
GetManagers --> SelectAll["selectedManagers ← 全部负责人<br/>(默认全选)"]
SelectAll --> RenderSidebar["渲染 CleanerSidebar<br/>显示负责人复选框"]
RoleCheck -->|User| SetSelf["selectedManagers ← {currentUsername}<br/>(仅选中自己)"]
SetSelf --> NoSidebar["不渲染 CleanerSidebar<br/>无侧边栏"]
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<br/>MaterialsToBeDeleted<br/>精确匹配 MaterialCode?"}
P1 -->|"匹配"| SetManager["managerName ← 表中记录<br/>isMarkedForDeletion = true"]
P1 -->|"未匹配"| P2{"优先级2<br/>MaterialsTypeToBeDeleted<br/>MaterialName 包含匹配?"}
P2 -->|"匹配"| SetType["managerName ← 类型关键词负责人<br/>matchedTypeKeyword ← 匹配项"]
P2 -->|"未匹配"| SetNull["managerName = null"]
SetManager --> RoleCheck{角色?}
SetType --> RoleCheck
SetNull --> RoleCheck
RoleCheck -->|"Admin"| Skip["跳过覆盖<br/>使用当前结果"]
RoleCheck -->|"User"| P3{"优先级3User 覆盖)<br/>自己的类型关键词匹配?"}
P3 -->|"匹配"| Override["强制覆盖<br/>managerName ← 当前用户"]
P3 -->|"未匹配"| Keep["保持当前结果"]
Skip --> Next(["下一条物料"])
Override --> Next
Keep --> Next
```
**匹配优先级说明**:
| 优先级 | 数据源 | 匹配方式 | 适用角色 |
|--------|--------|----------|----------|
| 1最高 | `MaterialsToBeDeleted` | MaterialCode 精确匹配 | 全部 |
| 2 | `MaterialsTypeToBeDeleted` | MaterialName 包含匹配 | 全部 |
| 3User 覆盖) | 当前用户的类型关键词 | 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["按侧边栏选中的负责人过滤<br/>selectedManagers.has(managerName)<br/>|| !managerName"]
RoleCheck -->|User| FilterSelf["仅显示自己的 + 无负责人的<br/>managerName === currentUsername<br/>|| !managerName"]
FilterManagers --> FilterHidden["排除已隐藏的物料<br/>!hiddenItems.has(materialCode)"]
FilterSelf --> FilterHidden
FilterHidden --> Output([filteredResults<br/>用于表格显示])
```
### 2.3 确认删除(同步数据库)
**源码位置**: `src/renderer/src/hooks/useCleaner.ts:289-344`
```mermaid
flowchart TB
Start([点击确认删除]) --> RoleScope{角色判断}
RoleScope -->|Admin| UseAll["resultsToProcess = validationResults<br/>处理全部校验结果"]
RoleScope -->|User| UseFiltered["resultsToProcess = filteredResults<br/>仅处理筛选后结果"]
UseAll --> BuildPlan["buildDeletionPlan(resultsToProcess, selectedItems)"]
UseFiltered --> BuildPlan
BuildPlan --> Loop["遍历 resultsToProcess"]
Loop --> Check{物料是否勾选?}
Check -->|"已勾选"| HasManager{有负责人?}
Check -->|"未勾选"| ToDelete["加入 materialsToDelete<br/>从数据库移除标记"]
HasManager -->|"有"| ToUpsert["加入 materialsToUpsert<br/>写入/更新到数据库"]
HasManager -->|"无"| Missing["加入 missingManager<br/>阻止操作"]
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<br/>WHERE ManagerName IN (@manager0, @manager1, ...)
Note over Main,DB: 按选中的负责人过滤<br/>从 MaterialsToBeDeleted 获取
else Admin + selectedManagers 为空
Main->>DB: SELECT DISTINCT MaterialCode FROM DiscreteMaterialPlanData<br/>WHERE SourceNumber IN (orderNumbers)
Note over Main,DB: 按订单号查询<br/>从 DiscreteMaterialPlanData 获取
else User
Main->>DB: SELECT MaterialCode FROM MaterialsToBeDeleted<br/>WHERE ManagerName = @username
Note over Main,DB: 按 ManagerName 过滤<br/>仅获取自己的物料代码
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<br/>FROM MaterialsToBeDeleted<br/>WHERE ManagerName IN (@manager0, ...)<br/>AND MaterialCode IS NOT NULL"]
end
subgraph AdminNoMgr["Admin + selectedManagers 为空"]
A2["SELECT DISTINCT MaterialCode<br/>FROM DiscreteMaterialPlanData<br/>WHERE SourceNumber IN (orderNumbers)"]
end
subgraph User["User 查询"]
U1["SELECT MaterialCode<br/>FROM MaterialsToBeDeleted<br/>WHERE ManagerName = @username<br/>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()<br/>User 仅看到自己的物料"]
end
subgraph layer2["第二层:同步范围"]
L2["handleConfirmDeletion()<br/>User 仅同步 filteredResults"]
end
subgraph layer3["第三层:后端查询"]
L3["loadMaterialCodesForCleaner()<br/>Admin: WHERE ManagerName IN (selectedManagers)<br/>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 |

View File

@@ -194,7 +194,8 @@ export function registerValidationHandlers(): void {
ipcMain.handle( ipcMain.handle(
IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA, IPC_CHANNELS.VALIDATION_GET_CLEANER_DATA,
async ( async (
event event,
params?: { selectedManagers?: string[] }
): Promise<{ ): Promise<{
success: boolean success: boolean
orderNumbers?: string[] 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 ?? []
)
} }
) )
} }

View File

@@ -231,7 +231,8 @@ export class ValidationApplicationService {
async getCleanerData( async getCleanerData(
userInfo: UserInfo, userInfo: UserInfo,
senderId: number senderId: number,
selectedManagers: string[] = []
): Promise<{ ): Promise<{
success: boolean success: boolean
orderNumbers?: string[] orderNumbers?: string[]
@@ -250,6 +251,7 @@ export class ValidationApplicationService {
userId: userInfo.id, userId: userInfo.id,
username, username,
isAdmin, isAdmin,
selectedManagers,
requestId 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', { log.info('Cleaner data retrieved', {
userId: userInfo.id, userId: userInfo.id,
orderCount: orderNumbers.length, orderCount: orderNumbers.length,
@@ -489,27 +497,54 @@ export class ValidationApplicationService {
private async loadMaterialCodesForCleaner( private async loadMaterialCodesForCleaner(
dbService: ValidationDatabaseService, dbService: ValidationDatabaseService,
username: string, username: string,
isAdmin: boolean isAdmin: boolean,
selectedManagers: string[],
orderNumbers: string[]
): Promise<string[]> { ): Promise<string[]> {
const markedTableName = getValidationTableName('dbo_MaterialsToBeDeleted') const markedTableName = getValidationTableName('dbo_MaterialsToBeDeleted')
if (isAdmin) { // Admin with selected managers: filter MaterialsToBeDeleted by ManagerName IN (selectedManagers)
const result = await dbService.query( if (isAdmin && selectedManagers && selectedManagers.length > 0) {
` const materialCodes = await this.queryMaterialCodesByManagers(
SELECT MaterialCode dbService,
FROM ${markedTableName} markedTableName,
WHERE MaterialCode IS NOT NULL selectedManagers
`
) )
const materialCodes = result.rows.map((row) => row.MaterialCode as string).filter(Boolean) log.info(`Admin with selected managers: got ${materialCodes.length} materials`, {
log.info(`Admin user: got ${materialCodes.length} materials`, {
userId: username, userId: username,
isAdmin: true, isAdmin: true,
selectedManagers,
materialCount: materialCodes.length materialCount: materialCodes.length
}) })
return materialCodes 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') { if (dbService.type === 'sqlserver') {
const sql = await import('mssql') const sql = await import('mssql')
const result = await (dbService as SqlServerService).queryWithParams( const result = await (dbService as SqlServerService).queryWithParams(
@@ -568,6 +603,57 @@ export class ValidationApplicationService {
return materialCodes return materialCodes
} }
private async queryMaterialCodesByManagers(
dbService: ValidationDatabaseService,
tableName: string,
managers: string[]
): Promise<string[]> {
if (dbService.type === 'sqlserver') {
const sql = await import('mssql')
const params: Record<string, { value: string; type: any }> = {}
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<string, unknown>) => 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<void> { private async disconnectQuietly(dbService: ValidationDatabaseService): Promise<void> {
try { try {
await dbService.disconnect() await dbService.disconnect()

View File

@@ -8,5 +8,6 @@ export const validationApi = {
invokeIpc(IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS, productionIds), invokeIpc(IPC_CHANNELS.VALIDATION_SET_SHARED_PRODUCTION_IDS, productionIds),
getSharedProductionIds: () => invokeIpc(IPC_CHANNELS.VALIDATION_GET_SHARED_PRODUCTION_IDS), getSharedProductionIds: () => invokeIpc(IPC_CHANNELS.VALIDATION_GET_SHARED_PRODUCTION_IDS),
clearSharedProductionIds: () => invokeIpc(IPC_CHANNELS.VALIDATION_CLEAR_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 } as const

View File

@@ -118,8 +118,11 @@ export async function runCleanerExecution(params: {
headless: boolean headless: boolean
queryBatchSize: number queryBatchSize: number
processConcurrency: number processConcurrency: number
selectedManagers: string[]
}): Promise<CleanerReportData> { }): Promise<CleanerReportData> {
const cleanerDataResult = await window.electron.validation.getCleanerData() const cleanerDataResult = await window.electron.validation.getCleanerData({
selectedManagers: params.selectedManagers
})
const cleanerData = cleanerDataResult.success const cleanerData = cleanerDataResult.success
? (cleanerDataResult.data as CleanerDataPayload | null) ? (cleanerDataResult.data as CleanerDataPayload | null)
: null : null

View File

@@ -376,7 +376,8 @@ export function useCleaner() {
dryRun, dryRun,
headless, headless,
queryBatchSize, queryBatchSize,
processConcurrency processConcurrency,
selectedManagers: Array.from(selectedManagers)
}) })
setReportData(result) setReportData(result)
} catch (err) { } catch (err) {

View File

@@ -324,26 +324,56 @@ describe('ValidationApplicationService', () => {
// ─── getCleanerData ─── // ─── getCleanerData ───
describe('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']) mockSharedIdsGet.mockReturnValue(['PROD001'])
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any 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.success).toBe(true)
expect(result.orderNumbers).toBeDefined() expect(result.orderNumbers).toBeDefined()
expect(result.orderNumbers!.length).toBeGreaterThan(0) expect(result.orderNumbers!.length).toBeGreaterThan(0)
// materialCodes come from MaterialsToBeDeleted query (mock returns M1)
expect(result.materialCodes).toBeDefined() 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([]) mockSharedIdsGet.mockReturnValue([])
const userInfo = { id: 1, username: 'admin', userType: 'Admin' } as any 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) const result = await service.getCleanerData(userInfo, 1)
expect(result.success).toBe(true) 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 () => { it('should handle errors gracefully', async () => {