180 lines
9.1 KiB
TypeScript
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 })
|
|
})
|
|
})
|