import { aggregateTeamCapacity } from './ding-workflow-team-capacity' function task(overrides: Partial<{ taskId: string activityId: string | null nodeName: string assigneeUserId: string assigneeName: string createdAt: string | null finishedAt: string | null isActive: boolean }> = {}) { return { taskId: 'task-1', activityId: 'activity-1', nodeName: '制作', assigneeUserId: 'user-1', assigneeName: '员工一', assigneeDepartment: '品牌中心/设计部', isBrandCenter: true, isLeadership: false, isActive: true, reviewPassed: false, result: null, createdAt: '2026-08-31T01:00:00.000Z', finishedAt: null, ...overrides, } } function workflow(overrides: Partial<{ id: string status: string processCode: string projectType: string sourceCreatedAt: string | null sourceFinishedAt: string | null activeTasks: ReturnType[] }> = {}) { return { id: 'flow-1', businessId: 'business-1', title: '流程一', status: 'RUNNING', processCode: 'PROC-3571C915-0506-4490-A91A-8AA7EA33F838', projectType: '美术设计流程', brand: '亿家达', requesterName: '需求人', requesterDepartment: '品牌中心', initiatorName: '需求人', initiatorDepartment: '品牌中心', sourceCreatedAt: '2026-08-31T01:00:00.000Z', sourceFinishedAt: null, activeTasks: [task()], ...overrides, } } describe('aggregateTeamCapacity', () => { it('按部门归属统计八周收到/完结及明细,并保留在职人数基数', () => { const result = aggregateTeamCapacity( [ { id: 'user-1', dingtalkUserId: 'user-1', name: '员工一', departmentName: '品牌创意A部' }, { id: 'user-2', dingtalkUserId: 'user-2', name: '员工二', departmentName: '品牌创意A部' }, ], [ workflow(), workflow({ id: 'flow-2', status: 'COMPLETED', sourceCreatedAt: '2026-08-20T01:00:00.000Z', sourceFinishedAt: '2026-08-27T01:00:00.000Z', activeTasks: [task({ taskId: 'task-2', assigneeUserId: 'user-2', assigneeName: '员工二', createdAt: '2026-08-20T01:00:00.000Z', finishedAt: '2026-08-27T00:00:00.000Z', isActive: false })], }), ], { lastSyncedAt: null, syncStatus: 'ready' }, new Date('2026-09-01T02:00:00.000Z'), ) expect(result.departments).toHaveLength(5) const department = result.departments[0] expect(department.personnelCount).toBe(2) expect(department.weeks).toHaveLength(8) const currentWeek = department.weeks.at(-1)! expect(currentWeek.receivedCount).toBe(1) expect(currentWeek.completedCount).toBe(0) expect(currentWeek.involvedPersonnelCount).toBe(1) expect(currentWeek.items[0]).toMatchObject({ id: 'flow-1', receivedInWeek: true, completedInWeek: false, isOngoing: true }) const priorWeek = department.weeks.find((week) => week.key === '2026-08-24')! expect(priorWeek.completedCount).toBe(1) expect(priorWeek.involvedPersonnelCount).toBe(1) expect(priorWeek.items[0]).toMatchObject({ id: 'flow-2', completedInWeek: true, isOngoing: false }) }) it('同一流程同一部门在同一周只计一次,且忽略无法映射的处理人', () => { const result = aggregateTeamCapacity( [ { id: 'user-1', dingtalkUserId: 'user-1', name: '员工一', departmentName: '品牌创意A部' }, { id: 'user-2', dingtalkUserId: 'user-2', name: '员工二', departmentName: '品牌创意B部' }, { id: 'user-3', dingtalkUserId: 'user-3', name: '员工三', departmentName: '综合支持部' }, { id: 'user-4', dingtalkUserId: 'user-4', name: '员工四', departmentName: '品牌设计部' }, { id: 'user-5', dingtalkUserId: 'user-5', name: '员工五', departmentName: '空间视觉部' }, { id: 'ignored', dingtalkUserId: 'ignored', name: '不展示人员', departmentName: '商品企划部' }, ], [workflow({ activeTasks: [task(), task({ taskId: 'task-2', createdAt: '2026-08-31T02:00:00.000Z' }), task({ taskId: 'task-3', assigneeUserId: 'unknown' })] })], { lastSyncedAt: null, syncStatus: 'pending' }, new Date('2026-09-01T02:00:00.000Z'), ) expect(result.departments.map((department) => department.name)).toEqual([ '品牌创意A部', '品牌创意B部', '综合支持部', '品牌设计部', '空间视觉部', ]) const currentWeek = result.departments[0].weeks.at(-1)! expect(currentWeek.receivedCount).toBe(1) expect(currentWeek.involvedPersonnelCount).toBe(1) expect(currentWeek.items).toHaveLength(1) }) it('按流程类型排除主管分配和审核节点,不计入负责人和参与人数', () => { const result = aggregateTeamCapacity( [ { id: 'worker-art', dingtalkUserId: 'worker-art', name: '美术员工', departmentName: '品牌创意A部' }, { id: 'manager-art', dingtalkUserId: 'manager-art', name: '策划主管', departmentName: '品牌创意A部' }, { id: 'worker-content', dingtalkUserId: 'worker-content', name: '内容员工', departmentName: '品牌创意A部' }, { id: 'manager-content', dingtalkUserId: 'manager-content', name: '内容主管', departmentName: '品牌创意A部' }, ], [ workflow({ activeTasks: [ task({ taskId: 'art-worker', assigneeUserId: 'worker-art', assigneeName: '美术员工', nodeName: '出图' }), task({ taskId: 'art-assignment', assigneeUserId: 'manager-art', assigneeName: '策划主管', activityId: '38da_544a', nodeName: '品牌策划主管分配任务' }), task({ taskId: 'art-review', assigneeUserId: 'manager-art', assigneeName: '策划主管', activityId: 'c255_84b3', nodeName: '品牌策划主管审核', finishedAt: '2026-08-31T02:00:00.000Z', isActive: false }), ], }), workflow({ id: 'content-flow', processCode: 'PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673', projectType: '内容视频和图片拍摄', activeTasks: [ task({ taskId: 'content-worker', assigneeUserId: 'worker-content', assigneeName: '内容员工', nodeName: '剪辑' }), task({ taskId: 'content-owner-assignment', assigneeUserId: 'manager-content', assigneeName: '内容主管', activityId: 'cc59_3e5a', nodeName: '内容负责人分配任务' }), task({ taskId: 'content-assignment', assigneeUserId: 'manager-content', assigneeName: '内容主管', activityId: '9fb8_32e7', nodeName: '内容主管分配任务' }), task({ taskId: 'content-review', assigneeUserId: 'manager-content', assigneeName: '内容主管', activityId: '6c43_575f', nodeName: '内容主管审核', finishedAt: '2026-08-31T02:00:00.000Z', isActive: false }), ], }), ], { lastSyncedAt: null, syncStatus: 'ready' }, new Date('2026-09-01T02:00:00.000Z'), ) const week = result.departments[0].weeks.at(-1)! expect(week.receivedCount).toBe(2) expect(week.involvedPersonnelCount).toBe(2) expect(week.items.map((item) => item.assigneeNames)).toEqual([ ['美术员工'], ['内容员工'], ]) }) it('已知 activityId 优先于旧节点名称,未知 activityId 不按文字猜测', () => { const result = aggregateTeamCapacity( [{ id: 'worker', dingtalkUserId: 'worker', name: '员工', departmentName: '品牌创意A部' }], [workflow({ activeTasks: [ task({ taskId: 'known-production', assigneeUserId: 'worker', assigneeName: '员工', activityId: 'a9e1_5be0', nodeName: '品牌策划主管审核' }), task({ taskId: 'unknown-label', assigneeUserId: 'worker', assigneeName: '员工', activityId: 'future_activity', nodeName: '品牌策划主管审核' }), ], })], { lastSyncedAt: null, syncStatus: 'ready' }, new Date('2026-09-01T02:00:00.000Z'), ) const week = result.departments[0].weeks.at(-1)! expect(week.receivedCount).toBe(1) expect(week.involvedPersonnelCount).toBe(1) expect(week.items[0].assigneeNames).toEqual(['员工']) }) it('所有周次都使用当前钉钉组织架构中的在职人数,并按流程类型拆分', () => { const result = aggregateTeamCapacity( [ { id: 'user-1', dingtalkUserId: 'user-1', name: '员工一', departmentName: '品牌创意A部' }, { id: 'user-2', dingtalkUserId: 'user-2', name: '员工二', departmentName: '品牌创意A部' }, ], [workflow({ projectType: '美术设计流程' }), workflow({ id: 'content-flow', processCode: 'PROC-AE9FF3E1-A442-4E6A-85CF-0F2B86409673', projectType: '内容视频和图片拍摄', activeTasks: [task({ taskId: 'content-task', assigneeUserId: 'user-2', assigneeName: '员工二' })], })], { lastSyncedAt: null, syncStatus: 'ready' }, new Date('2026-09-01T02:00:00.000Z'), ) const department = result.departments[0] expect(department.weeks.every((week) => week.personnelCount === 2)).toBe(true) expect(department.processes.map((process) => process.label)).toEqual(['美术设计流程', '内容视频流程']) expect(department.processes[0].weeks.at(-1)).toMatchObject({ receivedCount: 1, completedCount: 0 }) expect(department.processes[1].weeks.at(-1)).toMatchObject({ receivedCount: 1, completedCount: 0 }) }) })