From 3be7959067dcf29432e574ae14c23f5ca9a7140a Mon Sep 17 00:00:00 2001 From: Misaka Date: Mon, 6 Apr 2026 19:20:22 +0800 Subject: [PATCH] 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 --- docs/cleaner-role-based-flow.md | 308 ++++++++++++++++++ src/main/ipc/validation-handler.ts | 9 +- .../validation-application-service.ts | 110 ++++++- src/preload/api/validation.ts | 3 +- src/renderer/src/hooks/cleaner/api.ts | 5 +- src/renderer/src/hooks/useCleaner.ts | 3 +- .../validation-application-service.test.ts | 38 ++- 7 files changed, 455 insertions(+), 21 deletions(-) create mode 100644 docs/cleaner-role-based-flow.md 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 () => {