import { buildTeamWorkloadInsights } from '@/lib/ding-workflow-sync' jest.mock('mssql', () => ({ __esModule: true, default: {}, })) jest.mock('@/lib/prisma', () => ({ prisma: {} })) jest.mock('@/lib/brand-center-access', () => ({ resolveBrandCenterAccessForUser: jest.fn() })) function rosterRow(overrides: Partial<{ id: string dingtalkUserId: string | null displayName: string flowerName: string | null realName: string | null departmentName: string | null departmentPath: string | null employmentState: string | null departmentMemberships: Array<{ departmentName: string | null; departmentPath: string | null; isPrimary: boolean }> }> = {}) { return { id: 'person-1', dingtalkUserId: 'ding-1', displayName: '一号员工', flowerName: null, realName: null, departmentName: '设计部', departmentPath: '品牌中心/设计部', employmentState: 'ACTIVE', departmentMemberships: [], ...overrides, } } function snapshotRow(id: string, assignees: string[], title = id) { return { processInstanceId: id, businessId: `business-${id}`, title, processCode: 'PROC-ART', projectType: '美术设计流程', activeTasks: assignees.map((assigneeUserId, index) => ({ taskId: `${id}-task-${index}`, activityId: `activity-${index}`, nodeName: `节点${index + 1}`, assigneeUserId, assigneeName: assigneeUserId, assigneeDepartment: '品牌中心/设计部', isBrandCenter: true, isLeadership: false, isActive: true, reviewPassed: false, result: null, createdAt: null, finishedAt: null, })), } } describe('buildTeamWorkloadInsights', () => { it('排除指定人员、把空闲人员计入平均值,并按流程去重', () => { const insights = buildTeamWorkloadInsights( [ rosterRow({ id: 'busy', dingtalkUserId: 'busy', displayName: '忙碌员工' }), rosterRow({ id: 'idle', dingtalkUserId: 'idle', displayName: '空闲员工', departmentName: '内容部', departmentPath: '品牌中心/内容部' }), rosterRow({ id: 'excluded', dingtalkUserId: 'excluded', displayName: '元泽' }), rosterRow({ id: 'resigned', dingtalkUserId: 'resigned', displayName: '离职员工', employmentState: 'RESIGNED' }), ], [ snapshotRow('project-1', ['busy', 'busy']), snapshotRow('project-2', ['busy']), snapshotRow('project-3', ['busy']), snapshotRow('project-excluded', ['excluded']), ], { lastSyncedAt: '2026-08-31T02:00:00.000Z', syncStatus: 'ready' }, ) expect(insights.summary).toMatchObject({ averageLoadPercent: 75, personnelCount: 2, busyPersonnel: 1, idlePersonnel: 1, activeProjectAssignments: 3, sourceOngoingProjects: 4, }) expect(insights.departments).toEqual(expect.arrayContaining([ expect.objectContaining({ name: '设计部', personnelCount: 1, averageLoadPercent: 150 }), expect.objectContaining({ name: '内容部', personnelCount: 1, averageLoadPercent: 0 }), ])) const busy = insights.departments.find((department) => department.name === '设计部')!.people[0] expect(busy.activeProjectCount).toBe(3) expect(busy.loadPercent).toBe(150) expect(busy.status).toBe('overloaded') expect(busy.projects).toHaveLength(3) }) it('只统计当前活跃节点,并以当前花名册组织归属为准', () => { const insights = buildTeamWorkloadInsights( [rosterRow({ departmentName: '品牌中心', departmentPath: '品牌中心', departmentMemberships: [{ departmentName: '设计部', departmentPath: '品牌中心/设计部', isPrimary: false }], })], [{ ...snapshotRow('project-1', ['ding-1']), activeTasks: [ { ...snapshotRow('project-1', ['ding-1']).activeTasks[0], isActive: false }, { ...snapshotRow('project-1', ['ding-1']).activeTasks[0], taskId: 'other', isBrandCenter: false, isActive: true }, ], }], { lastSyncedAt: null, syncStatus: 'pending' }, ) expect(insights.summary.activeProjectAssignments).toBe(1) expect(insights.summary.averageLoadPercent).toBe(50) expect(insights.departments[0]).toMatchObject({ name: '设计部', personnelCount: 1 }) }) it('排除商品企划部及其流程归属,不纳入人员和负荷平均值', () => { const insights = buildTeamWorkloadInsights( [ rosterRow({ id: 'design', dingtalkUserId: 'design', displayName: '设计员工' }), rosterRow({ id: 'planning', dingtalkUserId: 'planning', displayName: '企划员工', departmentName: '商品企划部', departmentPath: '品牌中心/商品企划部', }), ], [ snapshotRow('design-project', ['design']), snapshotRow('planning-project', ['planning']), ], { lastSyncedAt: null, syncStatus: 'ready' }, ) expect(insights.summary).toMatchObject({ personnelCount: 1, busyPersonnel: 1, idlePersonnel: 0, activeProjectAssignments: 1, averageLoadPercent: 50, sourceOngoingProjects: 2, excludedDepartmentNames: ['商品企划部'], }) expect(insights.departments).toHaveLength(1) expect(insights.departments[0]).toMatchObject({ name: '设计部', personnelCount: 1, activeProjectAssignments: 1, }) expect(insights.departments.some((department) => department.name === '商品企划部')).toBe(false) expect(insights.departments.flatMap((department) => department.people).map((person) => person.name)).toEqual(['设计员工']) }) })