From 19174d5050f9b2dcd349c4f772aec92838420747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B1=9F=E6=9E=97=E9=93=96?= Date: Fri, 31 Jul 2026 09:40:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B1=95=E7=A4=BA=E5=B2=97=E4=BD=8D?= =?UTF-8?q?=E4=BA=BA=E5=91=98=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=85=AC=E5=85=B1?= =?UTF-8?q?=E7=9C=8B=E6=9D=BF=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PerformanceSurveyAdminConfig.test.tsx | 31 +++++++ .../PerformanceSurveyAdminConfig.tsx | 88 ++++++++++++++++--- .../performance-survey-position-admin.test.ts | 48 ++++++++++ .../performance-survey-service-policy.test.ts | 26 ++---- lib/performance-survey-position-config.ts | 9 +- lib/performance-survey-service.ts | 4 +- 6 files changed, 171 insertions(+), 35 deletions(-) diff --git a/app/performance-survey/PerformanceSurveyAdminConfig.test.tsx b/app/performance-survey/PerformanceSurveyAdminConfig.test.tsx index cfd5511..64b2d33 100644 --- a/app/performance-survey/PerformanceSurveyAdminConfig.test.tsx +++ b/app/performance-survey/PerformanceSurveyAdminConfig.test.tsx @@ -37,6 +37,17 @@ const config = { isActive: true, status: 'ready', memberCount: 6, + members: [{ + id: 'employee-ruolan', + displayName: '若兰', + departmentName: '品牌创意B部', + positionTitle: '营销策划主管', + }, { + id: 'employee-muyi', + displayName: '木一', + departmentName: '品牌创意A部', + positionTitle: '品牌策划', + }], rules: [{ id: 'rule-1', departmentPathPrefix: '集团/品牌中心', @@ -59,6 +70,7 @@ const config = { isActive: true, status: 'pending', memberCount: 2, + members: [], rules: [], effectivePlan: null, draftPlan: null, @@ -397,6 +409,25 @@ describe('PerformanceSurveyAdminConfig', () => { }))) }) + test('岗位人数可展开查看具体人员并可收起', async () => { + const request = createRequest() + render() + + const expandButton = (await screen.findAllByRole('button', { name: '展开人员名单' }))[0] + expect(expandButton).toHaveAttribute('aria-expanded', 'false') + expect(screen.queryByText('若兰')).not.toBeInTheDocument() + + fireEvent.click(expandButton) + + expect(expandButton).toHaveAttribute('aria-expanded', 'true') + expect(screen.getByRole('list', { name: '品牌设计师人员名单' })).toBeInTheDocument() + expect(screen.getByText('若兰')).toBeInTheDocument() + expect(screen.getByText('品牌创意B部 · 营销策划主管')).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: '收起人员名单' })) + expect(screen.queryByText('若兰')).not.toBeInTheDocument() + }) + test('人员批量分配提交最终 REST 请求体', async () => { const request = createRequest() render() diff --git a/app/performance-survey/PerformanceSurveyAdminConfig.tsx b/app/performance-survey/PerformanceSurveyAdminConfig.tsx index 49f72ea..379e26a 100644 --- a/app/performance-survey/PerformanceSurveyAdminConfig.tsx +++ b/app/performance-survey/PerformanceSurveyAdminConfig.tsx @@ -96,6 +96,13 @@ type PositionGroupRecord = { isActive: boolean status: 'ready' | 'pending' | 'disabled' memberCount: number + members?: Array<{ + id: string + displayName: string + departmentName?: string | null + departmentPath?: string | null + positionTitle?: string | null + }> rules: Array<{ id: string departmentPathPrefix: string @@ -837,6 +844,7 @@ function PositionConfigWorkspace({ const [editing, setEditing] = useState(Boolean(selected?.draftPlan) || !selected?.effectivePlan) const [creating, setCreating] = useState(false) const [newName, setNewName] = useState('') + const [expandedMemberGroupIds, setExpandedMemberGroupIds] = useState>(() => new Set()) const editorRef = useRef(null) useEffect(() => { @@ -850,6 +858,14 @@ function PositionConfigWorkspace({ }, [onDirtyChange, selected]) const markDirty = () => onDirtyChange(true) + const toggleMemberList = (groupId: string) => { + setExpandedMemberGroupIds((current) => { + const next = new Set(current) + if (next.has(groupId)) next.delete(groupId) + else next.add(groupId) + return next + }) + } const availableTemplateVersions = useMemo(() => data.templates.flatMap((template) => { const availability = getTemplateAvailability(template, month) @@ -1092,20 +1108,66 @@ function PositionConfigWorkspace({ {data.groups.map((group) => { const groupState = getPositionGroupState(group) + const memberListId = `position-group-members-${group.id}` + const groupLabelId = `position-group-label-${group.id}` + const isMemberListExpanded = expandedMemberGroupIds.has(group.id) return ( - +
+
+ +
+ +
+
+ {isMemberListExpanded && ( +
+ {(group.members?.length || 0) === 0 ? ( +

当前未配置人员

+ ) : ( +
    + {group.members?.map((member) => { + const memberMeta = [member.departmentName, member.positionTitle].filter(Boolean).join(' · ') + return ( +
  • + {member.displayName} + {memberMeta && {memberMeta}} +
  • + ) + })} +
+ )} +
+ )} +
) })} diff --git a/lib/__tests__/performance-survey-position-admin.test.ts b/lib/__tests__/performance-survey-position-admin.test.ts index fa122bf..3405d05 100644 --- a/lib/__tests__/performance-survey-position-admin.test.ts +++ b/lib/__tests__/performance-survey-position-admin.test.ts @@ -299,6 +299,54 @@ describe('performance survey position admin', () => { expect(prisma.performanceSurveyEmployeeProfile.findMany).not.toHaveBeenCalled() }) + test('岗位管理配置返回每个岗位的具体人员名单', async () => { + ;(prisma.performanceSurveyPositionGroup.findMany as jest.Mock).mockResolvedValue([{ + id: 'group-creative', + code: 'ai_planning_creative', + name: 'AI策划-创意部', + sortOrder: 10, + isActive: true, + isSystem: true, + archivedAt: null, + archiveReason: null, + rules: [], + plans: [], + _count: { surveys: 1, manualAssignments: 1 }, + }]) + ;(prisma.performanceSurveyTemplate.findMany as jest.Mock).mockResolvedValue([]) + ;(prisma.performanceSurveyEmployeeProfile.findMany as jest.Mock).mockResolvedValue([{ + id: 'employee-ruolan', + displayName: '若兰', + departmentName: '品牌创意B部', + departmentPath: '集团/品牌中心/品牌创意B部', + positionTitle: '营销策划主管', + departmentMemberships: [], + }]) + ;(prisma.performanceSurveyEmployeePositionAssignment.findMany as jest.Mock).mockResolvedValue([{ + id: 'assignment-ruolan', + employeeProfileId: 'employee-ruolan', + assignmentType: 'position', + positionGroupId: 'group-creative', + status: 'active', + startMonth: '2026-07', + endMonth: null, + createdAt: new Date('2026-07-01T00:00:00.000Z'), + }]) + + await expect(getPerformanceSurveyPositionAdminConfig('2026-07', 'positions')).resolves.toMatchObject({ + groups: [{ + id: 'group-creative', + memberCount: 1, + members: [{ + id: 'employee-ruolan', + displayName: '若兰', + departmentName: '品牌创意B部', + positionTitle: '营销策划主管', + }], + }], + }) + }) + test('复制模板会创建独立模板和完整草稿版本', async () => { const definition = structuredClone(DEFAULT_PERFORMANCE_SURVEY_TEMPLATES[0]) ;(prisma.performanceSurveyTemplateVersion.findUnique as jest.Mock).mockResolvedValue({ diff --git a/lib/__tests__/performance-survey-service-policy.test.ts b/lib/__tests__/performance-survey-service-policy.test.ts index 8e0eeef..5777981 100644 --- a/lib/__tests__/performance-survey-service-policy.test.ts +++ b/lib/__tests__/performance-survey-service-policy.test.ts @@ -623,7 +623,7 @@ describe('performance survey service policies', () => { expect(cell.rawMaxTotal).toBe(20) }) - test('公共看板关系查询继续应用领导组织范围及部门和人员筛选', async () => { + test('M级系统管理员查看公共看板时不再叠加领导组织范围', async () => { ;(ensurePerformanceSurveyEmployeeProfileForUser as jest.Mock).mockResolvedValue({ ...baseEmployee, levelType: 'M', @@ -640,26 +640,14 @@ describe('performance survey service policies', () => { const employeeWhere = (prisma.performanceSurveyTarget.findMany as jest.Mock).mock.calls[0][0] .where.employeeProfile.is - expect(employeeWhere.AND).toEqual(expect.arrayContaining([ - { - OR: [ - { departmentPath: '集团A/品牌中心/策划部' }, - { departmentPath: { startsWith: '集团A/品牌中心/策划部/' } }, - { - departmentMemberships: { - some: { - OR: [ - { departmentPath: '集团A/品牌中心/策划部' }, - { departmentPath: { startsWith: '集团A/品牌中心/策划部/' } }, - ], - }, - }, - }, - ], - }, + expect(employeeWhere).toEqual({ + AND: [ + expect.objectContaining({ OR: expect.any(Array) }), { id: 'employee-public' }, - ])) + ], + }) expect(JSON.stringify(employeeWhere)).toContain('集团A/品牌中心') + expect(JSON.stringify(employeeWhere)).not.toContain('集团A/品牌中心/策划部') expect(getPerformanceSurveyAccountProfileWhere).not.toHaveBeenCalled() }) diff --git a/lib/performance-survey-position-config.ts b/lib/performance-survey-position-config.ts index 5fce5df..aaf5436 100644 --- a/lib/performance-survey-position-config.ts +++ b/lib/performance-survey-position-config.ts @@ -1043,9 +1043,15 @@ export async function getPerformanceSurveyPositionAdminConfig( ), })) const memberCountByGroup = new Map() + const membersByGroup = new Map[]>() for (const item of memberships) { if (item.membership.group) { - memberCountByGroup.set(item.membership.group.id, (memberCountByGroup.get(item.membership.group.id) || 0) + 1) + const groupId = item.membership.group.id + memberCountByGroup.set(groupId, (memberCountByGroup.get(groupId) || 0) + 1) + membersByGroup.set(groupId, [ + ...(membersByGroup.get(groupId) || []), + serializePositionEmployee(item.employee), + ]) } } const unmatched = memberships @@ -1085,6 +1091,7 @@ export async function getPerformanceSurveyPositionAdminConfig( archiveReason: group.archiveReason, status: runtime.status, memberCount: memberCountByGroup.get(group.id) || 0, + members: membersByGroup.get(group.id) || [], canHardDelete: !group.isSystem && group._count.surveys === 0 && group._count.manualAssignments === 0 diff --git a/lib/performance-survey-service.ts b/lib/performance-survey-service.ts index 8183348..43252c1 100644 --- a/lib/performance-survey-service.ts +++ b/lib/performance-survey-service.ts @@ -1632,7 +1632,7 @@ export async function listPerformanceSurveyDepartments(session: Session | null, getSurveyMonthWhere(getMonthRange(dashboardMonth)!), ], }, - ...(ctx.isLeadership + ...(!ctx.isSystemAdmin && ctx.isLeadership ? { employeeProfile: { is: ctx.scopeDepartmentPath @@ -2146,7 +2146,7 @@ function getPublicDashboardEmployeeWhere(ctx: UserContext, params?: URLSearchPar const employeeId = clean(params?.get('employeeId')) const and: any[] = [] - if (ctx.isLeadership) { + if (!ctx.isSystemAdmin && ctx.isLeadership) { and.push( ctx.scopeDepartmentPath ? getScopeDepartmentPathWhere(ctx.scopeDepartmentPath)