gongxiangp2/lib/ding-workflow-team-capacity.test.ts

180 lines
9.1 KiB
TypeScript

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<typeof task>[]
}> = {}) {
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 })
})
})