157 lines
5.6 KiB
TypeScript
157 lines
5.6 KiB
TypeScript
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(['设计员工'])
|
|
})
|
|
})
|